Browse Source

MultiSelect: Demo: Assets Browser: added deletion support. Store ID in selection. Moved QueueDeletion to local var to emphasis that this is a user extension.

ocornut 1 year ago
parent
commit
750e23998f
2 changed files with 95 additions and 76 deletions
  1. 0 1
      imgui.h
  2. 95 75
      imgui_demo.cpp

+ 0 - 1
imgui.h

@@ -2871,7 +2871,6 @@ struct ImGuiSelectionBasicStorage
     void    AddItem(ImGuiID key)                { int* p_int = Storage.GetIntRef(key, 0); if (*p_int != 0) return; *p_int = 1; Size++; }
     void    RemoveItem(ImGuiID key)             { int* p_int = Storage.GetIntRef(key, 0); if (*p_int == 0) return; *p_int = 0; Size--; }
     void    UpdateItem(ImGuiID key, bool v)     { if (v) { AddItem(key); } else { RemoveItem(key); } }
-    int     GetSize() const                     { return Size; }
 
     // Methods: apply selection requests (that are coming from BeginMultiSelect() and EndMultiSelect() functions)
     IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count);

+ 95 - 75
imgui_demo.cpp

@@ -2773,10 +2773,9 @@ static const char* ExampleNames[] =
     "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber"
 };
 
-struct ExampleSelectionStorageWithDeletion : ImGuiSelectionBasicStorage
+// Extra functions to add deletion support to ImGuiSelectionBasicStorage
+struct ExampleSelectionWithDeletion : ImGuiSelectionBasicStorage
 {
-    bool QueueDeletion = false; // Track request deleting selected items
-
     // Find which item should be Focused after deletion.
     // Call _before_ item submission. Retunr an index in the before-deletion item list, your item loop should call SetKeyboardFocusHere() on it.
     // The subsequent ApplyDeletionPostLoop() code will use it to apply Selection.
@@ -2786,7 +2785,6 @@ struct ExampleSelectionStorageWithDeletion : ImGuiSelectionBasicStorage
     // FIXME-MULTISELECT: Doesn't take account of the possibility focus target will be moved during deletion. Need refocus or scroll offset.
     int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, int items_count)
     {
-        QueueDeletion = false;
         if (Size == 0)
             return -1;
 
@@ -3032,12 +3030,13 @@ static void ShowDemoWindowMultiSelect()
             ImGui::BulletText("Ctrl modifier to preserve and toggle selection.");
             ImGui::BulletText("Shift modifier for range selection.");
             ImGui::BulletText("CTRL+A to select all.");
-            ImGui::Text("Tip: Use 'Debug Log->Selection' to see selection requests as they happen.");
+            ImGui::BulletText("Escape to clear selection.");
+            ImGui::Text("Tip: Use 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.");
 
             // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection
             const int ITEMS_COUNT = 50;
             static ImGuiSelectionBasicStorage selection;
-            ImGui::Text("Selection: %d/%d", selection.GetSize(), ITEMS_COUNT);
+            ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT);
 
             // The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling region).
             if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20)))
@@ -3074,7 +3073,7 @@ static void ShowDemoWindowMultiSelect()
             ImGui::BulletText("Using ImGuiListClipper.");
 
             const int ITEMS_COUNT = 10000;
-            ImGui::Text("Selection: %d/%d", selection.GetSize(), ITEMS_COUNT);
+            ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT);
             if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20)))
             {
                 ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape;
@@ -3116,24 +3115,26 @@ static void ShowDemoWindowMultiSelect()
         IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (with deletion)");
         if (ImGui::TreeNode("Multi-Select (with deletion)"))
         {
-            // Intentionally separating items data from selection data!
-            // But you may decide to store selection data inside your item (aka intrusive storage).
-            // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection
-            static ImVector<int> items;
-            static ExampleSelectionStorageWithDeletion selection;
+            // Storing items data separately from selection data.
+            // (you may decide to store selection data inside your item (aka intrusive storage) if you don't need multiple views over same items)
+            // Use a custom selection.Adapter: store item identifier in Selection (instead of index)
+            static ImVector<ImGuiID> items;
+            static ExampleSelectionWithDeletion selection;
+            selection.AdapterData = (void*)&items;
+            selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { ImVector<ImGuiID>* p_items = (ImVector<ImGuiID>*)self->AdapterData; return (*p_items)[idx]; }; // Index -> ID
 
-            ImGui::Text("Adding features:");
+            ImGui::Text("Added features:");
             ImGui::BulletText("Dynamic list with Delete key support.");
-            ImGui::Text("Selection size: %d/%d", selection.GetSize(), items.Size);
+            ImGui::Text("Selection size: %d/%d", selection.Size, items.Size);
 
             // Initialize default list with 50 items + button to add/remove items.
-            static int items_next_id = 0;
+            static ImGuiID items_next_id = 0;
             if (items_next_id == 0)
-                for (int n = 0; n < 50; n++)
+                for (ImGuiID n = 0; n < 50; n++)
                     items.push_back(items_next_id++);
             if (ImGui::SmallButton("Add 20 items"))     { for (int n = 0; n < 20; n++) { items.push_back(items_next_id++); } }
             ImGui::SameLine();
-            if (ImGui::SmallButton("Remove 20 items"))  { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.RemoveItem((ImGuiID)(items.Size - 1)); items.pop_back(); } } // This is to test
+            if (ImGui::SmallButton("Remove 20 items"))  { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.RemoveItem(items.back()); items.pop_back(); } }
 
             // (1) Extra to support deletion: Submit scrolling range to avoid glitches on deletion
             const float items_height = ImGui::GetTextLineHeightWithSpacing();
