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

This commit is contained in:
ocornut 2024-06-11 17:14:00 +02:00
parent c3d7aa252b
commit e1fd25051e
3 changed files with 61 additions and 25 deletions

34
imgui.h
View File

@ -2817,35 +2817,39 @@ struct ImGuiSelectionRequest
// Optional helper to store multi-selection state + apply multi-selection requests. // 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. // - 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. // - 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: // 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. // - 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. // - Use your own external storage: e.g. std::set<MyObjectId>, std::vector<MyObjectId>, interval trees, intrusively stored selection etc.
// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Cannot have multiple views over same objects.
// In ImGuiSelectionBasicStorage we: // In ImGuiSelectionBasicStorage we:
// - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) // - 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 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. // 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. // 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. // See https://github.com/ocornut/imgui/wiki/Multi-Select for details and pseudo-code using this helper.
struct ImGuiSelectionBasicStorage struct ImGuiSelectionBasicStorage
{ {
// Members // 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. 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; 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; }; 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() // Methods
IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); 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 // [Internal, rarely called directly by end-user]
ImGuiSelectionBasicStorage() { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } ImGuiSelectionBasicStorage() { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; }
void Clear() { Storage.Data.resize(0); Size = 0; } 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 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--; } }
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); }
}; };
// Optional helper to apply multi-selection requests to existing randomly accessible storage. // 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; } void (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; }
// Methods // Methods
ImGuiSelectionExternalStorage() { UserData = NULL; AdapterSetItemSelected = NULL; } ImGuiSelectionExternalStorage() { UserData = NULL; AdapterSetItemSelected = NULL; }
IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Generic function, using AdapterSetItemSelected() IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Generic function, using AdapterSetItemSelected()
}; };
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

View File

@ -3441,24 +3441,24 @@ static void ShowDemoWindowMultiSelect()
// Drag and Drop // Drag and Drop
if (use_drag_drop && ImGui::BeginDragDropSource()) 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) // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease)
if (ImGui::GetDragDropPayload() == NULL) if (ImGui::GetDragDropPayload() == NULL)
{ {
ImVector<int> payload_items; ImVector<int> payload_items;
void* it = NULL;
if (!item_is_selected) if (!item_is_selected)
payload_items.push_back(item_id); payload_items.push_back(item_id);
else else
for (const ImGuiStoragePair& pair : selection.Storage.Data) while (int id = (int)selection.GetNextSelectedItem(&it))
if (pair.val_i) payload_items.push_back(id);
payload_items.push_back((int)pair.key);
ImGui::SetDragDropPayload("MULTISELECT_DEMO_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes()); ImGui::SetDragDropPayload("MULTISELECT_DEMO_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes());
} }
// Display payload content in tooltip // Display payload content in tooltip
const ImGuiPayload* payload = ImGui::GetDragDropPayload(); const ImGuiPayload* payload = ImGui::GetDragDropPayload();
const int* payload_items = (int*)payload->Data; 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) if (payload_count == 1)
ImGui::Text("Object %05d: %s", payload_items[0], ExampleNames[payload_items[0] % IM_ARRAYSIZE(ExampleNames)]); ImGui::Text("Object %05d: %s", payload_items[0], ExampleNames[payload_items[0] % IM_ARRAYSIZE(ExampleNames)]);
else else
@ -9896,13 +9896,26 @@ struct ExampleAssetsBrowser
// Drag and drop // Drag and drop
if (ImGui::BeginDragDropSource()) 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) // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease)
int payload_size = item_is_selected ? Selection.Size : 1;
if (ImGui::GetDragDropPayload() == NULL) 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(); ImGui::EndDragDropSource();
} }

View File

@ -7821,6 +7821,23 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
// - ImGuiSelectionExternalStorage // - 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(). // Apply requests coming from BeginMultiSelect() and EndMultiSelect().
// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. // - 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. // - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
@ -7852,7 +7869,7 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
Clear(); Clear();
if (req.Selected) if (req.Selected)
{ {
Storage.Data.reserve(ms_io->ItemsCount); _Storage.Data.reserve(ms_io->ItemsCount);
for (int idx = 0; idx < ms_io->ItemsCount; idx++) for (int idx = 0; idx < ms_io->ItemsCount; idx++)
SetItemSelected(GetStorageIdFromIndex(idx), true); SetItemSelected(GetStorageIdFromIndex(idx), true);
} }
@ -7863,6 +7880,8 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
} }
} }
//-------------------------------------------------------------------------
// Apply requests coming from BeginMultiSelect() and EndMultiSelect(). // Apply requests coming from BeginMultiSelect() and EndMultiSelect().
// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage // We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
// This makes no assumption about underlying storage. // This makes no assumption about underlying storage.