diff --git a/imgui.cpp b/imgui.cpp index 8f34acf92..26f0f36bc 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -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")) diff --git a/imgui.h b/imgui.h index d00adb1f7..cb4e27832 100644 --- a/imgui.h +++ b/imgui.h @@ -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)); } diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 7a1664473..8681ae47d 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -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 int CalcNextFocusIdxForBeforeDeletion(ImGuiMultiSelectIO* ms_io, ImVector& 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(); diff --git a/imgui_internal.h b/imgui_internal.h index 2e1a0bee3..eaeca4ece 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -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 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* windows, const char* label); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 350a9e86c..126727c23 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -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 //-------------------------------------------------------------------------