@@ -3147,18 +3148,16 @@ static void ShowDemoWindowMultiSelect()
 
                 // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to turn into 'ms_io->RequestDelete' signal -> need HasSelection passed.
                 // FIXME-MULTISELECT: If pressing Delete + another key we have ambiguous behavior.
-                const bool want_delete = selection.QueueDeletion || ((selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete));
-                int item_curr_idx_to_focus = -1;
-                if (want_delete)
-                    item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, items.Size);
+                const bool want_delete = (selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete);
+                const int item_curr_idx_to_focus = want_delete ? selection.ApplyDeletionPreLoop(ms_io, items.Size) : -1;
 
                 for (int n = 0; n < items.Size; n++)
                 {
-                    const int item_id = items[n];
+                    const ImGuiID item_id = items[n];
                     char label[64];
-                    sprintf(label, "Object %05d: %s", item_id, ExampleNames[item_id % IM_ARRAYSIZE(ExampleNames)]);
+                    sprintf(label, "Object %05u: %s", item_id, ExampleNames[item_id % IM_ARRAYSIZE(ExampleNames)]);
 
-                    bool item_is_selected = selection.Contains((ImGuiID)n);
+                    bool item_is_selected = selection.Contains(item_id);
                     ImGui::SetNextItemSelectionUserData(n);
                     ImGui::Selectable(label, item_is_selected);
                     if (item_curr_idx_to_focus == n)
@@ -3217,7 +3216,7 @@ static void ShowDemoWindowMultiSelect()
                 selection->ApplyRequests(ms_io, ITEMS_COUNT);
 
                 ImGui::SeparatorText("Selection scope");
-                ImGui::Text("Selection size: %d/%d", selection->GetSize(), ITEMS_COUNT);
+                ImGui::Text("Selection size: %d/%d", selection->Size, ITEMS_COUNT);
 
                 for (int n = 0; n < ITEMS_COUNT; n++)
                 {
@@ -3292,9 +3291,10 @@ static void ShowDemoWindowMultiSelect()
             static ImVector<int> items;
             static int items_next_id = 0;
             if (items_next_id == 0) { for (int n = 0; n < 1000; n++) { items.push_back(items_next_id++); } }
-            static ExampleSelectionStorageWithDeletion selection;
+            static ExampleSelectionWithDeletion selection;
+            static bool request_deletion_from_menu = false; // Queue deletion triggered from context menu
 
-            ImGui::Text("Selection size: %d/%d", selection.GetSize(), items.Size);
+            ImGui::Text("Selection size: %d/%d", selection.Size, items.Size);
 
             const float items_height = (widget_type == WidgetType_TreeNode) ? ImGui::GetTextLineHeight() : ImGui::GetTextLineHeightWithSpacing();
             ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height));
@@ -3308,10 +3308,9 @@ static void ShowDemoWindowMultiSelect()
                 selection.ApplyRequests(ms_io, items.Size);
 
                 // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to turn into 'ms_io->RequestDelete' signal -> need HasSelection passed.
