MultiSelect: (Breaking) BeginMultiSelect() doesn't need two last params maintained by users. Moving some storage from user to core. Proper deletion demo.

This commit is contained in:
ocornut 2023-06-02 14:34:22 +02:00
parent 564dde0ee3
commit 9223ffc255
5 changed files with 257 additions and 95 deletions

View File

@ -14957,6 +14957,15 @@ void ImGui::ShowMetricsWindow(bool* p_open)
TreePop();
}
// Details for MultiSelect
if (TreeNode("MultiSelect", "MultiSelect (%d)", g.MultiSelectStorage.GetAliveCount()))
{
for (int n = 0; n < g.MultiSelectStorage.GetMapSize(); n++)
if (ImGuiMultiSelectState* state = g.MultiSelectStorage.TryGetMapData(n))
DebugNodeMultiSelectState(state);
TreePop();
}
// Details for Docking
#ifdef IMGUI_HAS_DOCK
if (TreeNode("Docking"))

57
imgui.h
View File

@ -669,10 +669,9 @@ namespace ImGui
IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper.
// Multi-selection system for Selectable() and TreeNode() functions.
// This enables standard multi-selection/range-selection idioms (CTRL+Click/Arrow, SHIFT+Click/Arrow, etc) in a way that allow items to be fully clipped (= not submitted at all) when not visible.
// Read comments near ImGuiMultiSelectIO for details.
// When enabled, Selectable() and TreeNode() functions will return true when selection needs toggling.
IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected);
// - This enables standard multi-selection/range-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc.) in a way that also allow a clipper to be used.
// - Read comments near ImGuiMultiSelectIO for details.
IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags);
IMGUI_API ImGuiMultiSelectIO* EndMultiSelect();
IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data);
@ -908,7 +907,7 @@ namespace ImGui
IMGUI_API bool IsItemDeactivated(); // was the last item just made inactive (item was previously active). Useful for Undo/Redo patterns with widgets that require continuous editing.
IMGUI_API bool IsItemDeactivatedAfterEdit(); // was the last item just made inactive and made a value change when it was active? (e.g. Slider/Drag moved). Useful for Undo/Redo patterns with widgets that require continuous editing. Note that you may get false positives (some widgets such as Combo()/ListBox()/Selectable() will return true even when clicking an already selected item).
IMGUI_API bool IsItemToggledOpen(); // was the last item open state toggled? set by TreeNode().
IMGUI_API bool IsItemToggledSelection(); // was the last item selection state toggled? (after Selectable(), TreeNode() etc. We only returns toggle _event_ in order to handle clipping correctly)
IMGUI_API bool IsItemToggledSelection(); // was the last item selection state toggled? (after Selectable(), TreeNode() etc.) We only returns toggle _event_ in order to handle clipping correctly.
IMGUI_API bool IsAnyItemHovered(); // is any item hovered?
IMGUI_API bool IsAnyItemActive(); // is any item active?
IMGUI_API bool IsAnyItemFocused(); // is any item focused?
@ -2725,10 +2724,8 @@ struct ImColor
#define IMGUI_HAS_MULTI_SELECT // Multi-Select/Range-Select WIP branch // <-- This is currently _not_ in the top of imgui.h to prevent merge conflicts.
// Flags for BeginMultiSelect().
// This system is designed to allow mouse/keyboard multi-selection, including support for range-selection (SHIFT+click and SHIFT+keyboard),
// which is difficult to re-implement manually. If you disable multi-selection with ImGuiMultiSelectFlags_NoMultiSelect
// (which is provided for consistency and flexibility), the whole BeginMultiSelect() system becomes largely overkill as
// you can handle single-selection in a simpler manner by just calling Selectable() and reacting on clicks yourself.
// (we provide 'ImGuiMultiSelectFlags_NoMultiSelect' for consistency and flexiblity, but it essentially disable the main purpose of BeginMultiSelect().
// If you use 'ImGuiMultiSelectFlags_NoMultiSelect' you can handle single-selection in a simpler way by just calling Selectable()/TreeNode() and reacting on clicks).
enum ImGuiMultiSelectFlags_
{
ImGuiMultiSelectFlags_None = 0,
@ -2739,12 +2736,11 @@ enum ImGuiMultiSelectFlags_
//ImGuiMultiSelectFlags_ClearOnClickRectVoid= 1 << 4, // Clear selection when clicking on empty location within rectangle covered by selection scope (use if multiple BeginMultiSelect() are used in the same host window)
};
// Abstract:
// - This system helps you implements standard multi-selection idioms (CTRL+Click/Arrow, SHIFT+Click/Arrow, etc) in a way that allow
// selectable items to be fully clipped (= not submitted at all) when not visible. Clipping is typically provided by ImGuiListClipper.
// Handling all of this in a single pass imgui is a little tricky, and this is why we provide those functionalities.
// Note however that if you don't need SHIFT+Click/Arrow range-select + clipping, you can handle a simpler form of multi-selection
// yourself, by reacting to click/presses on Selectable() items and checking keyboard modifiers.
// Multi-selection system
// - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) in a way that
// allow a clipper to be used (so most non-visible items won't be submitted). Handling this correctly is tricky, this is why
// we provide the functionality. Note however that if you don't need SHIFT+Mouse/Keyboard range-select + clipping, you could use
// a simpler form of multi-selection yourself, by reacting to click/presses on Selectable() items and checking keyboard modifiers.
// The unusual complexity of this system is mostly caused by supporting SHIFT+Click/Arrow range-select with clipped elements.
// - In the spirit of Dear ImGui design, your code owns the selection data.
// So this is designed to handle all kind of selection data: e.g. instructive selection (store a bool inside each object),
@ -2762,36 +2758,33 @@ enum ImGuiMultiSelectFlags_
// between them to honor range selection. But the code never assume that sortable integers are used (you may store pointers to your object,
// and then from the pointer have your own way of iterating from RangeSrcItem to RangeDstItem).
// Usage flow:
// Begin
// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrcItem and its selection state.
// It is because you need to pass its selection state (and you own selection) that we don't store this value in Dear ImGui.
// (For the initial frame or when resetting your selection state: you may use the value for your first item or a "null" value that matches the type stored in your void*).
// 2) Honor Clear/SelectAll/SetRange requests by updating your selection data. (Only required if you are using a clipper in step 4: but you can use same code as step 6 anyway.)
// Loop
// 3) Set RangeSrcPassedBy=true if the RangeSrcItem item is part of the items clipped before the first submitted/visible item. [Only required if you are using a clipper in step 4]
// This is because for range-selection we need to know if we are currently "inside" or "outside" the range.
// If you are using integer indices everywhere, this is easy to compute: if (clipper.DisplayStart > (int)data->RangeSrcItem) { data->RangeSrcPassedBy = true; }
// 4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls.
// (You may optionally call IsItemToggledSelection() to query if the selection state has been toggled for a given item, if you need that info immediately for your display (before EndMultiSelect()).)
// (When cannot provide a "IsItemSelected()" value because we need to consider clipped/unprocessed items, this is why we return a "Toggled" event instead.)
// End
// 5) Call EndMultiSelect(). Save the value of ->RangeSrcItem for the next frame (you may convert the value in a format that is safe for persistance)
// 6) Honor Clear/SelectAll/SetRange requests by updating your selection data. Always process them in this order (as you will receive Clear+SetRange request simultaneously)
// If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable() on a per-item basis.
// BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result.
// - (2) [If using a clipper] Honor Clear/SelectAll/SetRange requests by updating your selection data. Can use same code as Step 6.
// LOOP - (3) [If using a clipper] Set RangeSrcPassedBy=true if the RangeSrcItem item is part of the items clipped before the first submitted/visible item.
// This is because for range-selection we need to know if we are currently "inside" or "outside" the range.
// If you are using integer indices everywhere, this is easy to compute: if (clipper.DisplayStart > (int)data->RangeSrcItem) { data->RangeSrcPassedBy = true; }
// - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls.
// (optionally call IsItemToggledSelection() to query if the selection state has been toggled for a given visible item, if you need that info immediately for your display, before EndMultiSelect())
// END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result.
// - (6) Honor Clear/SelectAll/SetRange requests by updating your selection data. Always process them in this order (as you will receive Clear+SetRange request simultaneously). Can use same code as Step 2.
// If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable()/TreeNode on a per-item basis.
struct ImGuiMultiSelectIO
{
// - Always process requests in this order: Clear, SelectAll, SetRange.
// - Some fields are only necessary if your list is dynamic and allows deletion (getting "post-deletion" state right is exhibited in the demo)
// - Below: who reads/writes each fields? 'r'=read, 'w'=write, 'ms'=multi-select code, 'app'=application/user code, 'BEGIN'=BeginMultiSelect() and after, 'END'=EndMultiSelect() and after.
// REQUESTS ----------------// BEGIN / LOOP / END
bool RequestClear; // ms:w, app:r / / ms:w, app:r // 1. Request app/user to clear selection.
bool RequestSelectAll; // ms:w, app:r / / ms:w, app:r // 2. Request app/user to select all.
bool RequestSetRange; // / / ms:w, app:r // 3. Request app/user to select/unselect [RangeSrcItem..RangeDstItem] items, based on RangeSelected. In practice, only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false.
// STATE/ARGUMENTS ---------// BEGIN / LOOP / END
void* RangeSrcItem; // ms:w / app:r / ms:w, app:r // Begin: Last known SetNextItemSelectionData() value for RangeSrcItem. End: parameter from RequestSetRange request.
void* RangeSrcItem; // ms:w / app:r / ms:w, app:r // Begin: Last known SetNextItemSelectionUserData() value for RangeSrcItem. End: parameter from RequestSetRange request.
void* RangeDstItem; // / / ms:w, app:r // End: parameter from RequestSetRange request.
ImS8 RangeDirection; // / / ms:w, app:r // End: parameter from RequestSetRange request. +1 if RangeSrcItem came before RangeDstItem, -1 otherwise. Available as an indicator in case you cannot infer order from the void* values. If your void* values are storing indices you will never need this.
bool RangeSelected; // / / ms:w, app:r // End: parameter from RequestSetRange request. true = Select Range, false = Unselect Range.
bool RangeSrcPassedBy; // / ms:rw app:w / ms:r // (If using clipper) Need to be set by app/user if RangeSrcItem was part of the clipped set before submitting the visible items. Ignore if not clipping.
bool RangeSrcReset; // / app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection).
void* NavIdItem; // ms:w, app:r / / ms:w app:r // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items)
ImGuiMultiSelectIO() { Clear(); }
void Clear() { memset(this, 0, sizeof(*this)); }

View File

@ -2773,17 +2773,14 @@ static void ShowDemoWindowWidgets()
// are generally appropriate. Even a large array of bool might work for you...
// - If you need to handle extremely large selections, it might be advantageous to support a "negative" mode in
// your storage, so "Select All" becomes "Negative=1 + Clear" and then sparse unselect can add to the storage.
// About RefItem:
// - The BeginMultiSelect() API requires you to store information about the reference/pivot item (generally the last clicked item).
struct ExampleSelection
{
// Data
ImGuiStorage Storage; // Selection set
int SelectionSize; // Number of selected items (== number of 1 in the Storage, maintained by this class). // FIXME-MULTISELECT: Imply more difficult to track with intrusive selection schemes?
int RangeRef; // Reference/pivot item (generally last clicked item)
// Functions
ExampleSelection() { RangeRef = 0; Clear(); }
ExampleSelection() { Clear(); }
void Clear() { Storage.Clear(); SelectionSize = 0; }
bool GetSelected(int n) const { return Storage.GetInt((ImGuiID)n, 0) != 0; }
void SetSelected(int n, bool v) { int* p_int = Storage.GetIntRef((ImGuiID)n, 0); if (*p_int == (int)v) return; if (v) SelectionSize++; else SelectionSize--; *p_int = (bool)v; }
@ -2806,6 +2803,17 @@ struct ExampleSelection
if (ms_io->RequestSetRange) { SetRange((int)(intptr_t)ms_io->RangeSrcItem, (int)(intptr_t)ms_io->RangeDstItem, ms_io->RangeSelected ? 1 : 0); }
}
void DebugTooltip()
{
if (ImGui::BeginTooltip())
{
for (auto& pair : Storage.Data)
if (pair.val_i)
ImGui::Text("0x%03X (%d)", pair.key, pair.key);
ImGui::EndTooltip();
}
}
// Call after BeginMultiSelect()
// We cannot provide this logic in core Dear ImGui because we don't have access to selection data.
// Essentially this would be a ms_io->RequestNextFocusBeforeDeletion
@ -2813,13 +2821,17 @@ struct ExampleSelection
template<typename ITEM_TYPE>
int CalcNextFocusIdxForBeforeDeletion(ImGuiMultiSelectIO* ms_io, ImVector<ITEM_TYPE>& items)
{
// FIXME-MULTISELECT: Need to avoid auto-select, aka SetKeyboardFocusHere() into public facing FocusItem() that doesn't activate.
if (!GetSelected((int)(intptr_t)ms_io->NavIdItem))
return (int)(intptr_t)ms_io->NavIdItem;
// Return first unselected item after RangeSrcItem
for (int n = (int)(intptr_t)ms_io->RangeSrcItem + 1; n < items.Size; n++)
if (!GetSelected(n))
return n;
// Otherwise return last unselected item
for (int n = (int)(intptr_t)ms_io->RangeSrcItem - 1; n >= 0; n--)
for (int n = IM_MIN((int)(intptr_t)ms_io->RangeSrcItem, items.Size) - 1; n >= 0; n--)
if (!GetSelected(n))
return n;
return -1;
@ -2833,7 +2845,7 @@ struct ExampleSelection
// This does two things:
// - (1) Update Items List (delete items from it)
// - (2) Convert the new focus index from old selection index (before deletion) to new selection index (after selection), and select it.
// FIXME: (2.3) if NavId is not selected, stay on same item -> facilitate persisting focus if ID change? (if ID is index-based) -> by setting focus again
// If NavId was not selected, next_focus_idx_in_old_selection == -1 and we stay on same item.
// You are expected to handle both of those in user-space because Dear ImGui rightfully doesn't own items data nor selection data.
// This particular ExampleSelection case is designed to showcase maintaining selection-state separated from items-data.
IM_UNUSED(ms_io);
@ -2850,6 +2862,7 @@ struct ExampleSelection
items.swap(new_items);
// Update selection
//IMGUI_DEBUG_LOG("ApplyDeletion(): next_focus_idx_in_new_selection = %d\n", next_focus_idx_in_new_selection);
Clear();
if (next_focus_idx_in_new_selection != -1)
SetSelected(next_focus_idx_in_new_selection, true);
@ -2922,11 +2935,10 @@ static void ShowDemoWindowMultiSelect()
// The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling region).
const int ITEMS_COUNT = 50;
ImGui::Text("Selection size: %d", selection.GetSize());
ImGui::Text("RangeRef: %d", selection.RangeRef);
if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20)))
{
ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape;
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef));
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags);
selection.ApplyRequests(ms_io, ITEMS_COUNT);
for (int n = 0; n < ITEMS_COUNT; n++)
@ -2940,7 +2952,6 @@ static void ShowDemoWindowMultiSelect()
// Apply multi-select requests
ms_io = ImGui::EndMultiSelect();
selection.RangeRef = (int)(intptr_t)ms_io->RangeSrcItem;
selection.ApplyRequests(ms_io, ITEMS_COUNT);
ImGui::EndListBox();
@ -2960,20 +2971,18 @@ static void ShowDemoWindowMultiSelect()
ImGui::Text("Adding features:");
ImGui::BulletText("Dynamic list with Delete key support.");
ImGui::Text("Selection size: %d/%d", selection.GetSize(), items.Size);
if (ImGui::IsItemHovered() && selection.GetSize() > 0)
selection.DebugTooltip();
// Initialize default list with 50 items + button to add more.
static int items_next_id = 0;
if (items_next_id == 0)
for (int n = 0; n < 50; n++)
items.push_back(items_next_id++);
ImGui::Text("Selection: %d/%d", selection.GetSize(), items.Size);
if (ImGui::SmallButton("Add 20 items")) { for (int n = 0; n < 20; n++) { items.push_back(items_next_id++); } }
ImGui::SameLine();
if (ImGui::SmallButton("Add 20 items"))
for (int n = 0; n < 20; n++)
items.push_back(items_next_id++);
ImGui::Text("Selection size: %d/%d", selection.GetSize(), items.Size);
ImGui::Text("RangeRef: %d", selection.RangeRef);
if (ImGui::SmallButton("Remove 20 items")) { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.SetSelected(items.Size - 1, false); items.pop_back(); } } // This is to test
// Extra to support deletion: Submit scrolling range to avoid glitches on deletion
const float items_height = ImGui::GetTextLineHeightWithSpacing();
@ -2981,7 +2990,7 @@ static void ShowDemoWindowMultiSelect()
if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20)))
{
ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape;
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef));
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags);
selection.ApplyRequests(ms_io, items.Size);
// FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal.
@ -2989,6 +2998,7 @@ static void ShowDemoWindowMultiSelect()
// FIXME-MULTISELECT: Test with intermediary modal dialog.
const bool want_delete = (selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete);
const int next_focus_item_idx = want_delete ? selection.CalcNextFocusIdxForBeforeDeletion(ms_io, items) : -1;
//if (want_delete) { IMGUI_DEBUG_LOG("next_focus_item_idx = %d\n", next_focus_item_idx); }
for (int n = 0; n < items.Size; n++)
{
@ -2997,22 +3007,44 @@ static void ShowDemoWindowMultiSelect()
sprintf(label, "Object %05d: %s", item_id, random_names[item_id % IM_ARRAYSIZE(random_names)]);
bool item_is_selected = selection.GetSelected(n);
ImGui::SetNextItemSelectionData((void*)(intptr_t)n);
ImGui::SetNextItemSelectionUserData(n);
ImGui::Selectable(label, item_is_selected);
if (ImGui::IsItemToggledSelection())
selection.SetSelected(n, !item_is_selected);
// FIXME-MULTISELECT: turn into a ms_io->RequestFocusIdx
if (next_focus_item_idx == n)
ImGui::SetKeyboardFocusHere(-1);
ImGui::SetKeyboardFocusHere(-1); // FIXME-MULTISELECT: Need to avoid selection.
}
// Apply multi-select requests
#if 0
bool nav_id_was_selected = selection.GetSelected((int)(intptr_t)ms_io->NavIdData);
if (want_delete && !nav_id_was_selected) // FIXME: would work without '&& !nav_id_was_selected' just take an extra frame to recover RangeSrc
ms_io->RangeSrcReset = true;
ms_io = ImGui::EndMultiSelect();
selection.RangeRef = (int)(intptr_t)ms_io->RangeSrcItem;
selection.ApplyRequests(ms_io, items.Size);
if (want_delete)
selection.ApplyDeletion(ms_io, items, next_focus_item_idx);
selection.ApplyDeletion(ms_io, items, nav_id_was_selected ? next_focus_item_idx : -1);
#else
// Apply multi-select requests
if (want_delete)
{
// When deleting: this handle details for scrolling/focus/selection to be updated correctly without any glitches.
bool nav_id_was_selected = selection.GetSelected((int)(intptr_t)ms_io->NavIdItem);
if (!nav_id_was_selected) // FIXME: would work without '&& !nav_id_was_selected' just take an extra frame to recover RangeSrc
ms_io->RangeSrcReset = true;
ms_io = ImGui::EndMultiSelect();
selection.ApplyRequests(ms_io, items.Size);
selection.ApplyDeletion(ms_io, items, nav_id_was_selected ? next_focus_item_idx : -1);
}
else
{
// Simple version
ms_io = ImGui::EndMultiSelect();
selection.ApplyRequests(ms_io, items.Size);
}
#endif
ImGui::EndListBox();
}
@ -3035,7 +3067,7 @@ static void ShowDemoWindowMultiSelect()
ImGui::PushID(selection_scope_n);
ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; // | ImGuiMultiSelectFlags_ClearOnClickRectVoid
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection->RangeRef, selection->GetSelected(selection->RangeRef));
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags);
selection->ApplyRequests(ms_io, ITEMS_COUNT);
for (int n = 0; n < ITEMS_COUNT; n++)
@ -3049,7 +3081,6 @@ static void ShowDemoWindowMultiSelect()
// Apply multi-select requests
ms_io = ImGui::EndMultiSelect();
selection->RangeRef = (int)(intptr_t)ms_io->RangeSrcItem;
selection->ApplyRequests(ms_io, ITEMS_COUNT);
ImGui::PopID();
}
@ -3105,7 +3136,7 @@ static void ShowDemoWindowMultiSelect()
if (widget_type == WidgetType_TreeNode)
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f));
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef));
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags);
selection.ApplyRequests(ms_io, items.Size);
// FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal.
@ -3113,6 +3144,7 @@ static void ShowDemoWindowMultiSelect()
// FIXME-MULTISELECT: Test with intermediary modal dialog.
const bool want_delete = (selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete);
const int next_focus_item_idx = want_delete ? selection.CalcNextFocusIdxForBeforeDeletion(ms_io, items) : -1;
//if (want_delete) { IMGUI_DEBUG_LOG("next_focus_item_idx = %d\n", next_focus_item_idx); }
if (show_in_table)
{
@ -3126,14 +3158,18 @@ static void ShowDemoWindowMultiSelect()
ImGuiListClipper clipper;
if (use_clipper)
{
clipper.Begin(items.Size);
if (next_focus_item_idx != -1)
clipper.IncludeItemByIndex(next_focus_item_idx); // Ensure item to focus is not clipped
}
while (!use_clipper || clipper.Step())
{
// IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeSrcItem was passed over.
if (use_clipper)
if ((int)(intptr_t)ms_io->RangeSrcItem <= clipper.DisplayStart)
ms_io->RangeSrcPassedBy = true;
// IF clipping is used: you need to set 'RangeSrcPassedBy = true' if RangeSrc was passed over.
// If you submit all items this is unnecessary as this is one by SetNextItemSelectionUserData()
if (use_clipper && clipper.DisplayStart > (int)(intptr_t)ms_io->RangeSrcItem)
ms_io->RangeSrcPassedBy = true;
const int item_begin = use_clipper ? clipper.DisplayStart : 0;
const int item_end = use_clipper ? clipper.DisplayEnd : items.Size;
@ -3217,6 +3253,12 @@ static void ShowDemoWindowMultiSelect()
break;
}
// If clipping is used: you need to set 'RangeSrcPassedBy = true' if RangeSrc was passed over.
// If you submit all items this is unnecessary as this is one by SetNextItemSelectionUserData()
// Here we essentially notify before EndMultiSelect() that RangeSrc is still present in our data set.
if (use_clipper && items.Size > (int)(intptr_t)ms_io->RangeSrcItem)
ms_io->RangeSrcPassedBy = true;
if (show_in_table)
{
ImGui::EndTable();
@ -3225,11 +3267,21 @@ static void ShowDemoWindowMultiSelect()
}
// Apply multi-select requests
#if 1
// full correct
bool nav_id_was_selected = selection.GetSelected((int)(intptr_t)ms_io->NavIdItem);
if (want_delete && !nav_id_was_selected)
ms_io->RangeSrcReset = true;
ms_io = ImGui::EndMultiSelect();
selection.RangeRef = (int)(intptr_t)ms_io->RangeSrcItem;
selection.ApplyRequests(ms_io, items.Size);
if (want_delete)
selection.ApplyDeletion(ms_io, items, next_focus_item_idx);
selection.ApplyDeletion(ms_io, items, nav_id_was_selected ? next_focus_item_idx : -1);
#else
ms_io = ImGui::EndMultiSelect();
selection.ApplyRequests(ms_io, items.Size);
if (want_delete)
selection.ApplyDeletion(ms_io, items, nav_id_was_selected ? next_focus_item_idx : -1);
#endif
if (widget_type == WidgetType_TreeNode)
ImGui::PopStyleVar();

View File

@ -134,6 +134,7 @@ struct ImGuiInputTextDeactivateData;// Short term storage to backup text of a de
struct ImGuiLastItemData; // Status storage for last submitted items
struct ImGuiLocEntry; // A localization entry.
struct ImGuiMenuColumns; // Simple column measurement, currently used for MenuItem() only
struct ImGuiMultiSelectState; // Multi-selection persistent state (for focused selection).
struct ImGuiMultiSelectTempData; // Multi-selection temporary state (while traversing).
struct ImGuiNavItemData; // Result of a gamepad/keyboard directional navigation move query result
struct ImGuiMetricsConfig; // Storage for ShowMetricsWindow() and DebugNodeXXX() functions
@ -1716,19 +1717,34 @@ struct ImGuiOldColumns
// Temporary storage for multi-select
struct IMGUI_API ImGuiMultiSelectTempData
{
ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually)
ImGuiMultiSelectState* Storage;
ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually)
ImGuiMultiSelectFlags Flags;
ImGuiKeyChord KeyMods;
ImGuiWindow* Window;
ImGuiMultiSelectIO BeginIO; // Requests are set and returned by BeginMultiSelect(), written to by user during the loop.
ImGuiMultiSelectIO EndIO; // Requests are set during the loop and returned by EndMultiSelect().
bool IsFocused; // Set if currently focusing the selection scope (any item of the selection). May be used if you have custom shortcut associated to selection.
bool IsSetRange; // Set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation.
bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set.
//ImRect Rect; // Extent of selection scope between BeginMultiSelect() / EndMultiSelect(), used by ImGuiMultiSelectFlags_ClearOnClickRectVoid.
ImGuiMultiSelectIO BeginIO; // Requests are set and returned by BeginMultiSelect(), written to by user during the loop.
ImGuiMultiSelectIO EndIO; // Requests are set during the loop and returned by EndMultiSelect().
bool IsFocused; // Set if currently focusing the selection scope (any item of the selection). May be used if you have custom shortcut associated to selection.
bool IsSetRange; // Set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation.
bool NavIdPassedBy;
bool RangeDstPassedBy; // Set by the the item that matches NavJustMovedToId when IsSetRange is set.
//ImRect Rect; // Extent of selection scope between BeginMultiSelect() / EndMultiSelect(), used by ImGuiMultiSelectFlags_ClearOnClickRectVoid.
ImGuiMultiSelectTempData() { Clear(); }
void Clear() { memset(this, 0, sizeof(*this)); }
void Clear() { memset(this, 0, sizeof(*this)); BeginIO.RangeSrcItem = EndIO.RangeSrcItem = BeginIO.RangeDstItem = EndIO.RangeDstItem = BeginIO.NavIdItem = EndIO.NavIdItem = (void*)-1; }
};
// Persistent storage for multi-select (as long as selection is alive)
struct IMGUI_API ImGuiMultiSelectState
{
ImGuiWindow* Window;
ImGuiID ID;
int LastFrameActive; // Last used frame-count, for GC.
ImS8 RangeSelected; // -1 (don't have) or true/false
void* RangeSrcItem; //
void* NavIdItem; // SetNextItemSelectionUserData() value for NavId (if part of submitted items)
ImGuiMultiSelectState() { Init(0); }
void Init(ImGuiID id) { Window = NULL; ID = id; LastFrameActive = 0; RangeSelected = -1; RangeSrcItem = NavIdItem = (void*)-1; }
};
#endif // #ifdef IMGUI_HAS_MULTI_SELECT
@ -2170,6 +2186,7 @@ struct ImGuiContext
// Multi-Select state
ImGuiMultiSelectTempData* CurrentMultiSelect; // FIXME-MULTISELECT: We currently don't support recursing/stacking multi-select
ImGuiMultiSelectTempData MultiSelectTempData[1];
ImPool<ImGuiMultiSelectState> MultiSelectStorage;
// Hover Delay system
ImGuiID HoverItemDelayId;
@ -3568,6 +3585,7 @@ namespace ImGui
IMGUI_API void DebugNodeTableSettings(ImGuiTableSettings* settings);
IMGUI_API void DebugNodeInputTextState(ImGuiInputTextState* state);
IMGUI_API void DebugNodeTypingSelectState(ImGuiTypingSelectState* state);
IMGUI_API void DebugNodeMultiSelectState(ImGuiMultiSelectState* state);
IMGUI_API void DebugNodeWindow(ImGuiWindow* window, const char* label);
IMGUI_API void DebugNodeWindowSettings(ImGuiWindowSettings* settings);
IMGUI_API void DebugNodeWindowsList(ImVector<ImGuiWindow*>* windows, const char* label);

