diff --git a/src/document.h b/src/document.h index e2339b1..7e99354 100644 --- a/src/document.h +++ b/src/document.h @@ -44,6 +44,7 @@ namespace anm2ed Storage& null = current.null; Storage& sound = current.sound; Storage& spritesheet = current.spritesheet; + Storage& items = current.items; Storage& frames = current.frames; std::string& message = current.message; diff --git a/src/imgui/imgui_.cpp b/src/imgui/imgui_.cpp index ee84858..6485712 100644 --- a/src/imgui/imgui_.cpp +++ b/src/imgui/imgui_.cpp @@ -12,6 +12,8 @@ using namespace glm; namespace anm2ed::imgui { + static auto isRenaming = false; + constexpr ImVec4 COLOR_LIGHT_BUTTON{0.98f, 0.98f, 0.98f, 1.0f}; constexpr ImVec4 COLOR_LIGHT_TITLE_BG{0.78f, 0.78f, 0.78f, 1.0f}; constexpr ImVec4 COLOR_LIGHT_TITLE_BG_ACTIVE{0.64f, 0.64f, 0.64f, 1.0f}; @@ -127,6 +129,7 @@ namespace anm2ed::imgui editID.clear(); isActivated = true; state = RENAME_FINISHED; + isRenaming = false; }; if (state == RENAME_BEGIN) @@ -150,6 +153,7 @@ namespace anm2ed::imgui state = RENAME_BEGIN; editID = id; isActivated = true; + isRenaming = true; } } @@ -207,11 +211,12 @@ namespace anm2ed::imgui void external_storage_set(ImGuiSelectionExternalStorage* self, int id, bool isSelected) { - auto* set = (std::set*)self->UserData; + auto* storage = static_cast(self->UserData); + auto value = storage ? storage->resolve_index(id) : id; if (isSelected) - set->insert(id); + storage->insert(value); else - set->erase(id); + storage->erase(value); }; std::string chord_to_string(ImGuiKeyChord chord) @@ -351,8 +356,7 @@ namespace anm2ed::imgui bool shortcut(ImGuiKeyChord chord, shortcut::Type type, bool isRepeat) { if (ImGui::GetTopMostPopupModal() != nullptr) return false; - - if (isRepeat) return chord_repeating(chord); + if (isRepeat && !isRenaming) return chord_repeating(chord); int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal : ImGuiInputFlags_RouteFocused; @@ -383,6 +387,15 @@ namespace anm2ed::imgui apply(); } + void MultiSelectStorage::set_index_map(std::vector* map) { indexMap = map; } + + int MultiSelectStorage::resolve_index(int index) const + { + if (!indexMap) return index; + if (index < 0 || index >= (int)indexMap->size()) return index; + return (*indexMap)[index]; + } + PopupHelper::PopupHelper(const char* label, PopupType type, PopupPosition position) { this->label = label; diff --git a/src/imgui/imgui_.h b/src/imgui/imgui_.h index 568062d..8fe4df1 100644 --- a/src/imgui/imgui_.h +++ b/src/imgui/imgui_.h @@ -194,6 +194,7 @@ namespace anm2ed::imgui public: ImGuiSelectionExternalStorage internal{}; ImGuiMultiSelectIO* io{}; + std::vector* indexMap{}; using std::set::set; using std::set::operator=; @@ -210,6 +211,8 @@ namespace anm2ed::imgui ImGuiMultiSelectFlags_ScopeWindow); void apply(); void finish(); + void set_index_map(std::vector*); + int resolve_index(int) const; }; class PopupHelper diff --git a/src/imgui/window/animations.cpp b/src/imgui/window/animations.cpp index da170a4..e5015ae 100644 --- a/src/imgui/window/animations.cpp +++ b/src/imgui/window/animations.cpp @@ -42,6 +42,9 @@ namespace anm2ed::imgui if (ImGui::Begin("Animations", &settings.windowIsAnimations)) { + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::IsKeyPressed(ImGuiKey_Escape)) + reference = {}; + auto childSize = size_without_footer_get(); if (ImGui::BeginChild("##Animations Child", childSize, ImGuiChildFlags_Borders)) diff --git a/src/imgui/window/spritesheets.cpp b/src/imgui/window/spritesheets.cpp index c9ea32c..fcd83b6 100644 --- a/src/imgui/window/spritesheets.cpp +++ b/src/imgui/window/spritesheets.cpp @@ -98,8 +98,7 @@ namespace anm2ed::imgui bool isTextureValid = spritesheet.texture.is_valid(); auto& texture = isTextureValid ? spritesheet.texture : resources.icons[icon::NONE]; auto textureRef = ImTextureRef(texture.id); - auto tintColor = - !isTextureValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + auto tintColor = !isTextureValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); const std::string pathString = spritesheet.path.empty() ? std::string{anm2::NO_PATH} : spritesheet.path.string(); const char* pathCStr = pathString.c_str(); diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index 59e4499..3bd2443 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -79,6 +80,7 @@ namespace anm2ed::imgui constexpr auto FRAME_MULTIPLE = 5; constexpr auto FRAME_DRAG_PAYLOAD_ID = "Frame Drag Drop"; constexpr auto FRAME_TOOLTIP_HOVER_DELAY = 0.75f; // Extra delay for frame info tooltip. + constexpr int ITEM_SELECTION_NULL_FLAG = 1 << 30; constexpr auto HELP_FORMAT = R"(- Press {} to decrement time. - Press {} to increment time. @@ -97,7 +99,40 @@ namespace anm2ed::imgui auto& playback = document.playback; auto& reference = document.reference; auto& frames = document.frames; + auto& items = document.items; + auto& itemSelection = items.selection; auto animation = document.animation_get(); + auto item_selection_encode = [&](anm2::Type type, int id) -> int + { + if (type == anm2::NULL_) return id | ITEM_SELECTION_NULL_FLAG; + return id; + }; + auto item_selection_decode = [&](int value) -> int { return value & ~ITEM_SELECTION_NULL_FLAG; }; + auto item_selection_value_type = [&](int value) -> anm2::Type + { + return (value & ITEM_SELECTION_NULL_FLAG) ? anm2::NULL_ : anm2::LAYER; + }; + std::vector itemSelectionIndexMap{}; + std::unordered_map layerSelectionIndex{}; + std::unordered_map nullSelectionIndex{}; + if (animation) + { + itemSelectionIndexMap.reserve(animation->layerOrder.size() + animation->nullAnimations.size()); + for (auto id : animation->layerOrder) + { + layerSelectionIndex[id] = (int)itemSelectionIndexMap.size(); + itemSelectionIndexMap.push_back(item_selection_encode(anm2::LAYER, id)); + } + for (auto& [id, nullAnimation] : animation->nullAnimations) + { + (void)nullAnimation; + nullSelectionIndex[id] = (int)itemSelectionIndexMap.size(); + itemSelectionIndexMap.push_back(item_selection_encode(anm2::NULL_, id)); + } + itemSelection.set_index_map(&itemSelectionIndexMap); + } + else + itemSelection.set_index_map(nullptr); style = ImGui::GetStyle(); auto isLightTheme = settings.theme == theme::LIGHT; @@ -137,6 +172,106 @@ namespace anm2ed::imgui return ITEM_COLOR_LIGHT_ACTIVE[type_index(type)]; }; + items.hovered = -1; + + auto item_selection_type_get = [&]() -> anm2::Type + { + if (itemSelection.empty() || !animation) return anm2::NONE; + + for (auto encoded : itemSelection) + { + auto valueType = item_selection_value_type(encoded); + auto valueID = item_selection_decode(encoded); + if (valueType == anm2::LAYER && animation->layerAnimations.contains(valueID)) return anm2::LAYER; + if (valueType == anm2::NULL_ && animation->nullAnimations.contains(valueID)) return anm2::NULL_; + } + + return anm2::NONE; + }; + + auto item_selection_clear = [&]() + { + itemSelection.clear(); + items.reference = -1; + document.layer.selection.clear(); + document.null.selection.clear(); + }; + + auto item_selection_sync = [&]() + { + if (itemSelection.empty()) + { + item_selection_clear(); + return; + } + + auto type = item_selection_type_get(); + items.reference = (int)type; + + auto assign_selection = [&](MultiSelectStorage& target, anm2::Type assignType) + { + target.clear(); + for (auto encoded : itemSelection) + { + if (item_selection_value_type(encoded) != assignType) continue; + target.insert(item_selection_decode(encoded)); + } + }; + + if (type == anm2::LAYER) + assign_selection(document.layer.selection, anm2::LAYER); + else if (type == anm2::NULL_) + assign_selection(document.null.selection, anm2::NULL_); + else + item_selection_clear(); + }; + + auto item_selection_prune = [&]() + { + if (itemSelection.empty()) + { + items.reference = -1; + return; + } + + if (!animation) + { + item_selection_clear(); + return; + } + + auto type = item_selection_type_get(); + if (type != anm2::LAYER && type != anm2::NULL_) + { + item_selection_clear(); + return; + } + + for (auto it = itemSelection.begin(); it != itemSelection.end();) + { + if (item_selection_value_type(*it) != type) + { + it = itemSelection.erase(it); + continue; + } + + auto valueID = item_selection_decode(*it); + bool exists = + type == anm2::LAYER ? animation->layerAnimations.contains(valueID) : animation->nullAnimations.contains(valueID); + if (!exists) + it = itemSelection.erase(it); + else + ++it; + } + + if (itemSelection.empty()) + item_selection_clear(); + else + item_selection_sync(); + }; + + item_selection_prune(); + auto iconTintDefault = isLightTheme ? ICON_TINT_DEFAULT_LIGHT : ICON_TINT_DEFAULT_DARK; auto itemIconTint = isLightTheme ? ICON_TINT_DEFAULT_LIGHT : iconTintDefault; auto frameBorderColor = isLightTheme ? FRAME_BORDER_COLOR_LIGHT : FRAME_BORDER_COLOR_DARK; @@ -288,9 +423,28 @@ namespace anm2ed::imgui addItemName.clear(); addItemSpritesheetID = {}; addItemID = -1; - unusedItems = reference.itemType == anm2::LAYER ? anm2.layers_unused(*animation) - : reference.itemType == anm2::NULL_ ? anm2.nulls_unused(*animation) - : std::set{}; + }; + + auto unused_items_get = [&](anm2::Type type) + { + if (!animation) return std::set{}; + if (type == anm2::LAYER) return anm2.layers_unused(*animation); + if (type == anm2::NULL_) return anm2.nulls_unused(*animation); + return std::set{}; + }; + + auto item_selection_index_get = [&](anm2::Type type, int id) + { + if (type == anm2::LAYER) + { + if (auto it = layerSelectionIndex.find(id); it != layerSelectionIndex.end()) return it->second; + } + else if (type == anm2::NULL_) + { + if (auto it = nullSelectionIndex.find(id); it != nullSelectionIndex.end()) return it->second; + } + + return -1; }; auto item_child = [&](anm2::Type type, int id, int& index) @@ -303,7 +457,7 @@ namespace anm2ed::imgui if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; auto isActive = reference.itemType == type && reference.itemID == id; - auto label = type == anm2::LAYER ? std::format(anm2::LAYER_FORMAT, id, anm2.content.layers.at(id).name, + auto label = type == anm2::LAYER ? std::format(anm2::LAYER_FORMAT, id, anm2.content.layers[id].name, anm2.content.layers[id].spritesheetID) : type == anm2::NULL_ ? std::format(anm2::NULL_FORMAT, id, anm2.content.nulls[id].name) : anm2::TYPE_STRINGS[type]; @@ -311,9 +465,14 @@ namespace anm2ed::imgui auto iconTintCurrent = isLightTheme && type == anm2::NONE ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : itemIconTint; auto baseColorVec = item_color_vec(type); auto activeColorVec = item_color_active_vec(type); + auto selectionType = item_selection_type_get(); + bool isSelectableItem = type == anm2::LAYER || type == anm2::NULL_; + auto selectionIndex = item_selection_index_get(type, id); + int selectionValue = item_selection_encode(type, id); + bool isMultiSelected = isSelectableItem && selectionType == type && itemSelection.contains(selectionValue); bool isTypeNone = type == anm2::NONE; auto colorVec = baseColorVec; - if (isActive && !isTypeNone) + if ((isActive || isMultiSelected) && !isTypeNone) { if (isLightTheme) colorVec = ITEM_COLOR_LIGHT_SELECTED[type_index(type)]; @@ -344,19 +503,29 @@ namespace anm2ed::imgui ImGui::PushStyleColor(ImGuiCol_Header, ImVec4()); ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4()); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4()); - if (ImGui::Selectable("##Item Button", false, ImGuiSelectableFlags_SelectOnNav, itemSize)) + if (isSelectableItem && selectionIndex != -1) ImGui::SetNextItemSelectionUserData(selectionIndex); + if (ImGui::Selectable("##Item Button", isSelectableItem && isMultiSelected, ImGuiSelectableFlags_SelectOnNav, + itemSize)) { - if (type == anm2::LAYER) + if (isSelectableItem) { - document.spritesheet.reference = anm2.content.layers[id].spritesheetID; - document.layer.selection = {id}; + auto previousType = item_selection_type_get(); + bool typeMismatch = + !itemSelection.empty() && previousType != anm2::NONE && previousType != type; + if (typeMismatch) + { + itemSelection.clear(); + itemSelection.insert(selectionValue); + } + item_selection_sync(); } - else if (type == anm2::NULL_) - document.null.selection = {id}; + + if (type == anm2::LAYER) document.spritesheet.reference = anm2.content.layers[id].spritesheetID; reference_set_item(type, id); } ImGui::PopStyleColor(3); + if (ImGui::IsItemHovered()) items.hovered = id; if (ImGui::IsItemHovered()) { if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) @@ -537,6 +706,9 @@ namespace anm2ed::imgui if (animation) { + item_selection_prune(); + itemSelection.start(itemSelectionIndexMap.size(), ImGuiMultiSelectFlags_ClearOnEscape); + item_child_row(anm2::ROOT); for (auto& id : animation->layerOrder) @@ -552,6 +724,9 @@ namespace anm2ed::imgui } item_child_row(anm2::TRIGGER); + + itemSelection.finish(); + item_selection_sync(); } if (isHorizontalScroll && ImGui::GetCurrentWindow()->ScrollbarY) @@ -583,19 +758,33 @@ namespace anm2ed::imgui set_item_tooltip_shortcut("Add a new item to the animation.", settings.shortcutAdd); ImGui::SameLine(); - ImGui::BeginDisabled(!document.item_get() && reference.itemType != anm2::LAYER && - reference.itemType != anm2::NULL_); + auto selectionType = item_selection_type_get(); + bool hasSelection = !itemSelection.empty() && (selectionType == anm2::LAYER || selectionType == anm2::NULL_); + bool hasReferenceItem = document.item_get() != nullptr; + ImGui::BeginDisabled(!hasSelection && !hasReferenceItem); { shortcut(manager.chords[SHORTCUT_REMOVE]); if (ImGui::Button("Remove", widgetSize)) { auto remove = [&]() { - animation->item_remove(reference.itemType, reference.itemID); + if (hasSelection) + { + std::vector ids{}; + ids.reserve(itemSelection.size()); + for (auto value : itemSelection) + ids.push_back(item_selection_decode(value)); + std::sort(ids.begin(), ids.end()); + for (auto id : ids) + animation->item_remove(selectionType, id); + item_selection_clear(); + } + else if (reference.itemType == anm2::LAYER || reference.itemType == anm2::NULL_) + animation->item_remove(reference.itemType, reference.itemID); reference_clear(); }; - DOCUMENT_EDIT(document, "Remove Item", Document::ITEMS, remove()); + DOCUMENT_EDIT(document, "Remove Item(s)", Document::ITEMS, remove()); } set_item_tooltip_shortcut("Remove the selected item(s) from the animation.", settings.shortcutRemove); } @@ -1434,9 +1623,6 @@ namespace anm2ed::imgui ImGui::SeparatorText("Source"); - bool isNewOnly = unusedItems.empty(); - if (isNewOnly) source = source::NEW; - if (ImGui::BeginChild("Source New", size)) { ImGui::RadioButton("New", &source, source::NEW); @@ -1448,10 +1634,13 @@ namespace anm2ed::imgui if (ImGui::BeginChild("Source Existing", size)) { - ImGui::BeginDisabled(isNewOnly); + auto hasUnusedItems = animation && !unused_items_get((anm2::Type)type).empty(); + ImGui::BeginDisabled(!hasUnusedItems); ImGui::RadioButton("Existing", &source, source::EXISTING); ImGui::EndDisabled(); - ImGui::SetItemTooltip("Use a pre-existing, presently unused item."); + auto tooltip = + hasUnusedItems ? "Use a pre-existing, presently unused item." : "No unused items are available."; + ImGui::SetItemTooltip("%s", tooltip); } ImGui::EndChild(); @@ -1494,6 +1683,9 @@ namespace anm2ed::imgui { if (animation && source == source::EXISTING) { + auto unusedItems = unused_items_get((anm2::Type)type); + if (addItemID != -1 && !unusedItems.contains(addItemID)) addItemID = -1; + for (auto id : unusedItems) { auto isSelected = addItemID == id; @@ -1522,6 +1714,7 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); + ImGui::BeginDisabled(source == source::EXISTING && addItemID == -1); if (ImGui::Button("Add", widgetSize)) { anm2::Reference addReference{}; @@ -1537,9 +1730,16 @@ namespace anm2ed::imgui document.change(Document::ITEMS); reference = addReference; + itemSelection.clear(); + if (addReference.itemType == anm2::LAYER || addReference.itemType == anm2::NULL_) + { + itemSelection.insert(item_selection_encode(addReference.itemType, addReference.itemID)); + item_selection_sync(); + } item_properties_close(); } + ImGui::EndDisabled(); ImGui::SetItemTooltip("Add the item, with the settings specified."); ImGui::SameLine(); diff --git a/src/imgui/window/timeline.h b/src/imgui/window/timeline.h index 6a1591c..575e02c 100644 --- a/src/imgui/window/timeline.h +++ b/src/imgui/window/timeline.h @@ -24,7 +24,7 @@ namespace anm2ed::imgui bool isWindowHovered{}; bool isHorizontalScroll{}; PopupHelper propertiesPopup{PopupHelper("Item Properties", POPUP_NORMAL)}; - PopupHelper bakePopup{PopupHelper("Bake", POPUP_TO_CONTENT)}; + PopupHelper bakePopup{PopupHelper("Bake", POPUP_SMALL_NO_HEIGHT)}; std::string addItemName{}; bool addItemIsRect{}; int addItemID{-1}; @@ -42,7 +42,6 @@ namespace anm2ed::imgui std::vector frameSelectionLocked{}; bool isFrameSelectionLocked{}; anm2::Reference frameSelectionSnapshotReference{}; - std::set unusedItems{}; glm::vec2 scroll{}; ImDrawList* pickerLineDrawList{}; ImGuiStyle style{};