Browse Source

MultiSelect: ImGuiSelectionBasicStorage: added GetNextSelectedItem() to abstract selection storage from user. Amend Assets Browser demo to handle drag and drop correctly.

ocornut 1 year ago
parent
commit
e1fd25051e
3 changed files with 61 additions and 25 deletions
  1. 19 15
      imgui.h
  2. 22 9
      imgui_demo.cpp
  3. 20 1
      imgui_widgets.cpp

+ 19 - 15
imgui.h

@@ -2817,35 +2817,39 @@ struct ImGuiSelectionRequest
 
 // Optional helper to store multi-selection state + apply multi-selection requests.
 // - Used by our demos and provided as a convenience to easily implement basic multi-selection.
+// - Iterate selection with 'void* it = NULL; while (ImGuiId id = selection.GetNextSelectedItem(&it)) { ... }'
+//   Or you can check 'if (Contains(id)) { ... }' for each possible object if their number is not too high to iterate.
 // - USING THIS IS NOT MANDATORY. This is only a helper and not a required API.
 // To store a multi-selection, in your application you could:
-// - A) Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set<ImGuiID> replacement.
-// - B) Use your own external storage: e.g. std::set<MyObjectId>, std::vector<MyObjectId>, interval trees, etc.
-// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Cannot have multiple views over same objects.
+// - Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set<ImGuiID> replacement.
+// - Use your own external storage: e.g. std::set<MyObjectId>, std::vector<MyObjectId>, interval trees, intrusively stored selection etc.
 // In ImGuiSelectionBasicStorage we:
 // - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO)
 // - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index.
+// - use decently optimized logic to allow queries and insertion of very large selection sets.
+// - do not preserve selection order.
 // Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection.
 // Large applications are likely to eventually want to get rid of this indirection layer and do their own thing.
 // See https://github.com/ocornut/imgui/wiki/Multi-Select for details and pseudo-code using this helper.
 struct ImGuiSelectionBasicStorage
 {
     // Members
-    ImGuiStorage    Storage;        // [Internal] Selection set. Think of this as similar to e.g. std::set<ImGuiID>
+    ImGuiStorage    _Storage;       // [Internal] Selection set. Think of this as similar to e.g. std::set<ImGuiID>. Prefer not accessing directly: iterate with GetNextSelectedItem().
     int             Size;           // Number of selected items (== number of 1 in the Storage), maintained by this helper.
     void*           UserData;       // User data for use by adapter function                // e.g. selection.UserData = (void*)my_items;
     ImGuiID         (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx);  // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; };
 
-    // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. Uses 'items_count' passed to BeginMultiSelect()
-    IMGUI_API void  ApplyRequests(ImGuiMultiSelectIO* ms_io);
+    // Methods
+    IMGUI_API void      ApplyRequests(ImGuiMultiSelectIO* ms_io);// Apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. It uses 'items_count' passed to BeginMultiSelect()
+    IMGUI_API ImGuiID   GetNextSelectedItem(void** opaque_it);   // Iterate selection with 'void* it = NULL; while (ImGuiId id = selection.GetNextSelectedItem(&it)) { ... }'
+    bool                Contains(ImGuiID id) const              { return _Storage.GetInt(id, 0) != 0; }         // Query if an item id is in selection.
+    ImGuiID             GetStorageIdFromIndex(int idx)          { return AdapterIndexToStorageId(this, idx); }  // Convert index to item id based on provided adapter.
 
-    // Methods: selection storage
-    ImGuiSelectionBasicStorage()                        { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; }
-    void            Clear()                             { Storage.Data.resize(0); Size = 0; }
-    void            Swap(ImGuiSelectionBasicStorage& r) { Storage.Data.swap(r.Storage.Data); int lhs_size = Size; Size = r.Size; r.Size = lhs_size; }
-    bool            Contains(ImGuiID id) const          { return Storage.GetInt(id, 0) != 0; }
-    void            SetItemSelected(ImGuiID id, bool v) { int* p_int = Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } }
-    ImGuiID         GetStorageIdFromIndex(int idx)      { return AdapterIndexToStorageId(this, idx); }
+    // [Internal, rarely called directly by end-user]
+    ImGuiSelectionBasicStorage()                                { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; }
+    void                Clear()                                 { _Storage.Data.resize(0); Size = 0; }
+    void                Swap(ImGuiSelectionBasicStorage& r)     { _Storage.Data.swap(r._Storage.Data); int lhs_size = Size; Size = r.Size; r.Size = lhs_size; }
+    void                SetItemSelected(ImGuiID id, bool v)     { int* p_int = _Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } }
 };
 
 // Optional helper to apply multi-selection requests to existing randomly accessible storage.
@@ -2857,8 +2861,8 @@ struct ImGuiSelectionExternalStorage
     void            (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; }
 
     // Methods