View File

@ -7132,7 +7132,7 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe
}
// Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect().
ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected)
ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags)
{
ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow;
@ -7141,21 +7141,38 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* r
g.CurrentMultiSelect = ms;
// FIXME: BeginFocusScope()
const ImGuiID id = window->IDStack.back();
ms->Clear();
ms->FocusScopeId = window->IDStack.back();
ms->FocusScopeId = id;
ms->Flags = flags;
ms->Window = window;
ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId);
PushFocusScope(ms->FocusScopeId);
// Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame.
ms->KeyMods = g.NavJustMovedToId ? g.NavJustMovedToKeyMods : g.IO.KeyMods;
// Bind storage
ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id);
storage->ID = id;
storage->LastFrameActive = g.FrameCount;
storage->Window = window;
ms->Storage = storage;
// FIXME-MULTISELECT: Set for the purpose of user calling RangeSrcPassedBy
// FIXME-MULTISELECT: Index vs Pointers.
ms->BeginIO.RangeSrcItem = storage->RangeSrcItem;
ms->BeginIO.NavIdItem = storage->NavIdItem;
if (!ms->IsFocused)
return &ms->BeginIO; // This is cleared at this point.
/*
if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0)
{
ms->BeginIO.RangeSrcItem = ms->EndIO.RangeSrcItem = range_ref;
ms->BeginIO.RangeSelected = ms->EndIO.RangeSelected = range_ref_is_selected;
}
*/
// Auto clear when using Navigation to move within the selection
// (we compare FocusScopeId so it possible to use multiple selections inside a same window)
@ -7163,19 +7180,21 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* r
{
if (ms->KeyMods & ImGuiMod_Shift)
ms->IsSetRange = true;
if (ms->IsSetRange)
IM_ASSERT(storage->RangeSrcItem != (void*)-1); // Not ready -> could clear?
if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0)
ms->BeginIO.RequestClear = true;
}
// Shortcut: Select all (CTRL+A)
if (ms->IsFocused && !(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
if (!(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A))
ms->BeginIO.RequestSelectAll = true;
// Shortcut: Clear selection (Escape)
// FIXME-MULTISELECT: Only hog shortcut if selection is not null, meaning we need "has selection or "selection size" data here.
// Otherwise may be done by caller but it means Shortcut() needs to be exposed.
if (ms->IsFocused && (flags & ImGuiMultiSelectFlags_ClearOnEscape))
if (flags & ImGuiMultiSelectFlags_ClearOnEscape)
if (Shortcut(ImGuiKey_Escape))
ms->BeginIO.RequestClear = true;
@ -7191,7 +7210,21 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect()
ImGuiContext& g = *GImGui;
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
IM_ASSERT(ms->FocusScopeId == g.CurrentFocusScopeId);
IM_ASSERT(g.CurrentMultiSelect != NULL && g.CurrentMultiSelect->Window == g.CurrentWindow);
IM_ASSERT(g.CurrentMultiSelect != NULL && ms->Storage->Window == g.CurrentWindow);
if (ms->IsFocused)
{
if (ms->BeginIO.RangeSrcReset || (ms->BeginIO.RangeSrcPassedBy == false && ms->BeginIO.RangeSrcItem != (void*)-1))
{
IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrc.\n"); // Will set be to NavId.
ms->Storage->RangeSrcItem = (void*)-1;
}
if (ms->NavIdPassedBy == false && ms->Storage->NavIdItem != (void*)-1)
{
IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdData.\n");
ms->Storage->NavIdItem = (void*)-1;
}
}
// Clear selection when clicking void?
// We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
@ -7205,7 +7238,6 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect()
// Unwind
ms->FocusScopeId = 0;
ms->Window = NULL;
ms->Flags = ImGuiMultiSelectFlags_None;
ms->BeginIO.Clear(); // Invalidate contents of BeginMultiSelect() to enforce scope.
PopFocusScope();
@ -7222,28 +7254,31 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d
// Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
// This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
ImGuiContext& g = *GImGui;
if (g.MultiSelectState.FocusScopeId != 0)
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
else
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
g.NextItemData.SelectionUserData = selection_user_data;
g.NextItemData.FocusScopeId = g.CurrentFocusScopeId;
// Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect)
{
// Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
if (ms->BeginIO.RangeSrcItem == (void*)selection_user_data)
ms->BeginIO.RangeSrcPassedBy = true;
}
else
{
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
}
}
void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected)
{
ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow;
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
if (!ms->IsFocused)
return;
ImGuiMultiSelectState* storage = ms->Storage;
IM_UNUSED(window);
IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
IM_ASSERT((g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid) && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
void* item_data = (void*)g.NextItemData.SelectionUserData;
// Apply Clear/SelectAll requests requested by BeginMultiSelect().
@ -7260,12 +7295,22 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected)
if (ms->IsSetRange)
{
IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0);
const bool is_range_src = (ms->BeginIO.RangeSrcItem == item_data);
const bool is_range_dst = !ms->RangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
if (is_range_dst)
{
ms->RangeDstPassedBy = true;
if (storage->RangeSrcItem == (void*)-1) // If we don't have RangeSrc, assign RangeSrc = RangeDst
{
storage->RangeSrcItem = item_data;
storage->RangeSelected = selected ? 1 : 0;
}
}
const bool is_range_src = storage->RangeSrcItem == item_data;
if (is_range_src || is_range_dst || ms->BeginIO.RangeSrcPassedBy != ms->RangeDstPassedBy)
selected = ms->BeginIO.RangeSelected;
{
IM_ASSERT(storage->RangeSrcItem != (void*)-1 && storage->RangeSelected != -1);
selected = (storage->RangeSelected != 0);
}
else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0)
selected = false;
}
@ -7277,16 +7322,36 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
{
ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow;
bool selected = *p_selected;
bool pressed = *p_pressed;
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
ImGuiMultiSelectState* storage = ms->Storage;
if (pressed)
{
ms->IsFocused = true;
//if (storage->Id != ms->FocusScopeId)
// storage->Init(ms->FocusScopeId);
}
if (!ms->IsFocused)
return;
void* item_data = (void*)g.NextItemData.SelectionUserData;
const bool is_multiselect = (ms->Flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0;
bool selected = *p_selected;
bool pressed = *p_pressed;
bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0;
bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0;
if (g.NavId == id)
storage->NavIdItem = item_data;
if (g.NavId == id && storage->RangeSrcItem == (void*)-1)
{
storage->RangeSrcItem = item_data;
storage->RangeSelected = selected; // Will be updated at the end of this function anyway.
}
if (storage->NavIdItem == item_data)
ms->NavIdPassedBy = true;
// Auto-select as you navigate a list
if (g.NavJustMovedToId == id)
{
@ -7343,15 +7408,16 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
{
// Shift+Arrow always select
// Ctrl+Shift+Arrow copy source selection state (alrady stored by BeginMultiSelect() in RangeSelected)
if (!is_ctrl)
ms->EndIO.RangeSelected = true;
//IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue);
ms->EndIO.RangeSrcItem = (storage->RangeSrcItem != (void*)-1) ? storage->RangeSrcItem : item_data;
ms->EndIO.RangeSelected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
ms->EndIO.RangeDirection = ms->BeginIO.RangeSrcPassedBy ? +1 : -1;
}
else
{
// Ctrl inverts selection, otherwise always select
selected = is_ctrl ? !selected : true;
ms->EndIO.RangeSrcItem = item_data;
ms->EndIO.RangeSrcItem = storage->RangeSrcItem = item_data;
ms->EndIO.RangeSelected = selected;
ms->EndIO.RangeDirection = +1;
}
@ -7371,13 +7437,37 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
}
// Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect)
if (storage->RangeSrcItem == item_data)
storage->RangeSelected = selected ? 1 : 0;
if (ms->EndIO.RangeSrcItem == item_data && is_ctrl && is_shift && is_multiselect)
{
if (ms->EndIO.RequestSetRange)
IM_ASSERT(storage->RangeSrcItem == ms->EndIO.RangeSrcItem);
ms->EndIO.RangeSelected = selected;
}
*p_selected = selected;
*p_pressed = pressed;
}
void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
{
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here.
if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); }
bool open = TreeNode((void*)(intptr_t)storage->ID, "MultiSelect 0x%08X%s", storage->ID, is_active ? "" : " *Inactive*");
if (!is_active) { PopStyleColor(); }
if (!open)
return;
Text("ID = 0x%08X", storage->ID);
Text("RangeSrcItem = %p, RangeSelected = %d", storage->RangeSrcItem, storage->RangeSelected);
Text("NavIdItem = %p", storage->NavIdItem);
TreePop();
#else
IM_UNUSED(storage);
#endif
}
//-------------------------------------------------------------------------
// [SECTION] Widgets: ListBox
//-------------------------------------------------------------------------