-                const bool want_delete = selection.QueueDeletion || ((selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete));
-                int item_curr_idx_to_focus = -1;
-                if (want_delete)
-                    item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, items.Size);
+                const bool want_delete = request_deletion_from_menu || ((selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete));
+                const int item_curr_idx_to_focus = want_delete ? selection.ApplyDeletionPreLoop(ms_io, items.Size) : -1;
+                request_deletion_from_menu = false;
 
                 if (show_in_table)
                 {
@@ -3328,7 +3327,7 @@ static void ShowDemoWindowMultiSelect()
                 {
                     clipper.Begin(items.Size);
                     if (item_curr_idx_to_focus != -1)
-                        clipper.IncludeItemByIndex(item_curr_idx_to_focus); // Ensure focused item is not clipped
+                        clipper.IncludeItemByIndex(item_curr_idx_to_focus); // Ensure focused item is not clipped.
                     if (ms_io->RangeSrcItem > 0)
                         clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped.
                 }
@@ -3417,9 +3416,10 @@ static void ShowDemoWindowMultiSelect()
                         // Right-click: context menu
                         if (ImGui::BeginPopupContextItem())
                         {
-                            ImGui::BeginDisabled(!use_deletion || selection.GetSize() == 0);
-                            sprintf(label, "Delete %d item(s)###DeleteSelected", selection.GetSize());
-                            selection.QueueDeletion |= ImGui::Selectable(label);
+                            ImGui::BeginDisabled(!use_deletion || selection.Size == 0);
+                            sprintf(label, "Delete %d item(s)###DeleteSelected", selection.Size);
+                            if (ImGui::Selectable(label))
+                                request_deletion_from_menu = true;
                             ImGui::EndDisabled();
                             ImGui::Selectable("Close");
                             ImGui::EndPopup();
@@ -9619,19 +9619,20 @@ const ImGuiTableSortSpecs* ExampleAsset::s_current_sort_specs = NULL;
 struct ExampleAssetsBrowser
 {
     // Options
-    bool                    ShowTypeOverlay = true;
-    bool                    AllowDragUnselected = false;
-    float                   IconSize = 32.0f;
-    int                     IconSpacing = 10;
-    int                     IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer.
-    bool                    StretchSpacing = true;
+    bool            ShowTypeOverlay = true;
+    bool            AllowDragUnselected = false;
+    float           IconSize = 32.0f;
+    int             IconSpacing = 10;
+    int             IconHitSpacing = 4;         // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer.
+    bool            StretchSpacing = true;
 
     // State
-    ImVector<ExampleAsset>  Items;
-    ImGuiSelectionBasicStorage Selection;
-    ImGuiID                 NextItemId = 0;
-    bool                    SortDirty = false;
-    float                   ZoomWheelAccum = 0.0f;
+    ImVector<ExampleAsset> Items;               // Our items
+    ExampleSelectionWithDeletion Selection;     // Our selection (ImGuiSelectionBasicStorage + helper funcs to handle deletion)
+    ImGuiID         NextItemId = 0;             // Unique identifier when creating new items
+    bool            RequestDelete = false;      // Deferred deletion request
+    bool            RequestSort = false;        // Deferred sort request
+    float           ZoomWheelAccum = 0.0f;      // Mouse wheel accumulator to handle smooth wheels better
 
     // Functions
     ExampleAssetsBrowser()
@@ -9645,7 +9646,7 @@ struct ExampleAssetsBrowser
         Items.reserve(Items.Size + count);
         for (int n = 0; n < count; n++, NextItemId++)
             Items.push_back(ExampleAsset(NextItemId, (NextItemId % 20) < 15 ? 0 : (NextItemId % 20) < 18 ? 1 : 2));
-        SortDirty = true;
+        RequestSort = true;
     }
     void ClearItems()
     {
@@ -9676,6 +9677,12 @@ struct ExampleAssetsBrowser
                     *p_open = false;
                 ImGui::EndMenu();
             }
+            if (ImGui::BeginMenu("Edit"))
+            {
+                if (ImGui::MenuItem("Delete", "Del", false, Selection.Size > 0))
+                    RequestDelete = true;
+                ImGui::EndMenu();
+            }
             if (ImGui::BeginMenu("Options"))
             {
                 ImGui::PushItemWidth(ImGui::GetFontSize() * 10);
@@ -9697,22 +9704,6 @@ struct ExampleAssetsBrowser
             ImGui::EndMenuBar();
         }
 
-        // Zooming with CTRL+Wheel
-        // FIXME-MULTISELECT: Try to maintain scroll.
-        ImGuiIO& io = ImGui::GetIO();
-        if (ImGui::IsWindowAppearing())
-            ZoomWheelAccum = 0.0f;
-        if (io.MouseWheel != 0.0f && ImGui::IsKeyDown(ImGuiMod_Ctrl) && ImGui::IsAnyItemActive() == false)
-        {
-            ZoomWheelAccum += io.MouseWheel;
-            if (fabsf(ZoomWheelAccum) >= 1.0f)
-            {
-                IconSize *= powf(1.1f, (float)(int)ZoomWheelAccum);
-                IconSize = IM_CLAMP(IconSize, 16.0f, 128.0f);
-                ZoomWheelAccum -= (int)ZoomWheelAccum;
-            }
-        }
-
         // Show a table with ONLY one header row to showcase the idea/possibility of using this to provide a sorting UI
         ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
         ImGuiTableFlags table_flags_for_sort_specs = ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Borders;
@@ -9722,10 +9713,10 @@ struct ExampleAssetsBrowser
             ImGui::TableSetupColumn("Type");
             ImGui::TableHeadersRow();
             if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs())
-                if (sort_specs->SpecsDirty || SortDirty)
+                if (sort_specs->SpecsDirty || RequestSort)
                 {
                     ExampleAsset::SortWithSortSpecs(sort_specs, Items.Data, Items.Size);
-                    sort_specs->SpecsDirty = SortDirty = false;
+                    sort_specs->SpecsDirty = RequestSort = false;
                 }
             ImGui::EndTable();
         }