-    ImGuiSelectionExternalStorage()                     { UserData = NULL; AdapterSetItemSelected = NULL; }
-    IMGUI_API void  ApplyRequests(ImGuiMultiSelectIO* ms_io);                           // Generic function, using AdapterSetItemSelected()
+    ImGuiSelectionExternalStorage()                         { UserData = NULL; AdapterSetItemSelected = NULL; }
+    IMGUI_API void  ApplyRequests(ImGuiMultiSelectIO* ms_io);   // Generic function, using AdapterSetItemSelected()
 };
 
 //-----------------------------------------------------------------------------

+ 22 - 9
imgui_demo.cpp

@@ -3441,24 +3441,24 @@ static void ShowDemoWindowMultiSelect()
                         // Drag and Drop
                         if (use_drag_drop && ImGui::BeginDragDropSource())
                         {
-                            // Consider payload to be full selection OR single unselected item.
+                            // Create payload with full selection OR single unselected item.
                             // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease)
                             if (ImGui::GetDragDropPayload() == NULL)
                             {
                                 ImVector<int> payload_items;
+                                void* it = NULL;
                                 if (!item_is_selected)
                                     payload_items.push_back(item_id);
                                 else
-                                    for (const ImGuiStoragePair& pair : selection.Storage.Data)
-                                        if (pair.val_i)
-                                            payload_items.push_back((int)pair.key);
+                                    while (int id = (int)selection.GetNextSelectedItem(&it))
+                                        payload_items.push_back(id);
                                 ImGui::SetDragDropPayload("MULTISELECT_DEMO_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes());
                             }
 
                             // Display payload content in tooltip
                             const ImGuiPayload* payload = ImGui::GetDragDropPayload();
                             const int* payload_items = (int*)payload->Data;
-                            const int payload_count = (int)payload->DataSize / (int)sizeof(payload_items[0]);
+                            const int payload_count = (int)payload->DataSize / (int)sizeof(int);
                             if (payload_count == 1)
                                 ImGui::Text("Object %05d: %s", payload_items[0], ExampleNames[payload_items[0] % IM_ARRAYSIZE(ExampleNames)]);
                             else
@@ -9896,13 +9896,26 @@ struct ExampleAssetsBrowser
                         // Drag and drop
                         if (ImGui::BeginDragDropSource())
                         {
-                            // Consider payload to be full selection OR single unselected item
+                            // Create payload with full selection OR single unselected item.
                             // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease)
-                            int payload_size = item_is_selected ? Selection.Size : 1;
                             if (ImGui::GetDragDropPayload() == NULL)
-                                ImGui::SetDragDropPayload("ASSETS_BROWSER_ITEMS", "Dummy", 5); // Dummy payload
+                            {
+                                ImVector<ImGuiID> payload_items;
+                                void* it = NULL;
+                                if (!item_is_selected)
+                                    payload_items.push_back(item_data->ID);
+                                else
+                                    while (ImGuiID id = Selection.GetNextSelectedItem(&it))
+                                        payload_items.push_back(id);
+                                ImGui::SetDragDropPayload("ASSETS_BROWSER_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes());
+                            }
+
+                            // Display payload content in tooltip, by extracting it from the payload data
+                            // (we could read from selection, but it is more correct and reusable to read from payload)
+                            const ImGuiPayload* payload = ImGui::GetDragDropPayload();
+                            const int payload_count = (int)payload->DataSize / (int)sizeof(ImGuiID);
+                            ImGui::Text("%d assets", payload_count);
 
-                            ImGui::Text("%d assets", payload_size);
                             ImGui::EndDragDropSource();
                         }
 

+ 20 - 1
imgui_widgets.cpp

@@ -7821,6 +7821,23 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
 // - ImGuiSelectionExternalStorage
 //-------------------------------------------------------------------------
 
+// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user.
+// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...)
+ImGuiID ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it)
+{
+    ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it;
+    ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size;
+    if (it == NULL)
+        it = _Storage.Data.Data;
+    IM_ASSERT(it >= _Storage.Data.Data && it <= it_end);
+    if (it != it_end)
+        while (it->val_i == 0 && it < it_end)
+            it++;
+    const bool has_more = (it != it_end);
+    *opaque_it = has_more ? (void**)(it + 1) : (void**)(it);
+    return has_more ? it->key : 0;
+}
+
 // Apply requests coming from BeginMultiSelect() and EndMultiSelect().
 // - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.
 // - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
@@ -7852,7 +7869,7 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
             Clear();
             if (req.Selected)
             {
-                Storage.Data.reserve(ms_io->ItemsCount);
+                _Storage.Data.reserve(ms_io->ItemsCount);
                 for (int idx = 0; idx < ms_io->ItemsCount; idx++)
                     SetItemSelected(GetStorageIdFromIndex(idx), true);
             }
@@ -7863,6 +7880,8 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
     }
 }
 
+//-------------------------------------------------------------------------
+
 // Apply requests coming from BeginMultiSelect() and EndMultiSelect().
 // We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
 // This makes no assumption about underlying storage.