@@ -9765,6 +9756,10 @@ struct ExampleAssetsBrowser
             Selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->AdapterData; return self->Items[idx].ID; };
             Selection.ApplyRequests(ms_io, Items.Size);
 
+            const bool want_delete = RequestDelete || ((Selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete));
+            const int item_curr_idx_to_focus = want_delete ? Selection.ApplyDeletionPreLoop(ms_io, Items.Size) : -1;
+            RequestDelete = false;
+
             // Altering ItemSpacing may seem unnecessary as we position every items using SetCursorScreenPos()...
             // But it is necessary for two reasons:
             // - Selectables uses it by default to visually fill the space between two items.
@@ -9781,8 +9776,10 @@ struct ExampleAssetsBrowser
             const float line_height = item_size.y + item_spacing;
             ImGuiListClipper clipper;
             clipper.Begin(line_count, line_height);
+            if (item_curr_idx_to_focus != -1)
+                clipper.IncludeItemByIndex(item_curr_idx_to_focus / column_count); // Ensure focused item line is not clipped.
             if (ms_io->RangeSrcItem != -1)
-                clipper.IncludeItemByIndex((int)(ms_io->RangeSrcItem / column_count));
+                clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem / column_count); // Ensure RangeSrc item line is not clipped.
             while (clipper.Step())
             {
                 for (int line_idx = clipper.DisplayStart; line_idx < clipper.DisplayEnd; line_idx++)
@@ -9812,6 +9809,10 @@ struct ExampleAssetsBrowser
                         if (ImGui::IsItemToggledSelection())
                             item_is_selected = !item_is_selected;
 
+                        // Focus (for after deletion)
+                        if (item_curr_idx_to_focus == item_idx)
+                            ImGui::SetKeyboardFocusHere(-1);
+
                         // Drag and drop
                         if (ImGui::BeginDragDropSource())
                         {
@@ -9825,15 +9826,6 @@ struct ExampleAssetsBrowser
                             ImGui::EndDragDropSource();
                         }
 
-                        // Popup menu
-                        if (ImGui::BeginPopupContextItem())
-                        {
-                            ImGui::Text("Selection: %d items", Selection.Size);
-                            if (ImGui::Button("Close"))
-                                ImGui::CloseCurrentPopup();
-                            ImGui::EndPopup();
-                        }
-
                         // A real app would likely display an image/thumbnail here.
                         draw_list->AddRectFilled(box_min, box_max, icon_bg_color);
                         if (ShowTypeOverlay && item_data->Type != 0)
@@ -9856,13 +9848,41 @@ struct ExampleAssetsBrowser
             clipper.End();
             ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing
 
+            // Context menu
+            if (ImGui::BeginPopupContextWindow())
+            {
+                ImGui::Text("Selection: %d items", Selection.Size);
+                ImGui::Separator();
+                if (ImGui::MenuItem("Delete", "Del", false, Selection.Size > 0))
+                    RequestDelete = true;
+                ImGui::EndPopup();
+            }
+
             ms_io = ImGui::EndMultiSelect();
             Selection.ApplyRequests(ms_io, Items.Size);
+            if (want_delete)
+                Selection.ApplyDeletionPostLoop(ms_io, Items, item_curr_idx_to_focus);
 
             // FIXME-MULTISELECT: Find a way to expose this in public API. This currently requires "imgui_internal.h"
             //ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX);
         }
 
+        // Zooming with CTRL+Wheel
+        // FIXME-MULTISELECT: Try to maintain scroll.
+        ImGuiIO& io = ImGui::GetIO();
+        if (ImGui::IsWindowAppearing())
+            ZoomWheelAccum = 0.0f;
+        if (ImGui::IsWindowHovered() && io.MouseWheel != 0.0f && ImGui::IsKeyDown(ImGuiMod_Ctrl) && ImGui::IsAnyItemActive() == false)
+        {
+            ZoomWheelAccum += io.MouseWheel;
+            if (fabsf(ZoomWheelAccum) >= 1.0f)
+            {
+                IconSize *= powf(1.1f, (float)(int)ZoomWheelAccum);
+                IconSize = IM_CLAMP(IconSize, 16.0f, 128.0f);
+                ZoomWheelAccum -= (int)ZoomWheelAccum;
+            }
+        }
+
         ImGui::EndChild();
         ImGui::Text("Selected: %d/%d items", Selection.Size, Items.Size);
         ImGui::End();