From 554db6bc0f3b8d8ebdcd596d95eaaa364f51e575 Mon Sep 17 00:00:00 2001 From: omar Date: Fri, 1 Feb 2019 12:22:57 +0100 Subject: [PATCH 001/132] MultiSelect: WIP range-select (#1861) (rebased six millions times) --- imgui.cpp | 4 + imgui.h | 82 ++++++++++++ imgui_demo.cpp | 155 ++++++++++++++++++----- imgui_internal.h | 36 ++++-- imgui_widgets.cpp | 315 ++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 533 insertions(+), 59 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 733a5dfe1287..b1e99035387e 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1273,6 +1273,7 @@ ImGuiStyle::ImGuiStyle() TableAngledHeadersTextAlign = ImVec2(0.5f,0.0f);// Alignment of angled headers within the cell ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. + SelectableSpacing = ImVec2(0.0f,0.0f);// Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing). SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. SeparatorTextBorderSize = 3.0f; // Thickkness of border in SeparatorText() SeparatorTextAlign = ImVec2(0.0f,0.5f);// Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). @@ -1321,6 +1322,7 @@ void ImGuiStyle::ScaleAllSizes(float scale_factor) LogSliderDeadzone = ImTrunc(LogSliderDeadzone * scale_factor); TabRounding = ImTrunc(TabRounding * scale_factor); TabMinWidthForCloseButton = (TabMinWidthForCloseButton != FLT_MAX) ? ImTrunc(TabMinWidthForCloseButton * scale_factor) : FLT_MAX; + SelectableSpacing = ImTrunc(SelectableSpacing * scale_factor); SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor); DisplayWindowPadding = ImTrunc(DisplayWindowPadding * scale_factor); DisplaySafeAreaPadding = ImTrunc(DisplaySafeAreaPadding * scale_factor); @@ -3257,6 +3259,7 @@ static const ImGuiDataVarInfo GStyleVarInfo[] = { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersAngle)}, // ImGuiStyleVar_TableAngledHeadersAngle { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign + { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SelectableSpacing) }, // ImGuiStyleVar_SelectableSpacing { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, SeparatorTextBorderSize)}, // ImGuiStyleVar_SeparatorTextBorderSize { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextAlign) }, // ImGuiStyleVar_SeparatorTextAlign @@ -3822,6 +3825,7 @@ void ImGui::Shutdown() g.MenusIdSubmittedThisFrame.clear(); g.InputTextState.ClearFreeMemory(); g.InputTextDeactivatedState.ClearFreeMemory(); + g.MultiSelectScopeWindow = NULL; g.SettingsWindows.clear(); g.SettingsHandlers.clear(); diff --git a/imgui.h b/imgui.h index 26965fd86ac4..80e6ca7e1294 100644 --- a/imgui.h +++ b/imgui.h @@ -44,6 +44,7 @@ Index of this file: // [SECTION] ImGuiIO // [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) // [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectData) // [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) // [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) // [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) @@ -174,6 +175,7 @@ struct ImGuiIO; // Main configuration and I/O between your a struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use) struct ImGuiKeyData; // Storage for ImGuiIO and IsKeyDown(), IsKeyPressed() etc functions. struct ImGuiListClipper; // Helper to manually clip large list of items +struct ImGuiMultiSelectData; // State for a BeginMultiSelect() block struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame struct ImGuiPayload; // User data payload for drag and drop operations struct ImGuiPlatformImeData; // Platform IME data for io.PlatformSetImeDataFn() function. @@ -227,6 +229,7 @@ typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: f typedef int ImGuiItemFlags; // -> enum ImGuiItemFlags_ // Flags: for PushItemFlag(), shared by all items typedef int ImGuiKeyChord; // -> ImGuiKey | ImGuiMod_XXX // Flags: for IsKeyChordPressed(), Shortcut() etc. an ImGuiKey optionally OR-ed with one or more ImGuiMod_XXX values. typedef int ImGuiPopupFlags; // -> enum ImGuiPopupFlags_ // Flags: for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() +typedef int ImGuiMultiSelectFlags; // -> enum ImGuiMultiSelectFlags_// Flags: for BeginMultiSelect() typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable() typedef int ImGuiSliderFlags; // -> enum ImGuiSliderFlags_ // Flags: for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar() @@ -262,6 +265,10 @@ typedef ImWchar32 ImWchar; typedef ImWchar16 ImWchar; #endif +// Multi-Selection item index or identifier when using SetNextItemSelectionUserData()/BeginMultiSelect() +// (Most users are likely to use this store an item INDEX but this may be used to store a POINTER as well.) +typedef ImS64 ImGuiSelectionUserData; + // Callback and functions types typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData* data); // Callback function for ImGui::InputText() typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); // Callback function for ImGui::SetNextWindowSizeConstraints() @@ -661,6 +668,14 @@ namespace ImGui IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height 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 ImGuiMultiSelectData for details. + // When enabled, Selectable() and TreeNode() functions will return true when selection needs toggling. + IMGUI_API ImGuiMultiSelectData* BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected); + IMGUI_API ImGuiMultiSelectData* EndMultiSelect(); + IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); + // Widgets: List Boxes // - This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. // - You can submit contents and manage your selection state however you want it, by creating e.g. Selectable() or any other items. @@ -893,6 +908,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 IsAnyItemHovered(); // is any item hovered? IMGUI_API bool IsAnyItemActive(); // is any item active? IMGUI_API bool IsAnyItemFocused(); // is any item focused? @@ -1683,6 +1699,7 @@ enum ImGuiStyleVar_ ImGuiStyleVar_TableAngledHeadersAngle, // float TableAngledHeadersAngle ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2 TableAngledHeadersTextAlign ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_SelectableSpacing, // ImVec2 SelectableSpacing ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign ImGuiStyleVar_SeparatorTextBorderSize, // float SeparatorTextBorderSize ImGuiStyleVar_SeparatorTextAlign, // ImVec2 SeparatorTextAlign @@ -2127,6 +2144,7 @@ struct ImGuiStyle ImVec2 TableAngledHeadersTextAlign;// Alignment of angled headers within the cell ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). + ImVec2 SelectableSpacing; // Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing). ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. float SeparatorTextBorderSize; // Thickkness of border in SeparatorText() ImVec2 SeparatorTextAlign; // Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). @@ -2702,6 +2720,70 @@ struct ImColor static ImColor HSV(float h, float s, float v, float a = 1.0f) { float r, g, b; ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b); return ImColor(r, g, b, a); } }; +//----------------------------------------------------------------------------- +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectData) +//----------------------------------------------------------------------------- + +// Flags for BeginMultiSelect(). +// This system is designed to allow mouse/keyboard multi-selection, including support for range-selection (SHIFT + click) 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. +enum ImGuiMultiSelectFlags_ +{ + ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0, + ImGuiMultiSelectFlags_NoUnselect = 1 << 1, // Disable unselecting items with CTRL+Click, CTRL+Space etc. + ImGuiMultiSelectFlags_NoSelectAll = 1 << 2, // Disable CTRL+A shortcut to set RequestSelectAll +}; + +// Abstract: +// - This system implements standard multi-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. 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, you can handle a simpler form of multi-selection yourself, +// by reacting to click/presses on Selectable() items and checking keyboard modifiers. +// The complexity of this system here is mostly caused by the handling of range-select while optionally allowing to clip elements. +// - The work involved to deal with multi-selection differs whether you want to only submit visible items (and clip others) or submit all items +// regardless of their visibility. Clipping items is more efficient and will allow you to deal with large lists (1k~100k items) with near zero +// performance penalty, but requires a little more work on the code. If you only have a few hundreds elements in your possible selection set, +// you may as well not bother with clipping, as the cost should be negligible (as least on imgui side). +// If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards. +// - The void* Src/Dst value represent a selectable object. They are the values you pass to SetNextItemMultiSelectData(). +// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points. But the code never assume that sortable integers are used. +// - In the spirit of imgui design, your code own the selection data. So this is designed to handle all kind of selection data: instructive (store a bool inside each object), +// external array (store an array aside from your objects), set (store only selected items in a hash/map/set), using intervals (store indices in an interval tree), etc. +// Usage flow: +// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc and its selection status. As a default value 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 requests by updating your selection data. [Only required if you are using a clipper in step 4] +// 3) Set RangeSrcPassedBy=true if the RangeSrc 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->RangeSrc) { data->RangeSrcPassedBy = true; } +// 4) Submit your items with SetNextItemMultiSelectData() + Selectable()/TreeNode() calls. +// Call IsItemSelectionToggled() to query with the selection state has been toggled, in which you need the info immediately (before EndMultiSelect()) for your display. +// When cannot reliably return a "IsItemSelected()" value because we need to consider clipped (unprocessed) item, this is why we return a toggle event instead. +// 5) Call EndMultiSelect(). Save the value of ->RangeSrc 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. +struct ImGuiMultiSelectData +{ + bool RequestClear; // Begin, End // Request user to clear selection + bool RequestSelectAll; // Begin, End // Request user to select all + bool RequestSetRange; // End // Request user to set or clear selection in the [RangeSrc..RangeDst] range + bool RangeSrcPassedBy; // After Begin // Need to be set by user is RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. + bool RangeValue; // End // End: parameter from RequestSetRange request. True = Select Range, False = Unselect range. + void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect() + void* RangeDst; // End // End: parameter from RequestSetRange request. + int RangeDirection; // End // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -1 otherwise. Available as an indicator in case you cannot infer order from the void* values. + + ImGuiMultiSelectData() { Clear(); } + void Clear() + { + RequestClear = RequestSelectAll = RequestSetRange = RangeSrcPassedBy = RangeValue = false; + RangeSrc = RangeDst = NULL; + RangeDirection = 0; + } +}; + //----------------------------------------------------------------------------- // [SECTION] Drawing API (ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) // Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index dd253454f797..8e7312602be5 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -72,6 +72,7 @@ Index of this file: // [SECTION] Demo Window / ShowDemoWindow() // - ShowDemoWindow() // - sub section: ShowDemoWindowWidgets() +// - sub section: ShowDemoWindowMultiSelect() // - sub section: ShowDemoWindowLayout() // - sub section: ShowDemoWindowPopups() // - sub section: ShowDemoWindowTables() @@ -214,6 +215,7 @@ static void ShowExampleMenuFile(); // We split the contents of the big ShowDemoWindow() function into smaller functions // (because the link time of very large functions grow non-linearly) static void ShowDemoWindowWidgets(); +static void ShowDemoWindowMultiSelect(); static void ShowDemoWindowLayout(); static void ShowDemoWindowPopups(); static void ShowDemoWindowTables(); @@ -251,6 +253,7 @@ void* GImGuiDemoMarkerCallbackUserData = NULL; //----------------------------------------------------------------------------- // - ShowDemoWindow() // - ShowDemoWindowWidgets() +// - ShowDemoWindowMultiSelect() // - ShowDemoWindowLayout() // - ShowDemoWindowPopups() // - ShowDemoWindowTables() @@ -1371,37 +1374,6 @@ static void ShowDemoWindowWidgets() ImGui::TreePop(); } - IMGUI_DEMO_MARKER("Widgets/Selectables/Single Selection"); - if (ImGui::TreeNode("Selection State: Single Selection")) - { - static int selected = -1; - for (int n = 0; n < 5; n++) - { - char buf[32]; - sprintf(buf, "Object %d", n); - if (ImGui::Selectable(buf, selected == n)) - selected = n; - } - ImGui::TreePop(); - } - IMGUI_DEMO_MARKER("Widgets/Selectables/Multiple Selection"); - if (ImGui::TreeNode("Selection State: Multiple Selection")) - { - HelpMarker("Hold CTRL and click to select multiple items."); - static bool selection[5] = { false, false, false, false, false }; - for (int n = 0; n < 5; n++) - { - char buf[32]; - sprintf(buf, "Object %d", n); - if (ImGui::Selectable(buf, selection[n])) - { - if (!ImGui::GetIO().KeyCtrl) // Clear selection when CTRL is not held - memset(selection, 0, sizeof(selection)); - selection[n] ^= 1; - } - } - ImGui::TreePop(); - } IMGUI_DEMO_MARKER("Widgets/Selectables/Rendering more items on the same line"); if (ImGui::TreeNode("Rendering more items on the same line")) { @@ -1461,6 +1433,15 @@ static void ShowDemoWindowWidgets() if (winning_state) ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f + 0.5f * cosf(time * 2.0f), 0.5f + 0.5f * sinf(time * 3.0f))); + static float spacing = 0.0f; + ImGui::PushItemWidth(100); + ImGui::SliderFloat("SelectableSpacing", &spacing, 0, 20, "%.0f"); + ImGui::SameLine(); HelpMarker("Selectable cancel out the regular spacing between items by extending itself by ItemSpacing/2 in each direction.\nThis has two purposes:\n- Avoid the gap between items so the mouse is always hitting something.\n- Avoid the gap between items so range-selected item looks connected.\nBy changing SelectableSpacing we can enforce spacing between selectables."); + ImGui::PopItemWidth(); + ImGui::Spacing(); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 8)); + ImGui::PushStyleVar(ImGuiStyleVar_SelectableSpacing, ImVec2(spacing, spacing)); + for (int y = 0; y < 4; y++) for (int x = 0; x < 4; x++) { @@ -1479,8 +1460,10 @@ static void ShowDemoWindowWidgets() ImGui::PopID(); } + ImGui::PopStyleVar(2); if (winning_state) ImGui::PopStyleVar(); + ImGui::TreePop(); } IMGUI_DEMO_MARKER("Widgets/Selectables/Alignment"); @@ -1509,6 +1492,8 @@ static void ShowDemoWindowWidgets() ImGui::TreePop(); } + ShowDemoWindowMultiSelect(); + // To wire InputText() with std::string or any other custom string type, // see the "Text Input > Resize Callback" section of this demo, and the misc/cpp/imgui_stdlib.h file. IMGUI_DEMO_MARKER("Widgets/Text Input"); @@ -2785,6 +2770,113 @@ static void ShowDemoWindowWidgets() } } +static void ShowDemoWindowMultiSelect() +{ + IMGUI_DEMO_MARKER("Widgets/Selection State"); + if (ImGui::TreeNode("Selection State")) + { + HelpMarker("Selections can be built under Selectable(), TreeNode() or other widgets. Selection state is owned by application code/data."); + + IMGUI_DEMO_MARKER("Widgets/Selection State/Single Selection"); + if (ImGui::TreeNode("Single Selection")) + { + static int selected = -1; + for (int n = 0; n < 5; n++) + { + char buf[32]; + sprintf(buf, "Object %d", n); + if (ImGui::Selectable(buf, selected == n)) + selected = n; + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (Basic)"); + if (ImGui::TreeNode("Multiple Selection (Basic)")) + { + HelpMarker("Hold CTRL and click to select multiple items."); + static bool selection[5] = { false, false, false, false, false }; + for (int n = 0; n < 5; n++) + { + char buf[32]; + sprintf(buf, "Object %d", n); + if (ImGui::Selectable(buf, selection[n])) + { + if (!ImGui::GetIO().KeyCtrl) // Clear selection when CTRL is not held + memset(selection, 0, sizeof(selection)); + selection[n] ^= 1; + } + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (Full)"); + if (ImGui::TreeNode("Multiple Selection (Full)")) + { + // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. + // In this demo we use ImGuiStorage (simple key->value storage) to avoid external dependencies but it's probably not optimal. + // In your real code you could use e.g std::unordered_set<> or your own data structure for storing selection. + // If you don't mind being limited to one view over your objects, the simplest way is to use an intrusive selection (e.g. store bool inside object, as used in examples above). + // Otherwise external set/hash/map/interval trees (storing indices, etc.) may be appropriate. + struct MySelection + { + ImGuiStorage Storage; + void Clear() { Storage.Clear(); } + void SelectAll(int count) { Storage.Data.reserve(count); Storage.Data.resize(0); for (int n = 0; n < count; n++) Storage.Data.push_back(ImGuiStoragePair((ImGuiID)n, 1)); } + void SetRange(int a, int b, int sel) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) Storage.SetInt((ImGuiID)n, sel); } + bool GetSelected(int id) const { return Storage.GetInt((ImGuiID)id) != 0; } + void SetSelected(int id, bool v) { SetRange(id, id, v ? 1 : 0); } + }; + + static int selection_ref = 0; // Selection pivot (last clicked item, we need to preserve this to handle range-select) + static MySelection selection; + const char* random_names[] = + { + "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", + "Bitter Gourd", "Bok Choy", "Broccoli", "Brussels Sprouts", "Burdock Root", "Cabbage", "Calabash", "Capers", "Carrot", "Cassava", + "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Celtuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" + }; + + int COUNT = 1000; + HelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range."); + ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", (unsigned int*)&ImGui::GetIO().ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); + + if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) + { + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(0, (void*)(intptr_t)selection_ref, selection.GetSelected((int)selection_ref)); + if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); } + ImGuiListClipper clipper; + clipper.Begin(COUNT); + while (clipper.Step()) + { + if (clipper.DisplayStart > (int)selection_ref) + multi_select_data->RangeSrcPassedBy = true; + for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) + { + ImGui::PushID(n); + char label[64]; + sprintf(label, "Object %05d (category: %s)", n, random_names[n % IM_ARRAYSIZE(random_names)]); + bool item_is_selected = selection.GetSelected(n); + ImGui::SetNextItemSelectionUserData(n); + if (ImGui::Selectable(label, item_is_selected)) + selection.SetSelected(n, !item_is_selected); + ImGui::PopID(); + } + } + multi_select_data = ImGui::EndMultiSelect(); + selection_ref = (int)(intptr_t)multi_select_data->RangeSrc; + ImGui::EndListBox(); + if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); } + if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } + } + ImGui::TreePop(); + } + ImGui::TreePop(); + } +} + static void ShowDemoWindowLayout() { IMGUI_DEMO_MARKER("Layout"); @@ -6771,6 +6863,7 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SliderFloat2("FramePadding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("ItemSpacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2("SelectableSpacing", (float*)&style.SelectableSpacing, 0.0f, 20.0f, "%.0f"); ImGui::SameLine(); HelpMarker("SelectableSpacing must be < ItemSpacing.\nSelectables display their highlight after canceling out the effect of ItemSpacing, so they can be look tightly packed. This setting allows to enforce spacing between them."); ImGui::SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, 0.0f, 10.0f, "%.0f"); ImGui::SliderFloat("IndentSpacing", &style.IndentSpacing, 0.0f, 30.0f, "%.0f"); ImGui::SliderFloat("ScrollbarSize", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); diff --git a/imgui_internal.h b/imgui_internal.h index 5d8f2332ccf5..63b80400ca36 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 state struct ImGuiNavItemData; // Result of a gamepad/keyboard directional navigation move query result struct ImGuiMetricsConfig; // Storage for ShowMetricsWindow() and DebugNodeXXX() functions struct ImGuiNextWindowData; // Storage for SetNextWindow** functions @@ -854,6 +855,7 @@ enum ImGuiItemFlagsPrivate_ // Controlled by widget code ImGuiItemFlags_Inputable = 1 << 20, // false // [WIP] Auto-activate input mode when tab focused. Currently only used and supported by a few items before it becomes a generic feature. ImGuiItemFlags_HasSelectionUserData = 1 << 21, // false // Set by SetNextItemSelectionUserData() + ImGuiItemFlags_IsMultiSelect = 1 << 22, // false // Set by SetNextItemSelectionUserData() ImGuiItemFlags_Default_ = ImGuiItemFlags_AutoClosePopups, // Please don't change, use PushItemFlag() instead. @@ -1201,10 +1203,6 @@ struct ImGuiNextWindowData inline void ClearFlags() { Flags = ImGuiNextWindowDataFlags_None; } }; -// Multi-Selection item index or identifier when using SetNextItemSelectionUserData()/BeginMultiSelect() -// (Most users are likely to use this store an item INDEX but this may be used to store a POINTER as well.) -typedef ImS64 ImGuiSelectionUserData; - enum ImGuiNextItemDataFlags_ { ImGuiNextItemDataFlags_None = 0, @@ -1711,8 +1709,20 @@ struct ImGuiOldColumns // We always assume that -1 is an invalid value (which works for indices and pointers) #define ImGuiSelectionUserData_Invalid ((ImGuiSelectionUserData)-1) +#define IMGUI_HAS_MULTI_SELECT 1 #ifdef IMGUI_HAS_MULTI_SELECT -// + +struct IMGUI_API ImGuiMultiSelectState +{ + ImGuiMultiSelectData In; // The In requests are set and returned by BeginMultiSelect() + ImGuiMultiSelectData Out; // The Out requests are finalized and returned by EndMultiSelect() + bool InRangeDstPassedBy; // (Internal) set by the the item that match NavJustMovedToId when InRequestRangeSetNav is set. + bool InRequestSetRangeNav; // (Internal) set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. + + ImGuiMultiSelectState() { Clear(); } + void Clear() { In.Clear(); Out.Clear(); InRangeDstPassedBy = InRequestSetRangeNav = false; } +}; + #endif // #ifdef IMGUI_HAS_MULTI_SELECT //----------------------------------------------------------------------------- @@ -2107,6 +2117,12 @@ struct ImGuiContext ImVec2 NavWindowingAccumDeltaPos; ImVec2 NavWindowingAccumDeltaSize; + // Range-Select/Multi-Select + ImGuiID MultiSelectScopeId; + ImGuiWindow* MultiSelectScopeWindow; + ImGuiMultiSelectFlags MultiSelectFlags; + ImGuiMultiSelectState MultiSelectState; + // Render float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list) @@ -2370,6 +2386,10 @@ struct ImGuiContext NavWindowingToggleLayer = false; NavWindowingToggleKey = ImGuiKey_None; + MultiSelectScopeId = 0; + MultiSelectScopeWindow = NULL; + MultiSelectFlags = 0; + DimBgRatio = 0.0f; DragDropActive = DragDropWithinSource = DragDropWithinTarget = false; @@ -3144,7 +3164,6 @@ namespace ImGui IMGUI_API ImVec2 CalcItemSize(ImVec2 size, float default_w, float default_h); IMGUI_API float CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x); IMGUI_API void PushMultiItemsWidths(int components, float width_full); - IMGUI_API bool IsItemToggledSelection(); // Was the last item selection toggled? (after Selectable(), TreeNode() etc. We only returns toggle _event_ in order to handle clipping correctly) IMGUI_API ImVec2 GetContentRegionMaxAbs(); IMGUI_API void ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess); @@ -3325,6 +3344,10 @@ namespace ImGui IMGUI_API int TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); IMGUI_API int TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data); + // Multi-Select/Range-Select API + IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected); + IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); + // Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API) IMGUI_API void SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect); IMGUI_API void BeginColumns(const char* str_id, int count, ImGuiOldColumnFlags flags = 0); // setup number of columns. use an identifier to distinguish multiple column sets. close with EndColumns(). @@ -3461,7 +3484,6 @@ namespace ImGui IMGUI_API bool DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags); IMGUI_API bool SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb); IMGUI_API bool SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend = 0.0f, float hover_visibility_delay = 0.0f, ImU32 bg_col = 0); - IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); // Widgets: Tree Nodes IMGUI_API bool TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end = NULL); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index a7ab721c83a3..a07908cd99be 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6465,6 +6465,26 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0; const bool was_selected = selected; + // Multi-selection support (header) + const bool is_multi_select = (g.MultiSelectScopeWindow == window); + if (is_multi_select) + { + flags |= ImGuiTreeNodeFlags_OpenOnArrow; + MultiSelectItemHeader(id, &selected); + button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; + + // To handle drag and drop of multiple items we need to avoid clearing selection on click. + // Enabling this test makes actions using CTRL+SHIFT delay their effect on the mouse release which is annoying, but it allows drag and drop of multiple items. + if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) + button_flags |= ImGuiButtonFlags_PressedOnClick; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + } + else + { + button_flags |= ImGuiButtonFlags_NoKeyModifiers; + } + bool hovered, held; bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); bool toggled = false; @@ -6472,7 +6492,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags { if (pressed && g.DragDropHoldJustPressedId != id) { - if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id)) + if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id && !is_multi_select)) toggled = true; if (flags & ImGuiTreeNodeFlags_OpenOnArrow) toggled |= is_mouse_x_over_arrow && !g.NavDisableMouseHover; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job @@ -6507,13 +6527,23 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags } } - // In this branch, TreeNodeBehavior() cannot toggle the selection so this will never trigger. - if (selected != was_selected) //-V547 + // Multi-selection support (footer) + if (is_multi_select) + { + bool pressed_copy = pressed && !toggled; + MultiSelectItemFooter(id, &selected, &pressed_copy); + if (pressed) + SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb); + } + + if (selected != was_selected) g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; // Render const ImU32 text_col = GetColorU32(ImGuiCol_Text); ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact; + if (is_multi_select) + nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle if (display_frame) { // Framed type @@ -6727,8 +6757,8 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl ImRect bb(min_x, pos.y, text_max.x, text_max.y); if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0) { - const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x; - const float spacing_y = style.ItemSpacing.y; + const float spacing_x = span_all_columns ? 0.0f : ImMax(style.ItemSpacing.x - style.SelectableSpacing.x, 0.0f); + const float spacing_y = ImMax(style.ItemSpacing.y - style.SelectableSpacing.y, 0.0f); const float spacing_L = IM_TRUNC(spacing_x * 0.50f); const float spacing_U = IM_TRUNC(spacing_y * 0.50f); bb.Min.x -= spacing_L; @@ -6783,20 +6813,43 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; } if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; } + // Multi-selection support (header) + const bool is_multi_select = (g.MultiSelectScopeWindow == window); const bool was_selected = selected; + if (is_multi_select) + { + MultiSelectItemHeader(id, &selected); + button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; + + // To handle drag and drop of multiple items we need to avoid clearing selection on click. + // Enabling this test makes actions using CTRL+SHIFT delay their effect on the mouse release which is annoying, but it allows drag and drop of multiple items. + if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) + button_flags |= ImGuiButtonFlags_PressedOnClick; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + } + bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); - // Auto-select when moved into - // - This will be more fully fleshed in the range-select branch - // - This is not exposed as it won't nicely work with some user side handling of shift/control - // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons - // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope()) - // - (2) usage will fail with clipped items - // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API. - if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId) - if (g.NavJustMovedToId == id) - selected = pressed = true; + // Multi-selection support (footer) + if (is_multi_select) + { + MultiSelectItemFooter(id, &selected, &pressed); + } + else + { + // Auto-select when moved into + // - This will be more fully fleshed in the range-select branch + // - This is not exposed as it won't nicely work with some user side handling of shift/control + // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons + // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope()) + // - (2) usage will fail with clipped items + // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API. + if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId) + if (g.NavJustMovedToId == id) + selected = pressed = true; + } // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with gamepad/keyboard if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover))) @@ -6810,18 +6863,27 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (pressed) MarkItemEdited(id); - // In this branch, Selectable() cannot toggle the selection so this will never trigger. - if (selected != was_selected) //-V547 + if (selected != was_selected) g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; // Render if (hovered || selected) { - const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + // FIXME-MULTISELECT, FIXME-STYLE: Color for 'selected' elements? ImGuiCol_HeaderSelected + ImU32 col; + if (selected && !hovered) + col = GetColorU32(ImLerp(GetStyleColorVec4(ImGuiCol_Header), GetStyleColorVec4(ImGuiCol_HeaderHovered), 0.5f)); + else + col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); RenderFrame(bb.Min, bb.Max, col, false, 0.0f); } if (g.NavId == id) - RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding); + { + ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding; + if (is_multi_select) + nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle + RenderNavHighlight(bb, id, nav_highlight_flags); + } if (span_all_columns) { @@ -6841,7 +6903,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl EndDisabled(); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); - return pressed; //-V1020 + return pressed || (was_selected != selected); //-V1020 } bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) @@ -7049,16 +7111,227 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) //------------------------------------------------------------------------- // [SECTION] Widgets: Multi-Select support //------------------------------------------------------------------------- +// - BeginMultiSelect() +// - EndMultiSelect() +// - SetNextItemMultiSelectData() +// - MultiSelectItemHeader() [Internal] +// - MultiSelectItemFooter() [Internal] +//------------------------------------------------------------------------- + +ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected) +{ + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiWindow* window = g.CurrentWindow; + + IM_ASSERT(g.MultiSelectScopeId == 0); // No recursion allowed yet (we could allow it if we deem it useful) + IM_ASSERT(g.MultiSelectFlags == 0); + + ImGuiMultiSelectState* ms = &g.MultiSelectState; + g.MultiSelectScopeId = window->IDStack.back(); + g.MultiSelectScopeWindow = window; + g.MultiSelectFlags = flags; + ms->Clear(); + + if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0) + { + ms->In.RangeSrc = ms->Out.RangeSrc = range_ref; + ms->In.RangeValue = ms->Out.RangeValue = range_ref_is_selected; + } + + // Auto clear when using Navigation to move within the selection (we compare SelectScopeId so it possible to use multiple lists inside a same window) + if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.MultiSelectScopeId) + { + if (g.IO.KeyShift) + ms->InRequestSetRangeNav = true; + if (!g.IO.KeyCtrl && !g.IO.KeyShift) + ms->In.RequestClear = true; + } + + // Select All helper shortcut + if (!(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) + if (IsWindowFocused() && g.IO.KeyCtrl && IsKeyPressed(GetKeyIndex(ImGuiKey_A))) + ms->In.RequestSelectAll = true; + +#ifdef IMGUI_DEBUG_MULTISELECT + if (ms->In.RequestClear) printf("[%05d] BeginMultiSelect: RequestClear\n", g.FrameCount); + if (ms->In.RequestSelectAll) printf("[%05d] BeginMultiSelect: RequestSelectAll\n", g.FrameCount); +#endif + + return &ms->In; +} + +ImGuiMultiSelectData* ImGui::EndMultiSelect() +{ + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiMultiSelectState* ms = &g.MultiSelectState; + IM_ASSERT(g.MultiSelectScopeId != 0); + if (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect) + ms->Out.RangeValue = true; + g.MultiSelectScopeId = 0; + g.MultiSelectScopeWindow = NULL; + g.MultiSelectFlags = 0; + +#ifdef IMGUI_DEBUG_MULTISELECT + if (ms->Out.RequestClear) printf("[%05d] EndMultiSelect: RequestClear\n", g.FrameCount); + if (ms->Out.RequestSelectAll) printf("[%05d] EndMultiSelect: RequestSelectAll\n", g.FrameCount); + if (ms->Out.RequestSetRange) printf("[%05d] EndMultiSelect: RequestSetRange %p..%p = %d\n", g.FrameCount, ms->Out.RangeSrc, ms->Out.RangeDst, ms->Out.RangeValue); +#endif + + return &ms->Out; +} void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data) { // 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; - g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData; + if (g.MultiSelectScopeId != 0) + g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect; + else + g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData; g.NextItemData.SelectionUserData = selection_user_data; } +void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) +{ + ImGuiContext& g = *GImGui; + ImGuiMultiSelectState* ms = &g.MultiSelectState; + + IM_ASSERT((g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid) && "Forgot to call SetNextItemMultiSelectData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); + void* item_data = (void*)g.NextItemData.SelectionUserData; + + // Apply Clear/SelectAll requests requested by BeginMultiSelect(). + // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. + // If you are using a clipper (aka not submitting every element of the list) you need to process the Clear/SelectAll request after calling BeginMultiSelect() + bool selected = *p_selected; + if (ms->In.RequestClear) + selected = false; + else if (ms->In.RequestSelectAll) + selected = true; + + const bool is_range_src = (ms->In.RangeSrc == item_data); + if (is_range_src) + ms->In.RangeSrcPassedBy = true; + + // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) + // For this to work, IF the user is clipping items, they need to set RangeSrcPassedBy = true to notify the system. + if (ms->InRequestSetRangeNav) + { + IM_ASSERT(id != 0); + IM_ASSERT(g.IO.KeyShift); + const bool is_range_dst = !ms->InRangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. + if (is_range_dst) + ms->InRangeDstPassedBy = true; + if (is_range_src || is_range_dst || ms->In.RangeSrcPassedBy != ms->InRangeDstPassedBy) + selected = ms->In.RangeValue; + else if (!g.IO.KeyCtrl) + selected = false; + } + + *p_selected = selected; +} + +void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiMultiSelectState* ms = &g.MultiSelectState; + + void* item_data = (void*)g.NextItemData.SelectionUserData; + + bool selected = *p_selected; + bool pressed = *p_pressed; + bool is_ctrl = g.IO.KeyCtrl; + bool is_shift = g.IO.KeyShift; + const bool is_multiselect = (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoMultiSelect) == 0; + + // Auto-select as you navigate a list + if (g.NavJustMovedToId == id) + { + if (!g.IO.KeyCtrl) + selected = pressed = true; + else if (g.IO.KeyCtrl && g.IO.KeyShift) + pressed = true; + } + + // Right-click handling: this could be moved at the Selectable() level. + bool hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); + if (hovered && IsMouseClicked(1)) + { + SetFocusID(g.LastItemData.ID, window); + if (!pressed && !selected) + { + pressed = true; + is_ctrl = is_shift = false; + } + } + + if (pressed) + { + //------------------------------------------------------------------------------------------------------------------------------------------------- + // ACTION | Begin | Item Old | Item New | End + //------------------------------------------------------------------------------------------------------------------------------------------------- + // Keys Navigated, Ctrl=0, Shift=0 | In.Clear | Clear -> Sel=0 | Src=item, Pressed -> Sel=1 | + // Keys Navigated, Ctrl=0, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange + // Keys Navigated, Ctrl=1, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=Src, Out.Clear, Out.SetRange=Src | Clear + SetRange + // Mouse Pressed, Ctrl=0, Shift=0 | n/a | n/a (Sel=1) | Src=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange + // Mouse Pressed, Ctrl=0, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange + //------------------------------------------------------------------------------------------------------------------------------------------------- + + ImGuiInputSource input_source = (g.NavJustMovedToId != 0 && g.NavWindow == window && g.NavJustMovedToId == g.LastItemData.ID) ? g.NavInputSource : ImGuiInputSource_Mouse; + if (is_shift && is_multiselect) + { + ms->Out.RequestSetRange = true; + ms->Out.RangeDst = item_data; + if (!is_ctrl) + ms->Out.RangeValue = true; + ms->Out.RangeDirection = ms->In.RangeSrcPassedBy ? +1 : -1; + } + else + { + selected = (!is_ctrl || (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect)) ? true : !selected; + ms->Out.RangeSrc = ms->Out.RangeDst = item_data; + ms->Out.RangeValue = selected; + } + + if (input_source == ImGuiInputSource_Mouse) + { + // Mouse click without CTRL clears the selection, unless the clicked item is already selected + bool preserve_existing_selection = g.DragDropActive; + if (is_multiselect && !is_ctrl && !preserve_existing_selection) + ms->Out.RequestClear = true; + if (is_multiselect && !is_shift && !preserve_existing_selection && ms->Out.RequestClear) + { + // For toggle selection unless there is a Clear request, we can handle it completely locally without sending a RangeSet request. + IM_ASSERT(ms->Out.RangeSrc == ms->Out.RangeDst); // Setup by block above + ms->Out.RequestSetRange = true; + ms->Out.RangeValue = selected; + ms->Out.RangeDirection = +1; + } + if (!is_multiselect) + { + // Clear selection, set single item range + IM_ASSERT(ms->Out.RangeSrc == item_data && ms->Out.RangeDst == item_data); // Setup by block above + ms->Out.RequestClear = true; + ms->Out.RequestSetRange = true; + } + } + else if (input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) + { + if (!is_multiselect) + ms->Out.RequestClear = true; + else if (is_shift && !is_ctrl && is_multiselect) + ms->Out.RequestClear = true; + } + } + + // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) + if (ms->Out.RangeSrc == item_data && is_ctrl && is_shift && is_multiselect && !(g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect)) + ms->Out.RangeValue = selected; + + *p_selected = selected; + *p_pressed = pressed; +} //------------------------------------------------------------------------- // [SECTION] Widgets: ListBox From 8947c35fa1e22084a2b832e1236195ac09810361 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 9 Dec 2020 19:36:04 +0100 Subject: [PATCH 002/132] MultiSelect: Removed SelectableSpacing as I'm not sure it is of use for now (history insert) --- imgui.cpp | 3 --- imgui.h | 2 -- imgui_demo.cpp | 12 ------------ imgui_widgets.cpp | 4 ++-- 4 files changed, 2 insertions(+), 19 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index b1e99035387e..3cdd6fbde6e6 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1273,7 +1273,6 @@ ImGuiStyle::ImGuiStyle() TableAngledHeadersTextAlign = ImVec2(0.5f,0.0f);// Alignment of angled headers within the cell ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. - SelectableSpacing = ImVec2(0.0f,0.0f);// Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing). SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. SeparatorTextBorderSize = 3.0f; // Thickkness of border in SeparatorText() SeparatorTextAlign = ImVec2(0.0f,0.5f);// Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). @@ -1322,7 +1321,6 @@ void ImGuiStyle::ScaleAllSizes(float scale_factor) LogSliderDeadzone = ImTrunc(LogSliderDeadzone * scale_factor); TabRounding = ImTrunc(TabRounding * scale_factor); TabMinWidthForCloseButton = (TabMinWidthForCloseButton != FLT_MAX) ? ImTrunc(TabMinWidthForCloseButton * scale_factor) : FLT_MAX; - SelectableSpacing = ImTrunc(SelectableSpacing * scale_factor); SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor); DisplayWindowPadding = ImTrunc(DisplayWindowPadding * scale_factor); DisplaySafeAreaPadding = ImTrunc(DisplaySafeAreaPadding * scale_factor); @@ -3259,7 +3257,6 @@ static const ImGuiDataVarInfo GStyleVarInfo[] = { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersAngle)}, // ImGuiStyleVar_TableAngledHeadersAngle { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign - { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SelectableSpacing) }, // ImGuiStyleVar_SelectableSpacing { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign { ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, SeparatorTextBorderSize)}, // ImGuiStyleVar_SeparatorTextBorderSize { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextAlign) }, // ImGuiStyleVar_SeparatorTextAlign diff --git a/imgui.h b/imgui.h index 80e6ca7e1294..0016ad7e9dc9 100644 --- a/imgui.h +++ b/imgui.h @@ -1699,7 +1699,6 @@ enum ImGuiStyleVar_ ImGuiStyleVar_TableAngledHeadersAngle, // float TableAngledHeadersAngle ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2 TableAngledHeadersTextAlign ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign - ImGuiStyleVar_SelectableSpacing, // ImVec2 SelectableSpacing ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign ImGuiStyleVar_SeparatorTextBorderSize, // float SeparatorTextBorderSize ImGuiStyleVar_SeparatorTextAlign, // ImVec2 SeparatorTextAlign @@ -2144,7 +2143,6 @@ struct ImGuiStyle ImVec2 TableAngledHeadersTextAlign;// Alignment of angled headers within the cell ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). - ImVec2 SelectableSpacing; // Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing). ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. float SeparatorTextBorderSize; // Thickkness of border in SeparatorText() ImVec2 SeparatorTextAlign; // Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 8e7312602be5..bab08ce9bd0f 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -1433,15 +1433,6 @@ static void ShowDemoWindowWidgets() if (winning_state) ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f + 0.5f * cosf(time * 2.0f), 0.5f + 0.5f * sinf(time * 3.0f))); - static float spacing = 0.0f; - ImGui::PushItemWidth(100); - ImGui::SliderFloat("SelectableSpacing", &spacing, 0, 20, "%.0f"); - ImGui::SameLine(); HelpMarker("Selectable cancel out the regular spacing between items by extending itself by ItemSpacing/2 in each direction.\nThis has two purposes:\n- Avoid the gap between items so the mouse is always hitting something.\n- Avoid the gap between items so range-selected item looks connected.\nBy changing SelectableSpacing we can enforce spacing between selectables."); - ImGui::PopItemWidth(); - ImGui::Spacing(); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 8)); - ImGui::PushStyleVar(ImGuiStyleVar_SelectableSpacing, ImVec2(spacing, spacing)); - for (int y = 0; y < 4; y++) for (int x = 0; x < 4; x++) { @@ -1460,10 +1451,8 @@ static void ShowDemoWindowWidgets() ImGui::PopID(); } - ImGui::PopStyleVar(2); if (winning_state) ImGui::PopStyleVar(); - ImGui::TreePop(); } IMGUI_DEMO_MARKER("Widgets/Selectables/Alignment"); @@ -6863,7 +6852,6 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SliderFloat2("FramePadding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("ItemSpacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); - ImGui::SliderFloat2("SelectableSpacing", (float*)&style.SelectableSpacing, 0.0f, 20.0f, "%.0f"); ImGui::SameLine(); HelpMarker("SelectableSpacing must be < ItemSpacing.\nSelectables display their highlight after canceling out the effect of ItemSpacing, so they can be look tightly packed. This setting allows to enforce spacing between them."); ImGui::SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, 0.0f, 10.0f, "%.0f"); ImGui::SliderFloat("IndentSpacing", &style.IndentSpacing, 0.0f, 30.0f, "%.0f"); ImGui::SliderFloat("ScrollbarSize", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index a07908cd99be..89f7e6716903 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6757,8 +6757,8 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl ImRect bb(min_x, pos.y, text_max.x, text_max.y); if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0) { - const float spacing_x = span_all_columns ? 0.0f : ImMax(style.ItemSpacing.x - style.SelectableSpacing.x, 0.0f); - const float spacing_y = ImMax(style.ItemSpacing.y - style.SelectableSpacing.y, 0.0f); + const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x; + const float spacing_y = style.ItemSpacing.y; const float spacing_L = IM_TRUNC(spacing_x * 0.50f); const float spacing_U = IM_TRUNC(spacing_y * 0.50f); bb.Min.x -= spacing_L; From 57da88093f54178b349e097e35b5ce12fb582d36 Mon Sep 17 00:00:00 2001 From: omar Date: Mon, 15 Apr 2019 19:13:36 +0200 Subject: [PATCH 003/132] MultiSelect: Added IMGUI_HAS_MULTI_SELECT define. Fixed right-click toggling selection without clearing active id, could lead to MarkItemEdited() asserting. Fixed demo. --- imgui.h | 2 ++ imgui_internal.h | 1 - imgui_widgets.cpp | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/imgui.h b/imgui.h index 0016ad7e9dc9..3b92a9ff8fe2 100644 --- a/imgui.h +++ b/imgui.h @@ -2722,6 +2722,8 @@ struct ImColor // [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectData) //----------------------------------------------------------------------------- +#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) 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 diff --git a/imgui_internal.h b/imgui_internal.h index 63b80400ca36..41dac9c90690 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1709,7 +1709,6 @@ struct ImGuiOldColumns // We always assume that -1 is an invalid value (which works for indices and pointers) #define ImGuiSelectionUserData_Invalid ((ImGuiSelectionUserData)-1) -#define IMGUI_HAS_MULTI_SELECT 1 #ifdef IMGUI_HAS_MULTI_SELECT struct IMGUI_API ImGuiMultiSelectState diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 89f7e6716903..53f0c57ace23 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7258,7 +7258,9 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) bool hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); if (hovered && IsMouseClicked(1)) { - SetFocusID(g.LastItemData.ID, window); + if (g.ActiveId != 0 && g.ActiveId != id) + ClearActiveID(); + SetFocusID(id, window); if (!pressed && !selected) { pressed = true; From 17c4c2154a17ba38108c60883285af71514d04dc Mon Sep 17 00:00:00 2001 From: omar Date: Sat, 21 Dec 2019 23:21:23 +0100 Subject: [PATCH 004/132] MultiSelect: Demo sharing selection helper code. Fixed static analyzer warnings. --- imgui.h | 8 ++++--- imgui_demo.cpp | 56 +++++++++++++++++++++++++++++++---------------- imgui_widgets.cpp | 10 ++++----- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/imgui.h b/imgui.h index 3b92a9ff8fe2..6acdab5806d1 100644 --- a/imgui.h +++ b/imgui.h @@ -2725,11 +2725,13 @@ 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) 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. +// This system is designed to allow mouse/keyboard multi-selection, including support for range-selection (SHIFT + click), +// 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. enum ImGuiMultiSelectFlags_ { + ImGuiMultiSelectFlags_None = 0, ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0, ImGuiMultiSelectFlags_NoUnselect = 1 << 1, // Disable unselecting items with CTRL+Click, CTRL+Space etc. ImGuiMultiSelectFlags_NoSelectAll = 1 << 2, // Disable CTRL+A shortcut to set RequestSelectAll diff --git a/imgui_demo.cpp b/imgui_demo.cpp index bab08ce9bd0f..291410c4d155 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2759,6 +2759,38 @@ static void ShowDemoWindowWidgets() } } +// [Advanced] Helper class to simulate storage of a multi-selection state, used by the advanced multi-selection demos. +// We use ImGuiStorage (simple key->value storage) to avoid external dependencies but it's probably not optimal. +// To store a single-selection: +// - You only need a single variable and don't need any of this! +// To store a multi-selection, in your real application you could: +// - Use intrusively stored selection (e.g. 'bool IsSelected' inside your object). This is by far the simplest +// way to store your selection data, but it means you cannot have multiple simultaneous views over your objects. +// This is what may of the simpler demos in this file are using (so they are not using this class). +// - Otherwise, any externally stored unordered_set/set/hash/map/interval trees (storing indices, objects id, etc.) +// are generally appropriate. Even a large array of bool might work for you... +struct ExampleSelectionData +{ + ImGuiStorage Storage; + int SelectedCount; // Number of selected items (storage will keep this updated) + + ExampleSelectionData() { Clear(); } + void Clear() { Storage.Clear(); SelectedCount = 0; } + bool GetSelected(int id) const { return Storage.GetInt((ImGuiID)id) != 0; } + void SetSelected(int id, bool v) { int* p_int = Storage.GetIntRef((ImGuiID)id); if (*p_int == (int)v) return; SelectedCount = v ? (SelectedCount + 1) : (SelectedCount - 1); *p_int = (bool)v; } + int GetSelectedCount() const { return SelectedCount; } + + // When using SelectAll() / SetRange() we assume that our objects ID are indices. + // In this demo we always store selection using indices and never in another manner (e.g. object ID or pointers). + // If your selection system is storing selection using object ID and you want to support Shift+Click range-selection, + // you will need a way to iterate from one object to another given the ID you use. + // You are likely to need some kind of data structure to convert 'view index' from/to 'ID'. + // FIXME-MULTISELECT: Would be worth providing a demo of doing this. + // FIXME-MULTISELECT: SetRange() is currently very inefficient since it doesn't take advantage of the fact that ImGuiStorage stores sorted key. + void SetRange(int a, int b, bool v) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) SetSelected(n, v); } + void SelectAll(int count) { Storage.Data.resize(count); for (int n = 0; n < count; n++) Storage.Data[n] = ImGuiStoragePair((ImGuiID)n, 1); SelectedCount = count; } // This could be using SetRange() but this is faster. +}; + static void ShowDemoWindowMultiSelect() { IMGUI_DEMO_MARKER("Widgets/Selection State"); @@ -2803,22 +2835,8 @@ static void ShowDemoWindowMultiSelect() if (ImGui::TreeNode("Multiple Selection (Full)")) { // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. - // In this demo we use ImGuiStorage (simple key->value storage) to avoid external dependencies but it's probably not optimal. - // In your real code you could use e.g std::unordered_set<> or your own data structure for storing selection. - // If you don't mind being limited to one view over your objects, the simplest way is to use an intrusive selection (e.g. store bool inside object, as used in examples above). - // Otherwise external set/hash/map/interval trees (storing indices, etc.) may be appropriate. - struct MySelection - { - ImGuiStorage Storage; - void Clear() { Storage.Clear(); } - void SelectAll(int count) { Storage.Data.reserve(count); Storage.Data.resize(0); for (int n = 0; n < count; n++) Storage.Data.push_back(ImGuiStoragePair((ImGuiID)n, 1)); } - void SetRange(int a, int b, int sel) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) Storage.SetInt((ImGuiID)n, sel); } - bool GetSelected(int id) const { return Storage.GetInt((ImGuiID)id) != 0; } - void SetSelected(int id, bool v) { SetRange(id, id, v ? 1 : 0); } - }; - static int selection_ref = 0; // Selection pivot (last clicked item, we need to preserve this to handle range-select) - static MySelection selection; + static ExampleSelectionData selection; const char* random_names[] = { "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", @@ -2832,8 +2850,8 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(0, (void*)(intptr_t)selection_ref, selection.GetSelected((int)selection_ref)); - if (multi_select_data->RequestClear) { selection.Clear(); } + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection_ref, selection.GetSelected((int)selection_ref)); + if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); } ImGuiListClipper clipper; clipper.Begin(COUNT); @@ -2856,9 +2874,9 @@ static void ShowDemoWindowMultiSelect() multi_select_data = ImGui::EndMultiSelect(); selection_ref = (int)(intptr_t)multi_select_data->RangeSrc; ImGui::EndListBox(); - if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); } - if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } + if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } } ImGui::TreePop(); } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 53f0c57ace23..091efc99bf69 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7248,10 +7248,10 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Auto-select as you navigate a list if (g.NavJustMovedToId == id) { - if (!g.IO.KeyCtrl) - selected = pressed = true; - else if (g.IO.KeyCtrl && g.IO.KeyShift) + if (is_ctrl && is_shift) pressed = true; + else if (!is_ctrl) + selected = pressed = true; } // Right-click handling: this could be moved at the Selectable() level. @@ -7320,9 +7320,9 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) } else if (input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) { - if (!is_multiselect) + if (is_multiselect && is_shift && !is_ctrl) ms->Out.RequestClear = true; - else if (is_shift && !is_ctrl && is_multiselect) + else if (!is_multiselect) ms->Out.RequestClear = true; } } From 4afbfd5e71913e1e6f5de44f822a159fa8fa868f Mon Sep 17 00:00:00 2001 From: omar Date: Tue, 14 Jan 2020 16:18:55 +0100 Subject: [PATCH 005/132] MultiSelect: Renamed SetNextItemMultiSelectData() to SetNextItemSelectionUserData() --- imgui.h | 4 ++-- imgui_demo.cpp | 10 +++++++++- imgui_widgets.cpp | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/imgui.h b/imgui.h index 6acdab5806d1..e57a297527b6 100644 --- a/imgui.h +++ b/imgui.h @@ -2749,7 +2749,7 @@ enum ImGuiMultiSelectFlags_ // performance penalty, but requires a little more work on the code. If you only have a few hundreds elements in your possible selection set, // you may as well not bother with clipping, as the cost should be negligible (as least on imgui side). // If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards. -// - The void* Src/Dst value represent a selectable object. They are the values you pass to SetNextItemMultiSelectData(). +// - The void* Src/Dst value represent a selectable object. They are the values you pass to SetNextItemSelectionUserData(). // Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points. But the code never assume that sortable integers are used. // - In the spirit of imgui design, your code own the selection data. So this is designed to handle all kind of selection data: instructive (store a bool inside each object), // external array (store an array aside from your objects), set (store only selected items in a hash/map/set), using intervals (store indices in an interval tree), etc. @@ -2760,7 +2760,7 @@ enum ImGuiMultiSelectFlags_ // 3) Set RangeSrcPassedBy=true if the RangeSrc 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->RangeSrc) { data->RangeSrcPassedBy = true; } -// 4) Submit your items with SetNextItemMultiSelectData() + Selectable()/TreeNode() calls. +// 4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. // Call IsItemSelectionToggled() to query with the selection state has been toggled, in which you need the info immediately (before EndMultiSelect()) for your display. // When cannot reliably return a "IsItemSelected()" value because we need to consider clipped (unprocessed) item, this is why we return a toggle event instead. // 5) Call EndMultiSelect(). Save the value of ->RangeSrc for the next frame (you may convert the value in a format that is safe for persistance) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 291410c4d155..fca877fd5803 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2845,7 +2845,7 @@ static void ShowDemoWindowMultiSelect() }; int COUNT = 1000; - HelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range."); + HelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range. Keyboard is also supported."); ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", (unsigned int*)&ImGui::GetIO().ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) @@ -2853,6 +2853,7 @@ static void ShowDemoWindowMultiSelect() ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection_ref, selection.GetSelected((int)selection_ref)); if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); } + ImVec2 color_button_sz(ImGui::GetFontSize(), ImGui::GetFontSize()); ImGuiListClipper clipper; clipper.Begin(COUNT); while (clipper.Step()) @@ -2865,6 +2866,13 @@ static void ShowDemoWindowMultiSelect() char label[64]; sprintf(label, "Object %05d (category: %s)", n, random_names[n % IM_ARRAYSIZE(random_names)]); bool item_is_selected = selection.GetSelected(n); + + // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part + // of the selection scope doesn't erroneously alter our selection. + ImVec4 dummy_col = ImColor((ImU32)ImGui::GetID(label)); + ImGui::ColorButton("##", dummy_col, ImGuiColorEditFlags_NoTooltip, color_button_sz); + ImGui::SameLine(); + ImGui::SetNextItemSelectionUserData(n); if (ImGui::Selectable(label, item_is_selected)) selection.SetSelected(n, !item_is_selected); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 091efc99bf69..62b063d181ef 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7113,7 +7113,7 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) //------------------------------------------------------------------------- // - BeginMultiSelect() // - EndMultiSelect() -// - SetNextItemMultiSelectData() +// - SetNextItemSelectionUserData() // - MultiSelectItemHeader() [Internal] // - MultiSelectItemFooter() [Internal] //------------------------------------------------------------------------- @@ -7197,7 +7197,7 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) ImGuiContext& g = *GImGui; ImGuiMultiSelectState* ms = &g.MultiSelectState; - IM_ASSERT((g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid) && "Forgot to call SetNextItemMultiSelectData() 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(). From 9c7183dd048ecd8f4c17f54662ca067c997d3bfb Mon Sep 17 00:00:00 2001 From: omar Date: Mon, 13 Jan 2020 15:05:53 +0100 Subject: [PATCH 006/132] MultiSelect: Transition to use FocusScope bits merged in master. Preserve ability to shift+arrow into an item that is part of FocusScope but doesn't carry a selection without breaking selection. --- imgui.cpp | 1 - imgui_internal.h | 15 ++++++++------- imgui_widgets.cpp | 37 +++++++++++++++++++++++-------------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 3cdd6fbde6e6..733a5dfe1287 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3822,7 +3822,6 @@ void ImGui::Shutdown() g.MenusIdSubmittedThisFrame.clear(); g.InputTextState.ClearFreeMemory(); g.InputTextDeactivatedState.ClearFreeMemory(); - g.MultiSelectScopeWindow = NULL; g.SettingsWindows.clear(); g.SettingsHandlers.clear(); diff --git a/imgui_internal.h b/imgui_internal.h index 41dac9c90690..dd58534662fc 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1210,6 +1210,7 @@ enum ImGuiNextItemDataFlags_ ImGuiNextItemDataFlags_HasOpen = 1 << 1, ImGuiNextItemDataFlags_HasShortcut = 1 << 2, ImGuiNextItemDataFlags_HasRefVal = 1 << 3, + ImGuiNextItemDataFlags_HasSelectionData = 1 << 4, }; struct ImGuiNextItemData @@ -1217,6 +1218,7 @@ struct ImGuiNextItemData ImGuiNextItemDataFlags Flags; ImGuiItemFlags ItemFlags; // Currently only tested/used for ImGuiItemFlags_AllowOverlap. // Non-flags members are NOT cleared by ItemAdd() meaning they are still valid during NavProcessItem() + ImGuiID FocusScopeId; // Set by SetNextItemSelectionUserData() (!= 0 signify value has been set) ImGuiSelectionUserData SelectionUserData; // Set by SetNextItemSelectionUserData() (note that NULL/0 is a valid value, we use -1 == ImGuiSelectionUserData_Invalid to mark invalid values) float Width; // Set by SetNextItemWidth() ImGuiKeyChord Shortcut; // Set by SetNextItemShortcut() @@ -1713,13 +1715,14 @@ struct ImGuiOldColumns struct IMGUI_API ImGuiMultiSelectState { + ImGuiID FocusScopeId; // Same as CurrentWindow->DC.FocusScopeIdCurrent (unless another selection scope was pushed manually) ImGuiMultiSelectData In; // The In requests are set and returned by BeginMultiSelect() ImGuiMultiSelectData Out; // The Out requests are finalized and returned by EndMultiSelect() bool InRangeDstPassedBy; // (Internal) set by the the item that match NavJustMovedToId when InRequestRangeSetNav is set. bool InRequestSetRangeNav; // (Internal) set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. ImGuiMultiSelectState() { Clear(); } - void Clear() { In.Clear(); Out.Clear(); InRangeDstPassedBy = InRequestSetRangeNav = false; } + void Clear() { FocusScopeId = 0; In.Clear(); Out.Clear(); InRangeDstPassedBy = InRequestSetRangeNav = false; } }; #endif // #ifdef IMGUI_HAS_MULTI_SELECT @@ -2117,10 +2120,9 @@ struct ImGuiContext ImVec2 NavWindowingAccumDeltaSize; // Range-Select/Multi-Select - ImGuiID MultiSelectScopeId; - ImGuiWindow* MultiSelectScopeWindow; + bool MultiSelectEnabled; ImGuiMultiSelectFlags MultiSelectFlags; - ImGuiMultiSelectState MultiSelectState; + ImGuiMultiSelectState MultiSelectState; // We currently don't support recursing/stacking multi-select // Render float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list) @@ -2385,9 +2387,8 @@ struct ImGuiContext NavWindowingToggleLayer = false; NavWindowingToggleKey = ImGuiKey_None; - MultiSelectScopeId = 0; - MultiSelectScopeWindow = NULL; - MultiSelectFlags = 0; + MultiSelectEnabled = false; + MultiSelectFlags = ImGuiMultiSelectFlags_None; DimBgRatio = 0.0f; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 62b063d181ef..4da2569609c5 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6466,7 +6466,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags const bool was_selected = selected; // Multi-selection support (header) - const bool is_multi_select = (g.MultiSelectScopeWindow == window); + const bool is_multi_select = g.MultiSelectEnabled; if (is_multi_select) { flags |= ImGuiTreeNodeFlags_OpenOnArrow; @@ -6814,7 +6814,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; } // Multi-selection support (header) - const bool is_multi_select = (g.MultiSelectScopeWindow == window); + const bool is_multi_select = g.MultiSelectEnabled; const bool was_selected = selected; if (is_multi_select) { @@ -7120,17 +7120,20 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected) { - ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - IM_ASSERT(g.MultiSelectScopeId == 0); // No recursion allowed yet (we could allow it if we deem it useful) + IM_ASSERT(g.MultiSelectEnabled == false); // No recursion allowed yet (we could allow it if we deem it useful) IM_ASSERT(g.MultiSelectFlags == 0); + IM_ASSERT(g.MultiSelectState.FocusScopeId == 0); + // FIXME: BeginFocusScope() ImGuiMultiSelectState* ms = &g.MultiSelectState; - g.MultiSelectScopeId = window->IDStack.back(); - g.MultiSelectScopeWindow = window; - g.MultiSelectFlags = flags; ms->Clear(); + ms->FocusScopeId = window->IDStack.back(); + PushFocusScope(ms->FocusScopeId); + g.MultiSelectEnabled = true; + g.MultiSelectFlags = flags; if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0) { @@ -7139,7 +7142,7 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* } // Auto clear when using Navigation to move within the selection (we compare SelectScopeId so it possible to use multiple lists inside a same window) - if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.MultiSelectScopeId) + if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) { if (g.IO.KeyShift) ms->InRequestSetRangeNav = true; @@ -7162,14 +7165,16 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* ImGuiMultiSelectData* ImGui::EndMultiSelect() { - ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiContext& g = *GImGui; ImGuiMultiSelectState* ms = &g.MultiSelectState; - IM_ASSERT(g.MultiSelectScopeId != 0); + IM_ASSERT(g.MultiSelectState.FocusScopeId == g.CurrentFocusScopeId); + if (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect) ms->Out.RangeValue = true; - g.MultiSelectScopeId = 0; - g.MultiSelectScopeWindow = NULL; - g.MultiSelectFlags = 0; + g.MultiSelectState.FocusScopeId = 0; + PopFocusScope(); + g.MultiSelectEnabled = false; + g.MultiSelectFlags = ImGuiMultiSelectFlags_None; #ifdef IMGUI_DEBUG_MULTISELECT if (ms->Out.RequestClear) printf("[%05d] EndMultiSelect: RequestClear\n", g.FrameCount); @@ -7185,18 +7190,22 @@ 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.MultiSelectScopeId != 0) + 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; } void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) { ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; ImGuiMultiSelectState* ms = &g.MultiSelectState; + 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; From 7abda179af618da2114153b00119dfe878652adc Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 11 Mar 2020 21:57:18 +0100 Subject: [PATCH 007/132] MultiSelect: Fix for TreeNode following merge of 011d4755. Demo: basic test for tree nodes. --- imgui_demo.cpp | 56 ++++++++++++++++++++++++++++++++++++----------- imgui_widgets.cpp | 10 +++++---- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index fca877fd5803..c571a922e414 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2844,18 +2844,29 @@ static void ShowDemoWindowMultiSelect() "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Celtuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" }; - int COUNT = 1000; - HelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range. Keyboard is also supported."); + // Test both Selectable() and TreeNode() widgets + enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; + static WidgetType widget_type = WidgetType_TreeNode; + if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } + ImGui::SameLine(); + if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", (unsigned int*)&ImGui::GetIO().ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); + ImGui::SameLine(); HelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range. Keyboard is also supported."); + // Open a scrolling region + const int ITEMS_COUNT = 1000; if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection_ref, selection.GetSelected((int)selection_ref)); - if (multi_select_data->RequestClear) { selection.Clear(); } - if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); } ImVec2 color_button_sz(ImGui::GetFontSize(), ImGui::GetFontSize()); + if (widget_type == WidgetType_TreeNode) + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); + + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection_ref, selection.GetSelected(selection_ref)); + if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } + ImGuiListClipper clipper; - clipper.Begin(COUNT); + clipper.Begin(ITEMS_COUNT); while (clipper.Step()) { if (clipper.DisplayStart > (int)selection_ref) @@ -2868,23 +2879,42 @@ static void ShowDemoWindowMultiSelect() bool item_is_selected = selection.GetSelected(n); // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part - // of the selection scope doesn't erroneously alter our selection. - ImVec4 dummy_col = ImColor((ImU32)ImGui::GetID(label)); - ImGui::ColorButton("##", dummy_col, ImGuiColorEditFlags_NoTooltip, color_button_sz); + // of the selection scope doesn't erroneously alter our selection (FIXME-TESTS: Add a test for that!). + ImU32 dummy_col = (ImU32)ImGui::GetID(label); + ImGui::ColorButton("##", ImColor(dummy_col), ImGuiColorEditFlags_NoTooltip, color_button_sz); ImGui::SameLine(); ImGui::SetNextItemSelectionUserData(n); - if (ImGui::Selectable(label, item_is_selected)) - selection.SetSelected(n, !item_is_selected); + if (widget_type == WidgetType_Selectable) + { + if (ImGui::Selectable(label, item_is_selected)) + selection.SetSelected(n, !item_is_selected); + } + else if (widget_type == WidgetType_TreeNode) + { + ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (item_is_selected) + tree_node_flags |= ImGuiTreeNodeFlags_Selected; + ImGui::TreeNodeEx(label, tree_node_flags); + if (ImGui::IsItemToggledSelection()) + selection.SetSelected(n, !item_is_selected); + } + ImGui::PopID(); } } + + // Apply multi-select requests multi_select_data = ImGui::EndMultiSelect(); selection_ref = (int)(intptr_t)multi_select_data->RangeSrc; - ImGui::EndListBox(); if (multi_select_data->RequestClear) { selection.Clear(); } - if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); } + if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } + + if (widget_type == WidgetType_TreeNode) + ImGui::PopStyleVar(); + + ImGui::EndListBox(); } ImGui::TreePop(); } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 4da2569609c5..9f546a5e33a9 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6443,8 +6443,6 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x; const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x; const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2); - if (window != g.HoveredWindow || !is_mouse_x_over_arrow) - button_flags |= ImGuiButtonFlags_NoKeyModifiers; // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags. // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support. @@ -6469,12 +6467,15 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags const bool is_multi_select = g.MultiSelectEnabled; if (is_multi_select) { - flags |= ImGuiTreeNodeFlags_OpenOnArrow; MultiSelectItemHeader(id, &selected); button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; + // We absolutely need to distinguish open vs select so this is the default when multi-select is enabled. + flags |= ImGuiTreeNodeFlags_OpenOnArrow; + // To handle drag and drop of multiple items we need to avoid clearing selection on click. // Enabling this test makes actions using CTRL+SHIFT delay their effect on the mouse release which is annoying, but it allows drag and drop of multiple items. + // FIXME-MULTISELECT: Consider opt-in for drag and drop behavior in ImGuiMultiSelectFlags? if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) button_flags |= ImGuiButtonFlags_PressedOnClick; else @@ -6482,7 +6483,8 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags } else { - button_flags |= ImGuiButtonFlags_NoKeyModifiers; + if (window != g.HoveredWindow || !is_mouse_x_over_arrow) + button_flags |= ImGuiButtonFlags_NoKeyModifiers; } bool hovered, held; From 9aeebd24f77fdc82b8e1c8fb7ed51ea53d981fd3 Mon Sep 17 00:00:00 2001 From: omar Date: Fri, 20 Mar 2020 12:34:52 +0100 Subject: [PATCH 008/132] MultiSelect: Fixed CTRL+A not testing focus scope id. Fixed CTRL+A not testing active id. Added demo code. Comments. --- imgui_demo.cpp | 43 ++++++++++++++++++++++++++++++++++--------- imgui_widgets.cpp | 6 ++++-- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index c571a922e414..87e2e0bacfec 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2769,26 +2769,28 @@ static void ShowDemoWindowWidgets() // This is what may of the simpler demos in this file are using (so they are not using this class). // - Otherwise, any externally stored unordered_set/set/hash/map/interval trees (storing indices, objects id, etc.) // 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. struct ExampleSelectionData { ImGuiStorage Storage; - int SelectedCount; // Number of selected items (storage will keep this updated) + int SelectionSize; // Number of selected items (== number of 1 in the Storage) ExampleSelectionData() { Clear(); } - void Clear() { Storage.Clear(); SelectedCount = 0; } - bool GetSelected(int id) const { return Storage.GetInt((ImGuiID)id) != 0; } - void SetSelected(int id, bool v) { int* p_int = Storage.GetIntRef((ImGuiID)id); if (*p_int == (int)v) return; SelectedCount = v ? (SelectedCount + 1) : (SelectedCount - 1); *p_int = (bool)v; } - int GetSelectedCount() const { return SelectedCount; } + 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; } + int GetSelectionSize() const { return SelectionSize; } // When using SelectAll() / SetRange() we assume that our objects ID are indices. // In this demo we always store selection using indices and never in another manner (e.g. object ID or pointers). // If your selection system is storing selection using object ID and you want to support Shift+Click range-selection, // you will need a way to iterate from one object to another given the ID you use. - // You are likely to need some kind of data structure to convert 'view index' from/to 'ID'. + // You are likely to need some kind of data structure to convert 'view index' <> 'object ID'. // FIXME-MULTISELECT: Would be worth providing a demo of doing this. // FIXME-MULTISELECT: SetRange() is currently very inefficient since it doesn't take advantage of the fact that ImGuiStorage stores sorted key. - void SetRange(int a, int b, bool v) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) SetSelected(n, v); } - void SelectAll(int count) { Storage.Data.resize(count); for (int n = 0; n < count; n++) Storage.Data[n] = ImGuiStoragePair((ImGuiID)n, 1); SelectedCount = count; } // This could be using SetRange() but this is faster. + void SetRange(int n1, int n2, bool v) { if (n2 < n1) { int tmp = n2; n2 = n1; n1 = tmp; } for (int n = n1; n <= n2; n++) SetSelected(n, v); } + void SelectAll(int count) { Storage.Data.resize(count); for (int idx = 0; idx < count; idx++) Storage.Data[idx] = ImGuiStoragePair((ImGuiID)idx, 1); SelectionSize = count; } // This could be using SetRange(), but it this way is faster. }; static void ShowDemoWindowMultiSelect() @@ -2846,10 +2848,13 @@ static void ShowDemoWindowMultiSelect() // Test both Selectable() and TreeNode() widgets enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; + static bool use_columns = false; static WidgetType widget_type = WidgetType_TreeNode; if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } ImGui::SameLine(); if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } + ImGui::SameLine(); + ImGui::Checkbox("Use 2 columns", &use_columns); ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", (unsigned int*)&ImGui::GetIO().ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); ImGui::SameLine(); HelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range. Keyboard is also supported."); @@ -2865,6 +2870,12 @@ static void ShowDemoWindowMultiSelect() if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } + if (use_columns) + { + ImGui::Columns(2); + //ImGui::PushStyleVar(ImGuiStyleVar_FrtemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); + } + ImGuiListClipper clipper; clipper.Begin(ITEMS_COUNT); while (clipper.Step()) @@ -2874,8 +2885,9 @@ static void ShowDemoWindowMultiSelect() for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) { ImGui::PushID(n); + const char* category = random_names[n % IM_ARRAYSIZE(random_names)]; char label[64]; - sprintf(label, "Object %05d (category: %s)", n, random_names[n % IM_ARRAYSIZE(random_names)]); + sprintf(label, "Object %05d (category: %s)", n, category); bool item_is_selected = selection.GetSelected(n); // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part @@ -2900,10 +2912,23 @@ static void ShowDemoWindowMultiSelect() selection.SetSelected(n, !item_is_selected); } + if (use_columns) + { + ImGui::NextColumn(); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::InputText("###NoLabel", (char*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); + ImGui::PopStyleVar(); + ImGui::NextColumn(); + } + ImGui::PopID(); } } + if (use_columns) + ImGui::Columns(1); + // Apply multi-select requests multi_select_data = ImGui::EndMultiSelect(); selection_ref = (int)(intptr_t)multi_select_data->RangeSrc; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 9f546a5e33a9..cada96d81b2e 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7153,9 +7153,11 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* } // Select All helper shortcut + // Note: we are comparing FocusScope so we don't need to be testing for IsWindowFocused() if (!(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) - if (IsWindowFocused() && g.IO.KeyCtrl && IsKeyPressed(GetKeyIndex(ImGuiKey_A))) - ms->In.RequestSelectAll = true; + if (ms->FocusScopeId == g.NavFocusScopeId && g.ActiveId == 0) + if (g.IO.KeyCtrl && IsKeyPressed(GetKeyIndex(ImGuiKey_A))) + ms->In.RequestSelectAll = true; #ifdef IMGUI_DEBUG_MULTISELECT if (ms->In.RequestClear) printf("[%05d] BeginMultiSelect: RequestClear\n", g.FrameCount); From 0479b188d063e49a3a1a58acf4217b6797804e57 Mon Sep 17 00:00:00 2001 From: omar Date: Wed, 1 Apr 2020 20:14:51 +0200 Subject: [PATCH 009/132] MultiSelect: Comments. Tweak demo. --- imgui.h | 41 +++++++++++++++++++++++------------------ imgui_demo.cpp | 27 ++++++++++++++++----------- imgui_widgets.cpp | 2 +- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/imgui.h b/imgui.h index e57a297527b6..96e86161a25c 100644 --- a/imgui.h +++ b/imgui.h @@ -2738,31 +2738,36 @@ enum ImGuiMultiSelectFlags_ }; // Abstract: -// - This system implements standard multi-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. Clipping is typically provided by ImGuiListClipper. +// - 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, you can handle a simpler form of multi-selection yourself, -// by reacting to click/presses on Selectable() items and checking keyboard modifiers. -// The complexity of this system here is mostly caused by the handling of range-select while optionally allowing to clip elements. +// 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. +// The unusual complexity of this system is mostly caused by supporting SHIFT+Click/Arrow range-select with clipped elements. +// - TreeNode() and Selectable() are supported. // - The work involved to deal with multi-selection differs whether you want to only submit visible items (and clip others) or submit all items // regardless of their visibility. Clipping items is more efficient and will allow you to deal with large lists (1k~100k items) with near zero // performance penalty, but requires a little more work on the code. If you only have a few hundreds elements in your possible selection set, -// you may as well not bother with clipping, as the cost should be negligible (as least on imgui side). +// you may as well not bother with clipping, as the cost should be negligible (as least on Dear ImGui side). // If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards. -// - The void* Src/Dst value represent a selectable object. They are the values you pass to SetNextItemSelectionUserData(). -// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points. But the code never assume that sortable integers are used. -// - In the spirit of imgui design, your code own the selection data. So this is designed to handle all kind of selection data: instructive (store a bool inside each object), -// external array (store an array aside from your objects), set (store only selected items in a hash/map/set), using intervals (store indices in an interval tree), etc. +// - The void* RangeSrc/RangeDst value represent a selectable object. They are the values you pass to SetNextItemSelectionUserData(). +// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points and you will need to interpolate +// 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 RangeSrc to RangeDst). +// - In the spirit of Dear ImGui design, your code own the selection data. So this is designed to handle all kind of selection data: +// e.g. instructive selection (store a bool inside each object), external array (store an array aside from your objects), +// hash/map/set (store only selected items in a hash/map/set), or other structures (store indices in an interval tree), etc. // Usage flow: -// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc and its selection status. As a default value 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 requests by updating your selection data. [Only required if you are using a clipper in step 4] -// 3) Set RangeSrcPassedBy=true if the RangeSrc item is part of the items clipped before the first submitted/visible item. [Only required if you are using a clipper in step 4] +// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc 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 requests by updating your selection data. [Only required if you are using a clipper in step 4] +// 3) Set RangeSrcPassedBy=true if the RangeSrc 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->RangeSrc) { data->RangeSrcPassedBy = true; } +// If you are using integer indices everywhere, this is easy to compute: if (clipper.DisplayStart > (int)data->RangeSrc) { data->RangeSrcPassedBy = true; } // 4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. -// Call IsItemSelectionToggled() to query with the selection state has been toggled, in which you need the info immediately (before EndMultiSelect()) for your display. -// When cannot reliably return a "IsItemSelected()" value because we need to consider clipped (unprocessed) item, this is why we return a toggle event instead. +// Call IsItemToggledSelection() to query if the selection state has been toggled, if you need the info immediately for your display (before EndMultiSelect()). +// When cannot return a "IsItemSelected()" value because we need to consider clipped/unprocessed items, this is why we return a "Toggle" event instead. // 5) Call EndMultiSelect(). Save the value of ->RangeSrc 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. @@ -2771,7 +2776,7 @@ struct ImGuiMultiSelectData bool RequestClear; // Begin, End // Request user to clear selection bool RequestSelectAll; // Begin, End // Request user to select all bool RequestSetRange; // End // Request user to set or clear selection in the [RangeSrc..RangeDst] range - bool RangeSrcPassedBy; // After Begin // Need to be set by user is RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. + bool RangeSrcPassedBy; // In loop // (If clipping) Need to be set by user if RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. bool RangeValue; // End // End: parameter from RequestSetRange request. True = Select Range, False = Unselect range. void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect() void* RangeDst; // End // End: parameter from RequestSetRange request. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 87e2e0bacfec..698e13a79bf3 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2771,12 +2771,15 @@ 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. -struct ExampleSelectionData +// About RefItem: +// - The MultiSelect API requires you to store information about the reference/pivot item (generally the last clicked item). +struct ExampleSelection { ImGuiStorage Storage; - int SelectionSize; // Number of selected items (== number of 1 in the Storage) + int SelectionSize; // Number of selected items (== number of 1 in the Storage, maintained by this class) + int RangeRef; // Reference/pivot item (generally last clicked item) - ExampleSelectionData() { Clear(); } + ExampleSelection() { RangeRef = 0; 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; } @@ -2837,8 +2840,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::TreeNode("Multiple Selection (Full)")) { // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. - static int selection_ref = 0; // Selection pivot (last clicked item, we need to preserve this to handle range-select) - static ExampleSelectionData selection; + static ExampleSelection selection; const char* random_names[] = { "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", @@ -2866,7 +2868,7 @@ static void ShowDemoWindowMultiSelect() if (widget_type == WidgetType_TreeNode) ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection_ref, selection.GetSelected(selection_ref)); + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } @@ -2880,7 +2882,7 @@ static void ShowDemoWindowMultiSelect() clipper.Begin(ITEMS_COUNT); while (clipper.Step()) { - if (clipper.DisplayStart > (int)selection_ref) + if (clipper.DisplayStart > selection.RangeRef) multi_select_data->RangeSrcPassedBy = true; for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) { @@ -2904,12 +2906,15 @@ static void ShowDemoWindowMultiSelect() } else if (widget_type == WidgetType_TreeNode) { - ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_OpenOnDoubleClick; + ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth; + tree_node_flags |= ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; if (item_is_selected) tree_node_flags |= ImGuiTreeNodeFlags_Selected; - ImGui::TreeNodeEx(label, tree_node_flags); + bool open = ImGui::TreeNodeEx(label, tree_node_flags); if (ImGui::IsItemToggledSelection()) selection.SetSelected(n, !item_is_selected); + if (open) + ImGui::TreePop(); } if (use_columns) @@ -2917,7 +2922,7 @@ static void ShowDemoWindowMultiSelect() ImGui::NextColumn(); ImGui::SetNextItemWidth(-FLT_MIN); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); - ImGui::InputText("###NoLabel", (char*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); + ImGui::InputText("###NoLabel", (char*)(void*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); ImGui::PopStyleVar(); ImGui::NextColumn(); } @@ -2931,7 +2936,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests multi_select_data = ImGui::EndMultiSelect(); - selection_ref = (int)(intptr_t)multi_select_data->RangeSrc; + selection.RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index cada96d81b2e..95224cac4729 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6474,7 +6474,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags flags |= ImGuiTreeNodeFlags_OpenOnArrow; // To handle drag and drop of multiple items we need to avoid clearing selection on click. - // Enabling this test makes actions using CTRL+SHIFT delay their effect on the mouse release which is annoying, but it allows drag and drop of multiple items. + // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items. // FIXME-MULTISELECT: Consider opt-in for drag and drop behavior in ImGuiMultiSelectFlags? if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) button_flags |= ImGuiButtonFlags_PressedOnClick; From 3ba3f0d905fb8db4eae1beaf67d7a6df037733f2 Mon Sep 17 00:00:00 2001 From: omar Date: Wed, 1 Apr 2020 20:34:30 +0200 Subject: [PATCH 010/132] MultiSelect: Fix Selectable() ambiguous return value, clarify need to use IsItemToggledSelection(). --- imgui_demo.cpp | 3 ++- imgui_widgets.cpp | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 698e13a79bf3..b4a7f099edb3 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2901,7 +2901,8 @@ static void ShowDemoWindowMultiSelect() ImGui::SetNextItemSelectionUserData(n); if (widget_type == WidgetType_Selectable) { - if (ImGui::Selectable(label, item_is_selected)) + ImGui::Selectable(label, item_is_selected); + if (ImGui::IsItemToggledSelection()) selection.SetSelected(n, !item_is_selected); } else if (widget_type == WidgetType_TreeNode) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 95224cac4729..ba59b802c28d 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6904,8 +6904,9 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (disabled_item && !disabled_global) EndDisabled(); + // Users of BeginMultiSelect() scope: call ImGui::IsItemToggledSelection() to retrieve selection toggle. Selectable() returns a pressed state! IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); - return pressed || (was_selected != selected); //-V1020 + return pressed; //-V1020 } bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) @@ -7144,6 +7145,7 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* } // Auto clear when using Navigation to move within the selection (we compare SelectScopeId so it possible to use multiple lists inside a same window) + // FIXME: Polling key mods after the fact (frame following the move request) is incorrect, but latching it would requires non-trivial change in MultiSelectItemFooter() if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) { if (g.IO.KeyShift) From 00c4b8f2a345f5dbf34fa4471f1acd8b117fdbd7 Mon Sep 17 00:00:00 2001 From: omar Date: Thu, 2 Apr 2020 16:46:53 +0200 Subject: [PATCH 011/132] MultiSelect: Fix testing key mods from after the nav request (remove need to hold the mod longer) --- imgui_internal.h | 2 ++ imgui_widgets.cpp | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index dd58534662fc..005df3065e6c 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -2123,6 +2123,7 @@ struct ImGuiContext bool MultiSelectEnabled; ImGuiMultiSelectFlags MultiSelectFlags; ImGuiMultiSelectState MultiSelectState; // We currently don't support recursing/stacking multi-select + ImGuiKeyChord MultiSelectKeyMods; // Render float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list) @@ -2389,6 +2390,7 @@ struct ImGuiContext MultiSelectEnabled = false; MultiSelectFlags = ImGuiMultiSelectFlags_None; + MultiSelectKeyMods = ImGuiMod_None; DimBgRatio = 0.0f; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index ba59b802c28d..5c344c7b236d 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7138,6 +7138,9 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* g.MultiSelectEnabled = true; g.MultiSelectFlags = flags; + // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame. + g.MultiSelectKeyMods = g.NavJustMovedToId ? g.NavJustMovedToKeyMods : g.IO.KeyMods; + if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0) { ms->In.RangeSrc = ms->Out.RangeSrc = range_ref; @@ -7148,9 +7151,9 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* // FIXME: Polling key mods after the fact (frame following the move request) is incorrect, but latching it would requires non-trivial change in MultiSelectItemFooter() if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) { - if (g.IO.KeyShift) + if (g.MultiSelectKeyMods & ImGuiMod_Shift) ms->InRequestSetRangeNav = true; - if (!g.IO.KeyCtrl && !g.IO.KeyShift) + if ((g.MultiSelectKeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) ms->In.RequestClear = true; } @@ -7233,13 +7236,13 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) if (ms->InRequestSetRangeNav) { IM_ASSERT(id != 0); - IM_ASSERT(g.IO.KeyShift); + IM_ASSERT((g.MultiSelectKeyMods & ImGuiMod_Shift) != 0); const bool is_range_dst = !ms->InRangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. if (is_range_dst) ms->InRangeDstPassedBy = true; if (is_range_src || is_range_dst || ms->In.RangeSrcPassedBy != ms->InRangeDstPassedBy) selected = ms->In.RangeValue; - else if (!g.IO.KeyCtrl) + else if ((g.MultiSelectKeyMods & ImGuiMod_Ctrl) == 0) selected = false; } @@ -7256,9 +7259,9 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) bool selected = *p_selected; bool pressed = *p_pressed; - bool is_ctrl = g.IO.KeyCtrl; - bool is_shift = g.IO.KeyShift; const bool is_multiselect = (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoMultiSelect) == 0; + bool is_ctrl = (g.MultiSelectKeyMods & ImGuiMod_Ctrl) != 0; + bool is_shift = (g.MultiSelectKeyMods & ImGuiMod_Shift) != 0; // Auto-select as you navigate a list if (g.NavJustMovedToId == id) From b9721c1ed71e06588bc822e8ca52c2736c622b3e Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 20 Aug 2020 20:46:57 +0200 Subject: [PATCH 012/132] MultiSelect: Temporary fix/work-around for child/popup to not inherit MultiSelectEnabled flag, until we make mulit-select data stackable. --- imgui_demo.cpp | 10 +++++++++- imgui_internal.h | 6 +++--- imgui_widgets.cpp | 10 +++++----- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index b4a7f099edb3..d7f7aa749993 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2857,7 +2857,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } ImGui::SameLine(); ImGui::Checkbox("Use 2 columns", &use_columns); - ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", (unsigned int*)&ImGui::GetIO().ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); + ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", &ImGui::GetIO().ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); ImGui::SameLine(); HelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range. Keyboard is also supported."); // Open a scrolling region @@ -2918,6 +2918,14 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } + // Right-click: context menu + if (ImGui::BeginPopupContextItem()) + { + ImGui::Text("(Testing Selectable inside an embedded popup)"); + ImGui::Selectable("Close"); + ImGui::EndPopup(); + } + if (use_columns) { ImGui::NextColumn(); diff --git a/imgui_internal.h b/imgui_internal.h index 005df3065e6c..60a0280038cb 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -2120,9 +2120,9 @@ struct ImGuiContext ImVec2 NavWindowingAccumDeltaSize; // Range-Select/Multi-Select - bool MultiSelectEnabled; + ImGuiWindow* MultiSelectEnabledWindow; // FIXME-MULTISELECT: We currently don't support recursing/stacking multi-select ImGuiMultiSelectFlags MultiSelectFlags; - ImGuiMultiSelectState MultiSelectState; // We currently don't support recursing/stacking multi-select + ImGuiMultiSelectState MultiSelectState; ImGuiKeyChord MultiSelectKeyMods; // Render @@ -2388,7 +2388,7 @@ struct ImGuiContext NavWindowingToggleLayer = false; NavWindowingToggleKey = ImGuiKey_None; - MultiSelectEnabled = false; + MultiSelectEnabledWindow = NULL; MultiSelectFlags = ImGuiMultiSelectFlags_None; MultiSelectKeyMods = ImGuiMod_None; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 5c344c7b236d..528a9183348d 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6464,7 +6464,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags const bool was_selected = selected; // Multi-selection support (header) - const bool is_multi_select = g.MultiSelectEnabled; + const bool is_multi_select = (g.MultiSelectEnabledWindow == window); if (is_multi_select) { MultiSelectItemHeader(id, &selected); @@ -6816,7 +6816,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; } // Multi-selection support (header) - const bool is_multi_select = g.MultiSelectEnabled; + const bool is_multi_select = (g.MultiSelectEnabledWindow == window); const bool was_selected = selected; if (is_multi_select) { @@ -7126,7 +7126,7 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - IM_ASSERT(g.MultiSelectEnabled == false); // No recursion allowed yet (we could allow it if we deem it useful) + IM_ASSERT(g.MultiSelectEnabledWindow == NULL); // No recursion allowed yet (we could allow it if we deem it useful) IM_ASSERT(g.MultiSelectFlags == 0); IM_ASSERT(g.MultiSelectState.FocusScopeId == 0); @@ -7135,7 +7135,7 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* ms->Clear(); ms->FocusScopeId = window->IDStack.back(); PushFocusScope(ms->FocusScopeId); - g.MultiSelectEnabled = true; + g.MultiSelectEnabledWindow = window; g.MultiSelectFlags = flags; // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame. @@ -7182,7 +7182,7 @@ ImGuiMultiSelectData* ImGui::EndMultiSelect() ms->Out.RangeValue = true; g.MultiSelectState.FocusScopeId = 0; PopFocusScope(); - g.MultiSelectEnabled = false; + g.MultiSelectEnabledWindow = NULL; g.MultiSelectFlags = ImGuiMultiSelectFlags_None; #ifdef IMGUI_DEBUG_MULTISELECT From ad5d3c9bff8ee15556e6c5ee21234accfd98d3f8 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 17 Jun 2022 15:55:59 +0200 Subject: [PATCH 013/132] MultiSelect: Fixed issue with Ctrl+click on TreeNode + amend demo to test drag and drop. --- imgui_demo.cpp | 25 ++++++++++++++++++++----- imgui_widgets.cpp | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index d7f7aa749993..7b899eb32fe7 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2817,8 +2817,8 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } - IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (Basic)"); - if (ImGui::TreeNode("Multiple Selection (Basic)")) + IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (Simplified)"); + if (ImGui::TreeNode("Multiple Selection (Simplified)")) { HelpMarker("Hold CTRL and click to select multiple items."); static bool selection[5] = { false, false, false, false, false }; @@ -2837,6 +2837,7 @@ static void ShowDemoWindowMultiSelect() } IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (Full)"); + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); if (ImGui::TreeNode("Multiple Selection (Full)")) { // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. @@ -2851,14 +2852,18 @@ static void ShowDemoWindowMultiSelect() // Test both Selectable() and TreeNode() widgets enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; static bool use_columns = false; + static bool use_drag_drop = true; static WidgetType widget_type = WidgetType_TreeNode; if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } ImGui::SameLine(); if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } ImGui::SameLine(); ImGui::Checkbox("Use 2 columns", &use_columns); + ImGui::SameLine(); + ImGui::Checkbox("Use drag & drop", &use_drag_drop); ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", &ImGui::GetIO().ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); ImGui::SameLine(); HelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range. Keyboard is also supported."); + ImGui::Text("Selection size: %d", selection.GetSelectionSize()); // Open a scrolling region const int ITEMS_COUNT = 1000; @@ -2869,7 +2874,7 @@ static void ShowDemoWindowMultiSelect() ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); - if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } if (use_columns) @@ -2904,6 +2909,11 @@ static void ShowDemoWindowMultiSelect() ImGui::Selectable(label, item_is_selected); if (ImGui::IsItemToggledSelection()) selection.SetSelected(n, !item_is_selected); + if (use_drag_drop && ImGui::BeginDragDropSource()) + { + ImGui::Text("(Dragging %d items)", selection.GetSelectionSize()); + ImGui::EndDragDropSource(); + } } else if (widget_type == WidgetType_TreeNode) { @@ -2914,6 +2924,11 @@ static void ShowDemoWindowMultiSelect() bool open = ImGui::TreeNodeEx(label, tree_node_flags); if (ImGui::IsItemToggledSelection()) selection.SetSelected(n, !item_is_selected); + if (use_drag_drop && ImGui::BeginDragDropSource()) + { + ImGui::Text("(Dragging %d items)", selection.GetSelectionSize()); + ImGui::EndDragDropSource(); + } if (open) ImGui::TreePop(); } @@ -2946,9 +2961,9 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests multi_select_data = ImGui::EndMultiSelect(); selection.RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; - if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } - if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } + if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 528a9183348d..5f245dd2b629 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6477,7 +6477,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items. // FIXME-MULTISELECT: Consider opt-in for drag and drop behavior in ImGuiMultiSelectFlags? if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) - button_flags |= ImGuiButtonFlags_PressedOnClick; + button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; else button_flags |= ImGuiButtonFlags_PressedOnClickRelease; } From 919cac14829e3e597c2886e7e655a82545ab714a Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 11 Apr 2023 17:38:23 +0200 Subject: [PATCH 014/132] MultiSelect: Demo: Add a simpler version. --- imgui_demo.cpp | 122 +++++++++++++++++++++++++++++++++++----------- imgui_internal.h | 2 +- imgui_widgets.cpp | 2 +- 3 files changed, 96 insertions(+), 30 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 7b899eb32fe7..3e50d095468e 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2817,8 +2817,8 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } - IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (Simplified)"); - if (ImGui::TreeNode("Multiple Selection (Simplified)")) + IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (simplfied, manual)"); + if (ImGui::TreeNode("Multiple Selection (simplified, manual)")) { HelpMarker("Hold CTRL and click to select multiple items."); static bool selection[5] = { false, false, false, false, false }; @@ -2836,33 +2836,88 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } - IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (Full)"); + const char* random_names[] = + { + "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", + "Bitter Gourd", "Bok Choy", "Broccoli", "Brussels Sprouts", "Burdock Root", "Cabbage", "Calabash", "Capers", "Carrot", "Cassava", + "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Celtuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" + }; + + // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. + // SHIFT+Click w/ CTRL and other standard features are supported. + IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full)"); //ImGui::SetNextItemOpen(true, ImGuiCond_Once); - if (ImGui::TreeNode("Multiple Selection (Full)")) + if (ImGui::TreeNode("Multiple Selection (full)")) { - // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. static ExampleSelection selection; - const char* random_names[] = + + ImGui::Text("Supported features:"); + ImGui::BulletText("Keyboard navigation (arrows, page up/down, home/end, space)."); + ImGui::BulletText("Ctrl modifier to preserve and toggle selection."); + ImGui::BulletText("Shift modifier for range selection."); + ImGui::BulletText("CTRL+A to select all."); + + // The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling regions). + const int ITEMS_COUNT = 50; + ImGui::Text("Selection size: %d", selection.GetSelectionSize()); + if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", - "Bitter Gourd", "Bok Choy", "Broccoli", "Brussels Sprouts", "Burdock Root", "Cabbage", "Calabash", "Capers", "Carrot", "Cassava", - "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Celtuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" - }; + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); + if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } + + for (int n = 0; n < ITEMS_COUNT; n++) + { + // FIXME-MULTISELECT: This should not be needed but currently is because coarse clipping break the auto-setup. + if (n > selection.RangeRef) + multi_select_data->RangeSrcPassedBy = true; + + char label[64]; + sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); + + bool item_is_selected = selection.GetSelected(n); + ImGui::SetNextItemSelectionData((void*)(intptr_t)n); + ImGui::Selectable(label, item_is_selected); + if (ImGui::IsItemToggledSelection()) + selection.SetSelected(n, !item_is_selected); + } + + // Apply multi-select requests + multi_select_data = ImGui::EndMultiSelect(); + selection.RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; + if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } + if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } + + ImGui::EndListBox(); + } + + ImGui::TreePop(); + } + + // Advanced demonstration of BeginMultiSelect() + // - Showcase clipping. + // - Showcase basic drag and drop. + // - Showcase TreeNode variant (note that tree node don't expand in the demo: supporting expanding tree nodes + clipping a separate thing). + // - Showcase using inside a table. + IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full, advanced)"); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (ImGui::TreeNode("Multiple Selection (full, advanced)")) + { + // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. + static ExampleSelection selection; // Test both Selectable() and TreeNode() widgets enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; - static bool use_columns = false; + static bool use_table = false; static bool use_drag_drop = true; static WidgetType widget_type = WidgetType_TreeNode; if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } ImGui::SameLine(); if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } - ImGui::SameLine(); - ImGui::Checkbox("Use 2 columns", &use_columns); - ImGui::SameLine(); + ImGui::Checkbox("Use table", &use_table); ImGui::Checkbox("Use drag & drop", &use_drag_drop); - ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", &ImGui::GetIO().ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); - ImGui::SameLine(); HelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range. Keyboard is also supported."); + ImGui::Text("Selection size: %d", selection.GetSelectionSize()); // Open a scrolling region @@ -2874,23 +2929,31 @@ static void ShowDemoWindowMultiSelect() ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); - if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } - if (use_columns) + if (use_table) { - ImGui::Columns(2); - //ImGui::PushStyleVar(ImGuiStyleVar_FrtemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f)); + ImGui::BeginTable("##Split", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_NoPadOuterX); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.70f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.30f); + //ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); } ImGuiListClipper clipper; clipper.Begin(ITEMS_COUNT); while (clipper.Step()) { + // IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeRef was passed over. if (clipper.DisplayStart > selection.RangeRef) multi_select_data->RangeSrcPassedBy = true; + for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) { + if (use_table) + ImGui::TableNextColumn(); + ImGui::PushID(n); const char* category = random_names[n % IM_ARRAYSIZE(random_names)]; char label[64]; @@ -2899,7 +2962,7 @@ static void ShowDemoWindowMultiSelect() // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part // of the selection scope doesn't erroneously alter our selection (FIXME-TESTS: Add a test for that!). - ImU32 dummy_col = (ImU32)ImGui::GetID(label); + ImU32 dummy_col = (ImU32)((unsigned int)n * 0xC250B74B) | IM_COL32_A_MASK; ImGui::ColorButton("##", ImColor(dummy_col), ImGuiColorEditFlags_NoTooltip, color_button_sz); ImGui::SameLine(); @@ -2941,35 +3004,38 @@ static void ShowDemoWindowMultiSelect() ImGui::EndPopup(); } - if (use_columns) + if (use_table) { - ImGui::NextColumn(); + ImGui::TableNextColumn(); ImGui::SetNextItemWidth(-FLT_MIN); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); ImGui::InputText("###NoLabel", (char*)(void*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); ImGui::PopStyleVar(); - ImGui::NextColumn(); } ImGui::PopID(); } } - if (use_columns) - ImGui::Columns(1); + if (use_table) + { + ImGui::EndTable(); + ImGui::PopStyleVar(); + } // Apply multi-select requests multi_select_data = ImGui::EndMultiSelect(); selection.RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; - if (multi_select_data->RequestClear) { selection.Clear(); } + if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } - if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } + if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); ImGui::EndListBox(); } + ImGui::TreePop(); } ImGui::TreePop(); diff --git a/imgui_internal.h b/imgui_internal.h index 60a0280038cb..88eaf1b283b1 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1715,7 +1715,7 @@ struct ImGuiOldColumns struct IMGUI_API ImGuiMultiSelectState { - ImGuiID FocusScopeId; // Same as CurrentWindow->DC.FocusScopeIdCurrent (unless another selection scope was pushed manually) + ImGuiID FocusScopeId; // Same as g.CurrentFocusScopeId (unless another selection scope was pushed manually) ImGuiMultiSelectData In; // The In requests are set and returned by BeginMultiSelect() ImGuiMultiSelectData Out; // The Out requests are finalized and returned by EndMultiSelect() bool InRangeDstPassedBy; // (Internal) set by the the item that match NavJustMovedToId when InRequestRangeSetNav is set. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 5f245dd2b629..3729283f9636 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7229,7 +7229,7 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) const bool is_range_src = (ms->In.RangeSrc == item_data); if (is_range_src) - ms->In.RangeSrcPassedBy = true; + ms->In.RangeSrcPassedBy = true; // FIXME-MULTISELECT: The promise that this would be automatically done is not because of ItemAdd() clipping. // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) // For this to work, IF the user is clipping items, they need to set RangeSrcPassedBy = true to notify the system. From 19086c1c48974c72b76c6dee064b8f90145930be Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 11 Apr 2023 17:59:06 +0200 Subject: [PATCH 015/132] MultiSelect: Added ImGuiMultiSelectFlags_ClearOnEscape (unsure of best design), expose IsFocused for custom shortcuts. --- imgui.h | 5 ++++- imgui_demo.cpp | 5 +++-- imgui_widgets.cpp | 30 ++++++++++++++++++++---------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/imgui.h b/imgui.h index 96e86161a25c..0cf6f43ed9a5 100644 --- a/imgui.h +++ b/imgui.h @@ -2725,7 +2725,7 @@ 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), +// 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. @@ -2735,6 +2735,7 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0, ImGuiMultiSelectFlags_NoUnselect = 1 << 1, // Disable unselecting items with CTRL+Click, CTRL+Space etc. ImGuiMultiSelectFlags_NoSelectAll = 1 << 2, // Disable CTRL+A shortcut to set RequestSelectAll + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 3, // Enable ESC shortcut to clear selection }; // Abstract: @@ -2773,6 +2774,7 @@ enum ImGuiMultiSelectFlags_ // If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable() on a per-item basis. struct ImGuiMultiSelectData { + bool IsFocused; // Begin // Set if currently focusing the selection scope (any item of the selection). May be used if you have custom shortcut associated to selection. bool RequestClear; // Begin, End // Request user to clear selection bool RequestSelectAll; // Begin, End // Request user to select all bool RequestSetRange; // End // Request user to set or clear selection in the [RangeSrc..RangeDst] range @@ -2785,6 +2787,7 @@ struct ImGuiMultiSelectData ImGuiMultiSelectData() { Clear(); } void Clear() { + IsFocused = false; RequestClear = RequestSelectAll = RequestSetRange = RangeSrcPassedBy = RangeValue = false; RangeSrc = RangeDst = NULL; RangeDirection = 0; diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 3e50d095468e..023d47cf6b90 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2862,7 +2862,8 @@ static void ShowDemoWindowMultiSelect() ImGui::Text("Selection size: %d", selection.GetSelectionSize()); if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } @@ -2876,7 +2877,7 @@ static void ShowDemoWindowMultiSelect() sprintf(label, "Object %05d: %s", n, random_names[n % 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); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 3729283f9636..ba4cce289adf 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7138,6 +7138,9 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* g.MultiSelectEnabledWindow = window; g.MultiSelectFlags = flags; + // Report focus + ms->In.IsFocused = ms->Out.IsFocused = (ms->FocusScopeId == g.NavFocusScopeId); + // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame. g.MultiSelectKeyMods = g.NavJustMovedToId ? g.NavJustMovedToKeyMods : g.IO.KeyMods; @@ -7157,16 +7160,23 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* ms->In.RequestClear = true; } - // Select All helper shortcut - // Note: we are comparing FocusScope so we don't need to be testing for IsWindowFocused() - if (!(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) - if (ms->FocusScopeId == g.NavFocusScopeId && g.ActiveId == 0) - if (g.IO.KeyCtrl && IsKeyPressed(GetKeyIndex(ImGuiKey_A))) + // Shortcuts + if (ms->In.IsFocused) + { + // Select All helper shortcut (CTRL+A) + // Note: we are comparing FocusScope so we don't need to be testing for IsWindowFocused() + if (!(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) + if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) ms->In.RequestSelectAll = true; + if (flags & ImGuiMultiSelectFlags_ClearOnEscape) + if (Shortcut(ImGuiKey_Escape)) + ms->In.RequestClear = true; + } + #ifdef IMGUI_DEBUG_MULTISELECT - if (ms->In.RequestClear) printf("[%05d] BeginMultiSelect: RequestClear\n", g.FrameCount); - if (ms->In.RequestSelectAll) printf("[%05d] BeginMultiSelect: RequestSelectAll\n", g.FrameCount); + if (ms->In.RequestClear) IMGUI_DEBUG_LOG("BeginMultiSelect: RequestClear\n"); + if (ms->In.RequestSelectAll) IMGUI_DEBUG_LOG("BeginMultiSelect: RequestSelectAll\n"); #endif return &ms->In; @@ -7186,9 +7196,9 @@ ImGuiMultiSelectData* ImGui::EndMultiSelect() g.MultiSelectFlags = ImGuiMultiSelectFlags_None; #ifdef IMGUI_DEBUG_MULTISELECT - if (ms->Out.RequestClear) printf("[%05d] EndMultiSelect: RequestClear\n", g.FrameCount); - if (ms->Out.RequestSelectAll) printf("[%05d] EndMultiSelect: RequestSelectAll\n", g.FrameCount); - if (ms->Out.RequestSetRange) printf("[%05d] EndMultiSelect: RequestSetRange %p..%p = %d\n", g.FrameCount, ms->Out.RangeSrc, ms->Out.RangeDst, ms->Out.RangeValue); + if (ms->Out.RequestClear) IMGUI_DEBUG_LOG("EndMultiSelect: RequestClear\n"); + if (ms->Out.RequestSelectAll) IMGUI_DEBUG_LOG("EndMultiSelect: RequestSelectAll\n"); + if (ms->Out.RequestSetRange) IMGUI_DEBUG_LOG("EndMultiSelect: RequestSetRange %p..%p = %d\n", ms->Out.RangeSrc, ms->Out.RangeDst, ms->Out.RangeValue); #endif return &ms->Out; From b91ae122e15e087adb19a68df04e7c9be5e93457 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 11 Apr 2023 19:33:38 +0200 Subject: [PATCH 016/132] MultiSelect: Demo: Added pointer indirection and indent level. This is to reduce noise for upcoming commits, ahead of adding a loop here. --- imgui_demo.cpp | 199 ++++++++++++++++++++++++++----------------------- 1 file changed, 104 insertions(+), 95 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 023d47cf6b90..9a3d2363e6a8 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2906,9 +2906,10 @@ static void ShowDemoWindowMultiSelect() if (ImGui::TreeNode("Multiple Selection (full, advanced)")) { // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. - static ExampleSelection selection; + static ExampleSelection selections_data[1]; + ExampleSelection* selection = &selections_data[0]; - // Test both Selectable() and TreeNode() widgets + // Options enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; static bool use_table = false; static bool use_drag_drop = true; @@ -2919,103 +2920,124 @@ static void ShowDemoWindowMultiSelect() ImGui::Checkbox("Use table", &use_table); ImGui::Checkbox("Use drag & drop", &use_drag_drop); - ImGui::Text("Selection size: %d", selection.GetSelectionSize()); - - // Open a scrolling region - const int ITEMS_COUNT = 1000; - if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) + // (spare brace to add a loop here later) { - ImVec2 color_button_sz(ImGui::GetFontSize(), ImGui::GetFontSize()); - if (widget_type == WidgetType_TreeNode) - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - - ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); - if (multi_select_data->RequestClear) { selection.Clear(); } - if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } + ImGui::Text("Selection size: %d", selection->GetSelectionSize()); - if (use_table) + // Open a scrolling region + const int ITEMS_COUNT = 1000; + if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f)); - ImGui::BeginTable("##Split", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_NoPadOuterX); - ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.70f); - ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.30f); - //ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - } + ImVec2 color_button_sz(ImGui::GetFontSize(), ImGui::GetFontSize()); + if (widget_type == WidgetType_TreeNode) + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - ImGuiListClipper clipper; - clipper.Begin(ITEMS_COUNT); - while (clipper.Step()) - { - // IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeRef was passed over. - if (clipper.DisplayStart > selection.RangeRef) - multi_select_data->RangeSrcPassedBy = true; + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection->RangeRef, selection->GetSelected(selection->RangeRef)); + if (multi_select_data->RequestClear) { selection->Clear(); } + if (multi_select_data->RequestSelectAll) { selection->SelectAll(ITEMS_COUNT); } - for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) + if (use_table) { - if (use_table) - ImGui::TableNextColumn(); - - ImGui::PushID(n); - const char* category = random_names[n % IM_ARRAYSIZE(random_names)]; - char label[64]; - sprintf(label, "Object %05d (category: %s)", n, category); - bool item_is_selected = selection.GetSelected(n); - - // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part - // of the selection scope doesn't erroneously alter our selection (FIXME-TESTS: Add a test for that!). - ImU32 dummy_col = (ImU32)((unsigned int)n * 0xC250B74B) | IM_COL32_A_MASK; - ImGui::ColorButton("##", ImColor(dummy_col), ImGuiColorEditFlags_NoTooltip, color_button_sz); - ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f)); + ImGui::BeginTable("##Split", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_NoPadOuterX); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.70f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.30f); + //ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); + } - ImGui::SetNextItemSelectionUserData(n); - if (widget_type == WidgetType_Selectable) + ImGuiListClipper clipper; + clipper.Begin(ITEMS_COUNT); + while (clipper.Step()) + { + // IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeRef was passed over. + if (clipper.DisplayStart > selection->RangeRef) + multi_select_data->RangeSrcPassedBy = true; + + for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) { - ImGui::Selectable(label, item_is_selected); - if (ImGui::IsItemToggledSelection()) - selection.SetSelected(n, !item_is_selected); - if (use_drag_drop && ImGui::BeginDragDropSource()) + if (use_table) + ImGui::TableNextColumn(); + + ImGui::PushID(n); + const char* category = random_names[n % IM_ARRAYSIZE(random_names)]; + char label[64]; + sprintf(label, "Object %05d (category: %s)", n, category); + bool item_is_selected = selection->GetSelected(n); + + // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part + // of the selection scope doesn't erroneously alter our selection (FIXME-TESTS: Add a test for that!). + ImU32 dummy_col = (ImU32)((unsigned int)n * 0xC250B74B) | IM_COL32_A_MASK; + ImGui::ColorButton("##", ImColor(dummy_col), ImGuiColorEditFlags_NoTooltip, color_button_sz); + ImGui::SameLine(); + + ImGui::SetNextItemSelectionUserData(n); + if (widget_type == WidgetType_Selectable) { - ImGui::Text("(Dragging %d items)", selection.GetSelectionSize()); - ImGui::EndDragDropSource(); + ImGui::Selectable(label, item_is_selected); + if (ImGui::IsItemToggledSelection()) + selection->SetSelected(n, !item_is_selected); + if (use_drag_drop && ImGui::BeginDragDropSource()) + { + ImGui::Text("(Dragging %d items)", selection->GetSelectionSize()); + ImGui::EndDragDropSource(); + } } - } - else if (widget_type == WidgetType_TreeNode) - { - ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth; - tree_node_flags |= ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; - if (item_is_selected) - tree_node_flags |= ImGuiTreeNodeFlags_Selected; - bool open = ImGui::TreeNodeEx(label, tree_node_flags); - if (ImGui::IsItemToggledSelection()) - selection.SetSelected(n, !item_is_selected); - if (use_drag_drop && ImGui::BeginDragDropSource()) + else if (widget_type == WidgetType_TreeNode) { - ImGui::Text("(Dragging %d items)", selection.GetSelectionSize()); - ImGui::EndDragDropSource(); + ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth; + tree_node_flags |= ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (item_is_selected) + tree_node_flags |= ImGuiTreeNodeFlags_Selected; + bool open = ImGui::TreeNodeEx(label, tree_node_flags); + if (ImGui::IsItemToggledSelection()) + selection->SetSelected(n, !item_is_selected); + if (use_drag_drop && ImGui::BeginDragDropSource()) + { + ImGui::Text("(Dragging %d items)", selection->GetSelectionSize()); + ImGui::EndDragDropSource(); + } + if (open) + ImGui::TreePop(); } - if (open) - ImGui::TreePop(); - } - // Right-click: context menu - if (ImGui::BeginPopupContextItem()) - { - ImGui::Text("(Testing Selectable inside an embedded popup)"); - ImGui::Selectable("Close"); - ImGui::EndPopup(); - } + // Right-click: context menu + if (ImGui::BeginPopupContextItem()) + { + ImGui::Text("(Testing Selectable inside an embedded popup)"); + ImGui::Selectable("Close"); + ImGui::EndPopup(); + } - if (use_table) - { - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(-FLT_MIN); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); - ImGui::InputText("###NoLabel", (char*)(void*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); - ImGui::PopStyleVar(); + if (use_table) + { + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::InputText("###NoLabel", (char*)(void*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); + ImGui::PopStyleVar(); + } + + ImGui::PopID(); } + } - ImGui::PopID(); + if (use_table) + { + ImGui::EndTable(); + ImGui::PopStyleVar(); } + + // Apply multi-select requests + multi_select_data = ImGui::EndMultiSelect(); + selection->RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; + if (multi_select_data->RequestClear) { selection->Clear(); } + if (multi_select_data->RequestSelectAll) { selection->SelectAll(ITEMS_COUNT); } + if (multi_select_data->RequestSetRange) { selection->SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } + + if (widget_type == WidgetType_TreeNode) + ImGui::PopStyleVar(); + + ImGui::EndListBox(); } if (use_table) @@ -3023,20 +3045,7 @@ static void ShowDemoWindowMultiSelect() ImGui::EndTable(); ImGui::PopStyleVar(); } - - // Apply multi-select requests - multi_select_data = ImGui::EndMultiSelect(); - selection.RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; - if (multi_select_data->RequestClear) { selection.Clear(); } - if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } - if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } - - if (widget_type == WidgetType_TreeNode) - ImGui::PopStyleVar(); - - ImGui::EndListBox(); } - ImGui::TreePop(); } ImGui::TreePop(); From 35bbadcf0c78634e0b5547f1670ccd304f25835a Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 11 Apr 2023 19:40:02 +0200 Subject: [PATCH 017/132] MultiSelect: Added ImGuiMultiSelectFlags_ClearOnClickWindowVoid. + Demo: showcase multiple selection scopes in same window. --- imgui.h | 12 ++++++----- imgui_demo.cpp | 51 ++++++++++++++++++++++++++++++++--------------- imgui_internal.h | 1 + imgui_widgets.cpp | 11 ++++++++++ 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/imgui.h b/imgui.h index 0cf6f43ed9a5..3268688d0428 100644 --- a/imgui.h +++ b/imgui.h @@ -2731,11 +2731,13 @@ struct ImColor // you can handle single-selection in a simpler manner by just calling Selectable() and reacting on clicks yourself. enum ImGuiMultiSelectFlags_ { - ImGuiMultiSelectFlags_None = 0, - ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0, - ImGuiMultiSelectFlags_NoUnselect = 1 << 1, // Disable unselecting items with CTRL+Click, CTRL+Space etc. - ImGuiMultiSelectFlags_NoSelectAll = 1 << 2, // Disable CTRL+A shortcut to set RequestSelectAll - ImGuiMultiSelectFlags_ClearOnEscape = 1 << 3, // Enable ESC shortcut to clear selection + ImGuiMultiSelectFlags_None = 0, + ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0, + ImGuiMultiSelectFlags_NoUnselect = 1 << 1, // Disable unselecting items with CTRL+Click, CTRL+Space etc. + ImGuiMultiSelectFlags_NoSelectAll = 1 << 2, // Disable CTRL+A shortcut to set RequestSelectAll + ImGuiMultiSelectFlags_ClearOnClickWindowVoid= 1 << 3, // Clear selection when clicking on empty location within host window (use if BeginMultiSelect() covers a whole window) + //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) + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 5, // Clear selection when pressing Escape while scope is focused. }; // Abstract: diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 9a3d2363e6a8..b683c495b2e9 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2900,42 +2900,64 @@ static void ShowDemoWindowMultiSelect() // - Showcase clipping. // - Showcase basic drag and drop. // - Showcase TreeNode variant (note that tree node don't expand in the demo: supporting expanding tree nodes + clipping a separate thing). + // - Showcase having multiple multi-selection scopes in the same window. // - Showcase using inside a table. IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full, advanced)"); ImGui::SetNextItemOpen(true, ImGuiCond_Once); if (ImGui::TreeNode("Multiple Selection (full, advanced)")) { - // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. - static ExampleSelection selections_data[1]; - ExampleSelection* selection = &selections_data[0]; - // Options enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; static bool use_table = false; static bool use_drag_drop = true; + static bool multiple_selection_scopes = false; static WidgetType widget_type = WidgetType_TreeNode; if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } ImGui::SameLine(); if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } ImGui::Checkbox("Use table", &use_table); ImGui::Checkbox("Use drag & drop", &use_drag_drop); + ImGui::Checkbox("Distinct selection scopes in same window", &multiple_selection_scopes); - // (spare brace to add a loop here later) + // When 'multiple_selection_scopes' is set we show 3 selection scopes in the host window instead of 1 in a scrolling window. + static ExampleSelection selections_data[3]; + const int selection_scope_count = multiple_selection_scopes ? 3 : 1; + for (int selection_scope_n = 0; selection_scope_n < selection_scope_count; selection_scope_n++) { - ImGui::Text("Selection size: %d", selection->GetSelectionSize()); + ExampleSelection* selection = &selections_data[selection_scope_n]; + + const int ITEMS_COUNT = multiple_selection_scopes ? 12 : 1000; + ImGui::PushID(selection_scope_n); // Open a scrolling region - const int ITEMS_COUNT = 1000; - if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) + bool draw_selection = true; + if (multiple_selection_scopes) + { + ImGui::SeparatorText("Selection scope"); + } + else + { + ImGui::Text("Selection size: %d", selection->GetSelectionSize()); + draw_selection = ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20)); + } + if (draw_selection) { ImVec2 color_button_sz(ImGui::GetFontSize(), ImGui::GetFontSize()); if (widget_type == WidgetType_TreeNode) ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection->RangeRef, selection->GetSelected(selection->RangeRef)); + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; + if (multiple_selection_scopes) + ;// flags |= ImGuiMultiSelectFlags_ClearOnClickRectVoid; + else + flags |= ImGuiMultiSelectFlags_ClearOnClickWindowVoid; + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection->RangeRef, selection->GetSelected(selection->RangeRef)); if (multi_select_data->RequestClear) { selection->Clear(); } if (multi_select_data->RequestSelectAll) { selection->SelectAll(ITEMS_COUNT); } + if (multiple_selection_scopes) + ImGui::Text("Selection size: %d", selection->GetSelectionSize()); // Draw counter below Separator and after BeginMultiSelect() + if (use_table) { ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f)); @@ -3037,15 +3059,12 @@ static void ShowDemoWindowMultiSelect() if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); - ImGui::EndListBox(); + if (multiple_selection_scopes == false) + ImGui::EndListBox(); } + ImGui::PopID(); // ImGui::PushID(selection_scope_n); + } // for each selection scope (1 or 3) - if (use_table) - { - ImGui::EndTable(); - ImGui::PopStyleVar(); - } - } ImGui::TreePop(); } ImGui::TreePop(); diff --git a/imgui_internal.h b/imgui_internal.h index 88eaf1b283b1..757204567ca7 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1720,6 +1720,7 @@ struct IMGUI_API ImGuiMultiSelectState ImGuiMultiSelectData Out; // The Out requests are finalized and returned by EndMultiSelect() bool InRangeDstPassedBy; // (Internal) set by the the item that match NavJustMovedToId when InRequestRangeSetNav is set. bool InRequestSetRangeNav; // (Internal) set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. + //ImRect Rect; // Extent of selection scope between BeginMultiSelect() / EndMultiSelect(), used by ImGuiMultiSelectFlags_ClearOnClickRectVoid. ImGuiMultiSelectState() { Clear(); } void Clear() { FocusScopeId = 0; In.Clear(); Out.Clear(); InRangeDstPassedBy = InRequestSetRangeNav = false; } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index ba4cce289adf..99f332640a51 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7188,6 +7188,17 @@ ImGuiMultiSelectData* ImGui::EndMultiSelect() ImGuiMultiSelectState* ms = &g.MultiSelectState; IM_ASSERT(g.MultiSelectState.FocusScopeId == g.CurrentFocusScopeId); + // Clear selection when clicking void? + // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! + if (g.MultiSelectFlags & ImGuiMultiSelectFlags_ClearOnClickWindowVoid) + if (IsWindowHovered() && g.HoveredId == 0) + if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) + { + ms->Out.RequestClear = true; + ms->Out.RequestSelectAll = ms->Out.RequestSetRange = false; + } + + // Unwind if (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect) ms->Out.RangeValue = true; g.MultiSelectState.FocusScopeId = 0; From a05700e3272d59c255846fdf36a013849b0fe5ac Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 12 Apr 2023 19:44:41 +0200 Subject: [PATCH 018/132] MultiSelect: Enter doesn't alter selection (unlike Space). Fix for changes done in 5606. --- imgui_widgets.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 99f332640a51..aa067502e1f3 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7307,7 +7307,11 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) } } - if (pressed) + // Unlike Space, Enter doesn't alter selection (but can still return a press) + const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput); + + // Alter selection + if (pressed && !enter_pressed) { //------------------------------------------------------------------------------------------------------------------------------------------------- // ACTION | Begin | Item Old | Item New | End @@ -7319,7 +7323,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Mouse Pressed, Ctrl=0, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange //------------------------------------------------------------------------------------------------------------------------------------------------- - ImGuiInputSource input_source = (g.NavJustMovedToId != 0 && g.NavWindow == window && g.NavJustMovedToId == g.LastItemData.ID) ? g.NavInputSource : ImGuiInputSource_Mouse; + ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; if (is_shift && is_multiselect) { ms->Out.RequestSetRange = true; @@ -7335,7 +7339,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ms->Out.RangeValue = selected; } - if (input_source == ImGuiInputSource_Mouse) + if (input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) { // Mouse click without CTRL clears the selection, unless the clicked item is already selected bool preserve_existing_selection = g.DragDropActive; From 78cb1661cb5fb0ebc9fb6d56f670b5d9a030ef33 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 12 Apr 2023 19:48:58 +0200 Subject: [PATCH 019/132] MultiSelect: Shallow tweaks/refactors. Including moving IsFocused back internally for now. --- imgui.h | 46 ++++++++++++++++++---------------- imgui_demo.cpp | 51 +++++++++++++++++++++---------------- imgui_internal.h | 17 ++++++------- imgui_widgets.cpp | 64 +++++++++++++++++++++-------------------------- 4 files changed, 88 insertions(+), 90 deletions(-) diff --git a/imgui.h b/imgui.h index 3268688d0428..987d7e7fb857 100644 --- a/imgui.h +++ b/imgui.h @@ -2754,6 +2754,7 @@ enum ImGuiMultiSelectFlags_ // you may as well not bother with clipping, as the cost should be negligible (as least on Dear ImGui side). // If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards. // - The void* RangeSrc/RangeDst value represent a selectable object. They are the values you pass to SetNextItemSelectionUserData(). +// Most likely you will want to store an index here. // Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points and you will need to interpolate // 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 RangeSrc to RangeDst). @@ -2761,35 +2762,36 @@ enum ImGuiMultiSelectFlags_ // e.g. instructive selection (store a bool inside each object), external array (store an array aside from your objects), // hash/map/set (store only selected items in a hash/map/set), or other structures (store indices in an interval tree), etc. // Usage flow: -// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc 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 requests by updating your selection data. [Only required if you are using a clipper in step 4] -// 3) Set RangeSrcPassedBy=true if the RangeSrc 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->RangeSrc) { data->RangeSrcPassedBy = true; } -// 4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. -// Call IsItemToggledSelection() to query if the selection state has been toggled, if you need the info immediately for your display (before EndMultiSelect()). -// When cannot return a "IsItemSelected()" value because we need to consider clipped/unprocessed items, this is why we return a "Toggle" event instead. -// 5) Call EndMultiSelect(). Save the value of ->RangeSrc 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) +// Begin +// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc 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 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 RangeSrc 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->RangeSrc) { data->RangeSrcPassedBy = true; } +// 4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. +// Call IsItemToggledSelection() to query if the selection state has been toggled, if you need the info immediately for your display (before EndMultiSelect()). +// When cannot return a "IsItemSelected()" value because we need to consider clipped/unprocessed items, this is why we return a "Toggle" event instead. +// End +// 5) Call EndMultiSelect(). Save the value of ->RangeSrc 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. struct ImGuiMultiSelectData { - bool IsFocused; // Begin // Set if currently focusing the selection scope (any item of the selection). May be used if you have custom shortcut associated to selection. - bool RequestClear; // Begin, End // Request user to clear selection - bool RequestSelectAll; // Begin, End // Request user to select all - bool RequestSetRange; // End // Request user to set or clear selection in the [RangeSrc..RangeDst] range - bool RangeSrcPassedBy; // In loop // (If clipping) Need to be set by user if RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. - bool RangeValue; // End // End: parameter from RequestSetRange request. True = Select Range, False = Unselect range. - void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect() - void* RangeDst; // End // End: parameter from RequestSetRange request. - int RangeDirection; // End // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -1 otherwise. Available as an indicator in case you cannot infer order from the void* values. + bool RequestClear; // Begin, End // 1. Request user to clear selection + bool RequestSelectAll; // Begin, End // 2. Request user to select all + bool RequestSetRange; // End // 3. Request user to set or clear selection in the [RangeSrc..RangeDst] range + bool RangeSrcPassedBy; // Loop // (If clipping) Need to be set by user if RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. + bool RangeValue; // End // End: parameter from RequestSetRange request. true = Select Range, false = Unselect Range. + void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect() + void* RangeDst; // End // End: parameter from RequestSetRange request. + int RangeDirection; // End // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -1 otherwise. Available as an indicator in case you cannot infer order from the void* values. ImGuiMultiSelectData() { Clear(); } void Clear() { - IsFocused = false; RequestClear = RequestSelectAll = RequestSetRange = RangeSrcPassedBy = RangeValue = false; RangeSrc = RangeDst = NULL; RangeDirection = 0; diff --git a/imgui_demo.cpp b/imgui_demo.cpp index b683c495b2e9..c8fc424dce29 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -608,6 +608,7 @@ void ImGui::ShowDemoWindow(bool* p_open) static void ShowDemoWindowWidgets() { IMGUI_DEMO_MARKER("Widgets"); + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); if (!ImGui::CollapsingHeader("Widgets")) return; @@ -1353,6 +1354,7 @@ static void ShowDemoWindowWidgets() } IMGUI_DEMO_MARKER("Widgets/Selectables"); + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); if (ImGui::TreeNode("Selectables")) { // Selectable() has 2 overloads: @@ -2759,26 +2761,28 @@ static void ShowDemoWindowWidgets() } } -// [Advanced] Helper class to simulate storage of a multi-selection state, used by the advanced multi-selection demos. +// [Advanced] Helper class to simulate storage of a multi-selection state, used by the BeginMultiSelect() demos. // We use ImGuiStorage (simple key->value storage) to avoid external dependencies but it's probably not optimal. // To store a single-selection: // - You only need a single variable and don't need any of this! // To store a multi-selection, in your real application you could: // - Use intrusively stored selection (e.g. 'bool IsSelected' inside your object). This is by far the simplest // way to store your selection data, but it means you cannot have multiple simultaneous views over your objects. -// This is what may of the simpler demos in this file are using (so they are not using this class). -// - Otherwise, any externally stored unordered_set/set/hash/map/interval trees (storing indices, objects id, etc.) +// This is what many of the simpler demos in this file are using (so they are not using this class). +// - Use external storage: e.g. unordered_set/set/hash/map/interval trees (storing indices, objects id, etc.) // 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. +// your storage, so "Select All" becomes "Negative=1 + Clear" and then sparse unselect can add to the storage. // About RefItem: -// - The MultiSelect API requires you to store information about the reference/pivot item (generally the last clicked item). +// - The BeginMultiSelect() API requires you to store information about the reference/pivot item (generally the last clicked item). struct ExampleSelection { - ImGuiStorage Storage; + // Data + ImGuiStorage Storage; // Selection set int SelectionSize; // Number of selected items (== number of 1 in the Storage, maintained by this class) int RangeRef; // Reference/pivot item (generally last clicked item) + // Functions ExampleSelection() { RangeRef = 0; Clear(); } void Clear() { Storage.Clear(); SelectionSize = 0; } bool GetSelected(int n) const { return Storage.GetInt((ImGuiID)n, 0) != 0; } @@ -2791,11 +2795,20 @@ struct ExampleSelection // you will need a way to iterate from one object to another given the ID you use. // You are likely to need some kind of data structure to convert 'view index' <> 'object ID'. // FIXME-MULTISELECT: Would be worth providing a demo of doing this. - // FIXME-MULTISELECT: SetRange() is currently very inefficient since it doesn't take advantage of the fact that ImGuiStorage stores sorted key. - void SetRange(int n1, int n2, bool v) { if (n2 < n1) { int tmp = n2; n2 = n1; n1 = tmp; } for (int n = n1; n <= n2; n++) SetSelected(n, v); } - void SelectAll(int count) { Storage.Data.resize(count); for (int idx = 0; idx < count; idx++) Storage.Data[idx] = ImGuiStoragePair((ImGuiID)idx, 1); SelectionSize = count; } // This could be using SetRange(), but it this way is faster. + // FIXME-MULTISELECT: This implementation of SetRange() is inefficient because it doesn't take advantage of the fact that ImGuiStorage stores sorted key. + void SetRange(int a, int b, bool v) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) SetSelected(n, v); } + void SelectAll(int count) { Storage.Data.resize(count); for (int idx = 0; idx < count; idx++) Storage.Data[idx] = ImGuiStoragePair((ImGuiID)idx, 1); SelectionSize = count; } // This could be using SetRange(), but it this way is faster. + + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). Must be done in this order! Order->SelectAll->SetRange. + void ApplyRequests(ImGuiMultiSelectData* ms_data, int items_count) + { + if (ms_data->RequestClear) { Clear(); } + if (ms_data->RequestSelectAll) { SelectAll(items_count); } + if (ms_data->RequestSetRange) { SetRange((int)(intptr_t)ms_data->RangeSrc, (int)(intptr_t)ms_data->RangeDst, ms_data->RangeValue ? 1 : 0); } + } }; + static void ShowDemoWindowMultiSelect() { IMGUI_DEMO_MARKER("Widgets/Selection State"); @@ -2864,8 +2877,7 @@ static void ShowDemoWindowMultiSelect() { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); - if (multi_select_data->RequestClear) { selection.Clear(); } - if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } + selection.ApplyRequests(multi_select_data, ITEMS_COUNT); for (int n = 0; n < ITEMS_COUNT; n++) { @@ -2886,9 +2898,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests multi_select_data = ImGui::EndMultiSelect(); selection.RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; - if (multi_select_data->RequestClear) { selection.Clear(); } - if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } - if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } + selection.ApplyRequests(multi_select_data, ITEMS_COUNT); ImGui::EndListBox(); } @@ -2903,7 +2913,7 @@ static void ShowDemoWindowMultiSelect() // - Showcase having multiple multi-selection scopes in the same window. // - Showcase using inside a table. IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full, advanced)"); - ImGui::SetNextItemOpen(true, ImGuiCond_Once); + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); if (ImGui::TreeNode("Multiple Selection (full, advanced)")) { // Options @@ -2952,8 +2962,7 @@ static void ShowDemoWindowMultiSelect() else flags |= ImGuiMultiSelectFlags_ClearOnClickWindowVoid; ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection->RangeRef, selection->GetSelected(selection->RangeRef)); - if (multi_select_data->RequestClear) { selection->Clear(); } - if (multi_select_data->RequestSelectAll) { selection->SelectAll(ITEMS_COUNT); } + selection->ApplyRequests(multi_select_data, ITEMS_COUNT); if (multiple_selection_scopes) ImGui::Text("Selection size: %d", selection->GetSelectionSize()); // Draw counter below Separator and after BeginMultiSelect() @@ -2971,8 +2980,8 @@ static void ShowDemoWindowMultiSelect() clipper.Begin(ITEMS_COUNT); while (clipper.Step()) { - // IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeRef was passed over. - if (clipper.DisplayStart > selection->RangeRef) + // IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeSrc was passed over. + if ((int)(intptr_t)multi_select_data->RangeSrc <= clipper.DisplayStart) multi_select_data->RangeSrcPassedBy = true; for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) @@ -3052,9 +3061,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests multi_select_data = ImGui::EndMultiSelect(); selection->RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; - if (multi_select_data->RequestClear) { selection->Clear(); } - if (multi_select_data->RequestSelectAll) { selection->SelectAll(ITEMS_COUNT); } - if (multi_select_data->RequestSetRange) { selection->SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } + selection->ApplyRequests(multi_select_data, ITEMS_COUNT); if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); diff --git a/imgui_internal.h b/imgui_internal.h index 757204567ca7..82f67dc6073a 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1715,15 +1715,19 @@ struct ImGuiOldColumns struct IMGUI_API ImGuiMultiSelectState { - ImGuiID FocusScopeId; // Same as g.CurrentFocusScopeId (unless another selection scope was pushed manually) + ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually) + ImGuiMultiSelectFlags Flags; + ImGuiKeyChord KeyMods; + ImGuiWindow* Window; ImGuiMultiSelectData In; // The In requests are set and returned by BeginMultiSelect() ImGuiMultiSelectData Out; // The Out requests are finalized 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 InRangeDstPassedBy; // (Internal) set by the the item that match NavJustMovedToId when InRequestRangeSetNav is set. bool InRequestSetRangeNav; // (Internal) set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. //ImRect Rect; // Extent of selection scope between BeginMultiSelect() / EndMultiSelect(), used by ImGuiMultiSelectFlags_ClearOnClickRectVoid. ImGuiMultiSelectState() { Clear(); } - void Clear() { FocusScopeId = 0; In.Clear(); Out.Clear(); InRangeDstPassedBy = InRequestSetRangeNav = false; } + void Clear() { FocusScopeId = 0; Flags = ImGuiMultiSelectFlags_None; KeyMods = ImGuiMod_None; Window = NULL; In.Clear(); Out.Clear(); InRangeDstPassedBy = InRequestSetRangeNav = false; } }; #endif // #ifdef IMGUI_HAS_MULTI_SELECT @@ -2121,10 +2125,7 @@ struct ImGuiContext ImVec2 NavWindowingAccumDeltaSize; // Range-Select/Multi-Select - ImGuiWindow* MultiSelectEnabledWindow; // FIXME-MULTISELECT: We currently don't support recursing/stacking multi-select - ImGuiMultiSelectFlags MultiSelectFlags; - ImGuiMultiSelectState MultiSelectState; - ImGuiKeyChord MultiSelectKeyMods; + ImGuiMultiSelectState MultiSelectState; // FIXME-MULTISELECT: We currently don't support recursing/stacking multi-select // Render float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list) @@ -2389,10 +2390,6 @@ struct ImGuiContext NavWindowingToggleLayer = false; NavWindowingToggleKey = ImGuiKey_None; - MultiSelectEnabledWindow = NULL; - MultiSelectFlags = ImGuiMultiSelectFlags_None; - MultiSelectKeyMods = ImGuiMod_None; - DimBgRatio = 0.0f; DragDropActive = DragDropWithinSource = DragDropWithinTarget = false; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index aa067502e1f3..5da30ea2e4bf 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6464,7 +6464,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags const bool was_selected = selected; // Multi-selection support (header) - const bool is_multi_select = (g.MultiSelectEnabledWindow == window); + const bool is_multi_select = (g.MultiSelectState.Window == window); if (is_multi_select) { MultiSelectItemHeader(id, &selected); @@ -6816,7 +6816,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; } // Multi-selection support (header) - const bool is_multi_select = (g.MultiSelectEnabledWindow == window); + const bool is_multi_select = (g.MultiSelectState.Window == window); const bool was_selected = selected; if (is_multi_select) { @@ -7126,23 +7126,19 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - IM_ASSERT(g.MultiSelectEnabledWindow == NULL); // No recursion allowed yet (we could allow it if we deem it useful) - IM_ASSERT(g.MultiSelectFlags == 0); - IM_ASSERT(g.MultiSelectState.FocusScopeId == 0); + ImGuiMultiSelectState* ms = &g.MultiSelectState; + IM_ASSERT(ms->Window == NULL && ms->Flags == 0 && ms->FocusScopeId == 0); // No recursion allowed yet (we could allow it if we deem it useful) // FIXME: BeginFocusScope() - ImGuiMultiSelectState* ms = &g.MultiSelectState; ms->Clear(); ms->FocusScopeId = window->IDStack.back(); + ms->Flags = flags; + ms->Window = window; + ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId); PushFocusScope(ms->FocusScopeId); - g.MultiSelectEnabledWindow = window; - g.MultiSelectFlags = flags; - - // Report focus - ms->In.IsFocused = ms->Out.IsFocused = (ms->FocusScopeId == g.NavFocusScopeId); // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame. - g.MultiSelectKeyMods = g.NavJustMovedToId ? g.NavJustMovedToKeyMods : g.IO.KeyMods; + ms->KeyMods = g.NavJustMovedToId ? g.NavJustMovedToKeyMods : g.IO.KeyMods; if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0) { @@ -7154,14 +7150,14 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* // FIXME: Polling key mods after the fact (frame following the move request) is incorrect, but latching it would requires non-trivial change in MultiSelectItemFooter() if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) { - if (g.MultiSelectKeyMods & ImGuiMod_Shift) + if (ms->KeyMods & ImGuiMod_Shift) ms->InRequestSetRangeNav = true; - if ((g.MultiSelectKeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) + if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) ms->In.RequestClear = true; } // Shortcuts - if (ms->In.IsFocused) + if (ms->IsFocused) { // Select All helper shortcut (CTRL+A) // Note: we are comparing FocusScope so we don't need to be testing for IsWindowFocused() @@ -7174,10 +7170,8 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* ms->In.RequestClear = true; } -#ifdef IMGUI_DEBUG_MULTISELECT - if (ms->In.RequestClear) IMGUI_DEBUG_LOG("BeginMultiSelect: RequestClear\n"); - if (ms->In.RequestSelectAll) IMGUI_DEBUG_LOG("BeginMultiSelect: RequestSelectAll\n"); -#endif + //if (ms->In.RequestClear) IMGUI_DEBUG_LOG("BeginMultiSelect: RequestClear\n"); + //if (ms->In.RequestSelectAll) IMGUI_DEBUG_LOG("BeginMultiSelect: RequestSelectAll\n"); return &ms->In; } @@ -7190,7 +7184,7 @@ ImGuiMultiSelectData* ImGui::EndMultiSelect() // Clear selection when clicking void? // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! - if (g.MultiSelectFlags & ImGuiMultiSelectFlags_ClearOnClickWindowVoid) + if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickWindowVoid) if (IsWindowHovered() && g.HoveredId == 0) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { @@ -7199,18 +7193,16 @@ ImGuiMultiSelectData* ImGui::EndMultiSelect() } // Unwind - if (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect) + if (ms->Flags & ImGuiMultiSelectFlags_NoUnselect) ms->Out.RangeValue = true; - g.MultiSelectState.FocusScopeId = 0; + ms->FocusScopeId = 0; + ms->Window = NULL; + ms->Flags = ImGuiMultiSelectFlags_None; PopFocusScope(); - g.MultiSelectEnabledWindow = NULL; - g.MultiSelectFlags = ImGuiMultiSelectFlags_None; -#ifdef IMGUI_DEBUG_MULTISELECT - if (ms->Out.RequestClear) IMGUI_DEBUG_LOG("EndMultiSelect: RequestClear\n"); - if (ms->Out.RequestSelectAll) IMGUI_DEBUG_LOG("EndMultiSelect: RequestSelectAll\n"); - if (ms->Out.RequestSetRange) IMGUI_DEBUG_LOG("EndMultiSelect: RequestSetRange %p..%p = %d\n", ms->Out.RangeSrc, ms->Out.RangeDst, ms->Out.RangeValue); -#endif + //if (ms->Out.RequestClear) IMGUI_DEBUG_LOG("EndMultiSelect: RequestClear\n"); + //if (ms->Out.RequestSelectAll) IMGUI_DEBUG_LOG("EndMultiSelect: RequestSelectAll\n"); + //if (ms->Out.RequestSetRange) IMGUI_DEBUG_LOG("EndMultiSelect: RequestSetRange %p..%p = %d\n", ms->Out.RangeSrc, ms->Out.RangeDst, ms->Out.RangeValue); return &ms->Out; } @@ -7257,13 +7249,13 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) if (ms->InRequestSetRangeNav) { IM_ASSERT(id != 0); - IM_ASSERT((g.MultiSelectKeyMods & ImGuiMod_Shift) != 0); + IM_ASSERT((ms->KeyMods & ImGuiMod_Shift) != 0); const bool is_range_dst = !ms->InRangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. if (is_range_dst) ms->InRangeDstPassedBy = true; if (is_range_src || is_range_dst || ms->In.RangeSrcPassedBy != ms->InRangeDstPassedBy) selected = ms->In.RangeValue; - else if ((g.MultiSelectKeyMods & ImGuiMod_Ctrl) == 0) + else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0) selected = false; } @@ -7278,11 +7270,11 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) void* item_data = (void*)g.NextItemData.SelectionUserData; + const bool is_multiselect = (ms->Flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0; bool selected = *p_selected; bool pressed = *p_pressed; - const bool is_multiselect = (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoMultiSelect) == 0; - bool is_ctrl = (g.MultiSelectKeyMods & ImGuiMod_Ctrl) != 0; - bool is_shift = (g.MultiSelectKeyMods & ImGuiMod_Shift) != 0; + bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0; + bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0; // Auto-select as you navigate a list if (g.NavJustMovedToId == id) @@ -7334,7 +7326,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) } else { - selected = (!is_ctrl || (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect)) ? true : !selected; + selected = (!is_ctrl || (ms->Flags & ImGuiMultiSelectFlags_NoUnselect)) ? true : !selected; ms->Out.RangeSrc = ms->Out.RangeDst = item_data; ms->Out.RangeValue = selected; } @@ -7371,7 +7363,7 @@ 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 (ms->Out.RangeSrc == item_data && is_ctrl && is_shift && is_multiselect && !(g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect)) + if (ms->Out.RangeSrc == item_data && is_ctrl && is_shift && is_multiselect && !(ms->Flags & ImGuiMultiSelectFlags_NoUnselect)) ms->Out.RangeValue = selected; *p_selected = selected; From 815c61b82eb5f00ea8eb0a772b646306a25dd2d2 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 12 Apr 2023 22:14:32 +0200 Subject: [PATCH 020/132] MultiSelect: Fixed needing to set RangeSrcPassedBy when not using clipper. --- imgui_demo.cpp | 4 ---- imgui_widgets.cpp | 9 +++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index c8fc424dce29..3b98bc5c452e 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2881,10 +2881,6 @@ static void ShowDemoWindowMultiSelect() for (int n = 0; n < ITEMS_COUNT; n++) { - // FIXME-MULTISELECT: This should not be needed but currently is because coarse clipping break the auto-setup. - if (n > selection.RangeRef) - multi_select_data->RangeSrcPassedBy = true; - char label[64]; sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 5da30ea2e4bf..f1da8b1689f2 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7218,6 +7218,10 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData; g.NextItemData.SelectionUserData = selection_user_data; g.NextItemData.FocusScopeId = g.CurrentFocusScopeId; + + // Auto updating RangeSrcPassedBy for cases were clipped is not used. + if (g.MultiSelectState.In.RangeSrc == (void*)selection_user_data) + g.MultiSelectState.In.RangeSrcPassedBy = true; } void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) @@ -7240,16 +7244,13 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) else if (ms->In.RequestSelectAll) selected = true; - const bool is_range_src = (ms->In.RangeSrc == item_data); - if (is_range_src) - ms->In.RangeSrcPassedBy = true; // FIXME-MULTISELECT: The promise that this would be automatically done is not because of ItemAdd() clipping. - // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) // For this to work, IF the user is clipping items, they need to set RangeSrcPassedBy = true to notify the system. if (ms->InRequestSetRangeNav) { IM_ASSERT(id != 0); IM_ASSERT((ms->KeyMods & ImGuiMod_Shift) != 0); + const bool is_range_src = (ms->In.RangeSrc == item_data); const bool is_range_dst = !ms->InRangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. if (is_range_dst) ms->InRangeDstPassedBy = true; From d2f208a30c426f48e918897bd5e3b97999aa80f2 Mon Sep 17 00:00:00 2001 From: ocornut Date: Sat, 20 May 2023 15:51:39 +0200 Subject: [PATCH 021/132] MultiSelect: made SetNextItemSelectionData() optional to allow disjoint selection (e.g. with a CollapsingHeader between items). Amend demo. --- imgui.h | 4 ++-- imgui_demo.cpp | 40 +++++++++++++++++++++++----------------- imgui_widgets.cpp | 7 ++++--- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/imgui.h b/imgui.h index 987d7e7fb857..6c65045e3d7c 100644 --- a/imgui.h +++ b/imgui.h @@ -2766,14 +2766,14 @@ enum ImGuiMultiSelectFlags_ // 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc 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 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. +// 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 RangeSrc 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->RangeSrc) { data->RangeSrcPassedBy = true; } // 4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. // Call IsItemToggledSelection() to query if the selection state has been toggled, if you need the info immediately for your display (before EndMultiSelect()). -// When cannot return a "IsItemSelected()" value because we need to consider clipped/unprocessed items, this is why we return a "Toggle" event instead. +// 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 ->RangeSrc 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) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 3b98bc5c452e..52e512e29acc 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2830,6 +2830,7 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } + // Demonstrate implementation a most-basic form of multi-selection manually IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (simplfied, manual)"); if (ImGui::TreeNode("Multiple Selection (simplified, manual)")) { @@ -2841,9 +2842,9 @@ static void ShowDemoWindowMultiSelect() sprintf(buf, "Object %d", n); if (ImGui::Selectable(buf, selection[n])) { - if (!ImGui::GetIO().KeyCtrl) // Clear selection when CTRL is not held + if (!ImGui::GetIO().KeyCtrl) // Clear selection when CTRL is not held memset(selection, 0, sizeof(selection)); - selection[n] ^= 1; + selection[n] ^= 1; // Toggle current item } } ImGui::TreePop(); @@ -2883,7 +2884,6 @@ static void ShowDemoWindowMultiSelect() { char label[64]; sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); - bool item_is_selected = selection.GetSelected(n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); @@ -2916,28 +2916,36 @@ static void ShowDemoWindowMultiSelect() enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; static bool use_table = false; static bool use_drag_drop = true; - static bool multiple_selection_scopes = false; + static bool use_multiple_scopes = false; + static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; static WidgetType widget_type = WidgetType_TreeNode; if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } ImGui::SameLine(); if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } ImGui::Checkbox("Use table", &use_table); ImGui::Checkbox("Use drag & drop", &use_drag_drop); - ImGui::Checkbox("Distinct selection scopes in same window", &multiple_selection_scopes); + ImGui::Checkbox("Multiple selection scopes in same window", &use_multiple_scopes); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoMultiSelect", &flags, ImGuiMultiSelectFlags_NoMultiSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoUnselect", &flags, ImGuiMultiSelectFlags_NoUnselect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); + ImGui::BeginDisabled(use_multiple_scopes); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickWindowVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickWindowVoid); + ImGui::EndDisabled(); - // When 'multiple_selection_scopes' is set we show 3 selection scopes in the host window instead of 1 in a scrolling window. + // When 'use_multiple_scopes' is set we show 3 selection scopes in the host window instead of 1 in a scrolling window. static ExampleSelection selections_data[3]; - const int selection_scope_count = multiple_selection_scopes ? 3 : 1; + const int selection_scope_count = use_multiple_scopes ? 3 : 1; for (int selection_scope_n = 0; selection_scope_n < selection_scope_count; selection_scope_n++) { ExampleSelection* selection = &selections_data[selection_scope_n]; - const int ITEMS_COUNT = multiple_selection_scopes ? 12 : 1000; + const int ITEMS_COUNT = use_multiple_scopes ? 12 : 1000; // Smaller count to make it easier to see multiple scopes in same screen. ImGui::PushID(selection_scope_n); // Open a scrolling region bool draw_selection = true; - if (multiple_selection_scopes) + if (use_multiple_scopes) { ImGui::SeparatorText("Selection scope"); } @@ -2952,15 +2960,13 @@ static void ShowDemoWindowMultiSelect() if (widget_type == WidgetType_TreeNode) ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; - if (multiple_selection_scopes) - ;// flags |= ImGuiMultiSelectFlags_ClearOnClickRectVoid; - else - flags |= ImGuiMultiSelectFlags_ClearOnClickWindowVoid; - ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection->RangeRef, selection->GetSelected(selection->RangeRef)); + ImGuiMultiSelectFlags local_flags = flags; + if (use_multiple_scopes) + local_flags &= ~ImGuiMultiSelectFlags_ClearOnClickWindowVoid; // local_flags |= ImGuiMultiSelectFlags_ClearOnClickRectVoid; + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(local_flags, (void*)(intptr_t)selection->RangeRef, selection->GetSelected(selection->RangeRef)); selection->ApplyRequests(multi_select_data, ITEMS_COUNT); - if (multiple_selection_scopes) + if (use_multiple_scopes) ImGui::Text("Selection size: %d", selection->GetSelectionSize()); // Draw counter below Separator and after BeginMultiSelect() if (use_table) @@ -3062,7 +3068,7 @@ static void ShowDemoWindowMultiSelect() if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); - if (multiple_selection_scopes == false) + if (use_multiple_scopes == false) ImGui::EndListBox(); } ImGui::PopID(); // ImGui::PushID(selection_scope_n); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index f1da8b1689f2..481853948f3c 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6391,6 +6391,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags // Compute open and multi-select states before ItemAdd() as it clear NextItem data. bool is_open = TreeNodeUpdateNextOpen(storage_id, flags); + const bool is_multi_select = (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasSelectionData) != 0; // Before ItemAdd() bool item_add = ItemAdd(interact_bb, id); g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; g.LastItemData.DisplayRect = frame_bb; @@ -6464,7 +6465,6 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags const bool was_selected = selected; // Multi-selection support (header) - const bool is_multi_select = (g.MultiSelectState.Window == window); if (is_multi_select) { MultiSelectItemHeader(id, &selected); @@ -6780,7 +6780,9 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl } const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0; + const bool is_multi_select = (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasSelectionData) != 0; // Before ItemAdd() const bool item_add = ItemAdd(bb, id, NULL, disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None); + if (span_all_columns) { window->ClipRect.Min.x = backup_clip_rect_min_x; @@ -6816,7 +6818,6 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; } // Multi-selection support (header) - const bool is_multi_select = (g.MultiSelectState.Window == window); const bool was_selected = selected; if (is_multi_select) { @@ -7166,7 +7167,7 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* ms->In.RequestSelectAll = true; if (flags & ImGuiMultiSelectFlags_ClearOnEscape) - if (Shortcut(ImGuiKey_Escape)) + if (Shortcut(ImGuiKey_Escape)) // FIXME-MULTISELECT: Only hog shortcut if selection is not null, meaning we need "has selection or "selection size" data here. ms->In.RequestClear = true; } From 85954c845e3f8c6abc08e5a60fac24a9f3ec83ea Mon Sep 17 00:00:00 2001 From: ocornut Date: Sat, 20 May 2023 15:59:30 +0200 Subject: [PATCH 022/132] MultiSelect: Enter can alter selection if current item is not selected. --- imgui_widgets.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 481853948f3c..71f0530c4bbd 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7301,11 +7301,13 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) } } - // Unlike Space, Enter doesn't alter selection (but can still return a press) + // Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected. + // The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something + // (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional? const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput); // Alter selection - if (pressed && !enter_pressed) + if (pressed && (!enter_pressed || !selected)) { //------------------------------------------------------------------------------------------------------------------------------------------------- // ACTION | Begin | Item Old | Item New | End From 5d71314f712649b08f590b3aafea569bbab2b9c1 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 22 May 2023 10:28:40 +0200 Subject: [PATCH 023/132] MultiSelect: removed DragDropActive/preserve_existing_selection logic which seems unused + comments. Can't find trace of early prototype for range-select but I couldn't find way to trigger this anymore. May be wrong. Will find out. --- imgui.h | 4 ++-- imgui_demo.cpp | 19 ++++++++++--------- imgui_internal.h | 8 ++++---- imgui_widgets.cpp | 47 +++++++++++++++++++++++------------------------ 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/imgui.h b/imgui.h index 6c65045e3d7c..550e643611ae 100644 --- a/imgui.h +++ b/imgui.h @@ -2732,7 +2732,7 @@ struct ImColor enum ImGuiMultiSelectFlags_ { ImGuiMultiSelectFlags_None = 0, - ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0, + ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0, // Disable selecting more than one item. This is not very useful at this kind of selection can be implemented without BeginMultiSelect(), but this is available for consistency. ImGuiMultiSelectFlags_NoUnselect = 1 << 1, // Disable unselecting items with CTRL+Click, CTRL+Space etc. ImGuiMultiSelectFlags_NoSelectAll = 1 << 2, // Disable CTRL+A shortcut to set RequestSelectAll ImGuiMultiSelectFlags_ClearOnClickWindowVoid= 1 << 3, // Clear selection when clicking on empty location within host window (use if BeginMultiSelect() covers a whole window) @@ -2787,7 +2787,7 @@ struct ImGuiMultiSelectData bool RangeValue; // End // End: parameter from RequestSetRange request. true = Select Range, false = Unselect Range. void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect() void* RangeDst; // End // End: parameter from RequestSetRange request. - int RangeDirection; // End // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -1 otherwise. Available as an indicator in case you cannot infer order from the void* values. + int RangeDirection; // End // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -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. ImGuiMultiSelectData() { Clear(); } void Clear() diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 52e512e29acc..3008ca94642f 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2787,7 +2787,7 @@ struct ExampleSelection 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; } - int GetSelectionSize() const { return SelectionSize; } + int GetSize() const { return SelectionSize; } // When using SelectAll() / SetRange() we assume that our objects ID are indices. // In this demo we always store selection using indices and never in another manner (e.g. object ID or pointers). @@ -2808,7 +2808,6 @@ struct ExampleSelection } }; - static void ShowDemoWindowMultiSelect() { IMGUI_DEMO_MARKER("Widgets/Selection State"); @@ -2873,7 +2872,7 @@ static void ShowDemoWindowMultiSelect() // The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling regions). const int ITEMS_COUNT = 50; - ImGui::Text("Selection size: %d", selection.GetSelectionSize()); + ImGui::Text("Selection size: %d", selection.GetSize()); if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; @@ -2918,7 +2917,8 @@ static void ShowDemoWindowMultiSelect() static bool use_drag_drop = true; static bool use_multiple_scopes = false; static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; - static WidgetType widget_type = WidgetType_TreeNode; + static WidgetType widget_type = WidgetType_Selectable; + if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } ImGui::SameLine(); if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } @@ -2951,7 +2951,7 @@ static void ShowDemoWindowMultiSelect() } else { - ImGui::Text("Selection size: %d", selection->GetSelectionSize()); + ImGui::Text("Selection size: %d", selection->GetSize()); draw_selection = ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20)); } if (draw_selection) @@ -2967,7 +2967,7 @@ static void ShowDemoWindowMultiSelect() selection->ApplyRequests(multi_select_data, ITEMS_COUNT); if (use_multiple_scopes) - ImGui::Text("Selection size: %d", selection->GetSelectionSize()); // Draw counter below Separator and after BeginMultiSelect() + ImGui::Text("Selection size: %d", selection->GetSize()); // Draw counter below Separator and after BeginMultiSelect() if (use_table) { @@ -2994,7 +2994,7 @@ static void ShowDemoWindowMultiSelect() ImGui::PushID(n); const char* category = random_names[n % IM_ARRAYSIZE(random_names)]; char label[64]; - sprintf(label, "Object %05d (category: %s)", n, category); + sprintf(label, "Object %05d: category: %s", n, category); bool item_is_selected = selection->GetSelected(n); // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part @@ -3011,7 +3011,7 @@ static void ShowDemoWindowMultiSelect() selection->SetSelected(n, !item_is_selected); if (use_drag_drop && ImGui::BeginDragDropSource()) { - ImGui::Text("(Dragging %d items)", selection->GetSelectionSize()); + ImGui::Text("(Dragging %d items)", selection->GetSize()); ImGui::EndDragDropSource(); } } @@ -3026,7 +3026,7 @@ static void ShowDemoWindowMultiSelect() selection->SetSelected(n, !item_is_selected); if (use_drag_drop && ImGui::BeginDragDropSource()) { - ImGui::Text("(Dragging %d items)", selection->GetSelectionSize()); + ImGui::Text("(Dragging %d items)", selection->GetSize()); ImGui::EndDragDropSource(); } if (open) @@ -3041,6 +3041,7 @@ static void ShowDemoWindowMultiSelect() ImGui::EndPopup(); } + // Demo content within a table if (use_table) { ImGui::TableNextColumn(); diff --git a/imgui_internal.h b/imgui_internal.h index 82f67dc6073a..c51f28759d04 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -2124,9 +2124,6 @@ struct ImGuiContext ImVec2 NavWindowingAccumDeltaPos; ImVec2 NavWindowingAccumDeltaSize; - // Range-Select/Multi-Select - ImGuiMultiSelectState MultiSelectState; // FIXME-MULTISELECT: We currently don't support recursing/stacking multi-select - // Render float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list) @@ -2169,6 +2166,9 @@ struct ImGuiContext ImVector CurrentTabBarStack; ImVector ShrinkWidthBuffer; + // Multi-Select state + ImGuiMultiSelectState MultiSelectState; // FIXME-MULTISELECT: We currently don't support recursing/stacking multi-select + // Hover Delay system ImGuiID HoverItemDelayId; ImGuiID HoverItemDelayIdPreviousFrame; @@ -3344,7 +3344,7 @@ namespace ImGui IMGUI_API int TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); IMGUI_API int TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data); - // Multi-Select/Range-Select API + // Multi-Select API IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected); IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 71f0530c4bbd..5fbe0821d99a 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7157,22 +7157,20 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* ms->In.RequestClear = true; } - // Shortcuts - if (ms->IsFocused) - { - // Select All helper shortcut (CTRL+A) - // Note: we are comparing FocusScope so we don't need to be testing for IsWindowFocused() - if (!(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) - if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) - ms->In.RequestSelectAll = true; - - if (flags & ImGuiMultiSelectFlags_ClearOnEscape) - if (Shortcut(ImGuiKey_Escape)) // FIXME-MULTISELECT: Only hog shortcut if selection is not null, meaning we need "has selection or "selection size" data here. - ms->In.RequestClear = true; - } + // Shortcut: Select all (CTRL+A) + if (ms->IsFocused && !(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) + if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) + ms->In.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 (Shortcut(ImGuiKey_Escape)) + ms->In.RequestClear = true; - //if (ms->In.RequestClear) IMGUI_DEBUG_LOG("BeginMultiSelect: RequestClear\n"); - //if (ms->In.RequestSelectAll) IMGUI_DEBUG_LOG("BeginMultiSelect: RequestSelectAll\n"); + //if (ms->In.RequestClear) IMGUI_DEBUG_LOG_SELECTION("BeginMultiSelect: RequestClear\n"); + //if (ms->In.RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("BeginMultiSelect: RequestSelectAll\n"); return &ms->In; } @@ -7201,9 +7199,9 @@ ImGuiMultiSelectData* ImGui::EndMultiSelect() ms->Flags = ImGuiMultiSelectFlags_None; PopFocusScope(); - //if (ms->Out.RequestClear) IMGUI_DEBUG_LOG("EndMultiSelect: RequestClear\n"); - //if (ms->Out.RequestSelectAll) IMGUI_DEBUG_LOG("EndMultiSelect: RequestSelectAll\n"); - //if (ms->Out.RequestSetRange) IMGUI_DEBUG_LOG("EndMultiSelect: RequestSetRange %p..%p = %d\n", ms->Out.RangeSrc, ms->Out.RangeDst, ms->Out.RangeValue); + //if (ms->Out.RequestClear) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestClear\n"); + //if (ms->Out.RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestSelectAll\n"); + //if (ms->Out.RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestSetRange %p..%p = %d (dir %+d)\n", ms->Out.RangeSrc, ms->Out.RangeDst, ms->Out.RangeValue, ms->Out.RangeDirection); return &ms->Out; } @@ -7220,7 +7218,7 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d g.NextItemData.SelectionUserData = selection_user_data; g.NextItemData.FocusScopeId = g.CurrentFocusScopeId; - // Auto updating RangeSrcPassedBy for cases were clipped is not used. + // Auto updating RangeSrcPassedBy for cases were clipper is not used. if (g.MultiSelectState.In.RangeSrc == (void*)selection_user_data) g.MultiSelectState.In.RangeSrcPassedBy = true; } @@ -7322,6 +7320,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; if (is_shift && is_multiselect) { + // Shift+Arrow always select, Ctrl+Shift+Arrow copy source selection state. ms->Out.RequestSetRange = true; ms->Out.RangeDst = item_data; if (!is_ctrl) @@ -7330,7 +7329,8 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) } else { - selected = (!is_ctrl || (ms->Flags & ImGuiMultiSelectFlags_NoUnselect)) ? true : !selected; + // Ctrl inverts selection, otherwise always select + selected = (is_ctrl && (ms->Flags & ImGuiMultiSelectFlags_NoUnselect) == 0) ? !selected : true; ms->Out.RangeSrc = ms->Out.RangeDst = item_data; ms->Out.RangeValue = selected; } @@ -7338,13 +7338,12 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) if (input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) { // Mouse click without CTRL clears the selection, unless the clicked item is already selected - bool preserve_existing_selection = g.DragDropActive; - if (is_multiselect && !is_ctrl && !preserve_existing_selection) + if (is_multiselect && !is_ctrl) ms->Out.RequestClear = true; - if (is_multiselect && !is_shift && !preserve_existing_selection && ms->Out.RequestClear) + if (is_multiselect && !is_shift && ms->Out.RequestClear) { // For toggle selection unless there is a Clear request, we can handle it completely locally without sending a RangeSet request. - IM_ASSERT(ms->Out.RangeSrc == ms->Out.RangeDst); // Setup by block above + IM_ASSERT(ms->Out.RangeSrc == ms->Out.RangeDst); // Setup by else block above ms->Out.RequestSetRange = true; ms->Out.RangeValue = selected; ms->Out.RangeDirection = +1; From 11bcae1ebd4a5098d09e322aeb6ace356e019e03 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 23 May 2023 19:36:11 +0200 Subject: [PATCH 024/132] MultiSelect: refactor before introducing persistant state pool and to facilitate adding recursion + debug log calls. This is mostly the noisy/shallow stuff committed here, to get this out of the way. --- imgui.cpp | 2 +- imgui.h | 7 ++++--- imgui_internal.h | 10 ++++++---- imgui_widgets.cpp | 40 +++++++++++++++++++++++++--------------- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 733a5dfe1287..8f34acf9274d 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -15799,7 +15799,7 @@ void ImGui::ShowDebugLogWindow(bool* p_open) ShowDebugLogFlag("IO", ImGuiDebugLogFlags_EventIO); ShowDebugLogFlag("Nav", ImGuiDebugLogFlags_EventNav); ShowDebugLogFlag("Popup", ImGuiDebugLogFlags_EventPopup); - //ShowDebugLogFlag("Selection", ImGuiDebugLogFlags_EventSelection); + ShowDebugLogFlag("Selection", ImGuiDebugLogFlags_EventSelection); ShowDebugLogFlag("InputRouting", ImGuiDebugLogFlags_EventInputRouting); if (SmallButton("Clear")) diff --git a/imgui.h b/imgui.h index 550e643611ae..349c77f3e568 100644 --- a/imgui.h +++ b/imgui.h @@ -2747,6 +2747,10 @@ enum ImGuiMultiSelectFlags_ // 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. // 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), +// external array (store an array aside from your objects), hash/map/set (store only selected items in a hash/map/set), +// or other structures (store indices in an interval tree), etc. // - TreeNode() and Selectable() are supported. // - The work involved to deal with multi-selection differs whether you want to only submit visible items (and clip others) or submit all items // regardless of their visibility. Clipping items is more efficient and will allow you to deal with large lists (1k~100k items) with near zero @@ -2758,9 +2762,6 @@ enum ImGuiMultiSelectFlags_ // Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points and you will need to interpolate // 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 RangeSrc to RangeDst). -// - In the spirit of Dear ImGui design, your code own the selection data. So this is designed to handle all kind of selection data: -// e.g. instructive selection (store a bool inside each object), external array (store an array aside from your objects), -// hash/map/set (store only selected items in a hash/map/set), or other structures (store indices in an interval tree), etc. // Usage flow: // Begin // 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc and its selection state. diff --git a/imgui_internal.h b/imgui_internal.h index c51f28759d04..ad5366765b5f 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -134,7 +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 state +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 struct ImGuiNextWindowData; // Storage for SetNextWindow** functions @@ -1713,7 +1713,7 @@ struct ImGuiOldColumns #ifdef IMGUI_HAS_MULTI_SELECT -struct IMGUI_API ImGuiMultiSelectState +struct IMGUI_API ImGuiMultiSelectTempData { ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually) ImGuiMultiSelectFlags Flags; @@ -1726,7 +1726,7 @@ struct IMGUI_API ImGuiMultiSelectState bool InRequestSetRangeNav; // (Internal) set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. //ImRect Rect; // Extent of selection scope between BeginMultiSelect() / EndMultiSelect(), used by ImGuiMultiSelectFlags_ClearOnClickRectVoid. - ImGuiMultiSelectState() { Clear(); } + ImGuiMultiSelectTempData() { Clear(); } void Clear() { FocusScopeId = 0; Flags = ImGuiMultiSelectFlags_None; KeyMods = ImGuiMod_None; Window = NULL; In.Clear(); Out.Clear(); InRangeDstPassedBy = InRequestSetRangeNav = false; } }; @@ -2167,7 +2167,8 @@ struct ImGuiContext ImVector ShrinkWidthBuffer; // Multi-Select state - ImGuiMultiSelectState MultiSelectState; // FIXME-MULTISELECT: We currently don't support recursing/stacking multi-select + ImGuiMultiSelectTempData* CurrentMultiSelect; // FIXME-MULTISELECT: We currently don't support recursing/stacking multi-select + ImGuiMultiSelectTempData MultiSelectTempData[1]; // Hover Delay system ImGuiID HoverItemDelayId; @@ -2409,6 +2410,7 @@ struct ImGuiContext CurrentTable = NULL; TablesTempDataStacked = 0; CurrentTabBar = NULL; + CurrentMultiSelect = NULL; HoverItemDelayId = HoverItemDelayIdPreviousFrame = HoverItemUnlockedStationaryId = HoverWindowUnlockedStationaryId = 0; HoverItemDelayTimer = HoverItemDelayClearTimer = 0.0f; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 5fbe0821d99a..55453dd50ebc 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7122,13 +7122,21 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) // - MultiSelectItemFooter() [Internal] //------------------------------------------------------------------------- +static void DebugLogMultiSelectRequests(const ImGuiMultiSelectData* data) +{ + ImGuiContext& g = *GImGui; + if (data->RequestClear) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestClear\n"); + if (data->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestSelectAll\n"); + if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestSetRange %p..%p = %d (dir %+d)\n", data->RangeSrc, data->RangeDst, data->RangeValue, data->RangeDirection); +} + ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - - ImGuiMultiSelectState* ms = &g.MultiSelectState; - IM_ASSERT(ms->Window == NULL && ms->Flags == 0 && ms->FocusScopeId == 0); // No recursion allowed yet (we could allow it if we deem it useful) + ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[0];; + IM_ASSERT(g.CurrentMultiSelect == NULL); // No recursion allowed yet (we could allow it if we deem it useful) + g.CurrentMultiSelect = ms; // FIXME: BeginFocusScope() ms->Clear(); @@ -7169,8 +7177,8 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* if (Shortcut(ImGuiKey_Escape)) ms->In.RequestClear = true; - //if (ms->In.RequestClear) IMGUI_DEBUG_LOG_SELECTION("BeginMultiSelect: RequestClear\n"); - //if (ms->In.RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("BeginMultiSelect: RequestSelectAll\n"); + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) + DebugLogMultiSelectRequests(&ms->In); return &ms->In; } @@ -7178,8 +7186,9 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* ImGuiMultiSelectData* ImGui::EndMultiSelect() { ImGuiContext& g = *GImGui; - ImGuiMultiSelectState* ms = &g.MultiSelectState; - IM_ASSERT(g.MultiSelectState.FocusScopeId == g.CurrentFocusScopeId); + ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; + IM_ASSERT(ms->FocusScopeId == g.CurrentFocusScopeId); + IM_ASSERT(g.CurrentMultiSelect != NULL && g.CurrentMultiSelect->Window == g.CurrentWindow); // Clear selection when clicking void? // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! @@ -7198,10 +7207,10 @@ ImGuiMultiSelectData* ImGui::EndMultiSelect() ms->Window = NULL; ms->Flags = ImGuiMultiSelectFlags_None; PopFocusScope(); + g.CurrentMultiSelect = NULL; - //if (ms->Out.RequestClear) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestClear\n"); - //if (ms->Out.RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestSelectAll\n"); - //if (ms->Out.RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestSetRange %p..%p = %d (dir %+d)\n", ms->Out.RangeSrc, ms->Out.RangeDst, ms->Out.RangeValue, ms->Out.RangeDirection); + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) + DebugLogMultiSelectRequests(&ms->Out); return &ms->Out; } @@ -7218,16 +7227,17 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d g.NextItemData.SelectionUserData = selection_user_data; g.NextItemData.FocusScopeId = g.CurrentFocusScopeId; - // Auto updating RangeSrcPassedBy for cases were clipper is not used. - if (g.MultiSelectState.In.RangeSrc == (void*)selection_user_data) - g.MultiSelectState.In.RangeSrcPassedBy = true; + // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping) + if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect) + if (ms->In.RangeSrc == (void*)selection_user_data) + ms->In.RangeSrcPassedBy = true; } void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - ImGuiMultiSelectState* ms = &g.MultiSelectState; + ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; IM_UNUSED(window); IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); @@ -7266,7 +7276,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - ImGuiMultiSelectState* ms = &g.MultiSelectState; + ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; void* item_data = (void*)g.NextItemData.SelectionUserData; From 35b5ebc9b55f92f685cacbe5caf1f0bac1226e6f Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 24 May 2023 15:16:17 +0200 Subject: [PATCH 025/132] MultiSelect: (Breaking) Rename ImGuiMultiSelectData to ImGuiMultiSelectIO. --- imgui.h | 18 +++++++++++------- imgui_demo.cpp | 32 ++++++++++++++++---------------- imgui_internal.h | 4 ++-- imgui_widgets.cpp | 6 +++--- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/imgui.h b/imgui.h index 349c77f3e568..b18a770bad08 100644 --- a/imgui.h +++ b/imgui.h @@ -175,7 +175,7 @@ struct ImGuiIO; // Main configuration and I/O between your a struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use) struct ImGuiKeyData; // Storage for ImGuiIO and IsKeyDown(), IsKeyPressed() etc functions. struct ImGuiListClipper; // Helper to manually clip large list of items -struct ImGuiMultiSelectData; // State for a BeginMultiSelect() block +struct ImGuiMultiSelectIO; // Structure to interact with a BeginMultiSelect()/EndMultiSelect() block struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame struct ImGuiPayload; // User data payload for drag and drop operations struct ImGuiPlatformImeData; // Platform IME data for io.PlatformSetImeDataFn() function. @@ -670,10 +670,10 @@ namespace ImGui // 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 ImGuiMultiSelectData for details. + // Read comments near ImGuiMultiSelectIO for details. // When enabled, Selectable() and TreeNode() functions will return true when selection needs toggling. - IMGUI_API ImGuiMultiSelectData* BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected); - IMGUI_API ImGuiMultiSelectData* EndMultiSelect(); + IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected); + IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); // Widgets: List Boxes @@ -2779,18 +2779,22 @@ enum ImGuiMultiSelectFlags_ // 5) Call EndMultiSelect(). Save the value of ->RangeSrc 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. -struct ImGuiMultiSelectData +struct ImGuiMultiSelectIO { + // Output (return by BeginMultiSelect()/EndMultiSelect() + // - Always process requests in their structure order. bool RequestClear; // Begin, End // 1. Request user to clear selection bool RequestSelectAll; // Begin, End // 2. Request user to select all bool RequestSetRange; // End // 3. Request user to set or clear selection in the [RangeSrc..RangeDst] range - bool RangeSrcPassedBy; // Loop // (If clipping) Need to be set by user if RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. bool RangeValue; // End // End: parameter from RequestSetRange request. true = Select Range, false = Unselect Range. void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect() void* RangeDst; // End // End: parameter from RequestSetRange request. int RangeDirection; // End // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -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. - ImGuiMultiSelectData() { Clear(); } + // Input (written by user between BeginMultiSelect()/EndMultiSelect() + bool RangeSrcPassedBy; // Loop // (If using a clipper) Need to be set by user if RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. + + ImGuiMultiSelectIO() { Clear(); } void Clear() { RequestClear = RequestSelectAll = RequestSetRange = RangeSrcPassedBy = RangeValue = false; diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 3008ca94642f..e476864d80a4 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2800,11 +2800,11 @@ struct ExampleSelection void SelectAll(int count) { Storage.Data.resize(count); for (int idx = 0; idx < count; idx++) Storage.Data[idx] = ImGuiStoragePair((ImGuiID)idx, 1); SelectionSize = count; } // This could be using SetRange(), but it this way is faster. // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). Must be done in this order! Order->SelectAll->SetRange. - void ApplyRequests(ImGuiMultiSelectData* ms_data, int items_count) + void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count) { - if (ms_data->RequestClear) { Clear(); } - if (ms_data->RequestSelectAll) { SelectAll(items_count); } - if (ms_data->RequestSetRange) { SetRange((int)(intptr_t)ms_data->RangeSrc, (int)(intptr_t)ms_data->RangeDst, ms_data->RangeValue ? 1 : 0); } + if (ms_io->RequestClear) { Clear(); } + if (ms_io->RequestSelectAll) { SelectAll(items_count); } + if (ms_io->RequestSetRange) { SetRange((int)(intptr_t)ms_io->RangeSrc, (int)(intptr_t)ms_io->RangeDst, ms_io->RangeValue ? 1 : 0); } } }; @@ -2876,8 +2876,8 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; - ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); - selection.ApplyRequests(multi_select_data, ITEMS_COUNT); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); + selection.ApplyRequests(ms_io, ITEMS_COUNT); for (int n = 0; n < ITEMS_COUNT; n++) { @@ -2891,9 +2891,9 @@ static void ShowDemoWindowMultiSelect() } // Apply multi-select requests - multi_select_data = ImGui::EndMultiSelect(); - selection.RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; - selection.ApplyRequests(multi_select_data, ITEMS_COUNT); + ms_io = ImGui::EndMultiSelect(); + selection.RangeRef = (int)(intptr_t)ms_io->RangeSrc; + selection.ApplyRequests(ms_io, ITEMS_COUNT); ImGui::EndListBox(); } @@ -2963,8 +2963,8 @@ static void ShowDemoWindowMultiSelect() ImGuiMultiSelectFlags local_flags = flags; if (use_multiple_scopes) local_flags &= ~ImGuiMultiSelectFlags_ClearOnClickWindowVoid; // local_flags |= ImGuiMultiSelectFlags_ClearOnClickRectVoid; - ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(local_flags, (void*)(intptr_t)selection->RangeRef, selection->GetSelected(selection->RangeRef)); - selection->ApplyRequests(multi_select_data, ITEMS_COUNT); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(local_flags, (void*)(intptr_t)selection->RangeRef, selection->GetSelected(selection->RangeRef)); + selection->ApplyRequests(ms_io, ITEMS_COUNT); if (use_multiple_scopes) ImGui::Text("Selection size: %d", selection->GetSize()); // Draw counter below Separator and after BeginMultiSelect() @@ -2983,8 +2983,8 @@ static void ShowDemoWindowMultiSelect() while (clipper.Step()) { // IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeSrc was passed over. - if ((int)(intptr_t)multi_select_data->RangeSrc <= clipper.DisplayStart) - multi_select_data->RangeSrcPassedBy = true; + if ((int)(intptr_t)ms_io->RangeSrc <= clipper.DisplayStart) + ms_io->RangeSrcPassedBy = true; for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) { @@ -3062,9 +3062,9 @@ static void ShowDemoWindowMultiSelect() } // Apply multi-select requests - multi_select_data = ImGui::EndMultiSelect(); - selection->RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; - selection->ApplyRequests(multi_select_data, ITEMS_COUNT); + ms_io = ImGui::EndMultiSelect(); + selection->RangeRef = (int)(intptr_t)ms_io->RangeSrc; + selection->ApplyRequests(ms_io, ITEMS_COUNT); if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); diff --git a/imgui_internal.h b/imgui_internal.h index ad5366765b5f..4770a2fd6eb9 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1719,8 +1719,8 @@ struct IMGUI_API ImGuiMultiSelectTempData ImGuiMultiSelectFlags Flags; ImGuiKeyChord KeyMods; ImGuiWindow* Window; - ImGuiMultiSelectData In; // The In requests are set and returned by BeginMultiSelect() - ImGuiMultiSelectData Out; // The Out requests are finalized and returned by EndMultiSelect() + ImGuiMultiSelectIO In; // The In requests are set and returned by BeginMultiSelect() + ImGuiMultiSelectIO Out; // The Out requests are finalized 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 InRangeDstPassedBy; // (Internal) set by the the item that match NavJustMovedToId when InRequestRangeSetNav is set. bool InRequestSetRangeNav; // (Internal) set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 55453dd50ebc..de53de54d217 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7122,7 +7122,7 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) // - MultiSelectItemFooter() [Internal] //------------------------------------------------------------------------- -static void DebugLogMultiSelectRequests(const ImGuiMultiSelectData* data) +static void DebugLogMultiSelectRequests(const ImGuiMultiSelectIO* data) { ImGuiContext& g = *GImGui; if (data->RequestClear) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestClear\n"); @@ -7130,7 +7130,7 @@ static void DebugLogMultiSelectRequests(const ImGuiMultiSelectData* data) if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestSetRange %p..%p = %d (dir %+d)\n", data->RangeSrc, data->RangeDst, data->RangeValue, data->RangeDirection); } -ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected) +ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; @@ -7183,7 +7183,7 @@ ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* return &ms->In; } -ImGuiMultiSelectData* ImGui::EndMultiSelect() +ImGuiMultiSelectIO* ImGui::EndMultiSelect() { ImGuiContext& g = *GImGui; ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; From c61ada200f9f4d3bf50fe70bcdbad4a76f6b7182 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 24 May 2023 16:31:00 +0200 Subject: [PATCH 026/132] MultiSelect: Demo tweak. Removed multi-scope from Advanced (too messy), made it a seperate mini-demo. --- imgui_demo.cpp | 264 +++++++++++++++++++++++++++---------------------- 1 file changed, 144 insertions(+), 120 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index e476864d80a4..8117437da2b9 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2870,9 +2870,10 @@ static void ShowDemoWindowMultiSelect() ImGui::BulletText("Shift modifier for range selection."); ImGui::BulletText("CTRL+A to select all."); - // The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling regions). + // 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; @@ -2901,11 +2902,49 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } + // Demonstrate individual selection scopes in same window + IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full, multiple scopes)"); + if (ImGui::TreeNode("Multiple Selection (full, multiple scopes)")) + { + const int SCOPES_COUNT = 3; + const int ITEMS_COUNT = 8; // Per scope + static ExampleSelection selections_data[SCOPES_COUNT]; + + for (int selection_scope_n = 0; selection_scope_n < SCOPES_COUNT; selection_scope_n++) + { + ExampleSelection* selection = &selections_data[selection_scope_n]; + ImGui::SeparatorText("Selection scope"); + ImGui::Text("Selection size: %d/%d", selection->GetSize(), ITEMS_COUNT); + 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)); + selection->ApplyRequests(ms_io, ITEMS_COUNT); + + for (int n = 0; n < ITEMS_COUNT; n++) + { + char label[64]; + sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); + bool item_is_selected = selection->GetSelected(n); + ImGui::SetNextItemSelectionUserData(n); + ImGui::Selectable(label, item_is_selected); + if (ImGui::IsItemToggledSelection()) + selection->SetSelected(n, !item_is_selected); + } + + // Apply multi-select requests + ms_io = ImGui::EndMultiSelect(); + selection->RangeRef = (int)(intptr_t)ms_io->RangeSrc; + selection->ApplyRequests(ms_io, ITEMS_COUNT); + ImGui::PopID(); + } + ImGui::TreePop(); + } + // Advanced demonstration of BeginMultiSelect() // - Showcase clipping. // - Showcase basic drag and drop. // - Showcase TreeNode variant (note that tree node don't expand in the demo: supporting expanding tree nodes + clipping a separate thing). - // - Showcase having multiple multi-selection scopes in the same window. // - Showcase using inside a table. IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full, advanced)"); //ImGui::SetNextItemOpen(true, ImGuiCond_Once); @@ -2913,167 +2952,152 @@ static void ShowDemoWindowMultiSelect() { // Options enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; - static bool use_table = false; + static bool use_clipper = true; static bool use_drag_drop = true; - static bool use_multiple_scopes = false; + static bool show_in_table = false; + static bool show_color_button = false; static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; static WidgetType widget_type = WidgetType_Selectable; if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } ImGui::SameLine(); if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } - ImGui::Checkbox("Use table", &use_table); - ImGui::Checkbox("Use drag & drop", &use_drag_drop); - ImGui::Checkbox("Multiple selection scopes in same window", &use_multiple_scopes); + ImGui::Checkbox("Enable clipper", &use_clipper); + ImGui::Checkbox("Enable drag & drop", &use_drag_drop); + ImGui::Checkbox("Show in a table", &show_in_table); + ImGui::Checkbox("Show color button", &show_color_button); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoMultiSelect", &flags, ImGuiMultiSelectFlags_NoMultiSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoUnselect", &flags, ImGuiMultiSelectFlags_NoUnselect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); - ImGui::BeginDisabled(use_multiple_scopes); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickWindowVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickWindowVoid); - ImGui::EndDisabled(); - // When 'use_multiple_scopes' is set we show 3 selection scopes in the host window instead of 1 in a scrolling window. - static ExampleSelection selections_data[3]; - const int selection_scope_count = use_multiple_scopes ? 3 : 1; - for (int selection_scope_n = 0; selection_scope_n < selection_scope_count; selection_scope_n++) + const int ITEMS_COUNT = 1000; + static ExampleSelection selection; + + ImGui::Text("Selection size: %d/%d", selection.GetSize(), ITEMS_COUNT); + if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ExampleSelection* selection = &selections_data[selection_scope_n]; + ImVec2 color_button_sz(ImGui::GetFontSize(), ImGui::GetFontSize()); + if (widget_type == WidgetType_TreeNode) + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - const int ITEMS_COUNT = use_multiple_scopes ? 12 : 1000; // Smaller count to make it easier to see multiple scopes in same screen. - ImGui::PushID(selection_scope_n); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); + selection.ApplyRequests(ms_io, ITEMS_COUNT); - // Open a scrolling region - bool draw_selection = true; - if (use_multiple_scopes) + if (show_in_table) { - ImGui::SeparatorText("Selection scope"); - } - else - { - ImGui::Text("Selection size: %d", selection->GetSize()); - draw_selection = ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20)); - } - if (draw_selection) - { - ImVec2 color_button_sz(ImGui::GetFontSize(), ImGui::GetFontSize()); if (widget_type == WidgetType_TreeNode) - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - - ImGuiMultiSelectFlags local_flags = flags; - if (use_multiple_scopes) - local_flags &= ~ImGuiMultiSelectFlags_ClearOnClickWindowVoid; // local_flags |= ImGuiMultiSelectFlags_ClearOnClickRectVoid; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(local_flags, (void*)(intptr_t)selection->RangeRef, selection->GetSelected(selection->RangeRef)); - selection->ApplyRequests(ms_io, ITEMS_COUNT); - - if (use_multiple_scopes) - ImGui::Text("Selection size: %d", selection->GetSize()); // Draw counter below Separator and after BeginMultiSelect() - - if (use_table) - { ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f)); - ImGui::BeginTable("##Split", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_NoPadOuterX); - ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.70f); - ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.30f); - //ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - } + ImGui::BeginTable("##Split", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_NoPadOuterX); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.70f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.30f); + //ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); + } - ImGuiListClipper clipper; + ImGuiListClipper clipper; + if (use_clipper) clipper.Begin(ITEMS_COUNT); - while (clipper.Step()) - { - // IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeSrc was passed over. + + while (!use_clipper || clipper.Step()) + { + // IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeSrc was passed over. + if (use_clipper) if ((int)(intptr_t)ms_io->RangeSrc <= clipper.DisplayStart) ms_io->RangeSrcPassedBy = true; - for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) - { - if (use_table) - ImGui::TableNextColumn(); + const int item_begin = use_clipper ? clipper.DisplayStart : 0; + const int item_end = use_clipper ? clipper.DisplayEnd : ITEMS_COUNT; + for (int n = item_begin; n < item_end; n++) + { + if (show_in_table) + ImGui::TableNextColumn(); - ImGui::PushID(n); - const char* category = random_names[n % IM_ARRAYSIZE(random_names)]; - char label[64]; - sprintf(label, "Object %05d: category: %s", n, category); - bool item_is_selected = selection->GetSelected(n); + ImGui::PushID(n); + const char* category = random_names[n % IM_ARRAYSIZE(random_names)]; + char label[64]; + sprintf(label, "Object %05d: %s", n, category); - // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part - // of the selection scope doesn't erroneously alter our selection (FIXME-TESTS: Add a test for that!). + // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part + // of the selection scope doesn't erroneously alter our selection (FIXME-TESTS: Add a test for that!). + if (show_color_button) + { ImU32 dummy_col = (ImU32)((unsigned int)n * 0xC250B74B) | IM_COL32_A_MASK; ImGui::ColorButton("##", ImColor(dummy_col), ImGuiColorEditFlags_NoTooltip, color_button_sz); ImGui::SameLine(); + } - ImGui::SetNextItemSelectionUserData(n); - if (widget_type == WidgetType_Selectable) - { - ImGui::Selectable(label, item_is_selected); - if (ImGui::IsItemToggledSelection()) - selection->SetSelected(n, !item_is_selected); - if (use_drag_drop && ImGui::BeginDragDropSource()) - { - ImGui::Text("(Dragging %d items)", selection->GetSize()); - ImGui::EndDragDropSource(); - } - } - else if (widget_type == WidgetType_TreeNode) + bool item_is_selected = selection.GetSelected(n); + ImGui::SetNextItemSelectionUserData(n); + if (widget_type == WidgetType_Selectable) + { + ImGui::Selectable(label, item_is_selected); + if (ImGui::IsItemToggledSelection()) + selection.SetSelected(n, !item_is_selected); + if (use_drag_drop && ImGui::BeginDragDropSource()) { - ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth; - tree_node_flags |= ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; - if (item_is_selected) - tree_node_flags |= ImGuiTreeNodeFlags_Selected; - bool open = ImGui::TreeNodeEx(label, tree_node_flags); - if (ImGui::IsItemToggledSelection()) - selection->SetSelected(n, !item_is_selected); - if (use_drag_drop && ImGui::BeginDragDropSource()) - { - ImGui::Text("(Dragging %d items)", selection->GetSize()); - ImGui::EndDragDropSource(); - } - if (open) - ImGui::TreePop(); + ImGui::Text("(Dragging %d items)", selection.GetSize()); + ImGui::EndDragDropSource(); } - - // Right-click: context menu - if (ImGui::BeginPopupContextItem()) + } + else if (widget_type == WidgetType_TreeNode) + { + ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth; + tree_node_flags |= ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (item_is_selected) + tree_node_flags |= ImGuiTreeNodeFlags_Selected; + bool open = ImGui::TreeNodeEx(label, tree_node_flags); + if (ImGui::IsItemToggledSelection()) + selection.SetSelected(n, !item_is_selected); + if (use_drag_drop && ImGui::BeginDragDropSource()) { - ImGui::Text("(Testing Selectable inside an embedded popup)"); - ImGui::Selectable("Close"); - ImGui::EndPopup(); + ImGui::Text("(Dragging %d items)", selection.GetSize()); + ImGui::EndDragDropSource(); } + if (open) + ImGui::TreePop(); + } - // Demo content within a table - if (use_table) - { - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(-FLT_MIN); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); - ImGui::InputText("###NoLabel", (char*)(void*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); - ImGui::PopStyleVar(); - } + // Right-click: context menu + if (ImGui::BeginPopupContextItem()) + { + ImGui::Text("(Testing Selectable inside an embedded popup)"); + ImGui::Selectable("Close"); + ImGui::EndPopup(); + } - ImGui::PopID(); + // Demo content within a table + if (show_in_table) + { + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::InputText("###NoLabel", (char*)(void*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); + ImGui::PopStyleVar(); } - } - if (use_table) - { - ImGui::EndTable(); - ImGui::PopStyleVar(); + ImGui::PopID(); } + if (!use_clipper) + break; + } - // Apply multi-select requests - ms_io = ImGui::EndMultiSelect(); - selection->RangeRef = (int)(intptr_t)ms_io->RangeSrc; - selection->ApplyRequests(ms_io, ITEMS_COUNT); - + if (show_in_table) + { + ImGui::EndTable(); if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); - - if (use_multiple_scopes == false) - ImGui::EndListBox(); } - ImGui::PopID(); // ImGui::PushID(selection_scope_n); - } // for each selection scope (1 or 3) + + // Apply multi-select requests + ms_io = ImGui::EndMultiSelect(); + selection.RangeRef = (int)(intptr_t)ms_io->RangeSrc; + selection.ApplyRequests(ms_io, ITEMS_COUNT); + + if (widget_type == WidgetType_TreeNode) + ImGui::PopStyleVar(); + ImGui::EndListBox(); + } ImGui::TreePop(); } From a39f9e766147a9d018814894314ec6a64d011479 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 1 Jun 2023 17:49:33 +0200 Subject: [PATCH 027/132] MultiSelect: Internals rename of IO fields to avoid ambiguity with io/rw concepts + memset constructors, tweaks. debug --- imgui.h | 34 +++++++---------- imgui_demo.cpp | 2 +- imgui_internal.h | 13 ++++--- imgui_widgets.cpp | 96 +++++++++++++++++++++++------------------------ 4 files changed, 70 insertions(+), 75 deletions(-) diff --git a/imgui.h b/imgui.h index b18a770bad08..7d0582a28ea9 100644 --- a/imgui.h +++ b/imgui.h @@ -2781,26 +2781,20 @@ enum ImGuiMultiSelectFlags_ // If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable() on a per-item basis. struct ImGuiMultiSelectIO { - // Output (return by BeginMultiSelect()/EndMultiSelect() - // - Always process requests in their structure order. - bool RequestClear; // Begin, End // 1. Request user to clear selection - bool RequestSelectAll; // Begin, End // 2. Request user to select all - bool RequestSetRange; // End // 3. Request user to set or clear selection in the [RangeSrc..RangeDst] range - bool RangeValue; // End // End: parameter from RequestSetRange request. true = Select Range, false = Unselect Range. - void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect() - void* RangeDst; // End // End: parameter from RequestSetRange request. - int RangeDirection; // End // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -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. - - // Input (written by user between BeginMultiSelect()/EndMultiSelect() - bool RangeSrcPassedBy; // Loop // (If using a clipper) Need to be set by user if RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. - - ImGuiMultiSelectIO() { Clear(); } - void Clear() - { - RequestClear = RequestSelectAll = RequestSetRange = RangeSrcPassedBy = RangeValue = false; - RangeSrc = RangeDst = NULL; - RangeDirection = 0; - } + // - Always process requests in this order: Clear, SelectAll, SetRange. + // - Below: who reads/writes each fields? 'r'=read, 'w'=write, 'ms'=multi-select code, 'app'=application/user code. + // // BEGIN / LOOP / END + bool RequestClear; // ms:w, app:r / / ms:w, app:r // 1. Request user to clear selection (processed by app code) + bool RequestSelectAll; // ms:w, app:r / / ms:w, app:r // 2. Request user to select all (processed by app code) + bool RequestSetRange; // / / ms:w, app:r // 3. Request user to alter selection in the [RangeSrc..RangeDst] range using RangeValue. In practice, only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. + void* RangeSrc; // ms:w / app:r / ms:w, app:r // Begin: Last known RangeSrc value. End: parameter from RequestSetRange request. + void* RangeDst; // / / ms:w, app:r // End: parameter from RequestSetRange request. + ImS8 RangeDirection; // / / ms:w, app:r // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -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 RangeValue; // / / 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 a clipper) Need to be set by user if RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. + + ImGuiMultiSelectIO() { Clear(); } + void Clear() { memset(this, 0, sizeof(*this)); } }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 8117437da2b9..a07cc6ebfde2 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2779,7 +2779,7 @@ struct ExampleSelection { // Data ImGuiStorage Storage; // Selection set - int SelectionSize; // Number of selected items (== number of 1 in the Storage, maintained by this class) + int SelectionSize; // Number of selected items (== number of 1 in the Storage, maintained by this class) // FIXME-RANGESELECT: Imply more difficult to track with intrusive selection schemes? int RangeRef; // Reference/pivot item (generally last clicked item) // Functions diff --git a/imgui_internal.h b/imgui_internal.h index 4770a2fd6eb9..42f822c385d2 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1713,21 +1713,22 @@ struct ImGuiOldColumns #ifdef IMGUI_HAS_MULTI_SELECT +// Temporary storage for multi-select struct IMGUI_API ImGuiMultiSelectTempData { ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually) ImGuiMultiSelectFlags Flags; ImGuiKeyChord KeyMods; ImGuiWindow* Window; - ImGuiMultiSelectIO In; // The In requests are set and returned by BeginMultiSelect() - ImGuiMultiSelectIO Out; // The Out requests are finalized and returned by EndMultiSelect() + 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 InRangeDstPassedBy; // (Internal) set by the the item that match NavJustMovedToId when InRequestRangeSetNav is set. - bool InRequestSetRangeNav; // (Internal) set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. + 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 SetRangeDstPassedBy; // 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() { FocusScopeId = 0; Flags = ImGuiMultiSelectFlags_None; KeyMods = ImGuiMod_None; Window = NULL; In.Clear(); Out.Clear(); InRangeDstPassedBy = InRequestSetRangeNav = false; } + ImGuiMultiSelectTempData() { Clear(); } + void Clear() { memset(this, 0, sizeof(*this)); } }; #endif // #ifdef IMGUI_HAS_MULTI_SELECT diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index de53de54d217..d9008e34056f 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7120,21 +7120,22 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) // - SetNextItemSelectionUserData() // - MultiSelectItemHeader() [Internal] // - MultiSelectItemFooter() [Internal] +// - DebugNodeMultiSelectState() [Internal] //------------------------------------------------------------------------- -static void DebugLogMultiSelectRequests(const ImGuiMultiSelectIO* data) +static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* data) { ImGuiContext& g = *GImGui; - if (data->RequestClear) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestClear\n"); - if (data->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestSelectAll\n"); - if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("EndMultiSelect: RequestSetRange %p..%p = %d (dir %+d)\n", data->RangeSrc, data->RangeDst, data->RangeValue, data->RangeDirection); + if (data->RequestClear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestClear\n", function); + if (data->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSelectAll\n", function); + if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %p..%p = %d (dir %+d)\n", function, data->RangeSrc, data->RangeDst, data->RangeValue, data->RangeDirection); } ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[0];; + ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[0]; IM_ASSERT(g.CurrentMultiSelect == NULL); // No recursion allowed yet (we could allow it if we deem it useful) g.CurrentMultiSelect = ms; @@ -7151,36 +7152,35 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* r if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0) { - ms->In.RangeSrc = ms->Out.RangeSrc = range_ref; - ms->In.RangeValue = ms->Out.RangeValue = range_ref_is_selected; + ms->BeginIO.RangeSrc = ms->EndIO.RangeSrc = range_ref; + ms->BeginIO.RangeValue = ms->EndIO.RangeValue = range_ref_is_selected; } // Auto clear when using Navigation to move within the selection (we compare SelectScopeId so it possible to use multiple lists inside a same window) - // FIXME: Polling key mods after the fact (frame following the move request) is incorrect, but latching it would requires non-trivial change in MultiSelectItemFooter() if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) { if (ms->KeyMods & ImGuiMod_Shift) - ms->InRequestSetRangeNav = true; + ms->IsSetRange = true; if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) - ms->In.RequestClear = true; + ms->BeginIO.RequestClear = true; } // Shortcut: Select all (CTRL+A) if (ms->IsFocused && !(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) - ms->In.RequestSelectAll = true; + 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 (Shortcut(ImGuiKey_Escape)) - ms->In.RequestClear = true; + ms->BeginIO.RequestClear = true; if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) - DebugLogMultiSelectRequests(&ms->In); + DebugLogMultiSelectRequests("BeginMultiSelect", &ms->BeginIO); - return &ms->In; + return &ms->BeginIO; } ImGuiMultiSelectIO* ImGui::EndMultiSelect() @@ -7196,13 +7196,13 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (IsWindowHovered() && g.HoveredId == 0) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { - ms->Out.RequestClear = true; - ms->Out.RequestSelectAll = ms->Out.RequestSetRange = false; + ms->EndIO.RequestClear = true; + ms->EndIO.RequestSelectAll = ms->EndIO.RequestSetRange = false; } // Unwind if (ms->Flags & ImGuiMultiSelectFlags_NoUnselect) - ms->Out.RangeValue = true; + ms->EndIO.RangeValue = true; ms->FocusScopeId = 0; ms->Window = NULL; ms->Flags = ImGuiMultiSelectFlags_None; @@ -7210,9 +7210,9 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() g.CurrentMultiSelect = NULL; if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) - DebugLogMultiSelectRequests(&ms->Out); + DebugLogMultiSelectRequests("EndMultiSelect", &ms->EndIO); - return &ms->Out; + return &ms->EndIO; } void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data) @@ -7229,8 +7229,8 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping) if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect) - if (ms->In.RangeSrc == (void*)selection_user_data) - ms->In.RangeSrcPassedBy = true; + if (ms->BeginIO.RangeSrc == (void*)selection_user_data) + ms->BeginIO.RangeSrcPassedBy = true; } void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) @@ -7248,23 +7248,23 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. // If you are using a clipper (aka not submitting every element of the list) you need to process the Clear/SelectAll request after calling BeginMultiSelect() bool selected = *p_selected; - if (ms->In.RequestClear) + if (ms->BeginIO.RequestClear) selected = false; - else if (ms->In.RequestSelectAll) + else if (ms->BeginIO.RequestSelectAll) selected = true; // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) // For this to work, IF the user is clipping items, they need to set RangeSrcPassedBy = true to notify the system. - if (ms->InRequestSetRangeNav) + if (ms->IsSetRange) { IM_ASSERT(id != 0); IM_ASSERT((ms->KeyMods & ImGuiMod_Shift) != 0); - const bool is_range_src = (ms->In.RangeSrc == item_data); - const bool is_range_dst = !ms->InRangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. + const bool is_range_src = (ms->BeginIO.RangeSrc == item_data); + const bool is_range_dst = !ms->SetRangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. if (is_range_dst) - ms->InRangeDstPassedBy = true; - if (is_range_src || is_range_dst || ms->In.RangeSrcPassedBy != ms->InRangeDstPassedBy) - selected = ms->In.RangeValue; + ms->SetRangeDstPassedBy = true; + if (is_range_src || is_range_dst || ms->BeginIO.RangeSrcPassedBy != ms->SetRangeDstPassedBy) + selected = ms->BeginIO.RangeValue; else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0) selected = false; } @@ -7331,53 +7331,53 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) if (is_shift && is_multiselect) { // Shift+Arrow always select, Ctrl+Shift+Arrow copy source selection state. - ms->Out.RequestSetRange = true; - ms->Out.RangeDst = item_data; + ms->EndIO.RequestSetRange = true; + ms->EndIO.RangeDst = item_data; if (!is_ctrl) - ms->Out.RangeValue = true; - ms->Out.RangeDirection = ms->In.RangeSrcPassedBy ? +1 : -1; + ms->EndIO.RangeValue = true; + ms->EndIO.RangeDirection = ms->BeginIO.RangeSrcPassedBy ? +1 : -1; } else { // Ctrl inverts selection, otherwise always select selected = (is_ctrl && (ms->Flags & ImGuiMultiSelectFlags_NoUnselect) == 0) ? !selected : true; - ms->Out.RangeSrc = ms->Out.RangeDst = item_data; - ms->Out.RangeValue = selected; + ms->EndIO.RangeSrc = ms->EndIO.RangeDst = item_data; + ms->EndIO.RangeValue = selected; } if (input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) { // Mouse click without CTRL clears the selection, unless the clicked item is already selected if (is_multiselect && !is_ctrl) - ms->Out.RequestClear = true; - if (is_multiselect && !is_shift && ms->Out.RequestClear) + ms->EndIO.RequestClear = true; + if (is_multiselect && !is_shift && ms->EndIO.RequestClear) { // For toggle selection unless there is a Clear request, we can handle it completely locally without sending a RangeSet request. - IM_ASSERT(ms->Out.RangeSrc == ms->Out.RangeDst); // Setup by else block above - ms->Out.RequestSetRange = true; - ms->Out.RangeValue = selected; - ms->Out.RangeDirection = +1; + IM_ASSERT(ms->EndIO.RangeSrc == ms->EndIO.RangeDst); // Setup by else block above + ms->EndIO.RequestSetRange = true; + ms->EndIO.RangeValue = selected; + ms->EndIO.RangeDirection = +1; } if (!is_multiselect) { // Clear selection, set single item range - IM_ASSERT(ms->Out.RangeSrc == item_data && ms->Out.RangeDst == item_data); // Setup by block above - ms->Out.RequestClear = true; - ms->Out.RequestSetRange = true; + IM_ASSERT(ms->EndIO.RangeSrc == item_data && ms->EndIO.RangeDst == item_data); // Setup by block above + ms->EndIO.RequestClear = true; + ms->EndIO.RequestSetRange = true; } } else if (input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) { if (is_multiselect && is_shift && !is_ctrl) - ms->Out.RequestClear = true; + ms->EndIO.RequestClear = true; else if (!is_multiselect) - ms->Out.RequestClear = true; + ms->EndIO.RequestClear = true; } } // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) - if (ms->Out.RangeSrc == item_data && is_ctrl && is_shift && is_multiselect && !(ms->Flags & ImGuiMultiSelectFlags_NoUnselect)) - ms->Out.RangeValue = selected; + if (ms->EndIO.RangeSrc == item_data && is_ctrl && is_shift && is_multiselect && !(ms->Flags & ImGuiMultiSelectFlags_NoUnselect)) + ms->EndIO.RangeValue = selected; *p_selected = selected; *p_pressed = pressed; From a83326bc52969f71b4ec0ad5368b4922f6e0f25b Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 2 Jun 2023 15:49:17 +0200 Subject: [PATCH 028/132] MultiSelect: (Breaking) Renamed 'RangeSrc -> 'RangeSrcItem', "RangeDst' -> 'RangeDstItem' This is necessary to have consistent names in upcoming fields (NavIdItem etc.) --- imgui.h | 22 +++++++++++----------- imgui_demo.cpp | 12 ++++++------ imgui_internal.h | 2 +- imgui_widgets.cpp | 24 ++++++++++++------------ 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/imgui.h b/imgui.h index 7d0582a28ea9..fc25f379850a 100644 --- a/imgui.h +++ b/imgui.h @@ -2757,26 +2757,26 @@ enum ImGuiMultiSelectFlags_ // performance penalty, but requires a little more work on the code. If you only have a few hundreds elements in your possible selection set, // you may as well not bother with clipping, as the cost should be negligible (as least on Dear ImGui side). // If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards. -// - The void* RangeSrc/RangeDst value represent a selectable object. They are the values you pass to SetNextItemSelectionUserData(). +// - The void* RangeSrcItem/RangeDstItem value represent a selectable object. They are the value you pass to SetNextItemSelectionUserData(). // Most likely you will want to store an index here. // Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points and you will need to interpolate // 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 RangeSrc to RangeDst). +// 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 ->RangeSrc and its selection state. +// 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 RangeSrc item is part of the items clipped before the first submitted/visible item. [Only required if you are using a clipper in step 4] +// 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->RangeSrc) { data->RangeSrcPassedBy = true; } +// 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. // Call IsItemToggledSelection() to query if the selection state has been toggled, if you need the 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 ->RangeSrc for the next frame (you may convert the value in a format that is safe for persistance) +// 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. struct ImGuiMultiSelectIO @@ -2786,12 +2786,12 @@ struct ImGuiMultiSelectIO // // BEGIN / LOOP / END bool RequestClear; // ms:w, app:r / / ms:w, app:r // 1. Request user to clear selection (processed by app code) bool RequestSelectAll; // ms:w, app:r / / ms:w, app:r // 2. Request user to select all (processed by app code) - bool RequestSetRange; // / / ms:w, app:r // 3. Request user to alter selection in the [RangeSrc..RangeDst] range using RangeValue. In practice, only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. - void* RangeSrc; // ms:w / app:r / ms:w, app:r // Begin: Last known RangeSrc value. End: parameter from RequestSetRange request. - void* RangeDst; // / / ms:w, app:r // End: parameter from RequestSetRange request. - ImS8 RangeDirection; // / / ms:w, app:r // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -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 RequestSetRange; // / / ms:w, app:r // 3. Request user to alter selection in the [RangeSrcItem..RangeDstItem] range using RangeValue. In practice, only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. + void* RangeSrcItem; // ms:w / app:r / ms:w, app:r // Begin: Last known SetNextItemSelectionData() value for RangeSrcItem value. 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 RangeValue; // / / 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 a clipper) Need to be set by user if RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. + bool RangeSrcPassedBy; // / ms:rw app:w / ms:r // (If using a clipper) Need to be set by user if RangeSrcItem was part of the clipped set before submitting the visible items. Ignore if not clipping. ImGuiMultiSelectIO() { Clear(); } void Clear() { memset(this, 0, sizeof(*this)); } diff --git a/imgui_demo.cpp b/imgui_demo.cpp index a07cc6ebfde2..17c976c83a15 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2804,7 +2804,7 @@ struct ExampleSelection { if (ms_io->RequestClear) { Clear(); } if (ms_io->RequestSelectAll) { SelectAll(items_count); } - if (ms_io->RequestSetRange) { SetRange((int)(intptr_t)ms_io->RangeSrc, (int)(intptr_t)ms_io->RangeDst, ms_io->RangeValue ? 1 : 0); } + if (ms_io->RequestSetRange) { SetRange((int)(intptr_t)ms_io->RangeSrcItem, (int)(intptr_t)ms_io->RangeDstItem, ms_io->RangeValue ? 1 : 0); } } }; @@ -2893,7 +2893,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection.RangeRef = (int)(intptr_t)ms_io->RangeSrc; + selection.RangeRef = (int)(intptr_t)ms_io->RangeSrcItem; selection.ApplyRequests(ms_io, ITEMS_COUNT); ImGui::EndListBox(); @@ -2934,7 +2934,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection->RangeRef = (int)(intptr_t)ms_io->RangeSrc; + selection->RangeRef = (int)(intptr_t)ms_io->RangeSrcItem; selection->ApplyRequests(ms_io, ITEMS_COUNT); ImGui::PopID(); } @@ -3001,9 +3001,9 @@ static void ShowDemoWindowMultiSelect() while (!use_clipper || clipper.Step()) { - // IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeSrc was passed over. + // IF clipping is used you need to set 'RangeSrcPassedBy = true' if RangeSrcItem was passed over. if (use_clipper) - if ((int)(intptr_t)ms_io->RangeSrc <= clipper.DisplayStart) + if ((int)(intptr_t)ms_io->RangeSrcItem <= clipper.DisplayStart) ms_io->RangeSrcPassedBy = true; const int item_begin = use_clipper ? clipper.DisplayStart : 0; @@ -3091,7 +3091,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection.RangeRef = (int)(intptr_t)ms_io->RangeSrc; + selection.RangeRef = (int)(intptr_t)ms_io->RangeSrcItem; selection.ApplyRequests(ms_io, ITEMS_COUNT); if (widget_type == WidgetType_TreeNode) diff --git a/imgui_internal.h b/imgui_internal.h index 42f822c385d2..2e1a0bee3278 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1724,7 +1724,7 @@ struct IMGUI_API ImGuiMultiSelectTempData 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 SetRangeDstPassedBy; // Set by the the item that matches NavJustMovedToId when IsSetRange is set. + 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. ImGuiMultiSelectTempData() { Clear(); } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index d9008e34056f..523d2add0097 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7128,7 +7128,7 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe ImGuiContext& g = *GImGui; if (data->RequestClear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestClear\n", function); if (data->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSelectAll\n", function); - if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %p..%p = %d (dir %+d)\n", function, data->RangeSrc, data->RangeDst, data->RangeValue, data->RangeDirection); + if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %p..%p = %d (dir %+d)\n", function, data->RangeSrcItem, data->RangeDstItem, data->RangeValue, data->RangeDirection); } ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected) @@ -7152,7 +7152,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* r if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0) { - ms->BeginIO.RangeSrc = ms->EndIO.RangeSrc = range_ref; + ms->BeginIO.RangeSrcItem = ms->EndIO.RangeSrcItem = range_ref; ms->BeginIO.RangeValue = ms->EndIO.RangeValue = range_ref_is_selected; } @@ -7229,7 +7229,7 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping) if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect) - if (ms->BeginIO.RangeSrc == (void*)selection_user_data) + if (ms->BeginIO.RangeSrcItem == (void*)selection_user_data) ms->BeginIO.RangeSrcPassedBy = true; } @@ -7259,11 +7259,11 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) { IM_ASSERT(id != 0); IM_ASSERT((ms->KeyMods & ImGuiMod_Shift) != 0); - const bool is_range_src = (ms->BeginIO.RangeSrc == item_data); - const bool is_range_dst = !ms->SetRangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. + 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. if (is_range_dst) - ms->SetRangeDstPassedBy = true; - if (is_range_src || is_range_dst || ms->BeginIO.RangeSrcPassedBy != ms->SetRangeDstPassedBy) + ms->RangeDstPassedBy = true; + if (is_range_src || is_range_dst || ms->BeginIO.RangeSrcPassedBy != ms->RangeDstPassedBy) selected = ms->BeginIO.RangeValue; else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0) selected = false; @@ -7332,7 +7332,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) { // Shift+Arrow always select, Ctrl+Shift+Arrow copy source selection state. ms->EndIO.RequestSetRange = true; - ms->EndIO.RangeDst = item_data; + ms->EndIO.RangeDstItem = item_data; if (!is_ctrl) ms->EndIO.RangeValue = true; ms->EndIO.RangeDirection = ms->BeginIO.RangeSrcPassedBy ? +1 : -1; @@ -7341,7 +7341,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) { // Ctrl inverts selection, otherwise always select selected = (is_ctrl && (ms->Flags & ImGuiMultiSelectFlags_NoUnselect) == 0) ? !selected : true; - ms->EndIO.RangeSrc = ms->EndIO.RangeDst = item_data; + ms->EndIO.RangeSrcItem = ms->EndIO.RangeDstItem = item_data; ms->EndIO.RangeValue = selected; } @@ -7353,7 +7353,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) if (is_multiselect && !is_shift && ms->EndIO.RequestClear) { // For toggle selection unless there is a Clear request, we can handle it completely locally without sending a RangeSet request. - IM_ASSERT(ms->EndIO.RangeSrc == ms->EndIO.RangeDst); // Setup by else block above + IM_ASSERT(ms->EndIO.RangeSrcItem == ms->EndIO.RangeDstItem); // Setup by else block above ms->EndIO.RequestSetRange = true; ms->EndIO.RangeValue = selected; ms->EndIO.RangeDirection = +1; @@ -7361,7 +7361,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) if (!is_multiselect) { // Clear selection, set single item range - IM_ASSERT(ms->EndIO.RangeSrc == item_data && ms->EndIO.RangeDst == item_data); // Setup by block above + IM_ASSERT(ms->EndIO.RangeSrcItem == item_data && ms->EndIO.RangeDstItem == item_data); // Setup by block above ms->EndIO.RequestClear = true; ms->EndIO.RequestSetRange = true; } @@ -7376,7 +7376,7 @@ 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 (ms->EndIO.RangeSrc == item_data && is_ctrl && is_shift && is_multiselect && !(ms->Flags & ImGuiMultiSelectFlags_NoUnselect)) + if (ms->EndIO.RangeSrcItem == item_data && is_ctrl && is_shift && is_multiselect && !(ms->Flags & ImGuiMultiSelectFlags_NoUnselect)) ms->EndIO.RangeValue = selected; *p_selected = selected; From ccf43d6a964e4a1cb81e9288636846ab39292879 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 2 Jun 2023 16:19:24 +0200 Subject: [PATCH 029/132] MultiSelect: (Breaking) Renamed 'RangeValue' -> 'RangeSelected' + amend comments. --- imgui.h | 17 +++++++++-------- imgui_demo.cpp | 3 ++- imgui_widgets.cpp | 16 ++++++++-------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/imgui.h b/imgui.h index fc25f379850a..3c9a4bc62a09 100644 --- a/imgui.h +++ b/imgui.h @@ -2782,16 +2782,17 @@ enum ImGuiMultiSelectFlags_ struct ImGuiMultiSelectIO { // - Always process requests in this order: Clear, SelectAll, SetRange. - // - Below: who reads/writes each fields? 'r'=read, 'w'=write, 'ms'=multi-select code, 'app'=application/user code. - // // BEGIN / LOOP / END - bool RequestClear; // ms:w, app:r / / ms:w, app:r // 1. Request user to clear selection (processed by app code) - bool RequestSelectAll; // ms:w, app:r / / ms:w, app:r // 2. Request user to select all (processed by app code) - bool RequestSetRange; // / / ms:w, app:r // 3. Request user to alter selection in the [RangeSrcItem..RangeDstItem] range using RangeValue. In practice, only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. - void* RangeSrcItem; // ms:w / app:r / ms:w, app:r // Begin: Last known SetNextItemSelectionData() value for RangeSrcItem value. End: parameter from RequestSetRange request. + // - 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* 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 RangeValue; // / / 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 a clipper) Need to be set by user if RangeSrcItem was part of the clipped set before submitting the visible items. Ignore if not clipping. + 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. ImGuiMultiSelectIO() { Clear(); } void Clear() { memset(this, 0, sizeof(*this)); } diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 17c976c83a15..16c53adf8baf 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -605,6 +605,7 @@ void ImGui::ShowDemoWindow(bool* p_open) ImGui::End(); } + static void ShowDemoWindowWidgets() { IMGUI_DEMO_MARKER("Widgets"); @@ -2804,7 +2805,7 @@ struct ExampleSelection { if (ms_io->RequestClear) { Clear(); } if (ms_io->RequestSelectAll) { SelectAll(items_count); } - if (ms_io->RequestSetRange) { SetRange((int)(intptr_t)ms_io->RangeSrcItem, (int)(intptr_t)ms_io->RangeDstItem, ms_io->RangeValue ? 1 : 0); } + if (ms_io->RequestSetRange) { SetRange((int)(intptr_t)ms_io->RangeSrcItem, (int)(intptr_t)ms_io->RangeDstItem, ms_io->RangeSelected ? 1 : 0); } } }; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 523d2add0097..5c9a0eedea65 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7128,7 +7128,7 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe ImGuiContext& g = *GImGui; if (data->RequestClear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestClear\n", function); if (data->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSelectAll\n", function); - if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %p..%p = %d (dir %+d)\n", function, data->RangeSrcItem, data->RangeDstItem, data->RangeValue, data->RangeDirection); + if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %p..%p = %d (dir %+d)\n", function, data->RangeSrcItem, data->RangeDstItem, data->RangeSelected, data->RangeDirection); } ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected) @@ -7153,7 +7153,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* r if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0) { ms->BeginIO.RangeSrcItem = ms->EndIO.RangeSrcItem = range_ref; - ms->BeginIO.RangeValue = ms->EndIO.RangeValue = range_ref_is_selected; + ms->BeginIO.RangeSelected = ms->EndIO.RangeSelected = range_ref_is_selected; } // Auto clear when using Navigation to move within the selection (we compare SelectScopeId so it possible to use multiple lists inside a same window) @@ -7202,7 +7202,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() // Unwind if (ms->Flags & ImGuiMultiSelectFlags_NoUnselect) - ms->EndIO.RangeValue = true; + ms->EndIO.RangeSelected = true; ms->FocusScopeId = 0; ms->Window = NULL; ms->Flags = ImGuiMultiSelectFlags_None; @@ -7264,7 +7264,7 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) if (is_range_dst) ms->RangeDstPassedBy = true; if (is_range_src || is_range_dst || ms->BeginIO.RangeSrcPassedBy != ms->RangeDstPassedBy) - selected = ms->BeginIO.RangeValue; + selected = ms->BeginIO.RangeSelected; else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0) selected = false; } @@ -7334,7 +7334,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ms->EndIO.RequestSetRange = true; ms->EndIO.RangeDstItem = item_data; if (!is_ctrl) - ms->EndIO.RangeValue = true; + ms->EndIO.RangeSelected = true; ms->EndIO.RangeDirection = ms->BeginIO.RangeSrcPassedBy ? +1 : -1; } else @@ -7342,7 +7342,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Ctrl inverts selection, otherwise always select selected = (is_ctrl && (ms->Flags & ImGuiMultiSelectFlags_NoUnselect) == 0) ? !selected : true; ms->EndIO.RangeSrcItem = ms->EndIO.RangeDstItem = item_data; - ms->EndIO.RangeValue = selected; + ms->EndIO.RangeSelected = selected; } if (input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) @@ -7355,7 +7355,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // For toggle selection unless there is a Clear request, we can handle it completely locally without sending a RangeSet request. IM_ASSERT(ms->EndIO.RangeSrcItem == ms->EndIO.RangeDstItem); // Setup by else block above ms->EndIO.RequestSetRange = true; - ms->EndIO.RangeValue = selected; + ms->EndIO.RangeSelected = selected; ms->EndIO.RangeDirection = +1; } if (!is_multiselect) @@ -7377,7 +7377,7 @@ 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 (ms->EndIO.RangeSrcItem == item_data && is_ctrl && is_shift && is_multiselect && !(ms->Flags & ImGuiMultiSelectFlags_NoUnselect)) - ms->EndIO.RangeValue = selected; + ms->EndIO.RangeSelected = selected; *p_selected = selected; *p_pressed = pressed; From 6ef70a97fd43276a306a5347ef11d1ba00939be7 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 8 Jun 2023 15:03:24 +0200 Subject: [PATCH 030/132] MultiSelect: Remove ImGuiMultiSelectFlags_NoUnselect because I currently can't find use for this specific design. And/or it seem partly broken. --- imgui.h | 9 ++++----- imgui_demo.cpp | 1 - imgui_widgets.cpp | 6 ++---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/imgui.h b/imgui.h index 3c9a4bc62a09..e42141782535 100644 --- a/imgui.h +++ b/imgui.h @@ -44,7 +44,7 @@ Index of this file: // [SECTION] ImGuiIO // [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) // [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) -// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectData) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO) // [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) // [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) // [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) @@ -2719,7 +2719,7 @@ struct ImColor }; //----------------------------------------------------------------------------- -// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectData) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO) //----------------------------------------------------------------------------- #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. @@ -2733,11 +2733,10 @@ enum ImGuiMultiSelectFlags_ { ImGuiMultiSelectFlags_None = 0, ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0, // Disable selecting more than one item. This is not very useful at this kind of selection can be implemented without BeginMultiSelect(), but this is available for consistency. - ImGuiMultiSelectFlags_NoUnselect = 1 << 1, // Disable unselecting items with CTRL+Click, CTRL+Space etc. - ImGuiMultiSelectFlags_NoSelectAll = 1 << 2, // Disable CTRL+A shortcut to set RequestSelectAll + ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to set RequestSelectAll + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 2, // Clear selection when pressing Escape while scope is focused. ImGuiMultiSelectFlags_ClearOnClickWindowVoid= 1 << 3, // Clear selection when clicking on empty location within host window (use if BeginMultiSelect() covers a whole window) //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) - ImGuiMultiSelectFlags_ClearOnEscape = 1 << 5, // Clear selection when pressing Escape while scope is focused. }; // Abstract: diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 16c53adf8baf..3bb6171a966a 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2968,7 +2968,6 @@ static void ShowDemoWindowMultiSelect() ImGui::Checkbox("Show in a table", &show_in_table); ImGui::Checkbox("Show color button", &show_color_button); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoMultiSelect", &flags, ImGuiMultiSelectFlags_NoMultiSelect); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoUnselect", &flags, ImGuiMultiSelectFlags_NoUnselect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickWindowVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickWindowVoid); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 5c9a0eedea65..6cf0f958ed2b 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7201,8 +7201,6 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() } // Unwind - if (ms->Flags & ImGuiMultiSelectFlags_NoUnselect) - ms->EndIO.RangeSelected = true; ms->FocusScopeId = 0; ms->Window = NULL; ms->Flags = ImGuiMultiSelectFlags_None; @@ -7340,7 +7338,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) else { // Ctrl inverts selection, otherwise always select - selected = (is_ctrl && (ms->Flags & ImGuiMultiSelectFlags_NoUnselect) == 0) ? !selected : true; + selected = is_ctrl ? !selected : true; ms->EndIO.RangeSrcItem = ms->EndIO.RangeDstItem = item_data; ms->EndIO.RangeSelected = selected; } @@ -7376,7 +7374,7 @@ 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 (ms->EndIO.RangeSrcItem == item_data && is_ctrl && is_shift && is_multiselect && !(ms->Flags & ImGuiMultiSelectFlags_NoUnselect)) + if (ms->EndIO.RangeSrcItem == item_data && is_ctrl && is_shift && is_multiselect) ms->EndIO.RangeSelected = selected; *p_selected = selected; From 1ea9ca748cdc24e9997e442e9cd93b4687e6bbcb Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 29 Jun 2023 17:00:43 +0200 Subject: [PATCH 031/132] MultiSelect: Remove the need for using IsItemToggledSelection(). Update comments. This is the simple version that past our tests. MultiSelectItemFooter() is in need of a cleanup. --- imgui.h | 4 ++-- imgui_demo.cpp | 8 -------- imgui_widgets.cpp | 33 +++++++++++++++++++++------------ 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/imgui.h b/imgui.h index e42141782535..d00adb1f7f24 100644 --- a/imgui.h +++ b/imgui.h @@ -2772,8 +2772,8 @@ enum ImGuiMultiSelectFlags_ // 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. -// Call IsItemToggledSelection() to query if the selection state has been toggled, if you need the 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. +// (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) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 3bb6171a966a..c66a9e434f50 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2888,8 +2888,6 @@ static void ShowDemoWindowMultiSelect() bool item_is_selected = selection.GetSelected(n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); - if (ImGui::IsItemToggledSelection()) - selection.SetSelected(n, !item_is_selected); } // Apply multi-select requests @@ -2929,8 +2927,6 @@ static void ShowDemoWindowMultiSelect() bool item_is_selected = selection->GetSelected(n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); - if (ImGui::IsItemToggledSelection()) - selection->SetSelected(n, !item_is_selected); } // Apply multi-select requests @@ -3032,8 +3028,6 @@ static void ShowDemoWindowMultiSelect() if (widget_type == WidgetType_Selectable) { ImGui::Selectable(label, item_is_selected); - if (ImGui::IsItemToggledSelection()) - selection.SetSelected(n, !item_is_selected); if (use_drag_drop && ImGui::BeginDragDropSource()) { ImGui::Text("(Dragging %d items)", selection.GetSize()); @@ -3047,8 +3041,6 @@ static void ShowDemoWindowMultiSelect() if (item_is_selected) tree_node_flags |= ImGuiTreeNodeFlags_Selected; bool open = ImGui::TreeNodeEx(label, tree_node_flags); - if (ImGui::IsItemToggledSelection()) - selection.SetSelected(n, !item_is_selected); if (use_drag_drop && ImGui::BeginDragDropSource()) { ImGui::Text("(Dragging %d items)", selection.GetSize()); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 6cf0f958ed2b..2cd774ee1133 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7156,7 +7156,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* r ms->BeginIO.RangeSelected = ms->EndIO.RangeSelected = range_ref_is_selected; } - // Auto clear when using Navigation to move within the selection (we compare SelectScopeId so it possible to use multiple lists inside a same window) + // Auto clear when using Navigation to move within the selection + // (we compare FocusScopeId so it possible to use multiple selections inside a same window) if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) { if (ms->KeyMods & ImGuiMod_Shift) @@ -7255,8 +7256,7 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) // For this to work, IF the user is clipping items, they need to set RangeSrcPassedBy = true to notify the system. if (ms->IsSetRange) { - IM_ASSERT(id != 0); - IM_ASSERT((ms->KeyMods & ImGuiMod_Shift) != 0); + 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. if (is_range_dst) @@ -7315,15 +7315,22 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Alter selection if (pressed && (!enter_pressed || !selected)) { - //------------------------------------------------------------------------------------------------------------------------------------------------- - // ACTION | Begin | Item Old | Item New | End - //------------------------------------------------------------------------------------------------------------------------------------------------- - // Keys Navigated, Ctrl=0, Shift=0 | In.Clear | Clear -> Sel=0 | Src=item, Pressed -> Sel=1 | - // Keys Navigated, Ctrl=0, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange - // Keys Navigated, Ctrl=1, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=Src, Out.Clear, Out.SetRange=Src | Clear + SetRange - // Mouse Pressed, Ctrl=0, Shift=0 | n/a | n/a (Sel=1) | Src=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange - // Mouse Pressed, Ctrl=0, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange - //------------------------------------------------------------------------------------------------------------------------------------------------- + //---------------------------------------------------------------------------------------- + // ACTION | Begin | Pressed/Activated | End + //---------------------------------------------------------------------------------------- + // Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1 + // Keys Navigated: Ctrl | n/a | n/a + // Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1 + // Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst + // Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1 + // Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1 + // Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1 + //---------------------------------------------------------------------------------------- + // Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1 + // Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1 + // Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1 + // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst + //---------------------------------------------------------------------------------------- ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; if (is_shift && is_multiselect) @@ -7341,6 +7348,8 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) selected = is_ctrl ? !selected : true; ms->EndIO.RangeSrcItem = ms->EndIO.RangeDstItem = item_data; ms->EndIO.RangeSelected = selected; + ms->EndIO.RequestSetRange = true; + ms->EndIO.RangeDirection = +1; } if (input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) From 961b81c362891b31c433fa130db57df2239fa17f Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 30 Jun 2023 14:17:16 +0200 Subject: [PATCH 032/132] MultiSelect: Tidying up/simpllifying MultiSelectItemFooter(). Intended to be entirely a no-op, merely a transform of source code for simplification. But committing separatey from behavior change in previous change. --- imgui_widgets.cpp | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 2cd774ee1133..911aa8b1dc7f 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7294,6 +7294,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) } // Right-click handling: this could be moved at the Selectable() level. + // FIXME-MULTISELECT: See https://github.com/ocornut/imgui/pull/5816 bool hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); if (hovered && IsMouseClicked(1)) { @@ -7333,11 +7334,12 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) //---------------------------------------------------------------------------------------- ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + ms->EndIO.RequestSetRange = true; + ms->EndIO.RangeDstItem = item_data; if (is_shift && is_multiselect) { - // Shift+Arrow always select, Ctrl+Shift+Arrow copy source selection state. - ms->EndIO.RequestSetRange = true; - ms->EndIO.RangeDstItem = item_data; + // Shift+Arrow always select + // Ctrl+Shift+Arrow copy source selection state (alrady stored by BeginMultiSelect() in RangeSelected) if (!is_ctrl) ms->EndIO.RangeSelected = true; ms->EndIO.RangeDirection = ms->BeginIO.RangeSrcPassedBy ? +1 : -1; @@ -7346,40 +7348,23 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) { // Ctrl inverts selection, otherwise always select selected = is_ctrl ? !selected : true; - ms->EndIO.RangeSrcItem = ms->EndIO.RangeDstItem = item_data; + ms->EndIO.RangeSrcItem = item_data; ms->EndIO.RangeSelected = selected; - ms->EndIO.RequestSetRange = true; ms->EndIO.RangeDirection = +1; } if (input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) { - // Mouse click without CTRL clears the selection, unless the clicked item is already selected if (is_multiselect && !is_ctrl) ms->EndIO.RequestClear = true; - if (is_multiselect && !is_shift && ms->EndIO.RequestClear) - { - // For toggle selection unless there is a Clear request, we can handle it completely locally without sending a RangeSet request. - IM_ASSERT(ms->EndIO.RangeSrcItem == ms->EndIO.RangeDstItem); // Setup by else block above - ms->EndIO.RequestSetRange = true; - ms->EndIO.RangeSelected = selected; - ms->EndIO.RangeDirection = +1; - } - if (!is_multiselect) - { - // Clear selection, set single item range - IM_ASSERT(ms->EndIO.RangeSrcItem == item_data && ms->EndIO.RangeDstItem == item_data); // Setup by block above - ms->EndIO.RequestClear = true; - ms->EndIO.RequestSetRange = true; - } } else if (input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) { - if (is_multiselect && is_shift && !is_ctrl) - ms->EndIO.RequestClear = true; - else if (!is_multiselect) + if (is_multiselect && is_shift && !is_ctrl) // Without Shift the RequestClear was done in BeginIO, not necessary to do again. ms->EndIO.RequestClear = true; } + else if (!is_multiselect) + ms->EndIO.RequestClear = true; } // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) From 387fc138945dae9a9d78af27b1110d1597bc4b7e Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 7 Jun 2023 16:25:19 +0200 Subject: [PATCH 033/132] MultiSelect: Clarify and better enforce lifetime of BeginMultiSelect() value. --- imgui_demo.cpp | 11 +++++------ imgui_widgets.cpp | 3 +++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index c66a9e434f50..09ac140571a0 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2780,7 +2780,7 @@ struct ExampleSelection { // Data ImGuiStorage Storage; // Selection set - int SelectionSize; // Number of selected items (== number of 1 in the Storage, maintained by this class) // FIXME-RANGESELECT: Imply more difficult to track with intrusive selection schemes? + 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 @@ -2790,13 +2790,12 @@ struct ExampleSelection 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; } int GetSize() const { return SelectionSize; } - // When using SelectAll() / SetRange() we assume that our objects ID are indices. + // When using SetRange() / SelectAll() we assume that our objects ID are indices. // In this demo we always store selection using indices and never in another manner (e.g. object ID or pointers). // If your selection system is storing selection using object ID and you want to support Shift+Click range-selection, - // you will need a way to iterate from one object to another given the ID you use. - // You are likely to need some kind of data structure to convert 'view index' <> 'object ID'. - // FIXME-MULTISELECT: Would be worth providing a demo of doing this. - // FIXME-MULTISELECT: This implementation of SetRange() is inefficient because it doesn't take advantage of the fact that ImGuiStorage stores sorted key. + // you will need a way to iterate from one item to the other item given the ID you use. + // You are likely to need some kind of data structure to convert 'view index' <> 'object ID' (FIXME-MULTISELECT: Would be worth providing a demo of doing this). + // Note: This implementation of SetRange() is inefficient because it doesn't take advantage of the fact that ImGuiStorage stores sorted key. void SetRange(int a, int b, bool v) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) SetSelected(n, v); } void SelectAll(int count) { Storage.Data.resize(count); for (int idx = 0; idx < count; idx++) Storage.Data[idx] = ImGuiStoragePair((ImGuiID)idx, 1); SelectionSize = count; } // This could be using SetRange(), but it this way is faster. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 911aa8b1dc7f..350a9e86c0f5 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7131,6 +7131,7 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %p..%p = %d (dir %+d)\n", function, data->RangeSrcItem, data->RangeDstItem, data->RangeSelected, data->RangeDirection); } +// Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected) { ImGuiContext& g = *GImGui; @@ -7184,6 +7185,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* r return &ms->BeginIO; } +// Return updated ImGuiMultiSelectIO structure. Lifetime: until EndFrame() or next BeginMultiSelect() call. ImGuiMultiSelectIO* ImGui::EndMultiSelect() { ImGuiContext& g = *GImGui; @@ -7205,6 +7207,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() ms->FocusScopeId = 0; ms->Window = NULL; ms->Flags = ImGuiMultiSelectFlags_None; + ms->BeginIO.Clear(); // Invalidate contents of BeginMultiSelect() to enforce scope. PopFocusScope(); g.CurrentMultiSelect = NULL; From 564dde0ee34675d630d8f4115b022dbd3884f9d6 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 2 Jun 2023 14:22:13 +0200 Subject: [PATCH 034/132] MultiSelect: Demo: first-draft of user-side deletion idioms. (will need support from lib) --- imgui_demo.cpp | 168 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 157 insertions(+), 11 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 09ac140571a0..7a1664473884 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -605,7 +605,6 @@ void ImGui::ShowDemoWindow(bool* p_open) ImGui::End(); } - static void ShowDemoWindowWidgets() { IMGUI_DEMO_MARKER("Widgets"); @@ -2806,11 +2805,61 @@ struct ExampleSelection if (ms_io->RequestSelectAll) { SelectAll(items_count); } if (ms_io->RequestSetRange) { SetRange((int)(intptr_t)ms_io->RangeSrcItem, (int)(intptr_t)ms_io->RangeDstItem, ms_io->RangeSelected ? 1 : 0); } } + + // 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 + // Important: This only works if the item ID are stable: aka not depend on their index, but on e.g. item id/ptr. + template + int CalcNextFocusIdxForBeforeDeletion(ImGuiMultiSelectIO* ms_io, ImVector& items) + { + // 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--) + if (!GetSelected(n)) + return n; + return -1; + } + + // Call after EndMultiSelect() + // Apply deletion request + return index of item to refocus, if any. + template + void ApplyDeletion(ImGuiMultiSelectIO* ms_io, ImVector& items, int next_focus_idx_in_old_selection) + { + // 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 + // 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); + ImVector new_items; + new_items.reserve(items.Size - SelectionSize); + int next_focus_idx_in_new_selection = -1; + for (int n = 0; n < items.Size; n++) + { + if (!GetSelected(n)) + new_items.push_back(items[n]); + if (next_focus_idx_in_old_selection == n) + next_focus_idx_in_new_selection = new_items.Size - 1; + } + items.swap(new_items); + + // Update selection + Clear(); + if (next_focus_idx_in_new_selection != -1) + SetSelected(next_focus_idx_in_new_selection, true); + } }; static void ShowDemoWindowMultiSelect() { IMGUI_DEMO_MARKER("Widgets/Selection State"); + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); if (ImGui::TreeNode("Selection State")) { HelpMarker("Selections can be built under Selectable(), TreeNode() or other widgets. Selection state is owned by application code/data."); @@ -2896,7 +2945,77 @@ static void ShowDemoWindowMultiSelect() ImGui::EndListBox(); } + ImGui::TreePop(); + } + + // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. + // SHIFT+Click w/ CTRL and other standard features are supported. + IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full, with deletion)"); + if (ImGui::TreeNode("Multiple Selection (full, with deletion)")) + { + // Intentionally separating items data from selection data! + // But you may decide to store selection data inside your item (aka ' + static ImVector items; + static ExampleSelection selection; + + ImGui::Text("Adding features:"); + ImGui::BulletText("Dynamic list with Delete key support."); + + // 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); + 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); + + // Extra to support deletion: Submit scrolling range to avoid glitches on deletion + const float items_height = ImGui::GetTextLineHeightWithSpacing(); + ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); + 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)); + selection.ApplyRequests(ms_io, items.Size); + + // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. + // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. + // 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; + + for (int n = 0; n < items.Size; n++) + { + const int item_id = items[n]; + char label[64]; + 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::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); + } + + // Apply multi-select requests + 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); + + ImGui::EndListBox(); + } ImGui::TreePop(); } @@ -2939,6 +3058,7 @@ static void ShowDemoWindowMultiSelect() // Advanced demonstration of BeginMultiSelect() // - Showcase clipping. + // - Showcase deletion. // - Showcase basic drag and drop. // - Showcase TreeNode variant (note that tree node don't expand in the demo: supporting expanding tree nodes + clipping a separate thing). // - Showcase using inside a table. @@ -2949,6 +3069,7 @@ static void ShowDemoWindowMultiSelect() // Options enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; static bool use_clipper = true; + static bool use_deletion = true; static bool use_drag_drop = true; static bool show_in_table = false; static bool show_color_button = false; @@ -2959,6 +3080,7 @@ static void ShowDemoWindowMultiSelect() ImGui::SameLine(); if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } ImGui::Checkbox("Enable clipper", &use_clipper); + ImGui::Checkbox("Enable deletion", &use_deletion); ImGui::Checkbox("Enable drag & drop", &use_drag_drop); ImGui::Checkbox("Show in a table", &show_in_table); ImGui::Checkbox("Show color button", &show_color_button); @@ -2967,10 +3089,16 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickWindowVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickWindowVoid); - const int ITEMS_COUNT = 1000; + // Initialize default list with 1000 items. + static ImVector items; + static int items_next_id = 0; + if (items_next_id == 0) { for (int n = 0; n < 1000; n++) { items.push_back(items_next_id++); } } static ExampleSelection selection; - ImGui::Text("Selection size: %d/%d", selection.GetSize(), ITEMS_COUNT); + ImGui::Text("Selection size: %d/%d", selection.GetSize(), items.Size); + + const float items_height = (widget_type == WidgetType_TreeNode) ? ImGui::GetTextLineHeight() : ImGui::GetTextLineHeightWithSpacing(); + ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { ImVec2 color_button_sz(ImGui::GetFontSize(), ImGui::GetFontSize()); @@ -2978,7 +3106,13 @@ static void ShowDemoWindowMultiSelect() 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)); - selection.ApplyRequests(ms_io, ITEMS_COUNT); + selection.ApplyRequests(ms_io, items.Size); + + // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. + // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. + // 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 (show_in_table) { @@ -2992,7 +3126,7 @@ static void ShowDemoWindowMultiSelect() ImGuiListClipper clipper; if (use_clipper) - clipper.Begin(ITEMS_COUNT); + clipper.Begin(items.Size); while (!use_clipper || clipper.Step()) { @@ -3002,16 +3136,22 @@ static void ShowDemoWindowMultiSelect() ms_io->RangeSrcPassedBy = true; const int item_begin = use_clipper ? clipper.DisplayStart : 0; - const int item_end = use_clipper ? clipper.DisplayEnd : ITEMS_COUNT; + const int item_end = use_clipper ? clipper.DisplayEnd : items.Size; for (int n = item_begin; n < item_end; n++) { if (show_in_table) ImGui::TableNextColumn(); - ImGui::PushID(n); - const char* category = random_names[n % IM_ARRAYSIZE(random_names)]; + const int item_id = items[n]; + const char* item_category = random_names[item_id % IM_ARRAYSIZE(random_names)]; char label[64]; - sprintf(label, "Object %05d: %s", n, category); + sprintf(label, "Object %05d: %s", item_id, item_category); + + // IMPORTANT: for deletion refocus to work we need object ID to be stable, + // aka not depend on their index in the list. Here we use our persistent item_id + // instead of index to build a unique ID that will persist. + // (If we used PushID(n) instead, focus wouldn't be restored correctly after deletion). + ImGui::PushID(item_id); // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part // of the selection scope doesn't erroneously alter our selection (FIXME-TESTS: Add a test for that!). @@ -3027,6 +3167,8 @@ static void ShowDemoWindowMultiSelect() if (widget_type == WidgetType_Selectable) { ImGui::Selectable(label, item_is_selected); + if (next_focus_item_idx == n) + ImGui::SetKeyboardFocusHere(-1); // FIXME-MULTISELECT: turn into a ms_io->RequestFocusIdx if (use_drag_drop && ImGui::BeginDragDropSource()) { ImGui::Text("(Dragging %d items)", selection.GetSize()); @@ -3040,6 +3182,8 @@ static void ShowDemoWindowMultiSelect() if (item_is_selected) tree_node_flags |= ImGuiTreeNodeFlags_Selected; bool open = ImGui::TreeNodeEx(label, tree_node_flags); + if (next_focus_item_idx == n) + ImGui::SetKeyboardFocusHere(-1); // FIXME-MULTISELECT: turn into a ms_io->RequestFocusIdx if (use_drag_drop && ImGui::BeginDragDropSource()) { ImGui::Text("(Dragging %d items)", selection.GetSize()); @@ -3063,7 +3207,7 @@ static void ShowDemoWindowMultiSelect() ImGui::TableNextColumn(); ImGui::SetNextItemWidth(-FLT_MIN); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); - ImGui::InputText("###NoLabel", (char*)(void*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); + ImGui::InputText("###NoLabel", (char*)(void*)item_category, strlen(item_category), ImGuiInputTextFlags_ReadOnly); ImGui::PopStyleVar(); } @@ -3083,7 +3227,9 @@ 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); + selection.ApplyRequests(ms_io, items.Size); + if (want_delete) + selection.ApplyDeletion(ms_io, items, next_focus_item_idx); if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); From 9223ffc2552dfd4112d351427714ea1a7a032291 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 2 Jun 2023 14:34:22 +0200 Subject: [PATCH 035/132] MultiSelect: (Breaking) BeginMultiSelect() doesn't need two last params maintained by users. Moving some storage from user to core. Proper deletion demo. --- imgui.cpp | 9 +++ imgui.h | 57 +++++++++---------- imgui_demo.cpp | 114 +++++++++++++++++++++++++++----------- imgui_internal.h | 36 +++++++++--- imgui_widgets.cpp | 136 ++++++++++++++++++++++++++++++++++++++-------- 5 files changed, 257 insertions(+), 95 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 8f34acf9274d..26f0f36bcefe 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 d00adb1f7f24..cb4e27832cb2 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 7a1664473884..8681ae47dcdd 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.ApplyRequests(ms_io, items.Size); + if (want_delete) + selection.ApplyDeletion(ms_io, items, nav_id_was_selected ? next_focus_item_idx : -1); +#else 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); +#endif if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); diff --git a/imgui_internal.h b/imgui_internal.h index 2e1a0bee3278..eaeca4eced8b 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 350a9e86c0f5..126727c23d73 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 //------------------------------------------------------------------------- From df1eeb9a20d70fdd6b712f292ec23881809465f7 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 2 Jun 2023 15:17:01 +0200 Subject: [PATCH 036/132] MultiSelect: Maintain NavIdSelected for user. Simplify deletion demo. --- imgui.h | 1 + imgui_demo.cpp | 44 +++++++------------------------------------- imgui_internal.h | 3 ++- imgui_widgets.cpp | 30 +++++++++++++++++------------- 4 files changed, 27 insertions(+), 51 deletions(-) diff --git a/imgui.h b/imgui.h index cb4e27832cb2..df38b174cc37 100644 --- a/imgui.h +++ b/imgui.h @@ -2784,6 +2784,7 @@ struct ImGuiMultiSelectIO 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). + bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). 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(); } diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 8681ae47dcdd..b0cc60bd890b 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2821,8 +2821,7 @@ 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)) + if (ms_io->NavIdSelected == false) return (int)(intptr_t)ms_io->NavIdItem; // Return first unselected item after RangeSrcItem @@ -2996,6 +2995,7 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. // FIXME-MULTISELECT: Test with intermediary modal dialog. + // FIXME-MULTISELECT: If pressing Delete + another key we have slightly ambiguous behavior. 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); } @@ -3017,34 +3017,13 @@ static void ShowDemoWindowMultiSelect() ImGui::SetKeyboardFocusHere(-1); // FIXME-MULTISELECT: Need to avoid selection. } -#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 + // Apply multi-select requests + if (want_delete && ms_io->NavIdSelected == false) // FIXME: would work without '&& !NavIdSelected' just take an extra frame to recover RangeSrc ms_io->RangeSrcReset = true; 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); -#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 - + selection.ApplyDeletion(ms_io, items, ms_io->NavIdSelected ? next_focus_item_idx : -1); ImGui::EndListBox(); } @@ -3267,21 +3246,12 @@ 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) + if (want_delete && ms_io->NavIdSelected == false) ms_io->RangeSrcReset = true; 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); -#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 + selection.ApplyDeletion(ms_io, items, ms_io->NavIdSelected ? next_focus_item_idx : -1); if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); diff --git a/imgui_internal.h b/imgui_internal.h index eaeca4eced8b..21d350c89be1 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1740,11 +1740,12 @@ struct IMGUI_API ImGuiMultiSelectState ImGuiID ID; int LastFrameActive; // Last used frame-count, for GC. ImS8 RangeSelected; // -1 (don't have) or true/false + ImS8 NavIdSelected; // -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; } + void Init(ImGuiID id) { Window = NULL; ID = id; LastFrameActive = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = (void*)-1; } }; #endif // #ifdef IMGUI_HAS_MULTI_SELECT diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 126727c23d73..f7b91dd47a7b 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7160,8 +7160,10 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) // 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; + // We want EndIO's NavIdItem/NavIdSelected to match BeginIO's one, so the value never changes after EndMultiSelect() + ms->BeginIO.RangeSrcItem = ms->EndIO.RangeSrcItem = storage->RangeSrcItem; + ms->BeginIO.NavIdItem = ms->EndIO.NavIdItem = storage->NavIdItem; + ms->BeginIO.NavIdSelected = ms->EndIO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; if (!ms->IsFocused) return &ms->BeginIO; // This is cleared at this point. @@ -7216,13 +7218,14 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() { 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. + IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\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"); + IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n"); ms->Storage->NavIdItem = (void*)-1; + ms->Storage->NavIdSelected = -1; } } @@ -7328,11 +7331,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* 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; @@ -7342,15 +7341,11 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* 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) @@ -7446,6 +7441,15 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ms->EndIO.RangeSelected = selected; } + // Update/store the selection state of focused item + if (g.NavId == id) + { + storage->NavIdItem = item_data; + storage->NavIdSelected = selected ? 1 : 0; + } + if (storage->NavIdItem == item_data) + ms->NavIdPassedBy = true; + *p_selected = selected; *p_pressed = pressed; } @@ -7461,7 +7465,7 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) return; Text("ID = 0x%08X", storage->ID); Text("RangeSrcItem = %p, RangeSelected = %d", storage->RangeSrcItem, storage->RangeSelected); - Text("NavIdItem = %p", storage->NavIdItem); + Text("NavIdData = %p, NavIdSelected = %d", storage->NavIdItem, storage->NavIdSelected); TreePop(); #else IM_UNUSED(storage); From c0035705cae649a3f0e8a576efbcd802da94102f Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 2 Jun 2023 15:29:55 +0200 Subject: [PATCH 037/132] MultiSelect: Further simplication of user code to support Deletion. Provide standard RequestFocusItem storage. --- imgui.h | 7 +++-- imgui_demo.cpp | 77 +++++++++++++++++++++++++++--------------------- imgui_internal.h | 2 +- 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/imgui.h b/imgui.h index df38b174cc37..98e6c2b34642 100644 --- a/imgui.h +++ b/imgui.h @@ -2777,18 +2777,19 @@ struct ImGuiMultiSelectIO 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. + void* RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implemention of deletion idiom (see demo). // STATE/ARGUMENTS ---------// BEGIN / LOOP / END 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). + bool RangeSrcReset; // app:w / app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). - void* NavIdItem; // ms:w, app:r / / ms:w app:r // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items) + void* NavIdItem; // 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)); } + void Clear() { memset(this, 0, sizeof(*this)); RequestFocusItem = NavIdItem = RangeSrcItem = RangeDstItem = (void*)-1; } }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index b0cc60bd890b..53f60067e0a5 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2814,32 +2814,38 @@ struct ExampleSelection } } - // 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 - // Important: This only works if the item ID are stable: aka not depend on their index, but on e.g. item id/ptr. + // Call after BeginMultiSelect(). + // - Calculate and set ms_io->RequestFocusItem, which we will focus during the loop. + // - 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 + // - Important: This only works if the item ID are stable: aka not depend on their index, but on e.g. item id/ptr. template - int CalcNextFocusIdxForBeforeDeletion(ImGuiMultiSelectIO* ms_io, ImVector& items) + int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, ImVector& items) { - if (ms_io->NavIdSelected == false) - return (int)(intptr_t)ms_io->NavIdItem; + // If current item is not selected. + if (ms_io->NavIdSelected == false) // Here 'NavIdSelected' should be == to 'GetSelected(ms_io->NavIdData)' + { + ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even without the !NavIdSelected test but it would take an extra frame to recover RangeSrc when deleting a selected item. + return (int)(intptr_t)ms_io->NavIdItem; // Request to land on same item after deletion. + } - // Return first unselected item after RangeSrcItem + // If current item is selected: land on first unselected item after RangeSrc. for (int n = (int)(intptr_t)ms_io->RangeSrcItem + 1; n < items.Size; n++) if (!GetSelected(n)) return n; - // Otherwise return last unselected item + // If current item is selected: otherwise return last unselected item. for (int n = IM_MIN((int)(intptr_t)ms_io->RangeSrcItem, items.Size) - 1; n >= 0; n--) if (!GetSelected(n)) return n; + return -1; } // Call after EndMultiSelect() // Apply deletion request + return index of item to refocus, if any. template - void ApplyDeletion(ImGuiMultiSelectIO* ms_io, ImVector& items, int next_focus_idx_in_old_selection) + void ApplyDeletionPostLoop(ImGuiMultiSelectIO* ms_io, ImVector& items) { // This does two things: // - (1) Update Items List (delete items from it) @@ -2850,6 +2856,7 @@ struct ExampleSelection IM_UNUSED(ms_io); ImVector new_items; new_items.reserve(items.Size - SelectionSize); + int next_focus_idx_in_old_selection = (int)(intptr_t)ms_io->RequestFocusItem; int next_focus_idx_in_new_selection = -1; for (int n = 0; n < items.Size; n++) { @@ -2861,9 +2868,8 @@ 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) + if (next_focus_idx_in_new_selection != -1 && ms_io->NavIdSelected) SetSelected(next_focus_idx_in_new_selection, true); } }; @@ -2917,7 +2923,7 @@ static void ShowDemoWindowMultiSelect() "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Celtuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" }; - // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. + // Demonstrate holding/updating multi-selection data using the BeginMultiSelect/EndMultiSelect API. // SHIFT+Click w/ CTRL and other standard features are supported. IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full)"); //ImGui::SetNextItemOpen(true, ImGuiCond_Once); @@ -2958,13 +2964,19 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } - // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping. + // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API + support dynamic item list and deletion. // SHIFT+Click w/ CTRL and other standard features are supported. + // In order to support Deletion without any glitches you need to: + // - (1) If items are submitted in their own scrolling area, submit contents size SetNextWindowContentSize() ahead of time to prevent one-frame readjustment of scrolling. + // - (2) Items needs to have persistent ID Stack identifier = ID needs to not depends on their index. PushID(index) = KO. PushID(item_id) = OK. This is in order to focus items reliably after a selection. + // - (3) BeginXXXX process + // - (4) Focus process + // - (5) EndXXXX process IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full, with deletion)"); if (ImGui::TreeNode("Multiple Selection (full, with deletion)")) { // Intentionally separating items data from selection data! - // But you may decide to store selection data inside your item (aka ' + // But you may decide to store selection data inside your item (aka intrusive storage). static ImVector items; static ExampleSelection selection; @@ -2974,7 +2986,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::IsItemHovered() && selection.GetSize() > 0) selection.DebugTooltip(); - // Initialize default list with 50 items + button to add more. + // Initialize default list with 50 items + button to add/remove items. static int items_next_id = 0; if (items_next_id == 0) for (int n = 0; n < 50; n++) @@ -2983,9 +2995,10 @@ static void ShowDemoWindowMultiSelect() ImGui::SameLine(); 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 + // (1) Extra to support deletion: Submit scrolling range to avoid glitches on deletion const float items_height = ImGui::GetTextLineHeightWithSpacing(); ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); + if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; @@ -2994,11 +3007,11 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. - // FIXME-MULTISELECT: Test with intermediary modal dialog. - // FIXME-MULTISELECT: If pressing Delete + another key we have slightly ambiguous behavior. + // FIXME-MULTISELECT: If pressing Delete + another key we have ambiguous behavior. 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 (want_delete) + ms_io->RequestFocusItem = (void*)(intptr_t)selection.ApplyDeletionPreLoop(ms_io, items); + const int next_focus_item_idx = (int)(intptr_t)ms_io->RequestFocusItem; for (int n = 0; n < items.Size; n++) { @@ -3011,19 +3024,15 @@ static void ShowDemoWindowMultiSelect() 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); // FIXME-MULTISELECT: Need to avoid selection. + ImGui::SetKeyboardFocusHere(-1); } // Apply multi-select requests - if (want_delete && ms_io->NavIdSelected == false) // FIXME: would work without '&& !NavIdSelected' just take an extra frame to recover RangeSrc - ms_io->RangeSrcReset = true; ms_io = ImGui::EndMultiSelect(); selection.ApplyRequests(ms_io, items.Size); if (want_delete) - selection.ApplyDeletion(ms_io, items, ms_io->NavIdSelected ? next_focus_item_idx : -1); + selection.ApplyDeletionPostLoop(ms_io, items); ImGui::EndListBox(); } @@ -3122,8 +3131,9 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. // 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 (want_delete) + selection.ApplyDeletionPreLoop(ms_io, items); + const int next_focus_item_idx = (int)(intptr_t)ms_io->RequestFocusItem; if (show_in_table) { @@ -3183,7 +3193,8 @@ static void ShowDemoWindowMultiSelect() { ImGui::Selectable(label, item_is_selected); if (next_focus_item_idx == n) - ImGui::SetKeyboardFocusHere(-1); // FIXME-MULTISELECT: turn into a ms_io->RequestFocusIdx + ImGui::SetKeyboardFocusHere(-1); + if (use_drag_drop && ImGui::BeginDragDropSource()) { ImGui::Text("(Dragging %d items)", selection.GetSize()); @@ -3198,7 +3209,7 @@ static void ShowDemoWindowMultiSelect() tree_node_flags |= ImGuiTreeNodeFlags_Selected; bool open = ImGui::TreeNodeEx(label, tree_node_flags); if (next_focus_item_idx == n) - ImGui::SetKeyboardFocusHere(-1); // FIXME-MULTISELECT: turn into a ms_io->RequestFocusIdx + ImGui::SetKeyboardFocusHere(-1); if (use_drag_drop && ImGui::BeginDragDropSource()) { ImGui::Text("(Dragging %d items)", selection.GetSize()); @@ -3246,12 +3257,10 @@ static void ShowDemoWindowMultiSelect() } // Apply multi-select requests - if (want_delete && ms_io->NavIdSelected == false) - ms_io->RangeSrcReset = true; ms_io = ImGui::EndMultiSelect(); selection.ApplyRequests(ms_io, items.Size); if (want_delete) - selection.ApplyDeletion(ms_io, items, ms_io->NavIdSelected ? next_focus_item_idx : -1); + selection.ApplyDeletionPostLoop(ms_io, items); if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); diff --git a/imgui_internal.h b/imgui_internal.h index 21d350c89be1..b076368e5711 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1730,7 +1730,7 @@ struct IMGUI_API ImGuiMultiSelectTempData //ImRect Rect; // Extent of selection scope between BeginMultiSelect() / EndMultiSelect(), used by ImGuiMultiSelectFlags_ClearOnClickRectVoid. ImGuiMultiSelectTempData() { Clear(); } - void Clear() { memset(this, 0, sizeof(*this)); BeginIO.RangeSrcItem = EndIO.RangeSrcItem = BeginIO.RangeDstItem = EndIO.RangeDstItem = BeginIO.NavIdItem = EndIO.NavIdItem = (void*)-1; } + void Clear() { memset(this, 0, sizeof(*this)); BeginIO.Clear(); EndIO.Clear(); } }; // Persistent storage for multi-select (as long as selection is alive) From e3616e151ff7a12121f1fff9360a772b735abfdc Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 7 Jun 2023 17:28:22 +0200 Subject: [PATCH 038/132] MultiSelect: Demo: Delete items from menu. --- imgui_demo.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 53f60067e0a5..76255107cbb0 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2778,10 +2778,11 @@ 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? + bool QueueDeletion; // Request deleting selected items // Functions ExampleSelection() { Clear(); } - void Clear() { Storage.Clear(); SelectionSize = 0; } + void Clear() { Storage.Clear(); SelectionSize = 0; QueueDeletion = false; } 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; } int GetSize() const { return SelectionSize; } @@ -2822,6 +2823,8 @@ struct ExampleSelection template int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, ImVector& items) { + QueueDeletion = false; + // If current item is not selected. if (ms_io->NavIdSelected == false) // Here 'NavIdSelected' should be == to 'GetSelected(ms_io->NavIdData)' { @@ -3130,7 +3133,7 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. // FIXME-MULTISELECT: Test with intermediary modal dialog. - const bool want_delete = (selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete); + const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); if (want_delete) selection.ApplyDeletionPreLoop(ms_io, items); const int next_focus_item_idx = (int)(intptr_t)ms_io->RequestFocusItem; @@ -3222,7 +3225,10 @@ static void ShowDemoWindowMultiSelect() // Right-click: context menu if (ImGui::BeginPopupContextItem()) { - ImGui::Text("(Testing Selectable inside an embedded popup)"); + ImGui::BeginDisabled(!use_deletion || selection.GetSize() == 0); + sprintf(label, "Delete %d item(s)###DeleteSelected", selection.GetSize()); + selection.QueueDeletion |= ImGui::Selectable(label); + ImGui::EndDisabled(); ImGui::Selectable("Close"); ImGui::EndPopup(); } From ab9326f4ae93c5861b2854cd321573fe62d5aeb8 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 7 Jun 2023 17:40:59 +0200 Subject: [PATCH 039/132] MultiSelect: Fixed right-click handling in MultiSelectItemFooter() when not focused. --- imgui_widgets.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index f7b91dd47a7b..712e709d5946 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7332,7 +7332,11 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ImGuiMultiSelectState* storage = ms->Storage; if (pressed) ms->IsFocused = true; - if (!ms->IsFocused) + + bool hovered = false; + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) + hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); + if (!ms->IsFocused && !hovered) return; void* item_data = (void*)g.NextItemData.SelectionUserData; @@ -7358,7 +7362,6 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Right-click handling: this could be moved at the Selectable() level. // FIXME-MULTISELECT: See https://github.com/ocornut/imgui/pull/5816 - bool hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); if (hovered && IsMouseClicked(1)) { if (g.ActiveId != 0 && g.ActiveId != id) From 0cf376348bc3c6726ede236fa96f9075db3a500b Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 8 Jun 2023 15:01:10 +0200 Subject: [PATCH 040/132] MultiSelect: Cleanup unused comments/code. --- imgui.h | 2 +- imgui_demo.cpp | 3 +-- imgui_widgets.cpp | 9 --------- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/imgui.h b/imgui.h index 98e6c2b34642..1ac315d70617 100644 --- a/imgui.h +++ b/imgui.h @@ -2777,7 +2777,7 @@ struct ImGuiMultiSelectIO 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. - void* RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implemention of deletion idiom (see demo). + void* RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implementing a deletion idiom (see demo). // STATE/ARGUMENTS ---------// BEGIN / LOOP / END 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. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 76255107cbb0..27aa356943e1 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2818,7 +2818,7 @@ struct ExampleSelection // Call after BeginMultiSelect(). // - Calculate and set ms_io->RequestFocusItem, which we will focus during the loop. // - 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 + // - Return value is stored into 'ms_io->RequestFocusItem' which is provided as a convenience for this idiom (but not used by core imgui) // - Important: This only works if the item ID are stable: aka not depend on their index, but on e.g. item id/ptr. template int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, ImVector& items) @@ -3132,7 +3132,6 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. - // FIXME-MULTISELECT: Test with intermediary modal dialog. const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); if (want_delete) selection.ApplyDeletionPreLoop(ms_io, items); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 712e709d5946..8ffbd27c3505 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7158,7 +7158,6 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) storage->Window = window; ms->Storage = storage; - // FIXME-MULTISELECT: Set for the purpose of user calling RangeSrcPassedBy // FIXME-MULTISELECT: Index vs Pointers. // We want EndIO's NavIdItem/NavIdSelected to match BeginIO's one, so the value never changes after EndMultiSelect() ms->BeginIO.RangeSrcItem = ms->EndIO.RangeSrcItem = storage->RangeSrcItem; @@ -7168,14 +7167,6 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) 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) if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) From 847b1dde8c5c9dbad9fced2cbed378e1429ff0fb Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 4 Aug 2023 10:23:44 +0200 Subject: [PATCH 041/132] MultiSelect: (Breaking) Fix + Rename ImGuiMultiSelectFlags_NoMultiSelect to ImGuiMultiSelectFlags_SingleSelect as it seems easier to grasp. Feature was broken by "Tidying up..." June 30 commit. --- imgui.h | 6 +++--- imgui_demo.cpp | 2 +- imgui_widgets.cpp | 16 +++++++++------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/imgui.h b/imgui.h index 1ac315d70617..aa9774759db2 100644 --- a/imgui.h +++ b/imgui.h @@ -2724,12 +2724,12 @@ 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(). -// (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). +// (we provide 'ImGuiMultiSelectFlags_SingleSelect' for consistency and flexiblity to allow a single-selection to use same code/logic, but it essentially disable the biggest purpose of BeginMultiSelect(). +// If you use 'ImGuiMultiSelectFlags_SingleSelect' you can handle single-selection in a simpler way by just calling Selectable()/TreeNode() and reacting on clicks). enum ImGuiMultiSelectFlags_ { ImGuiMultiSelectFlags_None = 0, - ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0, // Disable selecting more than one item. This is not very useful at this kind of selection can be implemented without BeginMultiSelect(), but this is available for consistency. + ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to use same code/logic is desired, but may not be very useful. ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to set RequestSelectAll ImGuiMultiSelectFlags_ClearOnEscape = 1 << 2, // Clear selection when pressing Escape while scope is focused. ImGuiMultiSelectFlags_ClearOnClickWindowVoid= 1 << 3, // Clear selection when clicking on empty location within host window (use if BeginMultiSelect() covers a whole window) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 27aa356943e1..c532ff9b42f3 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3106,7 +3106,7 @@ static void ShowDemoWindowMultiSelect() ImGui::Checkbox("Enable drag & drop", &use_drag_drop); ImGui::Checkbox("Show in a table", &show_in_table); ImGui::Checkbox("Show color button", &show_color_button); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoMultiSelect", &flags, ImGuiMultiSelectFlags_NoMultiSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickWindowVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickWindowVoid); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 8ffbd27c3505..58467a1de7cf 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7180,7 +7180,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) } // Shortcut: Select all (CTRL+A) - if (!(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) + if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) ms->BeginIO.RequestSelectAll = true; @@ -7332,7 +7332,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) void* item_data = (void*)g.NextItemData.SelectionUserData; - const bool is_multiselect = (ms->Flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0; + const bool is_multiselect = (ms->Flags & ImGuiMultiSelectFlags_SingleSelect) == 0; bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0; bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0; @@ -7411,18 +7411,20 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ms->EndIO.RangeDirection = +1; } - if (input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) + if (!is_multiselect) { - if (is_multiselect && !is_ctrl) + ms->EndIO.RequestClear = true; + } + else if (input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) + { + if (!is_ctrl) ms->EndIO.RequestClear = true; } else if (input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) { - if (is_multiselect && is_shift && !is_ctrl) // Without Shift the RequestClear was done in BeginIO, not necessary to do again. + if (is_shift && !is_ctrl) // Without Shift the RequestClear was done in BeginIO, not necessary to do again. ms->EndIO.RequestClear = true; } - else if (!is_multiselect) - ms->EndIO.RequestClear = true; } // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) From 140a2f0565b4f07056673c3922ba2995640f70dc Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 7 Aug 2023 12:34:20 +0200 Subject: [PATCH 042/132] MultiSelect: Comments, tweaks. + Alignment to reduce noise on next commit. --- imgui.h | 75 ++++++++++++++++++++++++----------------------- imgui_demo.cpp | 5 +++- imgui_widgets.cpp | 3 +- 3 files changed, 44 insertions(+), 39 deletions(-) diff --git a/imgui.h b/imgui.h index aa9774759db2..811bf940e446 100644 --- a/imgui.h +++ b/imgui.h @@ -2737,21 +2737,23 @@ enum ImGuiMultiSelectFlags_ }; // 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), -// external array (store an array aside from your objects), hash/map/set (store only selected items in a hash/map/set), -// or other structures (store indices in an interval tree), etc. -// - TreeNode() and Selectable() are supported. -// - The work involved to deal with multi-selection differs whether you want to only submit visible items (and clip others) or submit all items -// regardless of their visibility. Clipping items is more efficient and will allow you to deal with large lists (1k~100k items) with near zero -// performance penalty, but requires a little more work on the code. If you only have a few hundreds elements in your possible selection set, -// you may as well not bother with clipping, as the cost should be negligible (as least on Dear ImGui side). -// If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards. +// - Refer to 'Demo->Widgets->Selection State' for references using this. +// - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) +// and supports a clipper being used. Handling this manually may be tricky, this is why we provide the functionality. +// If you don't need SHIFT+Mouse/Keyboard range-select + clipping, you can use a simpler form of multi-selection yourself, +// by reacting to click/presses on Selectable() items and checking keyboard modifiers. +// The complexity of this system is mostly caused by supporting SHIFT+Click/Arrow range-select with clipped elements. +// - TreeNode() and Selectable() are supported but custom widgets may use it as well. +// - In the spirit of Dear ImGui design, your code owns actual selection data. +// This is designed to allow all kinds of selection storage you may use in your application: +// e.g. instructive selection (store a bool inside each object), external array (store an array in your view data, next +// to your objects), set/map/hash (store only selected items), or other structures (store indices in an interval tree), etc. +// - The work involved to deal with multi-selection differs whether you want to only submit visible items and clip others, +// or submit all items regardless of their visibility. Clipping items is more efficient and will allow you to deal with +// large lists (1k~100k items) with no performance penalty, but requires a little more work on the code. +// For small selection set (<100 items), you might want to not bother with using the clipper, as the cost you should +// be negligible (as least on Dear ImGui side). +// If you are not sure, always start without clipping and you can work your way to the optimized version afterwards. // - The void* RangeSrcItem/RangeDstItem value represent a selectable object. They are the value you pass to SetNextItemSelectionUserData(). // Most likely you will want to store an index here. // Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points and you will need to interpolate @@ -2759,34 +2761,35 @@ enum ImGuiMultiSelectFlags_ // and then from the pointer have your own way of iterating from RangeSrcItem to RangeDstItem). // Usage flow: // 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. +// - (2) [If using clipper] Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 6. +// LOOP - (3) [If using 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()) +// - If you are using integer indices, this is easy to compute: if (clipper.DisplayStart > data->RangeSrcItem) { data->RangeSrcPassedBy = true; } +// - If you are using pointers, you may need additional processing in each clipper step to tell if current DisplayStart comes after RangeSrcItem.. +// - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. (optionally call IsItemToggledSelection() if you need that info immediately for displaying your item, 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. +// - (6) Honor Clear/SelectAll/SetRange requests by updating your selection data. 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. +// However it is perfectly fine to honor all steps even if you don't use a clipper. struct ImGuiMultiSelectIO { - // - Always process requests in this order: Clear, SelectAll, SetRange. + // - Always process requests in this order: Clear, SelectAll, SetRange. Use 'Debug Log->Selection' to see requests as they happen. // - 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. - void* RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implementing a deletion idiom (see demo). - // STATE/ARGUMENTS ---------// BEGIN / LOOP / END - 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 / app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). - bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). - void* NavIdItem; // ms:w, app:r / / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + // 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. + void* RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implementing a deletion idiom (see demo). + // STATE/ARGUMENTS -------------------------// BEGIN / LOOP / END + 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 / app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). + bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). + void* NavIdItem; // 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)); RequestFocusItem = NavIdItem = RangeSrcItem = RangeDstItem = (void*)-1; } diff --git a/imgui_demo.cpp b/imgui_demo.cpp index c532ff9b42f3..bd4618e32282 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2796,7 +2796,8 @@ struct ExampleSelection void SetRange(int a, int b, bool v) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) SetSelected(n, v); } void SelectAll(int count) { Storage.Data.resize(count); for (int idx = 0; idx < count; idx++) Storage.Data[idx] = ImGuiStoragePair((ImGuiID)idx, 1); SelectionSize = count; } // This could be using SetRange(), but it this way is faster. - // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). Must be done in this order! Order->SelectAll->SetRange. + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). Must be done in this order! Clear->SelectAll->SetRange. + // Enable 'Debug Log->Selection' to see selection requests as they happen. void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count) { if (ms_io->RequestClear) { Clear(); } @@ -2934,6 +2935,8 @@ static void ShowDemoWindowMultiSelect() { static ExampleSelection selection; + ImGui::Text("Tips: Use 'Debug Log->Selection' to see selection requests as they happen."); + ImGui::Text("Supported features:"); ImGui::BulletText("Keyboard navigation (arrows, page up/down, home/end, space)."); ImGui::BulletText("Ctrl modifier to preserve and toggle selection."); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 58467a1de7cf..9dacb010dbd5 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7455,11 +7455,10 @@ 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*"); + bool open = TreeNode((void*)(intptr_t)storage->ID, "MultiSelect 0x%08X in '%s'%s", storage->ID, storage->Window ? storage->Window->Name : "N/A", 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("NavIdData = %p, NavIdSelected = %d", storage->NavIdItem, storage->NavIdSelected); TreePop(); From e82b49d2d468f9d26dbd4f7ad500016ebf4e2943 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 7 Aug 2023 12:38:24 +0200 Subject: [PATCH 043/132] MultiSelect: (Breaking) Use ImGuiSelectionUserData (= ImS64) instead of void* for selection user data. Less confusing for most users, less casting. --- imgui.h | 38 +++++++++++++++++++++++--------------- imgui_demo.cpp | 24 ++++++++++++------------ imgui_internal.h | 6 +++--- imgui_widgets.cpp | 30 +++++++++++++++--------------- 4 files changed, 53 insertions(+), 45 deletions(-) diff --git a/imgui.h b/imgui.h index 811bf940e446..4d6ecaf0bd43 100644 --- a/imgui.h +++ b/imgui.h @@ -265,8 +265,9 @@ typedef ImWchar32 ImWchar; typedef ImWchar16 ImWchar; #endif -// Multi-Selection item index or identifier when using SetNextItemSelectionUserData()/BeginMultiSelect() -// (Most users are likely to use this store an item INDEX but this may be used to store a POINTER as well.) +// Multi-Selection item index or identifier when using BeginMultiSelect() +// - Used by SetNextItemSelectionUserData() + and inside ImGuiMultiSelectIO structure. +// - Most users are likely to use this store an item INDEX but this may be used to store a POINTER as well. Read comments near ImGuiMultiSelectIO for details. typedef ImS64 ImGuiSelectionUserData; // Callback and functions types @@ -670,7 +671,7 @@ namespace ImGui // Multi-selection system for Selectable() and TreeNode() functions. // - 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. + // - ImGuiSelectionUserData is often used to store your item index. Read comments near ImGuiMultiSelectIO for details. IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags); IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); @@ -2754,18 +2755,25 @@ enum ImGuiMultiSelectFlags_ // For small selection set (<100 items), you might want to not bother with using the clipper, as the cost you should // be negligible (as least on Dear ImGui side). // If you are not sure, always start without clipping and you can work your way to the optimized version afterwards. -// - The void* RangeSrcItem/RangeDstItem value represent a selectable object. They are the value you pass to SetNextItemSelectionUserData(). -// Most likely you will want to store an index here. -// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points and you will need to interpolate -// 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). +// About ImGuiSelectionUserData: +// - This is your application-defined identifier in a selection set: +// - For each item is submitted by your calls to SetNextItemSelectionUserData(). +// - In return we store them into RangeSrcItem/RangeDstItem and other fields ImGuiMultiSelectIO. +// - Most applications will store an object INDEX, hence the chosen name and type. +// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points +// and you will need to interpolate between them to honor range selection. +// - However it is perfectly possible to store a POINTER inside this value! The multi-selection system never assume +// that you identify items by indices, and never attempt to interpolate between two ImGuiSelectionUserData values. +// - As most users will want to cast this to integer, for convenience and to reduce confusion we use ImS64 instead +// of void*, being syntactically easier to downcast. But feel free to reinterpret_cast a pointer into this. +// - If you need to wrap this API for another language/framework, feel free to expose this as 'int' if simpler. // Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. // - (2) [If using clipper] Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 6. // LOOP - (3) [If using 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, this is easy to compute: if (clipper.DisplayStart > data->RangeSrcItem) { data->RangeSrcPassedBy = true; } -// - If you are using pointers, you may need additional processing in each clipper step to tell if current DisplayStart comes after RangeSrcItem.. +// - If you are using integer indices in ImGuiSelectionUserData, this is easy to compute: if (clipper.DisplayStart > data->RangeSrcItem) { data->RangeSrcPassedBy = true; } +// - If you are using pointers in ImGuiSelectionUserData, you may need additional processing in each clipper step to tell if current DisplayStart comes after RangeSrcItem.. // - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. (optionally call IsItemToggledSelection() if you need that info immediately for displaying your item, before EndMultiSelect()) // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. // - (6) Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 2. @@ -2780,19 +2788,19 @@ struct ImGuiMultiSelectIO 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. - void* RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implementing a deletion idiom (see demo). + ImGuiSelectionUserData RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implementing a deletion idiom (see demo). // STATE/ARGUMENTS -------------------------// BEGIN / LOOP / END - 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. + ImGuiSelectionUserData RangeSrcItem; // ms:w / app:r / ms:w, app:r // Begin: Last known SetNextItemSelectionUserData() value for RangeSrcItem. End: parameter from RequestSetRange request. + ImGuiSelectionUserData 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 / app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). - void* NavIdItem; // ms:w, app:r / / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + ImGuiSelectionUserData NavIdItem; // 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)); RequestFocusItem = NavIdItem = RangeSrcItem = RangeDstItem = (void*)-1; } + void Clear() { memset(this, 0, sizeof(*this)); RequestFocusItem = NavIdItem = RangeSrcItem = RangeDstItem = (ImGuiSelectionUserData)-1; } }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index bd4618e32282..af8604182922 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2802,7 +2802,7 @@ struct ExampleSelection { if (ms_io->RequestClear) { Clear(); } if (ms_io->RequestSelectAll) { SelectAll(items_count); } - if (ms_io->RequestSetRange) { SetRange((int)(intptr_t)ms_io->RangeSrcItem, (int)(intptr_t)ms_io->RangeDstItem, ms_io->RangeSelected ? 1 : 0); } + if (ms_io->RequestSetRange) { SetRange((int)ms_io->RangeSrcItem, (int)ms_io->RangeDstItem, ms_io->RangeSelected ? 1 : 0); } } void DebugTooltip() @@ -2827,19 +2827,19 @@ struct ExampleSelection QueueDeletion = false; // If current item is not selected. - if (ms_io->NavIdSelected == false) // Here 'NavIdSelected' should be == to 'GetSelected(ms_io->NavIdData)' + if (ms_io->NavIdSelected == false) // Here 'NavIdSelected' should be == to 'GetSelected(ms_io->NavIdData)' { - ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even without the !NavIdSelected test but it would take an extra frame to recover RangeSrc when deleting a selected item. - return (int)(intptr_t)ms_io->NavIdItem; // Request to land on same item after deletion. + ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even without the !NavIdSelected test but it would take an extra frame to recover RangeSrc when deleting a selected item. + return (int)ms_io->NavIdItem; // Request to land on same item after deletion. } // If current item is selected: land on first unselected item after RangeSrc. - for (int n = (int)(intptr_t)ms_io->RangeSrcItem + 1; n < items.Size; n++) + for (int n = (int)ms_io->RangeSrcItem + 1; n < items.Size; n++) if (!GetSelected(n)) return n; // If current item is selected: otherwise return last unselected item. - for (int n = IM_MIN((int)(intptr_t)ms_io->RangeSrcItem, items.Size) - 1; n >= 0; n--) + for (int n = IM_MIN((int)ms_io->RangeSrcItem, items.Size) - 1; n >= 0; n--) if (!GetSelected(n)) return n; @@ -2860,7 +2860,7 @@ struct ExampleSelection IM_UNUSED(ms_io); ImVector new_items; new_items.reserve(items.Size - SelectionSize); - int next_focus_idx_in_old_selection = (int)(intptr_t)ms_io->RequestFocusItem; + int next_focus_idx_in_old_selection = (int)ms_io->RequestFocusItem; int next_focus_idx_in_new_selection = -1; for (int n = 0; n < items.Size; n++) { @@ -3016,8 +3016,8 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: If pressing Delete + another key we have ambiguous behavior. const bool want_delete = (selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete); if (want_delete) - ms_io->RequestFocusItem = (void*)(intptr_t)selection.ApplyDeletionPreLoop(ms_io, items); - const int next_focus_item_idx = (int)(intptr_t)ms_io->RequestFocusItem; + ms_io->RequestFocusItem = selection.ApplyDeletionPreLoop(ms_io, items); + const int next_focus_item_idx = (int)ms_io->RequestFocusItem; for (int n = 0; n < items.Size; n++) { @@ -3138,7 +3138,7 @@ static void ShowDemoWindowMultiSelect() const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); if (want_delete) selection.ApplyDeletionPreLoop(ms_io, items); - const int next_focus_item_idx = (int)(intptr_t)ms_io->RequestFocusItem; + const int next_focus_item_idx = (int)ms_io->RequestFocusItem; if (show_in_table) { @@ -3162,7 +3162,7 @@ static void ShowDemoWindowMultiSelect() { // 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) + if (use_clipper && !ms_io->RangeSrcPassedBy && clipper.DisplayStart > ms_io->RangeSrcItem) ms_io->RangeSrcPassedBy = true; const int item_begin = use_clipper ? clipper.DisplayStart : 0; @@ -3254,7 +3254,7 @@ static void ShowDemoWindowMultiSelect() // 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) + if (use_clipper && !ms_io->RangeSrcPassedBy && items.Size > ms_io->RangeSrcItem) ms_io->RangeSrcPassedBy = true; if (show_in_table) diff --git a/imgui_internal.h b/imgui_internal.h index b076368e5711..ce80d94be849 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1741,11 +1741,11 @@ struct IMGUI_API ImGuiMultiSelectState int LastFrameActive; // Last used frame-count, for GC. ImS8 RangeSelected; // -1 (don't have) or true/false ImS8 NavIdSelected; // -1 (don't have) or true/false - void* RangeSrcItem; // - void* NavIdItem; // SetNextItemSelectionUserData() value for NavId (if part of submitted items) + ImGuiSelectionUserData RangeSrcItem; // + ImGuiSelectionUserData NavIdItem; // SetNextItemSelectionUserData() value for NavId (if part of submitted items) ImGuiMultiSelectState() { Init(0); } - void Init(ImGuiID id) { Window = NULL; ID = id; LastFrameActive = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = (void*)-1; } + void Init(ImGuiID id) { Window = NULL; ID = id; LastFrameActive = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; } }; #endif // #ifdef IMGUI_HAS_MULTI_SELECT diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 9dacb010dbd5..e62a8e1006d6 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7128,7 +7128,7 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe ImGuiContext& g = *GImGui; if (data->RequestClear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestClear\n", function); if (data->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSelectAll\n", function); - if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %p..%p = %d (dir %+d)\n", function, data->RangeSrcItem, data->RangeDstItem, data->RangeSelected, data->RangeDirection); + if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %p..%p = %d (dir %+d)\n", function, (void*)data->RangeSrcItem, (void*)data->RangeDstItem, data->RangeSelected, data->RangeDirection); } // Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). @@ -7174,7 +7174,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if (ms->KeyMods & ImGuiMod_Shift) ms->IsSetRange = true; if (ms->IsSetRange) - IM_ASSERT(storage->RangeSrcItem != (void*)-1); // Not ready -> could clear? + IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear? if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) ms->BeginIO.RequestClear = true; } @@ -7207,15 +7207,15 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (ms->IsFocused) { - if (ms->BeginIO.RangeSrcReset || (ms->BeginIO.RangeSrcPassedBy == false && ms->BeginIO.RangeSrcItem != (void*)-1)) + if (ms->BeginIO.RangeSrcReset || (ms->BeginIO.RangeSrcPassedBy == false && ms->BeginIO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) { IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId. - ms->Storage->RangeSrcItem = (void*)-1; + ms->Storage->RangeSrcItem = ImGuiSelectionUserData_Invalid; } - if (ms->NavIdPassedBy == false && ms->Storage->NavIdItem != (void*)-1) + if (ms->NavIdPassedBy == false && ms->Storage->NavIdItem != ImGuiSelectionUserData_Invalid) { IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n"); - ms->Storage->NavIdItem = (void*)-1; + ms->Storage->NavIdItem = ImGuiSelectionUserData_Invalid; ms->Storage->NavIdSelected = -1; } } @@ -7255,7 +7255,7 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d { // 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) + if (ms->BeginIO.RangeSrcItem == selection_user_data) ms->BeginIO.RangeSrcPassedBy = true; } else @@ -7273,7 +7273,7 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) ImGuiMultiSelectState* storage = ms->Storage; IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); - void* item_data = (void*)g.NextItemData.SelectionUserData; + ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; // Apply Clear/SelectAll requests requested by BeginMultiSelect(). // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. @@ -7293,7 +7293,7 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) if (is_range_dst) { ms->RangeDstPassedBy = true; - if (storage->RangeSrcItem == (void*)-1) // If we don't have RangeSrc, assign RangeSrc = RangeDst + if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst { storage->RangeSrcItem = item_data; storage->RangeSelected = selected ? 1 : 0; @@ -7302,7 +7302,7 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) const bool is_range_src = storage->RangeSrcItem == item_data; if (is_range_src || is_range_dst || ms->BeginIO.RangeSrcPassedBy != ms->RangeDstPassedBy) { - IM_ASSERT(storage->RangeSrcItem != (void*)-1 && storage->RangeSelected != -1); + IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1); selected = (storage->RangeSelected != 0); } else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0) @@ -7330,13 +7330,13 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) if (!ms->IsFocused && !hovered) return; - void* item_data = (void*)g.NextItemData.SelectionUserData; + ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; const bool is_multiselect = (ms->Flags & ImGuiMultiSelectFlags_SingleSelect) == 0; bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0; bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0; - if (g.NavId == id && storage->RangeSrcItem == (void*)-1) + if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) { storage->RangeSrcItem = item_data; storage->RangeSelected = selected; // Will be updated at the end of this function anyway. @@ -7398,7 +7398,7 @@ 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) //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); - ms->EndIO.RangeSrcItem = (storage->RangeSrcItem != (void*)-1) ? storage->RangeSrcItem : item_data; + ms->EndIO.RangeSrcItem = (storage->RangeSrcItem != ImGuiSelectionUserData_Invalid) ? storage->RangeSrcItem : item_data; ms->EndIO.RangeSelected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; ms->EndIO.RangeDirection = ms->BeginIO.RangeSrcPassedBy ? +1 : -1; } @@ -7459,8 +7459,8 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) if (!is_active) { PopStyleColor(); } if (!open) return; - Text("RangeSrcItem = %p, RangeSelected = %d", storage->RangeSrcItem, storage->RangeSelected); - Text("NavIdData = %p, NavIdSelected = %d", storage->NavIdItem, storage->NavIdSelected); + Text("RangeSrcItem = %p, RangeSelected = %d", (void*)storage->RangeSrcItem, storage->RangeSelected); + Text("NavIdData = %p, NavIdSelected = %d", (void*)storage->NavIdItem, storage->NavIdSelected); TreePop(); #else IM_UNUSED(storage); From c9eb3714e8b0768117ce4492d6410a5b880045cc Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 7 Aug 2023 18:53:57 +0200 Subject: [PATCH 044/132] MultiSelect: move HasSelectionData to ImGuiItemFlags to facilitate copying around in standardized fieds. Required/motivated to simplify support for ImGuiTreeNodeFlags_NavLeftJumpsBackHere (bc3c0ce) in this branch. --- imgui_internal.h | 5 ++--- imgui_widgets.cpp | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index ce80d94be849..3bf729e3b81e 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1211,15 +1211,14 @@ enum ImGuiNextItemDataFlags_ ImGuiNextItemDataFlags_HasOpen = 1 << 1, ImGuiNextItemDataFlags_HasShortcut = 1 << 2, ImGuiNextItemDataFlags_HasRefVal = 1 << 3, - ImGuiNextItemDataFlags_HasSelectionData = 1 << 4, }; struct ImGuiNextItemData { ImGuiNextItemDataFlags Flags; - ImGuiItemFlags ItemFlags; // Currently only tested/used for ImGuiItemFlags_AllowOverlap. + ImGuiItemFlags ItemFlags; // Currently only tested/used for ImGuiItemFlags_AllowOverlap and ImGuiItemFlags_HasSelectionUserData. // Non-flags members are NOT cleared by ItemAdd() meaning they are still valid during NavProcessItem() - ImGuiID FocusScopeId; // Set by SetNextItemSelectionUserData() (!= 0 signify value has been set) + ImGuiID FocusScopeId; // Set by SetNextItemSelectionUserData() ImGuiSelectionUserData SelectionUserData; // Set by SetNextItemSelectionUserData() (note that NULL/0 is a valid value, we use -1 == ImGuiSelectionUserData_Invalid to mark invalid values) float Width; // Set by SetNextItemWidth() ImGuiKeyChord Shortcut; // Set by SetNextItemShortcut() diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index e62a8e1006d6..48336721d936 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6390,8 +6390,8 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags } // Compute open and multi-select states before ItemAdd() as it clear NextItem data. + const bool is_multi_select = (g.NextItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; // Before ItemAdd() bool is_open = TreeNodeUpdateNextOpen(storage_id, flags); - const bool is_multi_select = (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasSelectionData) != 0; // Before ItemAdd() bool item_add = ItemAdd(interact_bb, id); g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; g.LastItemData.DisplayRect = frame_bb; @@ -6780,7 +6780,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl } const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0; - const bool is_multi_select = (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasSelectionData) != 0; // Before ItemAdd() + const bool is_multi_select = (g.NextItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; // Before ItemAdd() const bool item_add = ItemAdd(bb, id, NULL, disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None); if (span_all_columns) From 6821401a3f00e637a1646d02a5c364728a653b21 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 8 Aug 2023 14:47:50 +0200 Subject: [PATCH 045/132] MultiSelect: Tweak debug log to print decimal+hex values for item data. Struggled to get standard PRIX64 to work on CI. --- imgui_widgets.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 48336721d936..9ccb63d60b02 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -70,6 +70,7 @@ Index of this file: #pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. #pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. #pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used // we define snprintf/vsnprintf on Windows so they are available, but not always used. #pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 #pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. #pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') @@ -7128,7 +7129,7 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe ImGuiContext& g = *GImGui; if (data->RequestClear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestClear\n", function); if (data->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSelectAll\n", function); - if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %p..%p = %d (dir %+d)\n", function, (void*)data->RangeSrcItem, (void*)data->RangeDstItem, data->RangeSelected, data->RangeDirection); + if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d (dir %+d)\n", function, data->RangeSrcItem, data->RangeDstItem, data->RangeSrcItem, data->RangeDstItem, data->RangeSelected, data->RangeDirection); } // Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). @@ -7459,8 +7460,8 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) if (!is_active) { PopStyleColor(); } if (!open) return; - Text("RangeSrcItem = %p, RangeSelected = %d", (void*)storage->RangeSrcItem, storage->RangeSelected); - Text("NavIdData = %p, NavIdSelected = %d", (void*)storage->NavIdItem, storage->NavIdSelected); + Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected); + Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected); TreePop(); #else IM_UNUSED(storage); From af83a3eea44a0a0397ea6f1e44a475b84a8958bf Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 15 Aug 2023 18:11:14 +0200 Subject: [PATCH 046/132] MultiSelect: clear selection when leaving a scope with a nav directional request. May need to clarify how to depends on actions being performed (e.g. click doesn't). May become optional? --- imgui.cpp | 3 +++ imgui_widgets.cpp | 34 ++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 26f0f36bcefe..19beed8ed63d 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -12251,6 +12251,7 @@ static void ImGui::NavUpdate() // Process navigation init request (select first/default focus) g.NavJustMovedToId = 0; + g.NavJustMovedToFocusScopeId = g.NavJustMovedFromFocusScopeId = 0; if (g.NavInitResult.ID != 0) NavInitRequestApplyResult(); g.NavInitRequest = false; @@ -12403,6 +12404,7 @@ void ImGui::NavInitRequestApplyResult() ImGuiNavItemData* result = &g.NavInitResult; if (g.NavId != result->ID) { + g.NavJustMovedFromFocusScopeId = g.NavFocusScopeId; g.NavJustMovedToId = result->ID; g.NavJustMovedToFocusScopeId = result->FocusScopeId; g.NavJustMovedToKeyMods = 0; @@ -12661,6 +12663,7 @@ void ImGui::NavMoveRequestApplyResult() // PageUp/PageDown however sets always set NavJustMovedTo (vs Home/End which doesn't) mimicking Windows behavior. if ((g.NavId != result->ID || (g.NavMoveFlags & ImGuiNavMoveFlags_IsPageMove)) && (g.NavMoveFlags & ImGuiNavMoveFlags_NoSelect) == 0) { + g.NavJustMovedFromFocusScopeId = g.NavFocusScopeId; g.NavJustMovedToId = result->ID; g.NavJustMovedToFocusScopeId = result->FocusScopeId; g.NavJustMovedToKeyMods = g.NavMoveKeyMods; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 9ccb63d60b02..94123de764d9 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7165,10 +7165,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ms->BeginIO.NavIdItem = ms->EndIO.NavIdItem = storage->NavIdItem; ms->BeginIO.NavIdSelected = ms->EndIO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; - if (!ms->IsFocused) - return &ms->BeginIO; // This is cleared at this point. - - // Auto clear when using Navigation to move within the selection + // Clear when using Navigation to move within the scope // (we compare FocusScopeId so it possible to use multiple selections inside a same window) if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) { @@ -7179,18 +7176,27 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) ms->BeginIO.RequestClear = true; } + else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId) + { + // Also clear on leaving scope (may be optional?) + if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) + ms->BeginIO.RequestClear = true; + } - // Shortcut: Select all (CTRL+A) - if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) - if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) - ms->BeginIO.RequestSelectAll = true; + if (ms->IsFocused) + { + // Shortcut: Select all (CTRL+A) + if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(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 (flags & ImGuiMultiSelectFlags_ClearOnEscape) - if (Shortcut(ImGuiKey_Escape)) - ms->BeginIO.RequestClear = 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 (flags & ImGuiMultiSelectFlags_ClearOnEscape) + if (Shortcut(ImGuiKey_Escape)) + ms->BeginIO.RequestClear = true; + } if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) DebugLogMultiSelectRequests("BeginMultiSelect", &ms->BeginIO); From ff95fdb668a05375dcde40df1c5cd40b6cc530c2 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 21 Aug 2023 19:20:51 +0200 Subject: [PATCH 047/132] MultiSelect: (Breaking) RequestSetRange's parameter are RangeFirstItem...RangeLastItem (which was always ordered unlike RangeSrcItem...RangeDstItme). Removed RangeDstItem. Removed RangeDirection. --- imgui.h | 14 +++++++------- imgui_demo.cpp | 4 ++-- imgui_widgets.cpp | 12 +++++++----- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/imgui.h b/imgui.h index 4d6ecaf0bd43..9e5a9aef2869 100644 --- a/imgui.h +++ b/imgui.h @@ -2758,7 +2758,7 @@ enum ImGuiMultiSelectFlags_ // About ImGuiSelectionUserData: // - This is your application-defined identifier in a selection set: // - For each item is submitted by your calls to SetNextItemSelectionUserData(). -// - In return we store them into RangeSrcItem/RangeDstItem and other fields ImGuiMultiSelectIO. +// - In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO. // - Most applications will store an object INDEX, hence the chosen name and type. // Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points // and you will need to interpolate between them to honor range selection. @@ -2787,20 +2787,20 @@ struct ImGuiMultiSelectIO // 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. + bool RequestSetRange; // / / ms:w, app:r // 3. Request app/user to select/unselect [RangeFirstItem..RangeLastItem] items, based on RangeSelected. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. ImGuiSelectionUserData RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implementing a deletion idiom (see demo). // STATE/ARGUMENTS -------------------------// BEGIN / LOOP / END - ImGuiSelectionUserData RangeSrcItem; // ms:w / app:r / ms:w, app:r // Begin: Last known SetNextItemSelectionUserData() value for RangeSrcItem. End: parameter from RequestSetRange request. - ImGuiSelectionUserData 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. + ImGuiSelectionUserData RangeSrcItem; // ms:w / app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point). Read during loop in order for user code to set RangeSrcPassedBy. + ImGuiSelectionUserData RangeFirstItem; // / / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) + ImGuiSelectionUserData RangeLastItem; // / / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) + bool RangeSelected; // / / ms:w, app:r // End: parameter for 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 / app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). ImGuiSelectionUserData NavIdItem; // 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)); RequestFocusItem = NavIdItem = RangeSrcItem = RangeDstItem = (ImGuiSelectionUserData)-1; } + void Clear() { memset(this, 0, sizeof(*this)); RequestFocusItem = NavIdItem = RangeSrcItem = RangeFirstItem = RangeLastItem = (ImGuiSelectionUserData)-1; } }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index af8604182922..7a1d74b96bda 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2793,7 +2793,7 @@ struct ExampleSelection // you will need a way to iterate from one item to the other item given the ID you use. // You are likely to need some kind of data structure to convert 'view index' <> 'object ID' (FIXME-MULTISELECT: Would be worth providing a demo of doing this). // Note: This implementation of SetRange() is inefficient because it doesn't take advantage of the fact that ImGuiStorage stores sorted key. - void SetRange(int a, int b, bool v) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) SetSelected(n, v); } + void SetRange(int a, int b, bool v) { for (int n = a; n <= b; n++) SetSelected(n, v); } void SelectAll(int count) { Storage.Data.resize(count); for (int idx = 0; idx < count; idx++) Storage.Data[idx] = ImGuiStoragePair((ImGuiID)idx, 1); SelectionSize = count; } // This could be using SetRange(), but it this way is faster. // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). Must be done in this order! Clear->SelectAll->SetRange. @@ -2802,7 +2802,7 @@ struct ExampleSelection { if (ms_io->RequestClear) { Clear(); } if (ms_io->RequestSelectAll) { SelectAll(items_count); } - if (ms_io->RequestSetRange) { SetRange((int)ms_io->RangeSrcItem, (int)ms_io->RangeDstItem, ms_io->RangeSelected ? 1 : 0); } + if (ms_io->RequestSetRange) { SetRange((int)ms_io->RangeFirstItem, (int)ms_io->RangeLastItem, ms_io->RangeSelected ? 1 : 0); } } void DebugTooltip() diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 94123de764d9..178f0991f993 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7129,7 +7129,7 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe ImGuiContext& g = *GImGui; if (data->RequestClear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestClear\n", function); if (data->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSelectAll\n", function); - if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d (dir %+d)\n", function, data->RangeSrcItem, data->RangeDstItem, data->RangeSrcItem, data->RangeDstItem, data->RangeSelected, data->RangeDirection); + if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, data->RangeFirstItem, data->RangeLastItem, data->RangeFirstItem, data->RangeLastItem, data->RangeSelected); } // Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). @@ -7159,7 +7159,6 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) storage->Window = window; ms->Storage = storage; - // FIXME-MULTISELECT: Index vs Pointers. // We want EndIO's NavIdItem/NavIdSelected to match BeginIO's one, so the value never changes after EndMultiSelect() ms->BeginIO.RangeSrcItem = ms->EndIO.RangeSrcItem = storage->RangeSrcItem; ms->BeginIO.NavIdItem = ms->EndIO.NavIdItem = storage->NavIdItem; @@ -7398,8 +7397,8 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) //---------------------------------------------------------------------------------------- ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + int range_direction; ms->EndIO.RequestSetRange = true; - ms->EndIO.RangeDstItem = item_data; if (is_shift && is_multiselect) { // Shift+Arrow always select @@ -7407,7 +7406,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); ms->EndIO.RangeSrcItem = (storage->RangeSrcItem != ImGuiSelectionUserData_Invalid) ? storage->RangeSrcItem : item_data; ms->EndIO.RangeSelected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; - ms->EndIO.RangeDirection = ms->BeginIO.RangeSrcPassedBy ? +1 : -1; + range_direction = ms->BeginIO.RangeSrcPassedBy ? +1 : -1; } else { @@ -7415,8 +7414,11 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) selected = is_ctrl ? !selected : true; ms->EndIO.RangeSrcItem = storage->RangeSrcItem = item_data; ms->EndIO.RangeSelected = selected; - ms->EndIO.RangeDirection = +1; + range_direction = +1; } + ImGuiSelectionUserData range_dst_item = item_data; + ms->EndIO.RangeFirstItem = (range_direction > 0) ? ms->EndIO.RangeSrcItem : range_dst_item; + ms->EndIO.RangeLastItem = (range_direction > 0) ? range_dst_item : ms->EndIO.RangeSrcItem; if (!is_multiselect) { From c3753809b1d44cef960843eabf70ceb2ea30229f Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 22 Aug 2023 17:46:19 +0200 Subject: [PATCH 048/132] MultiSelect: Demo: rework ExampleSelection names to map better to typical user code + variety of Comments tweaks. --- imgui.h | 47 +++++++++++-------- imgui_demo.cpp | 124 ++++++++++++++++++++++--------------------------- 2 files changed, 83 insertions(+), 88 deletions(-) diff --git a/imgui.h b/imgui.h index 9e5a9aef2869..730a53e2b18a 100644 --- a/imgui.h +++ b/imgui.h @@ -671,7 +671,8 @@ namespace ImGui // Multi-selection system for Selectable() and TreeNode() functions. // - 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. - // - ImGuiSelectionUserData is often used to store your item index. Read comments near ImGuiMultiSelectIO for details. + // - ImGuiSelectionUserData is often used to store your item index. + // - Read comments near ImGuiMultiSelectIO for instructions/details and see 'Demo->Widgets->Selection State' for demo. IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags); IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); @@ -2740,32 +2741,35 @@ enum ImGuiMultiSelectFlags_ // Multi-selection system // - Refer to 'Demo->Widgets->Selection State' for references using this. // - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) -// and supports a clipper being used. Handling this manually may be tricky, this is why we provide the functionality. -// If you don't need SHIFT+Mouse/Keyboard range-select + clipping, you can use a simpler form of multi-selection yourself, -// by reacting to click/presses on Selectable() items and checking keyboard modifiers. -// The complexity of this system is mostly caused by supporting SHIFT+Click/Arrow range-select with clipped elements. +// and supports a clipper being used. Handling this manually and correctly i tricky, this is why we provide +// the functionality. If you don't need SHIFT+Mouse/Keyboard range-select + clipping, you can implement a +// simple form of multi-selection yourself, by reacting to click/presses on Selectable() items. // - TreeNode() and Selectable() are supported but custom widgets may use it as well. // - In the spirit of Dear ImGui design, your code owns actual selection data. // This is designed to allow all kinds of selection storage you may use in your application: -// e.g. instructive selection (store a bool inside each object), external array (store an array in your view data, next -// to your objects), set/map/hash (store only selected items), or other structures (store indices in an interval tree), etc. -// - The work involved to deal with multi-selection differs whether you want to only submit visible items and clip others, -// or submit all items regardless of their visibility. Clipping items is more efficient and will allow you to deal with -// large lists (1k~100k items) with no performance penalty, but requires a little more work on the code. -// For small selection set (<100 items), you might want to not bother with using the clipper, as the cost you should -// be negligible (as least on Dear ImGui side). -// If you are not sure, always start without clipping and you can work your way to the optimized version afterwards. +// e.g. set/map/hash (store only selected items), instructive selection (store a bool inside each object), +// external array (store an array in your view data, next to your objects), or other structures (store indices +// in an interval tree), etc. +// - The work involved to deal with multi-selection differs whether you want to only submit visible items and +// clip others, or submit all items regardless of their visibility. Clipping items is more efficient and will +// allow you to deal with large lists (1k~100k items) with no performance penalty, but requires a little more +// work on the code. +// For small selection set (<100 items), you might want to not bother with using the clipper, as the cost +// should be negligible (as least on Dear ImGui side). +// If you are not sure, always start without clipping! You can work your way to the optimized version afterwards. // About ImGuiSelectionUserData: // - This is your application-defined identifier in a selection set: // - For each item is submitted by your calls to SetNextItemSelectionUserData(). // - In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO. // - Most applications will store an object INDEX, hence the chosen name and type. -// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points -// and you will need to interpolate between them to honor range selection. +// Storing an integer index is the easiest thing to do, as RequestSetRange requests will give you two end-points +// and you will need to iterate/interpolate between them to honor range selection. // - However it is perfectly possible to store a POINTER inside this value! The multi-selection system never assume -// that you identify items by indices, and never attempt to interpolate between two ImGuiSelectionUserData values. +// that you identify items by indices. It never attempt to iterate/interpolate between 2 ImGuiSelectionUserData values. // - As most users will want to cast this to integer, for convenience and to reduce confusion we use ImS64 instead // of void*, being syntactically easier to downcast. But feel free to reinterpret_cast a pointer into this. +// - You may store another type of (e.g. an data) but this may make your lookups and the interpolation between +// two values more cumbersome. // - If you need to wrap this API for another language/framework, feel free to expose this as 'int' if simpler. // Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. @@ -2777,12 +2781,15 @@ enum ImGuiMultiSelectFlags_ // - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. (optionally call IsItemToggledSelection() if you need that info immediately for displaying your item, before EndMultiSelect()) // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. // - (6) Honor Clear/SelectAll/SetRange requests by updating your selection data. 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. -// However it is perfectly fine to honor all steps even if you don't use a clipper. +// If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable()/TreeNode on a per-item basis. +// However it is perfectly fine to honor all steps even if you don't use a clipper. +// Advanced: +// - Deletion: If you need to handle items deletion a little more work if needed for post-deletion focus and scrolling to be correct. +// refer to 'Demo->Widgets->Selection State' for demos supporting deletion. struct ImGuiMultiSelectIO { - // - Always process requests in this order: Clear, SelectAll, SetRange. Use 'Debug Log->Selection' to see requests as they happen. - // - Some fields are only necessary if your list is dynamic and allows deletion (getting "post-deletion" state right is exhibited in the demo) + // - Always process requests in this order: Clear, SelectAll, SetRange. Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. + // - Some fields are only necessary if your list is dynamic and allows deletion (getting "post-deletion" state right is shown 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. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 7a1d74b96bda..3cd8bf6adc8e 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2766,54 +2766,45 @@ static void ShowDemoWindowWidgets() // To store a single-selection: // - You only need a single variable and don't need any of this! // To store a multi-selection, in your real application you could: -// - Use intrusively stored selection (e.g. 'bool IsSelected' inside your object). This is by far the simplest -// way to store your selection data, but it means you cannot have multiple simultaneous views over your objects. -// This is what many of the simpler demos in this file are using (so they are not using this class). // - Use external storage: e.g. unordered_set/set/hash/map/interval trees (storing indices, objects id, etc.) -// 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. +// are generally appropriate. Even a large array of bool might work for you... This is what we are doing here. +// - Use intrusively stored selection (e.g. 'bool IsSelected' inside your object). +// - This is simple, but it means you cannot have multiple simultaneous views over your objects. +// - This is what many of the simpler demos in other sections of this file are using (so they are not using this class). +// - Some of our features requires you to provide the selection size, which with this specific strategy require additional work: +// either you maintain it (which requires storage outside of objects) either you recompute (which may be costly for large sets). +// - So I would suggest that using intrusive selection for multi-select is not the most adequate. 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 Size; // 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? bool QueueDeletion; // Request deleting selected items // Functions ExampleSelection() { Clear(); } - void Clear() { Storage.Clear(); SelectionSize = 0; QueueDeletion = false; } - 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; } - int GetSize() const { return SelectionSize; } - - // When using SetRange() / SelectAll() we assume that our objects ID are indices. - // In this demo we always store selection using indices and never in another manner (e.g. object ID or pointers). - // If your selection system is storing selection using object ID and you want to support Shift+Click range-selection, - // you will need a way to iterate from one item to the other item given the ID you use. - // You are likely to need some kind of data structure to convert 'view index' <> 'object ID' (FIXME-MULTISELECT: Would be worth providing a demo of doing this). - // Note: This implementation of SetRange() is inefficient because it doesn't take advantage of the fact that ImGuiStorage stores sorted key. - void SetRange(int a, int b, bool v) { for (int n = a; n <= b; n++) SetSelected(n, v); } - void SelectAll(int count) { Storage.Data.resize(count); for (int idx = 0; idx < count; idx++) Storage.Data[idx] = ImGuiStoragePair((ImGuiID)idx, 1); SelectionSize = count; } // This could be using SetRange(), but it this way is faster. - - // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). Must be done in this order! Clear->SelectAll->SetRange. - // Enable 'Debug Log->Selection' to see selection requests as they happen. + void Clear() { Storage.Clear(); Size = 0; QueueDeletion = false; } + bool Contains(int n) const { return Storage.GetInt((ImGuiID)n, 0) != 0; } + void AddItem(int n) { int* p_int = Storage.GetIntRef((ImGuiID)n, 0); if (*p_int != 0) return; *p_int = 1; Size++; } + void RemoveItem(int n) { int* p_int = Storage.GetIntRef((ImGuiID)n, 0); if (*p_int == 0) return; *p_int = 0; Size--; } + void UpdateItem(int n, bool v) { if (v) AddItem(n); else RemoveItem(n); } + int GetSize() const { return Size; } + 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(); } } + + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). + // - Must be done in this order! Clear->SelectAll->SetRange. Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. + // - Honoring RequestSetRange requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. + // - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection. + // - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform + // a lookup and have some way to iterate between two values. + // - A full-featured application is likely to allow search/filtering which is likely to lead to using indices and + // constructing a view index <> object id/ptr data structure. (FIXME-MULTISELECT: Would be worth providing a demo of doing this). + // - (Our implementation is slightly inefficient because it doesn't take advantage of the fat that ImguiStorage stores sorted key) void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count) { if (ms_io->RequestClear) { Clear(); } - if (ms_io->RequestSelectAll) { SelectAll(items_count); } - if (ms_io->RequestSetRange) { SetRange((int)ms_io->RangeFirstItem, (int)ms_io->RangeLastItem, 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(); - } + if (ms_io->RequestSelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } + if (ms_io->RequestSetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } } // Call after BeginMultiSelect(). @@ -2827,20 +2818,20 @@ struct ExampleSelection QueueDeletion = false; // If current item is not selected. - if (ms_io->NavIdSelected == false) // Here 'NavIdSelected' should be == to 'GetSelected(ms_io->NavIdData)' + if (ms_io->NavIdSelected == false) // Here 'NavIdSelected' should be == to 'GetSelected(ms_io->NavIdData)' { - ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even without the !NavIdSelected test but it would take an extra frame to recover RangeSrc when deleting a selected item. - return (int)ms_io->NavIdItem; // Request to land on same item after deletion. + ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even without the !NavIdSelected test but it would take an extra frame to recover RangeSrc when deleting a selected item. + return (int)ms_io->NavIdItem; // Request to land on same item after deletion. } // If current item is selected: land on first unselected item after RangeSrc. for (int n = (int)ms_io->RangeSrcItem + 1; n < items.Size; n++) - if (!GetSelected(n)) + if (!Contains(n)) return n; // If current item is selected: otherwise return last unselected item. for (int n = IM_MIN((int)ms_io->RangeSrcItem, items.Size) - 1; n >= 0; n--) - if (!GetSelected(n)) + if (!Contains(n)) return n; return -1; @@ -2859,12 +2850,12 @@ struct ExampleSelection // This particular ExampleSelection case is designed to showcase maintaining selection-state separated from items-data. IM_UNUSED(ms_io); ImVector new_items; - new_items.reserve(items.Size - SelectionSize); + new_items.reserve(items.Size - Size); int next_focus_idx_in_old_selection = (int)ms_io->RequestFocusItem; int next_focus_idx_in_new_selection = -1; for (int n = 0; n < items.Size; n++) { - if (!GetSelected(n)) + if (!Contains(n)) new_items.push_back(items[n]); if (next_focus_idx_in_old_selection == n) next_focus_idx_in_new_selection = new_items.Size - 1; @@ -2874,20 +2865,19 @@ struct ExampleSelection // Update selection Clear(); if (next_focus_idx_in_new_selection != -1 && ms_io->NavIdSelected) - SetSelected(next_focus_idx_in_new_selection, true); + AddItem(next_focus_idx_in_new_selection); } }; static void ShowDemoWindowMultiSelect() { - IMGUI_DEMO_MARKER("Widgets/Selection State"); - //ImGui::SetNextItemOpen(true, ImGuiCond_Once); - if (ImGui::TreeNode("Selection State")) + IMGUI_DEMO_MARKER("Widgets/Selection State & Multi-Select"); + if (ImGui::TreeNode("Selection State & Multi-Select")) { HelpMarker("Selections can be built under Selectable(), TreeNode() or other widgets. Selection state is owned by application code/data."); - IMGUI_DEMO_MARKER("Widgets/Selection State/Single Selection"); - if (ImGui::TreeNode("Single Selection")) + IMGUI_DEMO_MARKER("Widgets/Selection State/Single-Select"); + if (ImGui::TreeNode("Single-Select")) { static int selected = -1; for (int n = 0; n < 5; n++) @@ -2901,8 +2891,9 @@ static void ShowDemoWindowMultiSelect() } // Demonstrate implementation a most-basic form of multi-selection manually - IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (simplfied, manual)"); - if (ImGui::TreeNode("Multiple Selection (simplified, manual)")) + // This doesn't support the SHIFT modifier which requires BeginMultiSelect()! + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (manual/simplified, without BeginMultiSelect)"); + if (ImGui::TreeNode("Multi-Select (manual/simplified, without BeginMultiSelect)")) { HelpMarker("Hold CTRL and click to select multiple items."); static bool selection[5] = { false, false, false, false, false }; @@ -2929,9 +2920,8 @@ static void ShowDemoWindowMultiSelect() // Demonstrate holding/updating multi-selection data using the BeginMultiSelect/EndMultiSelect API. // SHIFT+Click w/ CTRL and other standard features are supported. - IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full)"); - //ImGui::SetNextItemOpen(true, ImGuiCond_Once); - if (ImGui::TreeNode("Multiple Selection (full)")) + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select"); + if (ImGui::TreeNode("Multi-Select")) { static ExampleSelection selection; @@ -2956,7 +2946,7 @@ static void ShowDemoWindowMultiSelect() { char label[64]; sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); - bool item_is_selected = selection.GetSelected(n); + bool item_is_selected = selection.Contains(n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); } @@ -2978,8 +2968,8 @@ static void ShowDemoWindowMultiSelect() // - (3) BeginXXXX process // - (4) Focus process // - (5) EndXXXX process - IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full, with deletion)"); - if (ImGui::TreeNode("Multiple Selection (full, with deletion)")) + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (with deletion)"); + if (ImGui::TreeNode("Multi-Select (with deletion)")) { // Intentionally separating items data from selection data! // But you may decide to store selection data inside your item (aka intrusive storage). @@ -2999,7 +2989,7 @@ static void ShowDemoWindowMultiSelect() items.push_back(items_next_id++); if (ImGui::SmallButton("Add 20 items")) { for (int n = 0; n < 20; n++) { items.push_back(items_next_id++); } } ImGui::SameLine(); - if (ImGui::SmallButton("Remove 20 items")) { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.SetSelected(items.Size - 1, false); items.pop_back(); } } // This is to test + if (ImGui::SmallButton("Remove 20 items")) { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.RemoveItem(items.Size - 1); items.pop_back(); } } // This is to test // (1) Extra to support deletion: Submit scrolling range to avoid glitches on deletion const float items_height = ImGui::GetTextLineHeightWithSpacing(); @@ -3025,11 +3015,9 @@ static void ShowDemoWindowMultiSelect() char label[64]; sprintf(label, "Object %05d: %s", item_id, random_names[item_id % IM_ARRAYSIZE(random_names)]); - bool item_is_selected = selection.GetSelected(n); + bool item_is_selected = selection.Contains(n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); - if (ImGui::IsItemToggledSelection()) - selection.SetSelected(n, !item_is_selected); if (next_focus_item_idx == n) ImGui::SetKeyboardFocusHere(-1); } @@ -3046,8 +3034,8 @@ static void ShowDemoWindowMultiSelect() } // Demonstrate individual selection scopes in same window - IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full, multiple scopes)"); - if (ImGui::TreeNode("Multiple Selection (full, multiple scopes)")) + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (multiple scopes)"); + if (ImGui::TreeNode("Multi-Select (multiple scopes)")) { const int SCOPES_COUNT = 3; const int ITEMS_COUNT = 8; // Per scope @@ -3068,7 +3056,7 @@ static void ShowDemoWindowMultiSelect() { char label[64]; sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); - bool item_is_selected = selection->GetSelected(n); + bool item_is_selected = selection->Contains(n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); } @@ -3087,9 +3075,9 @@ static void ShowDemoWindowMultiSelect() // - Showcase basic drag and drop. // - Showcase TreeNode variant (note that tree node don't expand in the demo: supporting expanding tree nodes + clipping a separate thing). // - Showcase using inside a table. - IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (full, advanced)"); + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (advanced)"); //ImGui::SetNextItemOpen(true, ImGuiCond_Once); - if (ImGui::TreeNode("Multiple Selection (full, advanced)")) + if (ImGui::TreeNode("Multi-Select (advanced)")) { // Options enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; @@ -3137,7 +3125,7 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); if (want_delete) - selection.ApplyDeletionPreLoop(ms_io, items); + ms_io->RequestFocusItem = selection.ApplyDeletionPreLoop(ms_io, items); const int next_focus_item_idx = (int)ms_io->RequestFocusItem; if (show_in_table) @@ -3192,7 +3180,7 @@ static void ShowDemoWindowMultiSelect() ImGui::SameLine(); } - bool item_is_selected = selection.GetSelected(n); + bool item_is_selected = selection.Contains(n); ImGui::SetNextItemSelectionUserData(n); if (widget_type == WidgetType_Selectable) { From 6ddc5f38afa990875a38b9e8b644f4770173da7e Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 23 Aug 2023 19:47:24 +0200 Subject: [PATCH 049/132] MultiSelect: Demo: added simpler demo using Clipper. Clarify RangeSrcPassedBy doc. --- imgui.h | 12 +++++++----- imgui_demo.cpp | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/imgui.h b/imgui.h index 730a53e2b18a..ea47d0fd8bb1 100644 --- a/imgui.h +++ b/imgui.h @@ -2774,11 +2774,13 @@ enum ImGuiMultiSelectFlags_ // Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. // - (2) [If using clipper] Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 6. -// LOOP - (3) [If using clipper] Set RangeSrcPassedBy=true if the RangeSrcItem item is part of the items clipped before the first submitted/visible item. +// LOOP - (3) [If using clipper] Set RangeSrcPassedBy=true if the RangeSrcItem item was already passed by. // 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 in ImGuiSelectionUserData, this is easy to compute: if (clipper.DisplayStart > data->RangeSrcItem) { data->RangeSrcPassedBy = true; } -// - If you are using pointers in ImGuiSelectionUserData, you may need additional processing in each clipper step to tell if current DisplayStart comes after RangeSrcItem.. -// - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. (optionally call IsItemToggledSelection() if you need that info immediately for displaying your item, before EndMultiSelect()) +// - If you are using integer indices in ImGuiSelectionUserData, this is easy to compute: +// if (clipper.DisplayStart > data->RangeSrcItem) { data->RangeSrcPassedBy = true; } +// - If you are using pointers in ImGuiSelectionUserData, you may need additional processing, e.g. find the index of RangeSrcItem before applying the above operation. +// - This also needs to be done at the end of the clipper loop, otherwise we can't tell if the item still exist. +// - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. // - (6) Honor Clear/SelectAll/SetRange requests by updating your selection data. 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. @@ -2801,7 +2803,7 @@ struct ImGuiMultiSelectIO ImGuiSelectionUserData RangeFirstItem; // / / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) ImGuiSelectionUserData RangeLastItem; // / / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) bool RangeSelected; // / / ms:w, app:r // End: parameter for 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 RangeSrcPassedBy; // / ms:rw app:w / ms:r // (If using clipper) Need to be set by app/user if RangeSrcItem was passed by. Ignore if not clipping. bool RangeSrcReset; // app:w / app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). ImGuiSelectionUserData NavIdItem; // ms:w, app:r / / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 3cd8bf6adc8e..3cf95e64635b 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2911,7 +2911,7 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } - const char* random_names[] = + static const char* random_names[] = { "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", "Bitter Gourd", "Bok Choy", "Broccoli", "Brussels Sprouts", "Burdock Root", "Cabbage", "Calabash", "Capers", "Carrot", "Cassava", @@ -2935,7 +2935,7 @@ 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("Selection: %d/%d", selection.GetSize(), ITEMS_COUNT); if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; @@ -2951,7 +2951,49 @@ static void ShowDemoWindowMultiSelect() ImGui::Selectable(label, item_is_selected); } - // Apply multi-select requests + ms_io = ImGui::EndMultiSelect(); + selection.ApplyRequests(ms_io, ITEMS_COUNT); + + ImGui::EndListBox(); + } + ImGui::TreePop(); + } + + // Demonstrate using the clipper with BeginMultiSelect()/EndMultiSelect() + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (with clipper)"); + if (ImGui::TreeNode("Multi-Select (with clipper)")) + { + static ExampleSelection selection; + + ImGui::Text("Added features:"); + ImGui::BulletText("Using ImGuiListClipper."); + + const int ITEMS_COUNT = 10000; + ImGui::Text("Selection: %d/%d", selection.GetSize(), ITEMS_COUNT); + if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) + { + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + selection.ApplyRequests(ms_io, ITEMS_COUNT); + + ImGuiListClipper clipper; + clipper.Begin(ITEMS_COUNT); + while (clipper.Step()) + { + if (!ms_io->RangeSrcPassedBy && clipper.DisplayStart > ms_io->RangeSrcItem) + ms_io->RangeSrcPassedBy = true; + for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) + { + char label[64]; + sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); + bool item_is_selected = selection.Contains(n); + ImGui::SetNextItemSelectionUserData(n); + ImGui::Selectable(label, item_is_selected); + } + } + if (!ms_io->RangeSrcPassedBy && ITEMS_COUNT > ms_io->RangeSrcItem) + ms_io->RangeSrcPassedBy = true; + ms_io = ImGui::EndMultiSelect(); selection.ApplyRequests(ms_io, ITEMS_COUNT); From 8fe6b3195282b6247f53f75c0cfb5907448b05ec Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 23 Aug 2023 20:45:02 +0200 Subject: [PATCH 050/132] MultiSelect: (Breaking) Removed RangeSrcPassedBy in favor of favoring user to call IncludeByIndex(RangeSrcItem) which is easier/simpler to honor. Especially as recent changes made it required to also update RangeSrcPassedBy after last clipper Step. Should now be simpler. --- imgui.h | 14 ++++---------- imgui_demo.cpp | 21 +++++---------------- imgui_internal.h | 3 ++- imgui_widgets.cpp | 10 +++++----- 4 files changed, 16 insertions(+), 32 deletions(-) diff --git a/imgui.h b/imgui.h index ea47d0fd8bb1..efc7c68883e5 100644 --- a/imgui.h +++ b/imgui.h @@ -2774,13 +2774,8 @@ enum ImGuiMultiSelectFlags_ // Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. // - (2) [If using clipper] Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 6. -// LOOP - (3) [If using clipper] Set RangeSrcPassedBy=true if the RangeSrcItem item was already passed by. -// 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 in ImGuiSelectionUserData, this is easy to compute: -// if (clipper.DisplayStart > data->RangeSrcItem) { data->RangeSrcPassedBy = true; } -// - If you are using pointers in ImGuiSelectionUserData, you may need additional processing, e.g. find the index of RangeSrcItem before applying the above operation. -// - This also needs to be done at the end of the clipper loop, otherwise we can't tell if the item still exist. -// - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. +// - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeIndex(). If already using indices in ImGuiSelectionUserData, it is as simple as clipper.IncludeIndex((int)ms_io->RangeSrcItem); +// LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. // - (6) Honor Clear/SelectAll/SetRange requests by updating your selection data. 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. @@ -2799,12 +2794,11 @@ struct ImGuiMultiSelectIO bool RequestSetRange; // / / ms:w, app:r // 3. Request app/user to select/unselect [RangeFirstItem..RangeLastItem] items, based on RangeSelected. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. ImGuiSelectionUserData RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implementing a deletion idiom (see demo). // STATE/ARGUMENTS -------------------------// BEGIN / LOOP / END - ImGuiSelectionUserData RangeSrcItem; // ms:w / app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point). Read during loop in order for user code to set RangeSrcPassedBy. + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be cliped! ImGuiSelectionUserData RangeFirstItem; // / / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) ImGuiSelectionUserData RangeLastItem; // / / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) bool RangeSelected; // / / ms:w, app:r // End: parameter for 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 passed by. Ignore if not clipping. - bool RangeSrcReset; // app:w / app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). + bool RangeSrcReset; // app:w / / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). ImGuiSelectionUserData NavIdItem; // ms:w, app:r / / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 3cf95e64635b..5b889a271426 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2978,10 +2978,10 @@ static void ShowDemoWindowMultiSelect() ImGuiListClipper clipper; clipper.Begin(ITEMS_COUNT); + if (ms_io->RangeSrcItem > 0) + clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. while (clipper.Step()) { - if (!ms_io->RangeSrcPassedBy && clipper.DisplayStart > ms_io->RangeSrcItem) - ms_io->RangeSrcPassedBy = true; for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) { char label[64]; @@ -2991,8 +2991,6 @@ static void ShowDemoWindowMultiSelect() ImGui::Selectable(label, item_is_selected); } } - if (!ms_io->RangeSrcPassedBy && ITEMS_COUNT > ms_io->RangeSrcItem) - ms_io->RangeSrcPassedBy = true; ms_io = ImGui::EndMultiSelect(); selection.ApplyRequests(ms_io, ITEMS_COUNT); @@ -3185,16 +3183,13 @@ static void ShowDemoWindowMultiSelect() { clipper.Begin(items.Size); if (next_focus_item_idx != -1) - clipper.IncludeItemByIndex(next_focus_item_idx); // Ensure item to focus is not clipped + clipper.IncludeItemByIndex(next_focus_item_idx); // Ensure focused item is not clipped + if (ms_io->RangeSrcItem > 0) + clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. } while (!use_clipper || clipper.Step()) { - // 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 && !ms_io->RangeSrcPassedBy && clipper.DisplayStart > 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; for (int n = item_begin; n < item_end; n++) @@ -3281,12 +3276,6 @@ 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 && !ms_io->RangeSrcPassedBy && items.Size > ms_io->RangeSrcItem) - ms_io->RangeSrcPassedBy = true; - if (show_in_table) { ImGui::EndTable(); diff --git a/imgui_internal.h b/imgui_internal.h index 3bf729e3b81e..2e2999a924e0 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1725,7 +1725,8 @@ struct IMGUI_API ImGuiMultiSelectTempData 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. + bool RangeSrcPassedBy; // Set by the item that matches RangeSrcItem. + 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. ImGuiMultiSelectTempData() { Clear(); } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 178f0991f993..1930fd790b6c 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7213,7 +7213,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (ms->IsFocused) { - if (ms->BeginIO.RangeSrcReset || (ms->BeginIO.RangeSrcPassedBy == false && ms->BeginIO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) + if (ms->BeginIO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->BeginIO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) { IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId. ms->Storage->RangeSrcItem = ImGuiSelectionUserData_Invalid; @@ -7262,7 +7262,7 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d // 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 == selection_user_data) - ms->BeginIO.RangeSrcPassedBy = true; + ms->RangeSrcPassedBy = true; } else { @@ -7291,7 +7291,7 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) selected = true; // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) - // For this to work, IF the user is clipping items, they need to set RangeSrcPassedBy = true to notify the system. + // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function) if (ms->IsSetRange) { IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0); @@ -7306,7 +7306,7 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) } } const bool is_range_src = storage->RangeSrcItem == item_data; - if (is_range_src || is_range_dst || ms->BeginIO.RangeSrcPassedBy != ms->RangeDstPassedBy) + if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy) { IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1); selected = (storage->RangeSelected != 0); @@ -7406,7 +7406,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); ms->EndIO.RangeSrcItem = (storage->RangeSrcItem != ImGuiSelectionUserData_Invalid) ? storage->RangeSrcItem : item_data; ms->EndIO.RangeSelected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; - range_direction = ms->BeginIO.RangeSrcPassedBy ? +1 : -1; + range_direction = ms->RangeSrcPassedBy ? +1 : -1; } else { From 8c1f659b3dc709ba1838f3aed99e5db27c001bbb Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 23 Aug 2023 15:53:20 +0200 Subject: [PATCH 051/132] MultiSelect: Demo: rework ExampleSelection with an ExampleSelectionAdapter layer, allowing to share more code accross examples using different storage systems. Not ideal way to showcase this demo but this is really more flexible. --- imgui_demo.cpp | 101 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 27 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 5b889a271426..f7caa12efb35 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2761,19 +2761,41 @@ static void ShowDemoWindowWidgets() } } -// [Advanced] Helper class to simulate storage of a multi-selection state, used by the BeginMultiSelect() demos. -// We use ImGuiStorage (simple key->value storage) to avoid external dependencies but it's probably not optimal. +// We use a little extra indirection layer here in order to use and demonstrate different ways to +// - identify items in the multi-selection system (using index? using identifiers? using pointers?) +// - store our persistent selection data (using index? using identifiers? using pointers?) +// Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. +// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ExampleSelectionAdapter' INDIRECTION LOGIC. +// In theory we could add a IndexToUserData() function which would be used when calling SetNextItemSelectionUserData(), but omitting it makes things clearer. +struct ExampleSelectionAdapter +{ + void* Data = NULL; + int (*UserDataToIndex)(ExampleSelectionAdapter* self, ImGuiSelectionUserData item_data); // Function to convert item ImGuiSelectionUserData to item index + ImGuiID (*IndexToStorage)(ExampleSelectionAdapter* self, int idx); // Function to convert item index to data stored in persistent selection + + ExampleSelectionAdapter() { SetupForDirectIndexes(); } + + // Example for the simplest case: UserData==Index==SelectionStorage (this adapter doesn't even need to use the item data field) + void SetupForDirectIndexes() + { + UserDataToIndex = [](ExampleSelectionAdapter*, ImGuiSelectionUserData item_data) { return (int)item_data; }; // No transform: Pass indices to SetNextItemSelectionUserData() + IndexToStorage = [](ExampleSelectionAdapter*, int idx) { return (ImGuiID)idx; }; // No transform: Store indices inside persistent selection storage + } +}; + +// [Advanced] Helper class to store multi-selection state, used by the BeginMultiSelect() demos. +// Provide an abstraction layer for the purpose of the demo showcasing different forms of underlying selection data. // To store a single-selection: // - You only need a single variable and don't need any of this! // To store a multi-selection, in your real application you could: -// - Use external storage: e.g. unordered_set/set/hash/map/interval trees (storing indices, objects id, etc.) -// are generally appropriate. Even a large array of bool might work for you... This is what we are doing here. -// - Use intrusively stored selection (e.g. 'bool IsSelected' inside your object). -// - This is simple, but it means you cannot have multiple simultaneous views over your objects. -// - This is what many of the simpler demos in other sections of this file are using (so they are not using this class). -// - Some of our features requires you to provide the selection size, which with this specific strategy require additional work: +// - A) Use external storage: e.g. std::set, std::set, std::vector, interval trees, etc. +// are generally appropriate. Even a large array of bool might work for you... +// This code here use ImGuiStorage (a simple key->value storage) as a std::set replacement to avoid external dependencies. +// - B) Or use intrusively stored selection (e.g. 'bool IsSelected' inside your object). +// - It is simple, but it means you cannot have multiple simultaneous views over your objects. +// - Some of our features requires you to provide the selection _size_, which with this specific strategy require additional work: // either you maintain it (which requires storage outside of objects) either you recompute (which may be costly for large sets). -// - So I would suggest that using intrusive selection for multi-select is not the most adequate. +// - So we suggest using intrusive selection for multi-select is not really adequate. struct ExampleSelection { // Data @@ -2784,6 +2806,7 @@ struct ExampleSelection // Functions ExampleSelection() { Clear(); } void Clear() { Storage.Clear(); Size = 0; QueueDeletion = false; } + void Swap(ExampleSelection& rhs) { Storage.Data.swap(rhs.Storage.Data); } bool Contains(int n) const { return Storage.GetInt((ImGuiID)n, 0) != 0; } void AddItem(int n) { int* p_int = Storage.GetIntRef((ImGuiID)n, 0); if (*p_int != 0) return; *p_int = 1; Size++; } void RemoveItem(int n) { int* p_int = Storage.GetIntRef((ImGuiID)n, 0); if (*p_int == 0) return; *p_int = 0; Size--; } @@ -2796,15 +2819,33 @@ struct ExampleSelection // - Honoring RequestSetRange requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. // - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection. // - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform - // a lookup and have some way to iterate between two values. - // - A full-featured application is likely to allow search/filtering which is likely to lead to using indices and - // constructing a view index <> object id/ptr data structure. (FIXME-MULTISELECT: Would be worth providing a demo of doing this). - // - (Our implementation is slightly inefficient because it doesn't take advantage of the fat that ImguiStorage stores sorted key) - void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count) + // a lookup in order to have some way to iterate/interpolate between two items. + // - A full-featured application is likely to allow search/filtering which is likely to lead to using indices + // and constructing a view index <> object id/ptr data structure anyway. + // WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ExampleSelectionAdapter' INDIRECTION LOGIC. + // Notice that with the simplest adapter (using indices everywhere), all functions return their parameters. + // The most simple implementation (using indices everywhere) would look like: + // if (ms_io->RequestClear) { Clear(); } + // if (ms_io->RequestSelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } + // if (ms_io->RequestSetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } + void ApplyRequests(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, int items_count) { - if (ms_io->RequestClear) { Clear(); } - if (ms_io->RequestSelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } - if (ms_io->RequestSetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } + IM_ASSERT(adapter->UserDataToIndex != NULL && adapter->IndexToStorage != NULL); + + if (ms_io->RequestClear || ms_io->RequestSelectAll) + Clear(); + + if (ms_io->RequestSelectAll) + for (int idx = 0; idx < items_count; idx++) + AddItem(adapter->IndexToStorage(adapter, idx)); + + if (ms_io->RequestSetRange) + { + int first_item_idx = adapter->UserDataToIndex(adapter, ms_io->RangeFirstItem); + int last_item_idx = adapter->UserDataToIndex(adapter, ms_io->RangeLastItem); + for (int idx = first_item_idx; idx <= last_item_idx; idx++) + UpdateItem(adapter->IndexToStorage(adapter, idx), ms_io->RangeSelected); + } } // Call after BeginMultiSelect(). @@ -2924,6 +2965,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::TreeNode("Multi-Select")) { static ExampleSelection selection; + ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection ImGui::Text("Tips: Use 'Debug Log->Selection' to see selection requests as they happen."); @@ -2940,7 +2982,7 @@ static void ShowDemoWindowMultiSelect() { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - selection.ApplyRequests(ms_io, ITEMS_COUNT); + selection.ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); for (int n = 0; n < ITEMS_COUNT; n++) { @@ -2952,7 +2994,7 @@ static void ShowDemoWindowMultiSelect() } ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, ITEMS_COUNT); + selection.ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); ImGui::EndListBox(); } @@ -2964,6 +3006,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::TreeNode("Multi-Select (with clipper)")) { static ExampleSelection selection; + ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection ImGui::Text("Added features:"); ImGui::BulletText("Using ImGuiListClipper."); @@ -2974,7 +3017,7 @@ static void ShowDemoWindowMultiSelect() { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - selection.ApplyRequests(ms_io, ITEMS_COUNT); + selection.ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); ImGuiListClipper clipper; clipper.Begin(ITEMS_COUNT); @@ -2993,7 +3036,7 @@ static void ShowDemoWindowMultiSelect() } ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, ITEMS_COUNT); + selection.ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); ImGui::EndListBox(); } @@ -3015,6 +3058,8 @@ static void ShowDemoWindowMultiSelect() // But you may decide to store selection data inside your item (aka intrusive storage). static ImVector items; static ExampleSelection selection; + ExampleSelectionAdapter selection_adapter; + selection_adapter.SetupForDirectIndexes(); // Pass index to SetNextItemSelectionUserData(), store index in Selection ImGui::Text("Adding features:"); ImGui::BulletText("Dynamic list with Delete key support."); @@ -3039,7 +3084,7 @@ static void ShowDemoWindowMultiSelect() { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - selection.ApplyRequests(ms_io, items.Size); + selection.ApplyRequests(ms_io, &selection_adapter, items.Size); // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. @@ -3064,7 +3109,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, items.Size); + selection.ApplyRequests(ms_io, &selection_adapter, items.Size); if (want_delete) selection.ApplyDeletionPostLoop(ms_io, items); @@ -3080,6 +3125,7 @@ static void ShowDemoWindowMultiSelect() const int SCOPES_COUNT = 3; const int ITEMS_COUNT = 8; // Per scope static ExampleSelection selections_data[SCOPES_COUNT]; + ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection for (int selection_scope_n = 0; selection_scope_n < SCOPES_COUNT; selection_scope_n++) { @@ -3090,7 +3136,7 @@ static void ShowDemoWindowMultiSelect() ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; // | ImGuiMultiSelectFlags_ClearOnClickRectVoid ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - selection->ApplyRequests(ms_io, ITEMS_COUNT); + selection->ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); for (int n = 0; n < ITEMS_COUNT; n++) { @@ -3103,7 +3149,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection->ApplyRequests(ms_io, ITEMS_COUNT); + selection->ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); ImGui::PopID(); } ImGui::TreePop(); @@ -3147,6 +3193,7 @@ static void ShowDemoWindowMultiSelect() static int items_next_id = 0; if (items_next_id == 0) { for (int n = 0; n < 1000; n++) { items.push_back(items_next_id++); } } static ExampleSelection selection; + ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection ImGui::Text("Selection size: %d/%d", selection.GetSize(), items.Size); @@ -3159,7 +3206,7 @@ static void ShowDemoWindowMultiSelect() ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - selection.ApplyRequests(ms_io, items.Size); + selection.ApplyRequests(ms_io, &selection_adapter, items.Size); // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. @@ -3285,7 +3332,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, items.Size); + selection.ApplyRequests(ms_io, &selection_adapter, items.Size); if (want_delete) selection.ApplyDeletionPostLoop(ms_io, items); From 530155d85aaf68f06b97dce5ae5b655e06bc970c Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 1 Sep 2023 16:09:41 +0200 Subject: [PATCH 052/132] MultiSelect: Demo: Remove UserDataToIndex from ExampleSelectionAdapter. Seems to make a better demo this way. --- imgui_demo.cpp | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index f7caa12efb35..90cb7e993ac4 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2761,26 +2761,27 @@ static void ShowDemoWindowWidgets() } } -// We use a little extra indirection layer here in order to use and demonstrate different ways to -// - identify items in the multi-selection system (using index? using identifiers? using pointers?) -// - store our persistent selection data (using index? using identifiers? using pointers?) +// Our multi-selection system doesn't make assumption about: +// - how you want to identify items in multi-selection API? Indices(*) / Custom Identifiers / Pointers ? +// - how you want to store persistent selection data? Indices / Custom Identifiers(*) / Pointers ? +// (*) This is the suggested solution: pass indices to API (because easy to iterate/interpolate) + persist your custom identifiers inside selection data. +// In this demo we: +// - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) +// - use a little extra indirection layer in order to abstract how persistent selection data is derived from an index. +// - in some cases we use Index as custom identifier, in some cases we read from some custom item data structure. // Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. // WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ExampleSelectionAdapter' INDIRECTION LOGIC. -// In theory we could add a IndexToUserData() function which would be used when calling SetNextItemSelectionUserData(), but omitting it makes things clearer. +// In theory, for maximum abstraction, this class could contains IndexToUserData() and UserDataToIndex() functions, +// but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that for clarify. struct ExampleSelectionAdapter { void* Data = NULL; - int (*UserDataToIndex)(ExampleSelectionAdapter* self, ImGuiSelectionUserData item_data); // Function to convert item ImGuiSelectionUserData to item index - ImGuiID (*IndexToStorage)(ExampleSelectionAdapter* self, int idx); // Function to convert item index to data stored in persistent selection + ImGuiID (*IndexToStorage)(ExampleSelectionAdapter* self, int idx); // Function to convert item index to data stored in persistent selection - ExampleSelectionAdapter() { SetupForDirectIndexes(); } + ExampleSelectionAdapter() { SetupForDirectIndexes(); } - // Example for the simplest case: UserData==Index==SelectionStorage (this adapter doesn't even need to use the item data field) - void SetupForDirectIndexes() - { - UserDataToIndex = [](ExampleSelectionAdapter*, ImGuiSelectionUserData item_data) { return (int)item_data; }; // No transform: Pass indices to SetNextItemSelectionUserData() - IndexToStorage = [](ExampleSelectionAdapter*, int idx) { return (ImGuiID)idx; }; // No transform: Store indices inside persistent selection storage - } + // Example for the simplest case: Index==SelectionStorage (this adapter doesn't even need to use the item data field) + void SetupForDirectIndexes() { IndexToStorage = [](ExampleSelectionAdapter*, int idx) { return (ImGuiID)idx; }; } }; // [Advanced] Helper class to store multi-selection state, used by the BeginMultiSelect() demos. @@ -2830,7 +2831,7 @@ struct ExampleSelection // if (ms_io->RequestSetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } void ApplyRequests(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, int items_count) { - IM_ASSERT(adapter->UserDataToIndex != NULL && adapter->IndexToStorage != NULL); + IM_ASSERT(adapter->IndexToStorage != NULL); if (ms_io->RequestClear || ms_io->RequestSelectAll) Clear(); @@ -2840,12 +2841,8 @@ struct ExampleSelection AddItem(adapter->IndexToStorage(adapter, idx)); if (ms_io->RequestSetRange) - { - int first_item_idx = adapter->UserDataToIndex(adapter, ms_io->RangeFirstItem); - int last_item_idx = adapter->UserDataToIndex(adapter, ms_io->RangeLastItem); - for (int idx = first_item_idx; idx <= last_item_idx; idx++) + for (int idx = (int)ms_io->RangeFirstItem; idx <= (int)ms_io->RangeLastItem; idx++) UpdateItem(adapter->IndexToStorage(adapter, idx), ms_io->RangeSelected); - } } // Call after BeginMultiSelect(). From fa516c3d76503836472a9f3902ffa7f4ef0b409f Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 28 Aug 2023 17:36:59 +0200 Subject: [PATCH 053/132] MultiSelect: Demo: Make ExampleSelection use ImGuiID. More self-explanatory. --- imgui_demo.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 90cb7e993ac4..2351a177eeb8 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2801,17 +2801,17 @@ struct ExampleSelection { // Data ImGuiStorage Storage; // Selection set - int Size; // 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 Size; // Number of selected items (== number of 1 in the Storage, maintained by this class). bool QueueDeletion; // Request deleting selected items // Functions ExampleSelection() { Clear(); } void Clear() { Storage.Clear(); Size = 0; QueueDeletion = false; } void Swap(ExampleSelection& rhs) { Storage.Data.swap(rhs.Storage.Data); } - bool Contains(int n) const { return Storage.GetInt((ImGuiID)n, 0) != 0; } - void AddItem(int n) { int* p_int = Storage.GetIntRef((ImGuiID)n, 0); if (*p_int != 0) return; *p_int = 1; Size++; } - void RemoveItem(int n) { int* p_int = Storage.GetIntRef((ImGuiID)n, 0); if (*p_int == 0) return; *p_int = 0; Size--; } - void UpdateItem(int n, bool v) { if (v) AddItem(n); else RemoveItem(n); } + bool Contains(ImGuiID key) const { return Storage.GetInt(key, 0) != 0; } + void AddItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int != 0) return; *p_int = 1; Size++; } + void RemoveItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int == 0) return; *p_int = 0; Size--; } + void UpdateItem(ImGuiID key, bool v){ if (v) AddItem(key); else RemoveItem(key); } int GetSize() const { return Size; } 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(); } } @@ -2985,7 +2985,7 @@ static void ShowDemoWindowMultiSelect() { char label[64]; sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); - bool item_is_selected = selection.Contains(n); + bool item_is_selected = selection.Contains((ImGuiID)n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); } @@ -3026,7 +3026,7 @@ static void ShowDemoWindowMultiSelect() { char label[64]; sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); - bool item_is_selected = selection.Contains(n); + bool item_is_selected = selection.Contains((ImGuiID)n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); } @@ -3071,7 +3071,7 @@ static void ShowDemoWindowMultiSelect() items.push_back(items_next_id++); if (ImGui::SmallButton("Add 20 items")) { for (int n = 0; n < 20; n++) { items.push_back(items_next_id++); } } ImGui::SameLine(); - if (ImGui::SmallButton("Remove 20 items")) { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.RemoveItem(items.Size - 1); items.pop_back(); } } // This is to test + if (ImGui::SmallButton("Remove 20 items")) { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.RemoveItem((ImGuiID)(items.Size - 1)); items.pop_back(); } } // This is to test // (1) Extra to support deletion: Submit scrolling range to avoid glitches on deletion const float items_height = ImGui::GetTextLineHeightWithSpacing(); @@ -3097,7 +3097,7 @@ static void ShowDemoWindowMultiSelect() char label[64]; sprintf(label, "Object %05d: %s", item_id, random_names[item_id % IM_ARRAYSIZE(random_names)]); - bool item_is_selected = selection.Contains(n); + bool item_is_selected = selection.Contains((ImGuiID)n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); if (next_focus_item_idx == n) @@ -3139,7 +3139,7 @@ static void ShowDemoWindowMultiSelect() { char label[64]; sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); - bool item_is_selected = selection->Contains(n); + bool item_is_selected = selection->Contains((ImGuiID)n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); } @@ -3261,7 +3261,7 @@ static void ShowDemoWindowMultiSelect() ImGui::SameLine(); } - bool item_is_selected = selection.Contains(n); + bool item_is_selected = selection.Contains((ImGuiID)n); ImGui::SetNextItemSelectionUserData(n); if (widget_type == WidgetType_Selectable) { From e1d21092087b9aba367cb29bdd59202a1618bc2c Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 23 Aug 2023 16:48:04 +0200 Subject: [PATCH 054/132] MultiSelect: Demo: Deletion: Rework ApplyDeletionPreLoop to use adapter + fix PostLoop not using right value of RequestFocusItem. Recovery made it transparent visually but user side selection would be empty for a frame before recovery. --- imgui.h | 3 +-- imgui_demo.cpp | 64 +++++++++++++++++++++++--------------------------- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/imgui.h b/imgui.h index efc7c68883e5..5add92931fc4 100644 --- a/imgui.h +++ b/imgui.h @@ -2792,7 +2792,6 @@ struct ImGuiMultiSelectIO 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 [RangeFirstItem..RangeLastItem] items, based on RangeSelected. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. - ImGuiSelectionUserData RequestFocusItem; // app:w / app:r / app:r // (If using deletion) 4. Request user to focus item. This is actually only manipulated in user-space, but we provide storage to facilitate implementing a deletion idiom (see demo). // STATE/ARGUMENTS -------------------------// BEGIN / LOOP / END ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be cliped! ImGuiSelectionUserData RangeFirstItem; // / / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) @@ -2803,7 +2802,7 @@ struct ImGuiMultiSelectIO ImGuiSelectionUserData NavIdItem; // 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)); RequestFocusItem = NavIdItem = RangeSrcItem = RangeFirstItem = RangeLastItem = (ImGuiSelectionUserData)-1; } + void Clear() { memset(this, 0, sizeof(*this)); NavIdItem = RangeSrcItem = RangeFirstItem = RangeLastItem = (ImGuiSelectionUserData)-1; } }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 2351a177eeb8..e70b80c608e9 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2845,65 +2845,61 @@ struct ExampleSelection UpdateItem(adapter->IndexToStorage(adapter, idx), ms_io->RangeSelected); } - // Call after BeginMultiSelect(). - // - Calculate and set ms_io->RequestFocusItem, which we will focus during the loop. + // Find which item should be focused after deletion. // - We cannot provide this logic in core Dear ImGui because we don't have access to selection data. - // - Return value is stored into 'ms_io->RequestFocusItem' which is provided as a convenience for this idiom (but not used by core imgui) - // - Important: This only works if the item ID are stable: aka not depend on their index, but on e.g. item id/ptr. - template - int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, ImVector& items) + // - Important: Deletion only works if the underlying imgui id for your items are stable: aka not depend on their index, but on e.g. item id/ptr. + int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, int items_count, void* items = NULL) { QueueDeletion = false; - // If current item is not selected. - if (ms_io->NavIdSelected == false) // Here 'NavIdSelected' should be == to 'GetSelected(ms_io->NavIdData)' + // If current item is not selected: land on same item. + if (ms_io->NavIdSelected == false) // At this point 'ms_io->NavIdSelected == Contains(ms_io->NavIdItem)' should be true. { - ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even without the !NavIdSelected test but it would take an extra frame to recover RangeSrc when deleting a selected item. - return (int)ms_io->NavIdItem; // Request to land on same item after deletion. + int idx = adapter->UserDataToIndex(items, ms_io->NavIdItem); + ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even without the NavIdSelected==false test but it would take an extra frame to recover RangeSrc when deleting a selected item. + return idx; // Request to land on same item after deletion. } // If current item is selected: land on first unselected item after RangeSrc. - for (int n = (int)ms_io->RangeSrcItem + 1; n < items.Size; n++) - if (!Contains(n)) - return n; + int src_idx = adapter->UserDataToIndex(items, ms_io->RangeSrcItem); + for (int idx = src_idx + 1; idx < items_count; idx++) + if (!Contains(adapter->IndexToStorage(items, idx))) + return idx; // If current item is selected: otherwise return last unselected item. - for (int n = IM_MIN((int)ms_io->RangeSrcItem, items.Size) - 1; n >= 0; n--) - if (!Contains(n)) - return n; + for (int idx = IM_MIN(src_idx, items_count) - 1; idx >= 0; idx--) + if (!Contains(adapter->IndexToStorage(items, idx))) + return idx; return -1; } // Call after EndMultiSelect() - // Apply deletion request + return index of item to refocus, if any. + // Apply deletion request on items + apply deletion request on selection data template - void ApplyDeletionPostLoop(ImGuiMultiSelectIO* ms_io, ImVector& items) + void ApplyDeletionPostLoop(ImGuiMultiSelectIO* ms_io, ImVector& items, int next_focus_idx_in_old_list) { // 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. + // - (2) Convert from old selection index (before deletion) to new selection index (after selection), and select it. // 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); ImVector new_items; new_items.reserve(items.Size - Size); - int next_focus_idx_in_old_selection = (int)ms_io->RequestFocusItem; - int next_focus_idx_in_new_selection = -1; + int next_focus_idx_in_new_list = -1; for (int n = 0; n < items.Size; n++) { if (!Contains(n)) new_items.push_back(items[n]); - if (next_focus_idx_in_old_selection == n) - next_focus_idx_in_new_selection = new_items.Size - 1; + if (next_focus_idx_in_old_list == n) + next_focus_idx_in_new_list = new_items.Size - 1; } items.swap(new_items); // Update selection Clear(); - if (next_focus_idx_in_new_selection != -1 && ms_io->NavIdSelected) - AddItem(next_focus_idx_in_new_selection); + if (next_focus_idx_in_new_list != -1 && ms_io->NavIdSelected) + AddItem(next_focus_idx_in_new_list); } }; @@ -3086,10 +3082,10 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. // FIXME-MULTISELECT: If pressing Delete + another key we have ambiguous behavior. - const bool want_delete = (selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete); + const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); + int next_focus_item_idx = -1; if (want_delete) - ms_io->RequestFocusItem = selection.ApplyDeletionPreLoop(ms_io, items); - const int next_focus_item_idx = (int)ms_io->RequestFocusItem; + next_focus_item_idx = selection.ApplyDeletionPreLoop(ms_io, &selection_adapter, items.Size); for (int n = 0; n < items.Size; n++) { @@ -3108,7 +3104,7 @@ static void ShowDemoWindowMultiSelect() ms_io = ImGui::EndMultiSelect(); selection.ApplyRequests(ms_io, &selection_adapter, items.Size); if (want_delete) - selection.ApplyDeletionPostLoop(ms_io, items); + selection.ApplyDeletionPostLoop(ms_io, items, next_focus_item_idx); ImGui::EndListBox(); } @@ -3208,9 +3204,9 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); + int next_focus_item_idx = -1; if (want_delete) - ms_io->RequestFocusItem = selection.ApplyDeletionPreLoop(ms_io, items); - const int next_focus_item_idx = (int)ms_io->RequestFocusItem; + next_focus_item_idx = selection.ApplyDeletionPreLoop(ms_io, &selection_adapter, items.Size); if (show_in_table) { @@ -3331,7 +3327,7 @@ static void ShowDemoWindowMultiSelect() ms_io = ImGui::EndMultiSelect(); selection.ApplyRequests(ms_io, &selection_adapter, items.Size); if (want_delete) - selection.ApplyDeletionPostLoop(ms_io, items); + selection.ApplyDeletionPostLoop(ms_io, items, next_focus_item_idx); if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); From ba698df7bbda31bf56c151258649d277e348ced6 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 28 Aug 2023 16:33:30 +0200 Subject: [PATCH 055/132] MultiSelect: Demo: Deletion: Various renames to clarify. Use adapter and item list in both ApplyDeletion functions. This also minify the patch for an alternative/wip attmept at redesgining pre/post deletion logic. But turns out current attempt may be easier to grasp. --- imgui_demo.cpp | 89 +++++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index e70b80c608e9..3e91336dcdd5 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2845,61 +2845,63 @@ struct ExampleSelection UpdateItem(adapter->IndexToStorage(adapter, idx), ms_io->RangeSelected); } - // Find which item should be focused after deletion. + // Find which item should be Focused after deletion. + // We output an index in the before-deletion-items list, that user will call SetKeyboardFocusHere() on. + // The subsequent ApplyDeletionPostLoop() code will use it to apply Selection. // - We cannot provide this logic in core Dear ImGui because we don't have access to selection data. - // - Important: Deletion only works if the underlying imgui id for your items are stable: aka not depend on their index, but on e.g. item id/ptr. - int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, int items_count, void* items = NULL) + // - We don't actually manipulate the ImVector<> here, only in ApplyDeletionPostLoop(), but using similar API for consistency and flexibility. + // - Important: Deletion only works if the underlying ImGuiID for your items are stable: aka not depend on their index, but on e.g. item id/ptr. + // FIXME-MULTISELECT: Doesn't take account of the possibility focus target will be moved during deletion. Need refocus or offset. + template + int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, ImVector& items) { QueueDeletion = false; - // If current item is not selected: land on same item. - if (ms_io->NavIdSelected == false) // At this point 'ms_io->NavIdSelected == Contains(ms_io->NavIdItem)' should be true. + // If focused item is not selected... + const int focused_idx = adapter->UserDataToIndex(adapter, ms_io->NavIdItem); // Index of currently focused item + if (ms_io->NavIdSelected == false) // This is merely a shortcut, == Contains(adapter->IndexToStorage(items, focused_idx)) { - int idx = adapter->UserDataToIndex(items, ms_io->NavIdItem); - ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even without the NavIdSelected==false test but it would take an extra frame to recover RangeSrc when deleting a selected item. - return idx; // Request to land on same item after deletion. + ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even when NavIdSelected==true, but it would take an extra frame to recover RangeSrc when deleting a selected item. + return focused_idx; // Request to focus same item after deletion. } - // If current item is selected: land on first unselected item after RangeSrc. - int src_idx = adapter->UserDataToIndex(items, ms_io->RangeSrcItem); - for (int idx = src_idx + 1; idx < items_count; idx++) - if (!Contains(adapter->IndexToStorage(items, idx))) + // If focused item is selected: land on first unselected item after focused item. + for (int idx = focused_idx + 1; idx < items.Size; idx++) + if (!Contains(adapter->IndexToStorage(adapter, idx))) return idx; - // If current item is selected: otherwise return last unselected item. - for (int idx = IM_MIN(src_idx, items_count) - 1; idx >= 0; idx--) - if (!Contains(adapter->IndexToStorage(items, idx))) + // If focused item is selected: otherwise return last unselected item before focused item. + for (int idx = IM_MIN(focused_idx, items.Size) - 1; idx >= 0; idx--) + if (!Contains(adapter->IndexToStorage(adapter, idx))) return idx; return -1; } - // Call after EndMultiSelect() - // Apply deletion request on items + apply deletion request on selection data + // Rewrite item list (delete items) + update selection. + // - Call after EndMultiSelect() + // - We cannot provide this logic in core Dear ImGui because we don't have access to your items, nor to selection data. template - void ApplyDeletionPostLoop(ImGuiMultiSelectIO* ms_io, ImVector& items, int next_focus_idx_in_old_list) + void ApplyDeletionPostLoop(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, ImVector& items, int item_curr_idx_to_select) { - // This does two things: - // - (1) Update Items List (delete items from it) - // - (2) Convert from old selection index (before deletion) to new selection index (after selection), and select it. - // 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. + // Rewrite item list (delete items) + convert old selection index (before deletion) to new selection index (after selection). + // If NavId was not part of selection, we will stay on same item. ImVector new_items; new_items.reserve(items.Size - Size); - int next_focus_idx_in_new_list = -1; - for (int n = 0; n < items.Size; n++) + int item_next_idx_to_select = -1; + for (int idx = 0; idx < items.Size; idx++) { - if (!Contains(n)) - new_items.push_back(items[n]); - if (next_focus_idx_in_old_list == n) - next_focus_idx_in_new_list = new_items.Size - 1; + if (!Contains(adapter->IndexToStorage(adapter, idx))) + new_items.push_back(items[idx]); + if (item_curr_idx_to_select == idx) + item_next_idx_to_select = new_items.Size - 1; } items.swap(new_items); // Update selection Clear(); - if (next_focus_idx_in_new_list != -1 && ms_io->NavIdSelected) - AddItem(next_focus_idx_in_new_list); + if (item_next_idx_to_select != -1 && ms_io->NavIdSelected) + AddItem(adapter->IndexToStorage(adapter, item_next_idx_to_select)); } }; @@ -3051,8 +3053,7 @@ static void ShowDemoWindowMultiSelect() // But you may decide to store selection data inside your item (aka intrusive storage). static ImVector items; static ExampleSelection selection; - ExampleSelectionAdapter selection_adapter; - selection_adapter.SetupForDirectIndexes(); // Pass index to SetNextItemSelectionUserData(), store index in Selection + ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection ImGui::Text("Adding features:"); ImGui::BulletText("Dynamic list with Delete key support."); @@ -3083,9 +3084,9 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. // FIXME-MULTISELECT: If pressing Delete + another key we have ambiguous behavior. const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); - int next_focus_item_idx = -1; + int item_curr_idx_to_focus = -1; if (want_delete) - next_focus_item_idx = selection.ApplyDeletionPreLoop(ms_io, &selection_adapter, items.Size); + item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, &selection_adapter, items); for (int n = 0; n < items.Size; n++) { @@ -3096,7 +3097,7 @@ static void ShowDemoWindowMultiSelect() bool item_is_selected = selection.Contains((ImGuiID)n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); - if (next_focus_item_idx == n) + if (item_curr_idx_to_focus == n) ImGui::SetKeyboardFocusHere(-1); } @@ -3104,7 +3105,7 @@ static void ShowDemoWindowMultiSelect() ms_io = ImGui::EndMultiSelect(); selection.ApplyRequests(ms_io, &selection_adapter, items.Size); if (want_delete) - selection.ApplyDeletionPostLoop(ms_io, items, next_focus_item_idx); + selection.ApplyDeletionPostLoop(ms_io, &selection_adapter, items, item_curr_idx_to_focus); ImGui::EndListBox(); } @@ -3204,9 +3205,9 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); - int next_focus_item_idx = -1; + int item_curr_idx_to_focus = -1; if (want_delete) - next_focus_item_idx = selection.ApplyDeletionPreLoop(ms_io, &selection_adapter, items.Size); + item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, &selection_adapter, items); if (show_in_table) { @@ -3222,8 +3223,8 @@ static void ShowDemoWindowMultiSelect() if (use_clipper) { clipper.Begin(items.Size); - if (next_focus_item_idx != -1) - clipper.IncludeItemByIndex(next_focus_item_idx); // Ensure focused item is not clipped + if (item_curr_idx_to_focus != -1) + clipper.IncludeItemByIndex(item_curr_idx_to_focus); // Ensure focused item is not clipped if (ms_io->RangeSrcItem > 0) clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. } @@ -3262,7 +3263,7 @@ static void ShowDemoWindowMultiSelect() if (widget_type == WidgetType_Selectable) { ImGui::Selectable(label, item_is_selected); - if (next_focus_item_idx == n) + if (item_curr_idx_to_focus == n) ImGui::SetKeyboardFocusHere(-1); if (use_drag_drop && ImGui::BeginDragDropSource()) @@ -3278,7 +3279,7 @@ static void ShowDemoWindowMultiSelect() if (item_is_selected) tree_node_flags |= ImGuiTreeNodeFlags_Selected; bool open = ImGui::TreeNodeEx(label, tree_node_flags); - if (next_focus_item_idx == n) + if (item_curr_idx_to_focus == n) ImGui::SetKeyboardFocusHere(-1); if (use_drag_drop && ImGui::BeginDragDropSource()) { @@ -3327,7 +3328,7 @@ static void ShowDemoWindowMultiSelect() ms_io = ImGui::EndMultiSelect(); selection.ApplyRequests(ms_io, &selection_adapter, items.Size); if (want_delete) - selection.ApplyDeletionPostLoop(ms_io, items, next_focus_item_idx); + selection.ApplyDeletionPostLoop(ms_io, &selection_adapter, items, item_curr_idx_to_focus); if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); From dce02f5c4b0794b8003478013205a185d2182fcf Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 23 Aug 2023 15:53:50 +0200 Subject: [PATCH 056/132] Demo: Dual List Box: Added a dual list box (6648) --- imgui_demo.cpp | 183 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 170 insertions(+), 13 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 3e91336dcdd5..98e44e058739 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2761,6 +2761,13 @@ static void ShowDemoWindowWidgets() } } +static const char* ExampleNames[] = +{ + "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", + "Bitter Gourd", "Bok Choy", "Broccoli", "Brussels Sprouts", "Burdock Root", "Cabbage", "Calabash", "Capers", "Carrot", "Cassava", + "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Celtuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" +}; + // Our multi-selection system doesn't make assumption about: // - how you want to identify items in multi-selection API? Indices(*) / Custom Identifiers / Pointers ? // - how you want to store persistent selection data? Indices / Custom Identifiers(*) / Pointers ? @@ -2858,7 +2865,7 @@ struct ExampleSelection QueueDeletion = false; // If focused item is not selected... - const int focused_idx = adapter->UserDataToIndex(adapter, ms_io->NavIdItem); // Index of currently focused item + const int focused_idx = (int)ms_io->NavIdItem; // Index of currently focused item if (ms_io->NavIdSelected == false) // This is merely a shortcut, == Contains(adapter->IndexToStorage(items, focused_idx)) { ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even when NavIdSelected==true, but it would take an extra frame to recover RangeSrc when deleting a selected item. @@ -2905,6 +2912,147 @@ struct ExampleSelection } }; +// Example: Implement dual list box storage and interface +struct ExampleDualListBox +{ + ImVector Items[2]; // ID is index into ExampleName[] + ExampleSelection Selections[2]; // Store ExampleItemId into selection + bool OptKeepSorted = true; + + void MoveAll(int src, int dst) + { + IM_ASSERT((src == 0 && dst == 1) || (src == 1 && dst == 0)); + for (ImGuiID item_id : Items[src]) + Items[dst].push_back(item_id); + Items[src].clear(); + SortItems(dst); + Selections[src].Swap(Selections[dst]); + Selections[src].Clear(); + } + void MoveSelected(int src, int dst) + { + for (int src_n = 0; src_n < Items[src].Size; src_n++) + { + ImGuiID item_id = Items[src][src_n]; + if (!Selections[src].Contains(item_id)) + continue; + Items[src].erase(&Items[src][src_n]); // FIXME-OPT: Could be implemented more optimally (rebuild src items and swap) + Items[dst].push_back(item_id); + src_n--; + } + if (OptKeepSorted) + SortItems(dst); + Selections[src].Swap(Selections[dst]); + Selections[src].Clear(); + } + void ApplySelectionRequests(ImGuiMultiSelectIO* ms_io, int side) + { + // In this example we store item id in selection (instead of item index) + ExampleSelectionAdapter adapter; + adapter.Data = Items[side].Data; + adapter.IndexToStorage = [](ExampleSelectionAdapter* self, int idx) { return (ImGuiID)((ImGuiID*)self->Data)[idx]; }; + Selections[side].ApplyRequests(ms_io, &adapter, Items[side].Size); + } + static int IMGUI_CDECL CompareItemsByValue(const void* lhs, const void* rhs) + { + const int* a = (const int*)lhs; + const int* b = (const int*)rhs; + return (*a - *b) > 0 ? +1 : -1; + } + void SortItems(int n) + { + qsort(Items[n].Data, (size_t)Items[n].Size, sizeof(Items[n][0]), CompareItemsByValue); + } + void Show() + { + ImGui::Checkbox("Sorted", &OptKeepSorted); + if (ImGui::BeginTable("split", 3, ImGuiTableFlags_None)) + { + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); // Left side + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed); // Buttons + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); // Right side + ImGui::TableNextRow(); + + int request_move_selected = -1; + int request_move_all = -1; + for (int side = 0; side < 2; side++) + { + // FIXME-MULTISELECT: Dual List Box: Add context menus + // FIXME-NAV: Using ImGuiWindowFlags_NavFlattened exhibit many issues. + ImVector& items = Items[side]; + ExampleSelection& selection = Selections[side]; + + ImGui::TableSetColumnIndex((side == 0) ? 0 : 2); + ImGui::Text("%s (%d)", (side == 0) ? "Available" : "Basket", items.Size); + + // Submit scrolling range to avoid glitches on moving/deletion + const float items_height = ImGui::GetTextLineHeightWithSpacing(); + ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); + + if (ImGui::BeginChild(ImGui::GetID(side ? "1" : "0"), ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle)) + { + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + ApplySelectionRequests(ms_io, side); + + for (int item_n = 0; item_n < items.Size; item_n++) + { + ImGuiID item_id = items[item_n]; + bool item_is_selected = selection.Contains(item_id); + ImGui::SetNextItemSelectionUserData(item_n); + ImGui::Selectable(ExampleNames[item_id], item_is_selected, ImGuiSelectableFlags_AllowDoubleClick); + if (ImGui::IsItemFocused()) + { + // FIXME-MULTISELECT: Dual List Box: Transfer focus + if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) + request_move_selected = side; + if (ImGui::IsMouseDoubleClicked(0)) // FIXME-MULTISELECT: Double-click on multi-selection? + request_move_selected = side; + } + } + + ms_io = ImGui::EndMultiSelect(); + ApplySelectionRequests(ms_io, side); + } + ImGui::EndChild(); + } + + // Buttons columns + ImGui::TableSetColumnIndex(1); + ImGui::NewLine(); + //ImVec2 button_sz = { ImGui::CalcTextSize(">>").x + ImGui::GetStyle().FramePadding.x * 2.0f, ImGui::GetFrameHeight() + padding.y * 2.0f }; + ImVec2 button_sz = { ImGui::GetFrameHeight(), ImGui::GetFrameHeight() }; + + // (Using BeginDisabled()/EndDisabled() works but feels distracting given how it is currently visualized) + if (ImGui::Button(">>", button_sz)) + request_move_all = 0; + if (ImGui::Button(">", button_sz)) + request_move_selected = 0; + if (ImGui::Button("<", button_sz)) + request_move_selected = 1; + if (ImGui::Button("<<", button_sz)) + request_move_all = 1; + + // Process requests + if (request_move_all != -1) + MoveAll(request_move_all, request_move_all ^ 1); + if (request_move_selected != -1) + MoveSelected(request_move_selected, request_move_selected ^ 1); + + // FIXME-MULTISELECT: action from outside + if (OptKeepSorted == false) + { + ImGui::NewLine(); + if (ImGui::ArrowButton("MoveUp", ImGuiDir_Up)) {} + if (ImGui::ArrowButton("MoveDown", ImGuiDir_Down)) {} + } + + ImGui::EndTable(); + } + } +}; + + static void ShowDemoWindowMultiSelect() { IMGUI_DEMO_MARKER("Widgets/Selection State & Multi-Select"); @@ -2947,13 +3095,6 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } - static const char* random_names[] = - { - "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", - "Bitter Gourd", "Bok Choy", "Broccoli", "Brussels Sprouts", "Burdock Root", "Cabbage", "Calabash", "Capers", "Carrot", "Cassava", - "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Celtuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" - }; - // Demonstrate holding/updating multi-selection data using the BeginMultiSelect/EndMultiSelect API. // SHIFT+Click w/ CTRL and other standard features are supported. IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select"); @@ -2982,7 +3123,7 @@ static void ShowDemoWindowMultiSelect() for (int n = 0; n < ITEMS_COUNT; n++) { char label[64]; - sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); + sprintf(label, "Object %05d: %s", n, ExampleNames[n % IM_ARRAYSIZE(ExampleNames)]); bool item_is_selected = selection.Contains((ImGuiID)n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); @@ -3023,7 +3164,7 @@ static void ShowDemoWindowMultiSelect() for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) { char label[64]; - sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); + sprintf(label, "Object %05d: %s", n, ExampleNames[n % IM_ARRAYSIZE(ExampleNames)]); bool item_is_selected = selection.Contains((ImGuiID)n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); @@ -3092,7 +3233,7 @@ static void ShowDemoWindowMultiSelect() { const int item_id = items[n]; char label[64]; - sprintf(label, "Object %05d: %s", item_id, random_names[item_id % IM_ARRAYSIZE(random_names)]); + sprintf(label, "Object %05d: %s", item_id, ExampleNames[item_id % IM_ARRAYSIZE(ExampleNames)]); bool item_is_selected = selection.Contains((ImGuiID)n); ImGui::SetNextItemSelectionUserData(n); @@ -3112,6 +3253,22 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } + // Implement a Dual List Box (#6648) + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (dual list box)"); + if (ImGui::TreeNode("Multi-Select (dual list box)")) + { + // Init default state + static ExampleDualListBox dlb; + if (dlb.Items[0].Size == 0 && dlb.Items[1].Size == 0) + for (int item_id = 0; item_id < IM_ARRAYSIZE(ExampleNames); item_id++) + dlb.Items[0].push_back((ImGuiID)item_id); + + // Show + dlb.Show(); + + ImGui::TreePop(); + } + // Demonstrate individual selection scopes in same window IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (multiple scopes)"); if (ImGui::TreeNode("Multi-Select (multiple scopes)")) @@ -3135,7 +3292,7 @@ static void ShowDemoWindowMultiSelect() for (int n = 0; n < ITEMS_COUNT; n++) { char label[64]; - sprintf(label, "Object %05d: %s", n, random_names[n % IM_ARRAYSIZE(random_names)]); + sprintf(label, "Object %05d: %s", n, ExampleNames[n % IM_ARRAYSIZE(ExampleNames)]); bool item_is_selected = selection->Contains((ImGuiID)n); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); @@ -3239,7 +3396,7 @@ static void ShowDemoWindowMultiSelect() ImGui::TableNextColumn(); const int item_id = items[n]; - const char* item_category = random_names[item_id % IM_ARRAYSIZE(random_names)]; + const char* item_category = ExampleNames[item_id % IM_ARRAYSIZE(ExampleNames)]; char label[64]; sprintf(label, "Object %05d: %s", item_id, item_category); From a6f43dfadda53a9e3f04ebcaf85317ab58cae0af Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 29 Aug 2023 18:18:24 +0200 Subject: [PATCH 057/132] MultiSelect: ImGuiMultiSelectIO's field are not used during loop anymore, stripping them out of comments. --- imgui.h | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/imgui.h b/imgui.h index 5add92931fc4..177db867f2be 100644 --- a/imgui.h +++ b/imgui.h @@ -2788,18 +2788,18 @@ struct ImGuiMultiSelectIO // - Always process requests in this order: Clear, SelectAll, SetRange. Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. // - Some fields are only necessary if your list is dynamic and allows deletion (getting "post-deletion" state right is shown 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 [RangeFirstItem..RangeLastItem] items, based on RangeSelected. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. - // STATE/ARGUMENTS -------------------------// BEGIN / LOOP / END - ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be cliped! - ImGuiSelectionUserData RangeFirstItem; // / / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) - ImGuiSelectionUserData RangeLastItem; // / / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) - bool RangeSelected; // / / ms:w, app:r // End: parameter for RequestSetRange request. true = Select Range, false = Unselect Range. - bool RangeSrcReset; // app:w / / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). - bool NavIdSelected; // ms:w, app:r / / // (If using deletion) Last known selection state for NavId (if part of submitted items). - ImGuiSelectionUserData NavIdItem; // ms:w, app:r / / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + // REQUESTS --------------------------------// BEGIN / 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 [RangeFirstItem..RangeLastItem] items based on 'bool RangeSelected'. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. + // STATE/ARGUMENTS -------------------------// BEGIN / END + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be cliped! + ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) + ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) + bool RangeSelected; // / ms:w, app:r // End: parameter for RequestSetRange request. true = Select Range, false = Unselect Range. + bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). + bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). + ImGuiSelectionUserData NavIdItem; // 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)); NavIdItem = RangeSrcItem = RangeFirstItem = RangeLastItem = (ImGuiSelectionUserData)-1; } From 9da4efed2a1d288abac9eea80873e232f32edebb Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 29 Aug 2023 18:53:58 +0200 Subject: [PATCH 058/132] MultiSelect: moved RequestClear output so it'll match request list version better. Use Storage->RangeSrcItem in EndMultiSelect(). --- imgui_widgets.cpp | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 1930fd790b6c..987f50cb05e3 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7213,7 +7213,8 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (ms->IsFocused) { - if (ms->BeginIO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->BeginIO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) + // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here. + if (ms->BeginIO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->BeginIO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here! (see tests) { IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId. ms->Storage->RangeSrcItem = ImGuiSelectionUserData_Invalid; @@ -7396,7 +7397,14 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst //---------------------------------------------------------------------------------------- - ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + const ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + if (!is_multiselect) + ms->EndIO.RequestClear = true; + else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) + ms->EndIO.RequestClear = true; + else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) + ms->EndIO.RequestClear = true; // With is_false==false the RequestClear was done in BeginIO, not necessary to do again. + int range_direction; ms->EndIO.RequestSetRange = true; if (is_shift && is_multiselect) @@ -7419,21 +7427,6 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ImGuiSelectionUserData range_dst_item = item_data; ms->EndIO.RangeFirstItem = (range_direction > 0) ? ms->EndIO.RangeSrcItem : range_dst_item; ms->EndIO.RangeLastItem = (range_direction > 0) ? range_dst_item : ms->EndIO.RangeSrcItem; - - if (!is_multiselect) - { - ms->EndIO.RequestClear = true; - } - else if (input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) - { - if (!is_ctrl) - ms->EndIO.RequestClear = true; - } - else if (input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) - { - if (is_shift && !is_ctrl) // Without Shift the RequestClear was done in BeginIO, not necessary to do again. - ms->EndIO.RequestClear = true; - } } // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) From 5628dda5a5c27f9edde952270813082abff7ac79 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 31 Aug 2023 15:03:51 +0200 Subject: [PATCH 059/132] MultiSelect: move shared logic to MultiSelectItemHeader(). No logic change AFAIK but added an indent level in MultiSelectItemHeader(). Logic changes will come in next commit. --- imgui_demo.cpp | 3 +- imgui_internal.h | 2 +- imgui_widgets.cpp | 105 ++++++++++++++++++++++------------------------ 3 files changed, 54 insertions(+), 56 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 98e44e058739..ac0ddc269bf1 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3419,7 +3419,8 @@ static void ShowDemoWindowMultiSelect() ImGui::SetNextItemSelectionUserData(n); if (widget_type == WidgetType_Selectable) { - ImGui::Selectable(label, item_is_selected); + ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_None; + ImGui::Selectable(label, item_is_selected, selectable_flags); if (item_curr_idx_to_focus == n) ImGui::SetKeyboardFocusHere(-1); diff --git a/imgui_internal.h b/imgui_internal.h index 2e2999a924e0..bdfaac28c424 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -3366,7 +3366,7 @@ namespace ImGui IMGUI_API int TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data); // Multi-Select API - IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected); + IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags); IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); // Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 987f50cb05e3..69c758dc3ede 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6468,19 +6468,11 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags // Multi-selection support (header) if (is_multi_select) { - MultiSelectItemHeader(id, &selected); - button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; + // Handle multi-select + alter button flags for it + MultiSelectItemHeader(id, &selected, &button_flags); - // We absolutely need to distinguish open vs select so this is the default when multi-select is enabled. + // We absolutely need to distinguish open vs select so comes by default flags |= ImGuiTreeNodeFlags_OpenOnArrow; - - // To handle drag and drop of multiple items we need to avoid clearing selection on click. - // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items. - // FIXME-MULTISELECT: Consider opt-in for drag and drop behavior in ImGuiMultiSelectFlags? - if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) - button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; - else - button_flags |= ImGuiButtonFlags_PressedOnClickRelease; } else { @@ -6822,15 +6814,8 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl const bool was_selected = selected; if (is_multi_select) { - MultiSelectItemHeader(id, &selected); - button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; - - // To handle drag and drop of multiple items we need to avoid clearing selection on click. - // Enabling this test makes actions using CTRL+SHIFT delay their effect on the mouse release which is annoying, but it allows drag and drop of multiple items. - if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) - button_flags |= ImGuiButtonFlags_PressedOnClick; - else - button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + // Handle multi-select + alter button flags for it + MultiSelectItemHeader(id, &selected, &button_flags); } bool hovered, held; @@ -7271,52 +7256,64 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d } } -void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) +void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags) { ImGuiContext& g = *GImGui; ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; - if (!ms->IsFocused) - return; - ImGuiMultiSelectState* storage = ms->Storage; - IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); - ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; - - // Apply Clear/SelectAll requests requested by BeginMultiSelect(). - // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. - // If you are using a clipper (aka not submitting every element of the list) you need to process the Clear/SelectAll request after calling BeginMultiSelect() bool selected = *p_selected; - if (ms->BeginIO.RequestClear) - selected = false; - else if (ms->BeginIO.RequestSelectAll) - selected = true; - - // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) - // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function) - if (ms->IsSetRange) + if (ms->IsFocused) { - IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0); - const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. - if (is_range_dst) + ImGuiMultiSelectState* storage = ms->Storage; + ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; + IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); + + // Apply Clear/SelectAll requests requested by BeginMultiSelect(). + // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. + // If you are using a clipper (aka not submitting every element of the list) you need to process the Clear/SelectAll request after calling BeginMultiSelect() + if (ms->BeginIO.RequestClear) + selected = false; + else if (ms->BeginIO.RequestSelectAll) + selected = true; + + // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) + // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function) + if (ms->IsSetRange) { - ms->RangeDstPassedBy = true; - if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst + IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0); + const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. + if (is_range_dst) { - storage->RangeSrcItem = item_data; - storage->RangeSelected = selected ? 1 : 0; + ms->RangeDstPassedBy = true; + if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // 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->RangeSrcPassedBy != ms->RangeDstPassedBy) + { + IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1); + selected = (storage->RangeSelected != 0); + } + else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0) + selected = false; } - const bool is_range_src = storage->RangeSrcItem == item_data; - if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy) - { - IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1); - selected = (storage->RangeSelected != 0); - } - else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0) - selected = false; + *p_selected = selected; } - *p_selected = selected; + // Alter button behavior flags + // To handle drag and drop of multiple items we need to avoid clearing selection on click. + // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items. + // FIXME-MULTISELECT: Consider opt-in for drag and drop behavior in ImGuiMultiSelectFlags? + ImGuiButtonFlags button_flags = *p_button_flags; + button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; + if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) + button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + *p_button_flags = button_flags; } void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) From 82de6c470b1c73171d07b5b30ccdc973cbc3fb63 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 31 Aug 2023 15:50:01 +0200 Subject: [PATCH 060/132] MultiSelect: Added ImGuiMultiSelectFlags_SelectOnClickRelease to allow dragging an unselected item without altering selection + update drag and drop demo. --- imgui.h | 2 ++ imgui_demo.cpp | 63 ++++++++++++++++++++++++++++++----------------- imgui_widgets.cpp | 3 +-- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/imgui.h b/imgui.h index 177db867f2be..f7f01a4ff5ea 100644 --- a/imgui.h +++ b/imgui.h @@ -2736,6 +2736,8 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_ClearOnEscape = 1 << 2, // Clear selection when pressing Escape while scope is focused. ImGuiMultiSelectFlags_ClearOnClickWindowVoid= 1 << 3, // Clear selection when clicking on empty location within host window (use if BeginMultiSelect() covers a whole window) //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) + ImGuiMultiSelectFlags_SelectOnClick = 1 << 5, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 6, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. }; // Multi-selection system diff --git a/imgui_demo.cpp b/imgui_demo.cpp index ac0ddc269bf1..6192b6343f78 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3338,6 +3338,7 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickWindowVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickWindowVoid); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClickRelease", &flags, ImGuiMultiSelectFlags_SelectOnClickRelease); ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); // Initialize default list with 1000 items. static ImVector items; @@ -3403,11 +3404,11 @@ static void ShowDemoWindowMultiSelect() // IMPORTANT: for deletion refocus to work we need object ID to be stable, // aka not depend on their index in the list. Here we use our persistent item_id // instead of index to build a unique ID that will persist. - // (If we used PushID(n) instead, focus wouldn't be restored correctly after deletion). + // (If we used PushID(index) instead, focus wouldn't be restored correctly after deletion). ImGui::PushID(item_id); // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part - // of the selection scope doesn't erroneously alter our selection (FIXME-TESTS: Add a test for that!). + // of the selection scope doesn't erroneously alter our selection. if (show_color_button) { ImU32 dummy_col = (ImU32)((unsigned int)n * 0xC250B74B) | IM_COL32_A_MASK; @@ -3415,39 +3416,57 @@ static void ShowDemoWindowMultiSelect() ImGui::SameLine(); } + // Submit item bool item_is_selected = selection.Contains((ImGuiID)n); + bool item_is_open = false; ImGui::SetNextItemSelectionUserData(n); if (widget_type == WidgetType_Selectable) { - ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_None; - ImGui::Selectable(label, item_is_selected, selectable_flags); - if (item_curr_idx_to_focus == n) - ImGui::SetKeyboardFocusHere(-1); - - if (use_drag_drop && ImGui::BeginDragDropSource()) - { - ImGui::Text("(Dragging %d items)", selection.GetSize()); - ImGui::EndDragDropSource(); - } + ImGui::Selectable(label, item_is_selected, ImGuiSelectableFlags_None); } else if (widget_type == WidgetType_TreeNode) { - ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth; - tree_node_flags |= ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; + ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; if (item_is_selected) tree_node_flags |= ImGuiTreeNodeFlags_Selected; - bool open = ImGui::TreeNodeEx(label, tree_node_flags); - if (item_curr_idx_to_focus == n) - ImGui::SetKeyboardFocusHere(-1); - if (use_drag_drop && ImGui::BeginDragDropSource()) + item_is_open = ImGui::TreeNodeEx(label, tree_node_flags); + } + + // Focus (for after deletion) + if (item_curr_idx_to_focus == n) + ImGui::SetKeyboardFocusHere(-1); + + // Drag and Drop + if (use_drag_drop && ImGui::BeginDragDropSource()) + { + // Write payload with full selection OR single unselected item (only possible with ImGuiMultiSelectFlags_SelectOnClickRelease) + if (ImGui::GetDragDropPayload() == NULL) { - ImGui::Text("(Dragging %d items)", selection.GetSize()); - ImGui::EndDragDropSource(); + ImVector payload_items; + if (!item_is_selected) + payload_items.push_back(item_id); + else + for (const ImGuiStoragePair& pair : selection.Storage.Data) + if (pair.val_i) + payload_items.push_back((int)pair.key); + ImGui::SetDragDropPayload("MULTISELECT_DEMO_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes()); } - if (open) - ImGui::TreePop(); + + // Display payload content in tooltip + const ImGuiPayload* payload = ImGui::GetDragDropPayload(); + const int* payload_items = (int*)payload->Data; + const int payload_count = (int)payload->DataSize / (int)sizeof(payload_items[0]); + if (payload_count == 1) + ImGui::Text("Object %05d: %s", payload_items[0], ExampleNames[payload_items[0] % IM_ARRAYSIZE(ExampleNames)]); + else + ImGui::Text("Dragging %d objects", payload_count); + + ImGui::EndDragDropSource(); } + if (widget_type == WidgetType_TreeNode && item_is_open) + ImGui::TreePop(); + // Right-click: context menu if (ImGui::BeginPopupContextItem()) { diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 69c758dc3ede..102fa167a61b 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7306,10 +7306,9 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags // Alter button behavior flags // To handle drag and drop of multiple items we need to avoid clearing selection on click. // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items. - // FIXME-MULTISELECT: Consider opt-in for drag and drop behavior in ImGuiMultiSelectFlags? ImGuiButtonFlags button_flags = *p_button_flags; button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; - if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) + if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; else button_flags |= ImGuiButtonFlags_PressedOnClickRelease; From d18e57e673271c1c8374d3e193c32babeb4946d3 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 31 Aug 2023 20:06:22 +0200 Subject: [PATCH 061/132] Demo: Assets Browser: Added assets browser demo. --- imgui_demo.cpp | 206 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 6192b6343f78..982d846c452b 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -94,6 +94,7 @@ Index of this file: // [SECTION] Example App: Manipulating window titles / ShowExampleAppWindowTitles() // [SECTION] Example App: Custom Rendering using ImDrawList API / ShowExampleAppCustomRendering() // [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() +// [SECTION] Example App: Assets Browser / ShowExampleAppAssetsBrowser() */ @@ -198,6 +199,7 @@ Index of this file: // Forward Declarations static void ShowExampleAppMainMenuBar(); +static void ShowExampleAppAssetsBrowser(bool* p_open); static void ShowExampleAppConsole(bool* p_open); static void ShowExampleAppCustomRendering(bool* p_open); static void ShowExampleAppDocuments(bool* p_open); @@ -275,6 +277,7 @@ void ImGui::ShowDemoWindow(bool* p_open) // Examples Apps (accessible from the "Examples" menu) static bool show_app_main_menu_bar = false; + static bool show_app_assets_browser = false; static bool show_app_console = false; static bool show_app_custom_rendering = false; static bool show_app_documents = false; @@ -290,6 +293,7 @@ void ImGui::ShowDemoWindow(bool* p_open) if (show_app_main_menu_bar) ShowExampleAppMainMenuBar(); if (show_app_documents) ShowExampleAppDocuments(&show_app_documents); + if (show_app_assets_browser) ShowExampleAppAssetsBrowser(&show_app_assets_browser); if (show_app_console) ShowExampleAppConsole(&show_app_console); if (show_app_custom_rendering) ShowExampleAppCustomRendering(&show_app_custom_rendering); if (show_app_log) ShowExampleAppLog(&show_app_log); @@ -385,6 +389,7 @@ void ImGui::ShowDemoWindow(bool* p_open) ImGui::MenuItem("Main menu bar", NULL, &show_app_main_menu_bar); ImGui::SeparatorText("Mini apps"); + ImGui::MenuItem("Assets Browser", NULL, &show_app_assets_browser); ImGui::MenuItem("Console", NULL, &show_app_console); ImGui::MenuItem("Custom rendering", NULL, &show_app_custom_rendering); ImGui::MenuItem("Documents", NULL, &show_app_documents); @@ -3306,6 +3311,12 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } + if (ImGui::TreeNode("Multi-Select (tiled assets browser)")) + { + ImGui::BulletText("See 'Examples->Assets Browser' in menu"); + ImGui::TreePop(); + } + // Advanced demonstration of BeginMultiSelect() // - Showcase clipping. // - Showcase deletion. @@ -9623,6 +9634,201 @@ void ShowExampleAppDocuments(bool* p_open) ImGui::End(); } +//----------------------------------------------------------------------------- +// [SECTION] Example App: Assets Browser / ShowExampleAppAssetsBrowser() +//----------------------------------------------------------------------------- + +//#include "imgui_internal.h" // NavMoveRequestTryWrapping() + +struct ExampleAssetsBrowser +{ + // State + int ItemsCount = 10000; + ExampleSelection Selection; + float IconSize = 32.0f; + int IconSpacing = 7; + bool StretchSpacing = true; + float ZoomWheelAccum = 0.0f; + + // Functions + void Draw(const char* title, bool* p_open) + { + if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_MenuBar)) + { + ImGui::End(); + return; + } + + // Menu bar + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + if (ImGui::MenuItem("Close", NULL, false, p_open != NULL)) + *p_open = false; + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Options")) + { + ImGui::PushItemWidth(ImGui::GetFontSize() * 10); + ImGui::SliderFloat("Icon Size", &IconSize, 16.0f, 128.0f, "%.0f"); + ImGui::SliderInt("Icon Spacing", &IconSpacing, 0, 32); + ImGui::Checkbox("Stretch Spacing", &StretchSpacing); + ImGui::PopItemWidth(); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + // Zooming with CTRL+Wheel + // FIXME-MULTISELECT: Try to maintain scroll. + ImGuiIO& io = ImGui::GetIO(); + if (ImGui::IsWindowAppearing()) + ZoomWheelAccum = 0.0f; + if (io.MouseWheel != 0.0f && ImGui::IsKeyDown(ImGuiMod_Ctrl) && ImGui::IsAnyItemActive() == false) + { + ZoomWheelAccum += io.MouseWheel; + if (fabsf(ZoomWheelAccum) >= 1.0f) + { + IconSize *= powf(1.1f, (float)(int)ZoomWheelAccum); + IconSize = IM_CLAMP(IconSize, 16.0f, 128.0f); + ZoomWheelAccum -= (int)ZoomWheelAccum; + } + } + + // Show a table with ONLY one header row to showcase the idea/possibility of using this to provide a sorting UI + // FIXME-MULTISELECT: Showcase sorting. + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGuiTableFlags table_flags_for_sort_specs = ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Borders; + if (ImGui::BeginTable("for_sort_specs_only", 3, table_flags_for_sort_specs, ImVec2(0.0f, ImGui::GetFrameHeight()))) + { + ImGui::TableSetupColumn("Index"); + ImGui::TableSetupColumn("Color"); + ImGui::TableSetupColumn("Type"); + ImGui::TableHeadersRow(); + if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs()) + sort_specs->SpecsDirty = false; // No actual sorting in this demo yet + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + + if (ImGui::BeginChild("Assets", ImVec2(0, 0), true, ImGuiWindowFlags_NoMove)) + { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const ImVec2 item_size(floorf(IconSize), floorf(IconSize)); + + // Layout: when not stretching: allow extending into right-most spacing. + float item_spacing = (float)IconSpacing; + const float avail_width = ImGui::GetContentRegionAvail().x + (StretchSpacing ? 0.0f : floorf(item_spacing * 0.5f)); + + // Layout: calculate number of icon per line and number of lines + const int column_count = IM_MAX((int)(avail_width / (item_size.x + IconSpacing)), 1); + const int line_count = (ItemsCount + column_count - 1) / column_count; + + // Layout: when stretching: allocate remaining space to more spacing. Round before division, so item_spacing may be non-integer. + if (StretchSpacing && column_count > 1) + item_spacing = floorf(avail_width - item_size.x * column_count) / column_count; + + // Calculate and store start position. + const float outer_padding = floorf(item_spacing * 0.5f); + ImVec2 start_pos = ImGui::GetCursorScreenPos(); + start_pos = ImVec2(start_pos.x + outer_padding, start_pos.y + outer_padding); + ImGui::SetCursorScreenPos(start_pos); + + // Multi-select + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnClickWindowVoid; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + ExampleSelectionAdapter selection_adapter; + Selection.ApplyRequests(ms_io, &selection_adapter, ItemsCount); + + // Altering ItemSpacing may seem unnecessary as we position every items using SetCursorScreenPos()... + // But it is necessary for two reasons: + // - Selectables uses it by default to visually fill the space between two items. + // - The vertical spacing would be measured by Clipper to calculate line height if we didn't provide it explicitly (here we do). + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(item_spacing, item_spacing)); + + const float line_height = item_size.y + item_spacing; + ImGuiListClipper clipper; + clipper.Begin(line_count, line_height); + if (ms_io->RangeSrcItem != -1) + clipper.IncludeItemByIndex((int)(ms_io->RangeSrcItem / column_count)); + while (clipper.Step()) + { + for (int line_idx = clipper.DisplayStart; line_idx < clipper.DisplayEnd; line_idx++) + { + const int item_min_idx_for_current_line = line_idx * column_count; + const int item_max_idx_for_current_line = IM_MIN((line_idx + 1) * column_count, ItemsCount); + for (int item_idx = item_min_idx_for_current_line; item_idx < item_max_idx_for_current_line; ++item_idx) + { + ImGui::PushID(item_idx); + + // Position item + ImVec2 pos = ImVec2(start_pos.x + (item_idx % column_count) * (item_size.x + item_spacing), start_pos.y + (line_idx * line_height)); + ImGui::SetCursorScreenPos(pos); + + // Draw box + ImVec2 box_min(pos.x - 1, pos.y - 1); + ImVec2 box_max(box_min.x + item_size.x + 2, box_min.y + item_size.y + 2); + draw_list->AddRect(box_min, box_max, IM_COL32(90, 90, 90, 255)); + + bool item_is_selected = Selection.Contains((ImGuiID)item_idx); + ImGui::SetNextItemSelectionUserData(item_idx); + ImGui::Selectable("##select", item_is_selected, ImGuiSelectableFlags_None, item_size); + + // Update our selection state immediately (without waiting for EndMultiSelect() requests) + // because we use this to alter the color of our text/icon. + if (ImGui::IsItemToggledSelection()) + item_is_selected = !item_is_selected; + + // Drag and drop + if (ImGui::BeginDragDropSource()) + { + ImGui::SetDragDropPayload("ASSETS_BROWSER_ITEMS", "Dummy", 5); + ImGui::Text("%d assets", Selection.Size); + ImGui::EndDragDropSource(); + } + + // Popup menu + if (ImGui::BeginPopupContextItem()) + { + ImGui::Text("Selection: %d items", Selection.Size); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + + // A real app would likely display an image/thumbnail here. + char label[32]; + sprintf(label, "%d", item_idx); + draw_list->AddRectFilled(box_min, box_max, IM_COL32(48, 48, 48, 128)); + draw_list->AddText(ImVec2(box_min.x, box_max.y - ImGui::GetFontSize()), item_is_selected ? IM_COL32(255, 255, 255, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled), label); + + ImGui::PopID(); + } + } + } + clipper.End(); + ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing + + ms_io = ImGui::EndMultiSelect(); + Selection.ApplyRequests(ms_io, &selection_adapter, ItemsCount); + + // FIXME-MULTISELECT: Find a way to expose this in public API. This currently requires "imgui_internal.h" + //ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); + } + + ImGui::EndChild(); + ImGui::End(); + } +}; + +void ShowExampleAppAssetsBrowser(bool* p_open) +{ + IMGUI_DEMO_MARKER("Examples/Assets Browser"); + static ExampleAssetsBrowser assets_browser; + assets_browser.Draw("Example: Assets Browser", p_open); +} + // End of Demo code #else From 88df5901458100a6f4268385f3fb5c36bdb6ac8b Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 1 Sep 2023 14:54:28 +0200 Subject: [PATCH 062/132] Demo: Assets Browser: store items, sorting, type overlay. --- imgui_demo.cpp | 146 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 121 insertions(+), 25 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 982d846c452b..93addbb1c413 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2770,7 +2770,7 @@ static const char* ExampleNames[] = { "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", "Bitter Gourd", "Bok Choy", "Broccoli", "Brussels Sprouts", "Burdock Root", "Cabbage", "Calabash", "Capers", "Carrot", "Cassava", - "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Celtuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" + "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" }; // Our multi-selection system doesn't make assumption about: @@ -9640,19 +9640,84 @@ void ShowExampleAppDocuments(bool* p_open) //#include "imgui_internal.h" // NavMoveRequestTryWrapping() +struct ExampleAsset +{ + int ID; + int Type; + + ExampleAsset(int id, int type) { ID = id; Type = type; } + + static const ImGuiTableSortSpecs* s_current_sort_specs; + + static void SortWithSortSpecs(ImGuiTableSortSpecs* sort_specs, ExampleAsset* items, int items_count) + { + s_current_sort_specs = sort_specs; // Store in variable accessible by the sort function. + if (items_count > 1) + qsort(items, (size_t)items_count, sizeof(items[0]), ExampleAsset::CompareWithSortSpecs); + s_current_sort_specs = NULL; + } + + // Compare function to be used by qsort() + static int IMGUI_CDECL CompareWithSortSpecs(const void* lhs, const void* rhs) + { + const ExampleAsset* a = (const ExampleAsset*)lhs; + const ExampleAsset* b = (const ExampleAsset*)rhs; + for (int n = 0; n < s_current_sort_specs->SpecsCount; n++) + { + const ImGuiTableColumnSortSpecs* sort_spec = &s_current_sort_specs->Specs[n]; + int delta = 0; + if (sort_spec->ColumnIndex == 0) + delta = (a->ID - b->ID); + else if (sort_spec->ColumnIndex == 1) + delta = (a->Type - b->Type); + if (delta > 0) + return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? +1 : -1; + if (delta < 0) + return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? -1 : +1; + } + return (a->ID - b->ID); + } +}; +const ImGuiTableSortSpecs* ExampleAsset::s_current_sort_specs = NULL; + struct ExampleAssetsBrowser { + // Options + bool ShowTypeOverlay = true; + float IconSize = 32.0f; + int IconSpacing = 7; + bool StretchSpacing = true; + // State - int ItemsCount = 10000; - ExampleSelection Selection; - float IconSize = 32.0f; - int IconSpacing = 7; - bool StretchSpacing = true; - float ZoomWheelAccum = 0.0f; + ImVector Items; + ExampleSelection Selection; + int NextItemId = 0; + bool SortDirty = false; + float ZoomWheelAccum = 0.0f; // Functions + ExampleAssetsBrowser() + { + AddItems(10000); + } + void AddItems(int count) + { + if (Items.Size == 0) + NextItemId = 0; + Items.reserve(Items.Size + count); + for (int n = 0; n < count; n++, NextItemId++) + Items.push_back(ExampleAsset(NextItemId, (NextItemId % 20) < 15 ? 0 : (NextItemId % 20) < 18 ? 1 : 2)); + SortDirty = true; + } + void ClearItems() + { + Items.clear(); + Selection.Clear(); + } + void Draw(const char* title, bool* p_open) { + ImGui::SetNextWindowSize(ImVec2(IconSize * 25, IconSize * 15), ImGuiCond_FirstUseEver); if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_MenuBar)) { ImGui::End(); @@ -9664,6 +9729,11 @@ struct ExampleAssetsBrowser { if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Add 10000 items")) + AddItems(10000); + if (ImGui::MenuItem("Clear items")) + ClearItems(); + ImGui::Separator(); if (ImGui::MenuItem("Close", NULL, false, p_open != NULL)) *p_open = false; ImGui::EndMenu(); @@ -9671,6 +9741,11 @@ struct ExampleAssetsBrowser if (ImGui::BeginMenu("Options")) { ImGui::PushItemWidth(ImGui::GetFontSize() * 10); + + ImGui::SeparatorText("Contents"); + ImGui::Checkbox("Show Type Overlay", &ShowTypeOverlay); + + ImGui::SeparatorText("Layout"); ImGui::SliderFloat("Icon Size", &IconSize, 16.0f, 128.0f, "%.0f"); ImGui::SliderInt("Icon Spacing", &IconSpacing, 0, 32); ImGui::Checkbox("Stretch Spacing", &StretchSpacing); @@ -9697,22 +9772,24 @@ struct ExampleAssetsBrowser } // Show a table with ONLY one header row to showcase the idea/possibility of using this to provide a sorting UI - // FIXME-MULTISELECT: Showcase sorting. ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); ImGuiTableFlags table_flags_for_sort_specs = ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Borders; - if (ImGui::BeginTable("for_sort_specs_only", 3, table_flags_for_sort_specs, ImVec2(0.0f, ImGui::GetFrameHeight()))) + if (ImGui::BeginTable("for_sort_specs_only", 2, table_flags_for_sort_specs, ImVec2(0.0f, ImGui::GetFrameHeight()))) { ImGui::TableSetupColumn("Index"); - ImGui::TableSetupColumn("Color"); ImGui::TableSetupColumn("Type"); ImGui::TableHeadersRow(); if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs()) - sort_specs->SpecsDirty = false; // No actual sorting in this demo yet + if (sort_specs->SpecsDirty || SortDirty) + { + ExampleAsset::SortWithSortSpecs(sort_specs, Items.Data, Items.Size); + sort_specs->SpecsDirty = SortDirty = false; + } ImGui::EndTable(); } ImGui::PopStyleVar(); - if (ImGui::BeginChild("Assets", ImVec2(0, 0), true, ImGuiWindowFlags_NoMove)) + if (ImGui::BeginChild("Assets", ImVec2(0, -ImGui::GetTextLineHeightWithSpacing()), true, ImGuiWindowFlags_NoMove)) { ImDrawList* draw_list = ImGui::GetWindowDrawList(); const ImVec2 item_size(floorf(IconSize), floorf(IconSize)); @@ -9723,7 +9800,7 @@ struct ExampleAssetsBrowser // Layout: calculate number of icon per line and number of lines const int column_count = IM_MAX((int)(avail_width / (item_size.x + IconSpacing)), 1); - const int line_count = (ItemsCount + column_count - 1) / column_count; + const int line_count = (Items.Size + column_count - 1) / column_count; // Layout: when stretching: allocate remaining space to more spacing. Round before division, so item_spacing may be non-integer. if (StretchSpacing && column_count > 1) @@ -9736,10 +9813,12 @@ struct ExampleAssetsBrowser ImGui::SetCursorScreenPos(start_pos); // Multi-select - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnClickWindowVoid; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ClearOnClickWindowVoid; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags); ExampleSelectionAdapter selection_adapter; - Selection.ApplyRequests(ms_io, &selection_adapter, ItemsCount); + selection_adapter.Data = this; + selection_adapter.IndexToStorage = [](ExampleSelectionAdapter* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->Data; return (ImGuiID)self->Items[idx].ID; }; + Selection.ApplyRequests(ms_io, &selection_adapter, Items.Size); // Altering ItemSpacing may seem unnecessary as we position every items using SetCursorScreenPos()... // But it is necessary for two reasons: @@ -9747,6 +9826,12 @@ struct ExampleAssetsBrowser // - The vertical spacing would be measured by Clipper to calculate line height if we didn't provide it explicitly (here we do). ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(item_spacing, item_spacing)); + // Rendering parameters + const ImU32 icon_bg_color = IM_COL32(48, 48, 48, 128); + const ImU32 icon_type_overlay_colors[3] = { 0, IM_COL32(200, 70, 70, 255), IM_COL32(70, 170, 70, 255) }; + const ImVec2 icon_type_overlay_size = ImVec2(4.0f, 4.0f); + const bool display_label = (item_size.x >= ImGui::CalcTextSize("999").x); + const float line_height = item_size.y + item_spacing; ImGuiListClipper clipper; clipper.Begin(line_count, line_height); @@ -9757,10 +9842,11 @@ struct ExampleAssetsBrowser for (int line_idx = clipper.DisplayStart; line_idx < clipper.DisplayEnd; line_idx++) { const int item_min_idx_for_current_line = line_idx * column_count; - const int item_max_idx_for_current_line = IM_MIN((line_idx + 1) * column_count, ItemsCount); + const int item_max_idx_for_current_line = IM_MIN((line_idx + 1) * column_count, Items.Size); for (int item_idx = item_min_idx_for_current_line; item_idx < item_max_idx_for_current_line; ++item_idx) { - ImGui::PushID(item_idx); + ExampleAsset* item_data = &Items[item_idx]; + ImGui::PushID(item_data->ID); // Position item ImVec2 pos = ImVec2(start_pos.x + (item_idx % column_count) * (item_size.x + item_spacing), start_pos.y + (line_idx * line_height)); @@ -9771,9 +9857,9 @@ struct ExampleAssetsBrowser ImVec2 box_max(box_min.x + item_size.x + 2, box_min.y + item_size.y + 2); draw_list->AddRect(box_min, box_max, IM_COL32(90, 90, 90, 255)); - bool item_is_selected = Selection.Contains((ImGuiID)item_idx); ImGui::SetNextItemSelectionUserData(item_idx); - ImGui::Selectable("##select", item_is_selected, ImGuiSelectableFlags_None, item_size); + bool item_is_selected = Selection.Contains((ImGuiID)item_data->ID); + ImGui::Selectable("", item_is_selected, ImGuiSelectableFlags_None, item_size); // Update our selection state immediately (without waiting for EndMultiSelect() requests) // because we use this to alter the color of our text/icon. @@ -9798,10 +9884,19 @@ struct ExampleAssetsBrowser } // A real app would likely display an image/thumbnail here. - char label[32]; - sprintf(label, "%d", item_idx); - draw_list->AddRectFilled(box_min, box_max, IM_COL32(48, 48, 48, 128)); - draw_list->AddText(ImVec2(box_min.x, box_max.y - ImGui::GetFontSize()), item_is_selected ? IM_COL32(255, 255, 255, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled), label); + draw_list->AddRectFilled(box_min, box_max, icon_bg_color); + if (ShowTypeOverlay && item_data->Type != 0) + { + ImU32 type_col = icon_type_overlay_colors[item_data->Type % IM_ARRAYSIZE(icon_type_overlay_colors)]; + draw_list->AddRectFilled(ImVec2(box_max.x - 2 - icon_type_overlay_size.x, box_min.y + 2), ImVec2(box_max.x - 2, box_min.y + 2 + icon_type_overlay_size.y), type_col); + } + if (display_label) + { + ImU32 label_col = item_is_selected ? IM_COL32(255, 255, 255, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled); + char label[32]; + sprintf(label, "%d", item_data->ID); + draw_list->AddText(ImVec2(box_min.x, box_max.y - ImGui::GetFontSize()), label_col, label); + } ImGui::PopID(); } @@ -9811,13 +9906,14 @@ struct ExampleAssetsBrowser ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing ms_io = ImGui::EndMultiSelect(); - Selection.ApplyRequests(ms_io, &selection_adapter, ItemsCount); + Selection.ApplyRequests(ms_io, &selection_adapter, Items.Size); // FIXME-MULTISELECT: Find a way to expose this in public API. This currently requires "imgui_internal.h" //ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); } ImGui::EndChild(); + ImGui::Text("Selected: %d/%d items", Selection.Size, Items.Size); ImGui::End(); } }; From 2765fdb43ea38aaba20109b2a9f4935ccb9ebae7 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 20 Sep 2023 19:10:08 +0200 Subject: [PATCH 063/132] MultiSelect: removed seemingly unnecessary block in BeginMultiSelect(). - EndIO.RangeSelected always set along with EndIO.RequestSetRange - Trying to assert for the assignment making a difference when EndIO.RequestSetRange is already set couldn't find a case (tests passing). --- imgui.h | 2 +- imgui_widgets.cpp | 24 +++++++++--------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/imgui.h b/imgui.h index f7f01a4ff5ea..95622e852f29 100644 --- a/imgui.h +++ b/imgui.h @@ -2795,7 +2795,7 @@ struct ImGuiMultiSelectIO 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 [RangeFirstItem..RangeLastItem] items based on 'bool RangeSelected'. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. // STATE/ARGUMENTS -------------------------// BEGIN / END - ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be cliped! + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) bool RangeSelected; // / ms:w, app:r // End: parameter for RequestSetRange request. true = Select Range, false = Unselect Range. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 102fa167a61b..7bb21c25290c 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7109,12 +7109,12 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) // - DebugNodeMultiSelectState() [Internal] //------------------------------------------------------------------------- -static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* data) +static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) { ImGuiContext& g = *GImGui; - if (data->RequestClear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestClear\n", function); - if (data->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSelectAll\n", function); - if (data->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, data->RangeFirstItem, data->RangeLastItem, data->RangeFirstItem, data->RangeLastItem, data->RangeSelected); + if (io->RequestClear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestClear\n", function); + if (io->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSelectAll\n", function); + if (io->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, io->RangeFirstItem, io->RangeLastItem, io->RangeFirstItem, io->RangeLastItem, io->RangeSelected); } // Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). @@ -7169,17 +7169,17 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if (ms->IsFocused) { - // Shortcut: Select all (CTRL+A) - if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(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 (flags & ImGuiMultiSelectFlags_ClearOnEscape) if (Shortcut(ImGuiKey_Escape)) ms->BeginIO.RequestClear = true; + + // Shortcut: Select all (CTRL+A) + if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) + if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) + ms->BeginIO.RequestSelectAll = true; } if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) @@ -7428,12 +7428,6 @@ 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; - } // Update/store the selection state of focused item if (g.NavId == id) From c3998b70ccb07c30e57872d84446fdb1a707350e Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 21 Sep 2023 20:40:21 +0200 Subject: [PATCH 064/132] MultiSelect: clarified purpose and use of IsItemToggledSelection(). Added assert. Moved to multi-selection section of imgui.h. --- imgui.cpp | 6 ++++++ imgui.h | 2 +- imgui_widgets.cpp | 12 +++++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 19beed8ed63d..59c7580bb041 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -5446,9 +5446,15 @@ bool ImGui::IsItemToggledOpen() return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledOpen) ? true : false; } +// Call after a Selectable() or TreeNode() involved in multi-selection. +// Useful if you need the per-item information before reaching EndMultiSelect(), e.g. for rendering purpose. +// This is only meant to be called inside a BeginMultiSelect()/EndMultiSelect() block. +// (Outside of multi-select, it would be misleading/ambiguous to report this signal, as widgets +// return e.g. a pressed event and user code is in charge of altering selection in ways we cannot predict.) bool ImGui::IsItemToggledSelection() { ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentMultiSelect != NULL); // Can only be used inside a BeginMultiSelect()/EndMultiSelect() return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledSelection) ? true : false; } diff --git a/imgui.h b/imgui.h index 95622e852f29..96b29d560dc9 100644 --- a/imgui.h +++ b/imgui.h @@ -676,6 +676,7 @@ namespace ImGui IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags); IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); + IMGUI_API bool IsItemToggledSelection(); // Was the last item selection state toggled? Useful if you need the per-item information _before_ reaching EndMultiSelect(). We only returns toggle _event_ in order to handle clipping correctly. // Widgets: List Boxes // - This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. @@ -909,7 +910,6 @@ 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 IsAnyItemHovered(); // is any item hovered? IMGUI_API bool IsAnyItemActive(); // is any item active? IMGUI_API bool IsAnyItemFocused(); // is any item focused? diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 7bb21c25290c..b2ae35cb0ad3 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6891,7 +6891,9 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (disabled_item && !disabled_global) EndDisabled(); - // Users of BeginMultiSelect() scope: call ImGui::IsItemToggledSelection() to retrieve selection toggle. Selectable() returns a pressed state! + // Selectable() always returns a pressed state! + // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve + // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect(). IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return pressed; //-V1020 } @@ -7335,7 +7337,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; - const bool is_multiselect = (ms->Flags & ImGuiMultiSelectFlags_SingleSelect) == 0; + const bool is_singleselect = (ms->Flags & ImGuiMultiSelectFlags_SingleSelect) != 0; bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0; bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0; @@ -7394,16 +7396,16 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) //---------------------------------------------------------------------------------------- const ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; - if (!is_multiselect) + if (is_singleselect) ms->EndIO.RequestClear = true; else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) ms->EndIO.RequestClear = true; else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) - ms->EndIO.RequestClear = true; // With is_false==false the RequestClear was done in BeginIO, not necessary to do again. + ms->EndIO.RequestClear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. int range_direction; ms->EndIO.RequestSetRange = true; - if (is_shift && is_multiselect) + if (is_shift && !is_singleselect) { // Shift+Arrow always select // Ctrl+Shift+Arrow copy source selection state (alrady stored by BeginMultiSelect() in RangeSelected) From a6adfb2b49482f117a44a98e79bb92b049843f06 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 22 Sep 2023 14:30:56 +0200 Subject: [PATCH 065/132] MultiSelect: added missing call on Shutdown(). Better reuse selection buffer. --- imgui.cpp | 2 ++ imgui_demo.cpp | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/imgui.cpp b/imgui.cpp index 59c7580bb041..f932e4efa7e5 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3818,6 +3818,8 @@ void ImGui::Shutdown() g.TablesTempData.clear_destruct(); g.DrawChannelsTempMergeBuffer.clear(); + g.MultiSelectStorage.Clear(); + g.ClipboardHandlerData.clear(); g.MenusIdSubmittedThisFrame.clear(); g.InputTextState.ClearFreeMemory(); diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 93addbb1c413..59b7162acce5 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2818,7 +2818,7 @@ struct ExampleSelection // Functions ExampleSelection() { Clear(); } - void Clear() { Storage.Clear(); Size = 0; QueueDeletion = false; } + void Clear() { Storage.Data.resize(0); Size = 0; QueueDeletion = false; } void Swap(ExampleSelection& rhs) { Storage.Data.swap(rhs.Storage.Data); } bool Contains(ImGuiID key) const { return Storage.GetInt(key, 0) != 0; } void AddItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int != 0) return; *p_int = 1; Size++; } @@ -2849,8 +2849,11 @@ struct ExampleSelection Clear(); if (ms_io->RequestSelectAll) + { + Storage.Data.reserve(items_count); for (int idx = 0; idx < items_count; idx++) AddItem(adapter->IndexToStorage(adapter, idx)); + } if (ms_io->RequestSetRange) for (int idx = (int)ms_io->RangeFirstItem; idx <= (int)ms_io->RangeLastItem; idx++) From 6feff6ff0514db74b387843768cb023e39d148fc Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 22 Sep 2023 14:23:40 +0200 Subject: [PATCH 066/132] MultiSelect: (Breaking) io contains a ImVector list. --- imgui.h | 53 +++++++++++++++++++++++++++----------------- imgui_demo.cpp | 33 +++++++++++++++------------- imgui_internal.h | 4 +++- imgui_widgets.cpp | 56 +++++++++++++++++++++++++++++++---------------- 4 files changed, 91 insertions(+), 55 deletions(-) diff --git a/imgui.h b/imgui.h index 96b29d560dc9..2ce1f1983846 100644 --- a/imgui.h +++ b/imgui.h @@ -44,7 +44,7 @@ Index of this file: // [SECTION] ImGuiIO // [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) // [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) -// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO) // [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) // [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) // [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) @@ -2720,7 +2720,7 @@ struct ImColor }; //----------------------------------------------------------------------------- -// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO) +// [SECTION] Multi-Select API flags & structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO) //----------------------------------------------------------------------------- #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. @@ -2775,36 +2775,49 @@ enum ImGuiMultiSelectFlags_ // - If you need to wrap this API for another language/framework, feel free to expose this as 'int' if simpler. // Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (2) [If using clipper] Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 6. +// - (2) [If using clipper] Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. Same code as Step 6. // - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeIndex(). If already using indices in ImGuiSelectionUserData, it is as simple as clipper.IncludeIndex((int)ms_io->RangeSrcItem); // LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (6) Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 2. +// - (6) Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. 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. // However it is perfectly fine to honor all steps even if you don't use a clipper. // Advanced: // - Deletion: If you need to handle items deletion a little more work if needed for post-deletion focus and scrolling to be correct. // refer to 'Demo->Widgets->Selection State' for demos supporting deletion. + +enum ImGuiSelectionRequestType +{ + ImGuiSelectionRequestType_None = 0, + ImGuiSelectionRequestType_Clear, // Request app to clear selection. + ImGuiSelectionRequestType_SelectAll, // Request app to select all. + ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items based on 'bool RangeSelected'. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. +}; + +// List of requests stored in ImGuiMultiSelectIO +// - Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. +// - Some fields are only necessary if your list is dynamic and allows deletion (handling deletion and getting "post-deletion" state right is shown 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. +struct ImGuiSelectionRequest +{ + ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r + bool RangeSelected; // / ms:w, app:r // Parameter for SetRange request (true = select range, false = unselect range) + ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) + ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) + + ImGuiSelectionRequest(ImGuiSelectionRequestType type = ImGuiSelectionRequestType_None) { Type = type; RangeSelected = false; RangeFirstItem = RangeLastItem = (ImGuiSelectionUserData)-1; } +}; + struct ImGuiMultiSelectIO { - // - Always process requests in this order: Clear, SelectAll, SetRange. Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. - // - Some fields are only necessary if your list is dynamic and allows deletion (getting "post-deletion" state right is shown 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 / 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 [RangeFirstItem..RangeLastItem] items based on 'bool RangeSelected'. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. - // STATE/ARGUMENTS -------------------------// BEGIN / END - ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! - ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) - ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) - bool RangeSelected; // / ms:w, app:r // End: parameter for RequestSetRange request. true = Select Range, false = Unselect Range. - bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). - bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). - ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + ImVector Requests; // ms:w, app:r / ms:w app:r // Requests + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! + ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). + bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). ImGuiMultiSelectIO() { Clear(); } - void Clear() { memset(this, 0, sizeof(*this)); NavIdItem = RangeSrcItem = RangeFirstItem = RangeLastItem = (ImGuiSelectionUserData)-1; } + void Clear() { Requests.resize(0); RangeSrcItem = NavIdItem = (ImGuiSelectionUserData)-1; NavIdSelected = RangeSrcReset = false; } }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 59b7162acce5..770eb3c9d2ed 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2838,26 +2838,29 @@ struct ExampleSelection // WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ExampleSelectionAdapter' INDIRECTION LOGIC. // Notice that with the simplest adapter (using indices everywhere), all functions return their parameters. // The most simple implementation (using indices everywhere) would look like: - // if (ms_io->RequestClear) { Clear(); } - // if (ms_io->RequestSelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } - // if (ms_io->RequestSetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } + // for (ImGuiSelectionRequest& req : ms_io->Requests) + // { + // if (req.Type == ImGuiSelectionRequestType_Clear) { Clear(); } + // if (req.Type == ImGuiSelectionRequestType_SelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } + // if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } + // } void ApplyRequests(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, int items_count) { IM_ASSERT(adapter->IndexToStorage != NULL); - - if (ms_io->RequestClear || ms_io->RequestSelectAll) - Clear(); - - if (ms_io->RequestSelectAll) + for (ImGuiSelectionRequest& req : ms_io->Requests) { - Storage.Data.reserve(items_count); - for (int idx = 0; idx < items_count; idx++) - AddItem(adapter->IndexToStorage(adapter, idx)); + if (req.Type == ImGuiSelectionRequestType_Clear || req.Type == ImGuiSelectionRequestType_SelectAll) + Clear(); + if (req.Type == ImGuiSelectionRequestType_SelectAll) + { + Storage.Data.reserve(items_count); + for (int idx = 0; idx < items_count; idx++) + AddItem(adapter->IndexToStorage(adapter, idx)); + } + if (req.Type == ImGuiSelectionRequestType_SetRange) + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) + UpdateItem(adapter->IndexToStorage(adapter, idx), req.RangeSelected); } - - if (ms_io->RequestSetRange) - for (int idx = (int)ms_io->RangeFirstItem; idx <= (int)ms_io->RangeLastItem; idx++) - UpdateItem(adapter->IndexToStorage(adapter, idx), ms_io->RangeSelected); } // Find which item should be Focused after deletion. diff --git a/imgui_internal.h b/imgui_internal.h index bdfaac28c424..9df3b01e8199 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1722,6 +1722,8 @@ struct IMGUI_API ImGuiMultiSelectTempData ImGuiKeyChord KeyMods; 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 LoopRequestClear; + bool LoopRequestSelectAll; 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; @@ -1730,7 +1732,7 @@ struct IMGUI_API ImGuiMultiSelectTempData //ImRect Rect; // Extent of selection scope between BeginMultiSelect() / EndMultiSelect(), used by ImGuiMultiSelectFlags_ClearOnClickRectVoid. ImGuiMultiSelectTempData() { Clear(); } - void Clear() { memset(this, 0, sizeof(*this)); BeginIO.Clear(); EndIO.Clear(); } + void Clear() { Storage = NULL; FocusScopeId = 0; Flags = 0; KeyMods = 0; BeginIO.Clear(); EndIO.Clear(); LoopRequestClear = LoopRequestSelectAll = IsFocused = IsSetRange = NavIdPassedBy = RangeSrcPassedBy = RangeDstPassedBy = false; } }; // Persistent storage for multi-select (as long as selection is alive) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index b2ae35cb0ad3..17162e393ebc 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7114,9 +7114,12 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) { ImGuiContext& g = *GImGui; - if (io->RequestClear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestClear\n", function); - if (io->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSelectAll\n", function); - if (io->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, io->RangeFirstItem, io->RangeLastItem, io->RangeFirstItem, io->RangeLastItem, io->RangeSelected); + for (const ImGuiSelectionRequest& req : io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_Clear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: Clear\n", function); + if (req.Type == ImGuiSelectionRequestType_SelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SelectAll\n", function); + if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.RangeSelected); + } } // Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). @@ -7151,6 +7154,9 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ms->BeginIO.NavIdItem = ms->EndIO.NavIdItem = storage->NavIdItem; ms->BeginIO.NavIdSelected = ms->EndIO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; + bool request_clear = false; + bool request_select_all = false; + // Clear when using Navigation to move within the scope // (we compare FocusScopeId so it possible to use multiple selections inside a same window) if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) @@ -7160,13 +7166,13 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if (ms->IsSetRange) IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear? if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) - ms->BeginIO.RequestClear = true; + request_clear = true; } else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId) { // Also clear on leaving scope (may be optional?) if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) - ms->BeginIO.RequestClear = true; + request_clear = true; } if (ms->IsFocused) @@ -7176,14 +7182,19 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) // Otherwise may be done by caller but it means Shortcut() needs to be exposed. if (flags & ImGuiMultiSelectFlags_ClearOnEscape) if (Shortcut(ImGuiKey_Escape)) - ms->BeginIO.RequestClear = true; + request_clear = true; // Shortcut: Select all (CTRL+A) if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) - ms->BeginIO.RequestSelectAll = true; + request_select_all = true; } + if (request_clear || request_select_all) + ms->BeginIO.Requests.push_back(ImGuiSelectionRequest(request_select_all ? ImGuiSelectionRequestType_SelectAll : ImGuiSelectionRequestType_Clear)); + ms->LoopRequestClear = request_clear; + ms->LoopRequestSelectAll = request_select_all; + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) DebugLogMultiSelectRequests("BeginMultiSelect", &ms->BeginIO); @@ -7220,8 +7231,8 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (IsWindowHovered() && g.HoveredId == 0) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { - ms->EndIO.RequestClear = true; - ms->EndIO.RequestSelectAll = ms->EndIO.RequestSetRange = false; + ms->EndIO.Requests.resize(0); + ms->EndIO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); } // Unwind @@ -7273,9 +7284,9 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags // Apply Clear/SelectAll requests requested by BeginMultiSelect(). // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. // If you are using a clipper (aka not submitting every element of the list) you need to process the Clear/SelectAll request after calling BeginMultiSelect() - if (ms->BeginIO.RequestClear) + if (ms->LoopRequestClear) selected = false; - else if (ms->BeginIO.RequestSelectAll) + else if (ms->LoopRequestSelectAll) selected = true; // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) @@ -7396,22 +7407,28 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) //---------------------------------------------------------------------------------------- const ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + bool request_clear = false; if (is_singleselect) - ms->EndIO.RequestClear = true; + request_clear = true; else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) - ms->EndIO.RequestClear = true; + request_clear = true; else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) - ms->EndIO.RequestClear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. + request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. + if (request_clear) + { + ms->EndIO.Requests.resize(0); + ms->EndIO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); + } int range_direction; - ms->EndIO.RequestSetRange = true; + ImGuiSelectionRequest req(ImGuiSelectionRequestType_SetRange); if (is_shift && !is_singleselect) { // Shift+Arrow always select // Ctrl+Shift+Arrow copy source selection state (alrady stored by BeginMultiSelect() in RangeSelected) //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); ms->EndIO.RangeSrcItem = (storage->RangeSrcItem != ImGuiSelectionUserData_Invalid) ? storage->RangeSrcItem : item_data; - ms->EndIO.RangeSelected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + req.RangeSelected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; range_direction = ms->RangeSrcPassedBy ? +1 : -1; } else @@ -7419,12 +7436,13 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Ctrl inverts selection, otherwise always select selected = is_ctrl ? !selected : true; ms->EndIO.RangeSrcItem = storage->RangeSrcItem = item_data; - ms->EndIO.RangeSelected = selected; + req.RangeSelected = selected; range_direction = +1; } ImGuiSelectionUserData range_dst_item = item_data; - ms->EndIO.RangeFirstItem = (range_direction > 0) ? ms->EndIO.RangeSrcItem : range_dst_item; - ms->EndIO.RangeLastItem = (range_direction > 0) ? range_dst_item : ms->EndIO.RangeSrcItem; + req.RangeFirstItem = (range_direction > 0) ? ms->EndIO.RangeSrcItem : range_dst_item; + req.RangeLastItem = (range_direction > 0) ? range_dst_item : ms->EndIO.RangeSrcItem; + ms->EndIO.Requests.push_back(req); } // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) From c527cba470066440c6f42a872c8f553007bcafe2 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 22 Sep 2023 15:05:38 +0200 Subject: [PATCH 067/132] MultiSelect: we don't need to ever write to EndIO.RangeSrcItem as this is not meant to be used. --- imgui_widgets.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 17162e393ebc..a3f06f3ad001 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7212,7 +7212,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (ms->IsFocused) { // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here. - if (ms->BeginIO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->BeginIO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here! (see tests) + if (ms->BeginIO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->BeginIO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at begining of the scope (see tests for easy failure) { IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId. ms->Storage->RangeSrcItem = ImGuiSelectionUserData_Invalid; @@ -7427,7 +7427,8 @@ 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) //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); - ms->EndIO.RangeSrcItem = (storage->RangeSrcItem != ImGuiSelectionUserData_Invalid) ? storage->RangeSrcItem : item_data; + if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) + storage->RangeSrcItem = item_data; req.RangeSelected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; range_direction = ms->RangeSrcPassedBy ? +1 : -1; } @@ -7435,13 +7436,13 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) { // Ctrl inverts selection, otherwise always select selected = is_ctrl ? !selected : true; - ms->EndIO.RangeSrcItem = storage->RangeSrcItem = item_data; + storage->RangeSrcItem = item_data; req.RangeSelected = selected; range_direction = +1; } ImGuiSelectionUserData range_dst_item = item_data; - req.RangeFirstItem = (range_direction > 0) ? ms->EndIO.RangeSrcItem : range_dst_item; - req.RangeLastItem = (range_direction > 0) ? range_dst_item : ms->EndIO.RangeSrcItem; + req.RangeFirstItem = (range_direction > 0) ? storage->RangeSrcItem : range_dst_item; + req.RangeLastItem = (range_direction > 0) ? range_dst_item : storage->RangeSrcItem; ms->EndIO.Requests.push_back(req); } From 5941edd9f7ca30226a4afb3c8c23431d405f4f2a Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 22 Sep 2023 15:28:04 +0200 Subject: [PATCH 068/132] MultiSelect: added support for recovery in ErrorCheckEndWindowRecover(). --- imgui.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/imgui.cpp b/imgui.cpp index f932e4efa7e5..ab9cc59b6633 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -10068,6 +10068,11 @@ void ImGui::ErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, vo if (log_callback) log_callback(user_data, "Recovered from missing EndTabBar() in '%s'", window->Name); EndTabBar(); } + while (g.CurrentMultiSelect != NULL && g.CurrentMultiSelect->Storage->Window == window) + { + if (log_callback) log_callback(user_data, "Recovered from missing EndMultiSelect() in '%s'", window->Name); + EndMultiSelect(); + } while (window->DC.TreeDepth > 0) { if (log_callback) log_callback(user_data, "Recovered from missing TreePop() in '%s'", window->Name); From 33fc61a091e0c631594e7354491ce91c233e69ff Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 22 Sep 2023 15:34:25 +0200 Subject: [PATCH 069/132] MultiSelect: use a single ImGuiMultiSelectIO buffer. + using local storage var in EndMultiSelect(), should be no-op. --- imgui_internal.h | 6 +++--- imgui_widgets.cpp | 52 +++++++++++++++++++++++++++-------------------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 9df3b01e8199..63772203d277 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1720,10 +1720,10 @@ struct IMGUI_API ImGuiMultiSelectTempData ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually) ImGuiMultiSelectFlags Flags; ImGuiKeyChord KeyMods; - 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(). + ImGuiMultiSelectIO IO; // Requests are set and returned by BeginMultiSelect()/EndMultiSelect() + written to by user during the loop. bool LoopRequestClear; bool LoopRequestSelectAll; + bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. 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; @@ -1732,7 +1732,7 @@ struct IMGUI_API ImGuiMultiSelectTempData //ImRect Rect; // Extent of selection scope between BeginMultiSelect() / EndMultiSelect(), used by ImGuiMultiSelectFlags_ClearOnClickRectVoid. ImGuiMultiSelectTempData() { Clear(); } - void Clear() { Storage = NULL; FocusScopeId = 0; Flags = 0; KeyMods = 0; BeginIO.Clear(); EndIO.Clear(); LoopRequestClear = LoopRequestSelectAll = IsFocused = IsSetRange = NavIdPassedBy = RangeSrcPassedBy = RangeDstPassedBy = false; } + void Clear() { Storage = NULL; FocusScopeId = 0; Flags = 0; KeyMods = 0; IO.Clear(); IsEndIO = LoopRequestClear = LoopRequestSelectAll = IsFocused = IsSetRange = NavIdPassedBy = RangeSrcPassedBy = RangeDstPassedBy = false; } }; // Persistent storage for multi-select (as long as selection is alive) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index a3f06f3ad001..edf48aa68c31 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7149,10 +7149,10 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) storage->Window = window; ms->Storage = storage; - // We want EndIO's NavIdItem/NavIdSelected to match BeginIO's one, so the value never changes after EndMultiSelect() - ms->BeginIO.RangeSrcItem = ms->EndIO.RangeSrcItem = storage->RangeSrcItem; - ms->BeginIO.NavIdItem = ms->EndIO.NavIdItem = storage->NavIdItem; - ms->BeginIO.NavIdSelected = ms->EndIO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; + ms->IO.RangeSrcItem = storage->RangeSrcItem; + ms->IO.NavIdItem = storage->NavIdItem; + ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; + ms->IO.Requests.resize(0); bool request_clear = false; bool request_select_all = false; @@ -7191,14 +7191,14 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) } if (request_clear || request_select_all) - ms->BeginIO.Requests.push_back(ImGuiSelectionRequest(request_select_all ? ImGuiSelectionRequestType_SelectAll : ImGuiSelectionRequestType_Clear)); + ms->IO.Requests.push_back(ImGuiSelectionRequest(request_select_all ? ImGuiSelectionRequestType_SelectAll : ImGuiSelectionRequestType_Clear)); ms->LoopRequestClear = request_clear; ms->LoopRequestSelectAll = request_select_all; if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) - DebugLogMultiSelectRequests("BeginMultiSelect", &ms->BeginIO); + DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO); - return &ms->BeginIO; + return &ms->IO; } // Return updated ImGuiMultiSelectIO structure. Lifetime: until EndFrame() or next BeginMultiSelect() call. @@ -7206,46 +7206,49 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() { ImGuiContext& g = *GImGui; ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; + ImGuiMultiSelectState* storage = ms->Storage; IM_ASSERT(ms->FocusScopeId == g.CurrentFocusScopeId); - IM_ASSERT(g.CurrentMultiSelect != NULL && ms->Storage->Window == g.CurrentWindow); + IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow); if (ms->IsFocused) { // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here. - if (ms->BeginIO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->BeginIO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at begining of the scope (see tests for easy failure) + if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at begining of the scope (see tests for easy failure) { IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId. - ms->Storage->RangeSrcItem = ImGuiSelectionUserData_Invalid; + storage->RangeSrcItem = ImGuiSelectionUserData_Invalid; } - if (ms->NavIdPassedBy == false && ms->Storage->NavIdItem != ImGuiSelectionUserData_Invalid) + if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid) { IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n"); - ms->Storage->NavIdItem = ImGuiSelectionUserData_Invalid; - ms->Storage->NavIdSelected = -1; + storage->NavIdItem = ImGuiSelectionUserData_Invalid; + storage->NavIdSelected = -1; } } + if (ms->IsEndIO == false) + ms->IO.Requests.resize(0); + // Clear selection when clicking void? // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickWindowVoid) if (IsWindowHovered() && g.HoveredId == 0) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { - ms->EndIO.Requests.resize(0); - ms->EndIO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); + ms->IO.Requests.resize(0); + ms->IO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); } // Unwind ms->FocusScopeId = 0; ms->Flags = ImGuiMultiSelectFlags_None; - ms->BeginIO.Clear(); // Invalidate contents of BeginMultiSelect() to enforce scope. PopFocusScope(); g.CurrentMultiSelect = NULL; if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) - DebugLogMultiSelectRequests("EndMultiSelect", &ms->EndIO); + DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO); - return &ms->EndIO; + return &ms->IO; } void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data) @@ -7260,7 +7263,7 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d { // 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 == selection_user_data) + if (ms->IO.RangeSrcItem == selection_user_data) ms->RangeSrcPassedBy = true; } else @@ -7357,6 +7360,11 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) storage->RangeSrcItem = item_data; storage->RangeSelected = selected; // Will be updated at the end of this function anyway. } + if (ms->IsEndIO == false) + { + ms->IO.Requests.resize(0); + ms->IsEndIO = true; + } // Auto-select as you navigate a list if (g.NavJustMovedToId == id) @@ -7416,8 +7424,8 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. if (request_clear) { - ms->EndIO.Requests.resize(0); - ms->EndIO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); + ms->IO.Requests.resize(0); + ms->IO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); } int range_direction; @@ -7443,7 +7451,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ImGuiSelectionUserData range_dst_item = item_data; req.RangeFirstItem = (range_direction > 0) ? storage->RangeSrcItem : range_dst_item; req.RangeLastItem = (range_direction > 0) ? range_dst_item : storage->RangeSrcItem; - ms->EndIO.Requests.push_back(req); + ms->IO.Requests.push_back(req); } // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) From 3d41994a6326b113fa51dd7b04237c68e06b3ee4 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 27 Sep 2023 14:24:23 +0200 Subject: [PATCH 070/132] MultiSelect: simplify clearing ImGuiMultiSelectTempData. --- imgui_internal.h | 4 ++-- imgui_widgets.cpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 63772203d277..9bb378f23bdb 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1716,11 +1716,11 @@ struct ImGuiOldColumns // Temporary storage for multi-select struct IMGUI_API ImGuiMultiSelectTempData { + ImGuiMultiSelectIO IO; // MUST BE FIRST FIELD. Requests are set and returned by BeginMultiSelect()/EndMultiSelect() + written to by user during the loop. ImGuiMultiSelectState* Storage; ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually) ImGuiMultiSelectFlags Flags; ImGuiKeyChord KeyMods; - ImGuiMultiSelectIO IO; // Requests are set and returned by BeginMultiSelect()/EndMultiSelect() + written to by user during the loop. bool LoopRequestClear; bool LoopRequestSelectAll; bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. @@ -1732,7 +1732,7 @@ struct IMGUI_API ImGuiMultiSelectTempData //ImRect Rect; // Extent of selection scope between BeginMultiSelect() / EndMultiSelect(), used by ImGuiMultiSelectFlags_ClearOnClickRectVoid. ImGuiMultiSelectTempData() { Clear(); } - void Clear() { Storage = NULL; FocusScopeId = 0; Flags = 0; KeyMods = 0; IO.Clear(); IsEndIO = LoopRequestClear = LoopRequestSelectAll = IsFocused = IsSetRange = NavIdPassedBy = RangeSrcPassedBy = RangeDstPassedBy = false; } + void Clear() { size_t io_sz = sizeof(IO); IO.Clear(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); } // Zero-clear except IO }; // Persistent storage for multi-select (as long as selection is alive) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index edf48aa68c31..500be32f1e07 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7129,6 +7129,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ImGuiWindow* window = g.CurrentWindow; ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[0]; IM_ASSERT(g.CurrentMultiSelect == NULL); // No recursion allowed yet (we could allow it if we deem it useful) + IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that. g.CurrentMultiSelect = ms; // FIXME: BeginFocusScope() From bf017954830bc664fb07380b09e5ca1cf7fcf337 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 25 Sep 2023 17:21:19 +0200 Subject: [PATCH 071/132] Demo: Assets Browser: add hit spacing, requierd for box-select patterns. --- imgui_demo.cpp | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 770eb3c9d2ed..9ae3c68cf6e0 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3456,7 +3456,8 @@ static void ShowDemoWindowMultiSelect() // Drag and Drop if (use_drag_drop && ImGui::BeginDragDropSource()) { - // Write payload with full selection OR single unselected item (only possible with ImGuiMultiSelectFlags_SelectOnClickRelease) + // Consider payload to be full selection OR single unselected item. + // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease) if (ImGui::GetDragDropPayload() == NULL) { ImVector payload_items; @@ -9690,8 +9691,10 @@ struct ExampleAssetsBrowser { // Options bool ShowTypeOverlay = true; + bool AllowDragUnselected = false; float IconSize = 32.0f; - int IconSpacing = 7; + int IconSpacing = 10; + int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer. bool StretchSpacing = true; // State @@ -9751,9 +9754,13 @@ struct ExampleAssetsBrowser ImGui::SeparatorText("Contents"); ImGui::Checkbox("Show Type Overlay", &ShowTypeOverlay); + ImGui::SeparatorText("Selection Behavior"); + ImGui::Checkbox("Allow dragging unselected item", &AllowDragUnselected); + ImGui::SeparatorText("Layout"); ImGui::SliderFloat("Icon Size", &IconSize, 16.0f, 128.0f, "%.0f"); ImGui::SliderInt("Icon Spacing", &IconSpacing, 0, 32); + ImGui::SliderInt("Icon Hit Spacing", &IconHitSpacing, 0, 32); ImGui::Checkbox("Stretch Spacing", &StretchSpacing); ImGui::PopItemWidth(); ImGui::EndMenu(); @@ -9820,6 +9827,9 @@ struct ExampleAssetsBrowser // Multi-select ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ClearOnClickWindowVoid; + if (AllowDragUnselected) + ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags); ExampleSelectionAdapter selection_adapter; selection_adapter.Data = this; @@ -9830,7 +9840,8 @@ struct ExampleAssetsBrowser // But it is necessary for two reasons: // - Selectables uses it by default to visually fill the space between two items. // - The vertical spacing would be measured by Clipper to calculate line height if we didn't provide it explicitly (here we do). - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(item_spacing, item_spacing)); + const float selectable_spacing = IM_MAX(floorf(item_spacing) - IconHitSpacing, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(selectable_spacing, selectable_spacing)); // Rendering parameters const ImU32 icon_bg_color = IM_COL32(48, 48, 48, 128); @@ -9875,8 +9886,13 @@ struct ExampleAssetsBrowser // Drag and drop if (ImGui::BeginDragDropSource()) { - ImGui::SetDragDropPayload("ASSETS_BROWSER_ITEMS", "Dummy", 5); - ImGui::Text("%d assets", Selection.Size); + // Consider payload to be full selection OR single unselected item + // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease) + int payload_size = item_is_selected ? Selection.Size : 1; + if (ImGui::GetDragDropPayload() == NULL) + ImGui::SetDragDropPayload("ASSETS_BROWSER_ITEMS", "Dummy", 5); // Dummy payload + + ImGui::Text("%d assets", payload_size); ImGui::EndDragDropSource(); } From 90305c57e43d57338c2743ecc1644f4ca8e5e368 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 25 Sep 2023 19:53:02 +0200 Subject: [PATCH 072/132] MultiSelect: (breaking) renamed ImGuiMultiSelectFlags_ClearOnClickWindowVoid -> ImGuiMultiSelectFlags_ClearOnClickVoid. Added ImGuiMultiSelectFlags_ScopeWindow, ImGuiMultiSelectFlags_ScopeRect. --- imgui.h | 9 +++++---- imgui_demo.cpp | 30 +++++++++++++++++++++++------- imgui_internal.h | 3 ++- imgui_widgets.cpp | 13 +++++++++++-- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/imgui.h b/imgui.h index 2ce1f1983846..53a51c42ab91 100644 --- a/imgui.h +++ b/imgui.h @@ -2734,10 +2734,11 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to use same code/logic is desired, but may not be very useful. ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to set RequestSelectAll ImGuiMultiSelectFlags_ClearOnEscape = 1 << 2, // Clear selection when pressing Escape while scope is focused. - ImGuiMultiSelectFlags_ClearOnClickWindowVoid= 1 << 3, // Clear selection when clicking on empty location within host window (use if BeginMultiSelect() covers a whole window) - //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) - ImGuiMultiSelectFlags_SelectOnClick = 1 << 5, // Apply selection on mouse down when clicking on unselected item. (Default) - ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 6, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 3, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 4, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if (use if BeginMultiSelect() covers a whole window. + ImGuiMultiSelectFlags_ScopeRect = 1 << 5, // Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. Use if multiple BeginMultiSelect() are used in the same host window. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 7, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 8, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. }; // Multi-selection system diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 9ae3c68cf6e0..00b02eb08ef6 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3289,17 +3289,25 @@ static void ShowDemoWindowMultiSelect() static ExampleSelection selections_data[SCOPES_COUNT]; ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection + // Use ImGuiMultiSelectFlags_ScopeRect to not affect other selections in same window. + static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ScopeRect | ImGuiMultiSelectFlags_ClearOnEscape;// | ImGuiMultiSelectFlags_ClearOnClickVoid; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeWindow", &flags, ImGuiMultiSelectFlags_ScopeWindow) && (flags & ImGuiMultiSelectFlags_ScopeWindow)) + flags &= ~ImGuiMultiSelectFlags_ScopeRect; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeRect", &flags, ImGuiMultiSelectFlags_ScopeRect) && (flags & ImGuiMultiSelectFlags_ScopeRect)) + flags &= ~ImGuiMultiSelectFlags_ScopeWindow; + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); + for (int selection_scope_n = 0; selection_scope_n < SCOPES_COUNT; selection_scope_n++) { ExampleSelection* selection = &selections_data[selection_scope_n]; - ImGui::SeparatorText("Selection scope"); - ImGui::Text("Selection size: %d/%d", selection->GetSize(), ITEMS_COUNT); - ImGui::PushID(selection_scope_n); - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; // | ImGuiMultiSelectFlags_ClearOnClickRectVoid ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection->ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); + ImGui::SeparatorText("Selection scope"); + ImGui::Text("Selection size: %d/%d", selection->GetSize(), ITEMS_COUNT); + ImGui::PushID(selection_scope_n); + for (int n = 0; n < ITEMS_COUNT; n++) { char label[64]; @@ -3354,8 +3362,16 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickWindowVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickWindowVoid); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClickRelease", &flags, ImGuiMultiSelectFlags_SelectOnClickRelease); ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeWindow", &flags, ImGuiMultiSelectFlags_ScopeWindow) && (flags & ImGuiMultiSelectFlags_ScopeWindow)) + flags &= ~ImGuiMultiSelectFlags_ScopeRect; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeRect", &flags, ImGuiMultiSelectFlags_ScopeRect) && (flags & ImGuiMultiSelectFlags_ScopeRect)) + flags &= ~ImGuiMultiSelectFlags_ScopeWindow; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClick", &flags, ImGuiMultiSelectFlags_SelectOnClick) && (flags & ImGuiMultiSelectFlags_SelectOnClick)) + flags &= ~ImGuiMultiSelectFlags_SelectOnClickRelease; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClickRelease", &flags, ImGuiMultiSelectFlags_SelectOnClickRelease) && (flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) + flags &= ~ImGuiMultiSelectFlags_SelectOnClick; + ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); // Initialize default list with 1000 items. static ImVector items; @@ -9826,7 +9842,7 @@ struct ExampleAssetsBrowser ImGui::SetCursorScreenPos(start_pos); // Multi-select - ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ClearOnClickWindowVoid; + ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ClearOnClickVoid; if (AllowDragUnselected) ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. diff --git a/imgui_internal.h b/imgui_internal.h index 9bb378f23bdb..6f930aec4e33 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1720,6 +1720,8 @@ struct IMGUI_API ImGuiMultiSelectTempData ImGuiMultiSelectState* Storage; ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually) ImGuiMultiSelectFlags Flags; + ImVec2 ScopeRectMin; + ImVec2 BackupCursorMaxPos; ImGuiKeyChord KeyMods; bool LoopRequestClear; bool LoopRequestSelectAll; @@ -1729,7 +1731,6 @@ struct IMGUI_API ImGuiMultiSelectTempData bool NavIdPassedBy; bool RangeSrcPassedBy; // Set by the item that matches RangeSrcItem. 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. ImGuiMultiSelectTempData() { Clear(); } void Clear() { size_t io_sz = sizeof(IO); IO.Clear(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); } // Zero-clear except IO diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 500be32f1e07..dade5fb38baf 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7138,6 +7138,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ms->FocusScopeId = id; ms->Flags = flags; ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId); + ms->BackupCursorMaxPos = window->DC.CursorMaxPos; + ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos; 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. @@ -7208,6 +7210,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() ImGuiContext& g = *GImGui; ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; ImGuiMultiSelectState* storage = ms->Storage; + ImGuiWindow* window = g.CurrentWindow; IM_ASSERT(ms->FocusScopeId == g.CurrentFocusScopeId); IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow); @@ -7230,17 +7233,23 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (ms->IsEndIO == false) ms->IO.Requests.resize(0); + const ImRect scope_rect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin)); + const bool scope_hovered = (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) ? IsMouseHoveringRect(scope_rect.Min, scope_rect.Max) : IsWindowHovered(); + // Clear selection when clicking void? // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! - if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickWindowVoid) - if (IsWindowHovered() && g.HoveredId == 0) + if (scope_hovered && g.HoveredId == 0) + { + if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { ms->IO.Requests.resize(0); ms->IO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); } + } // Unwind + window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos); ms->FocusScopeId = 0; ms->Flags = ImGuiMultiSelectFlags_None; PopFocusScope(); From f904a6646c2b144c9b28e50b65cc66cd4c6f18b1 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 29 Aug 2023 16:27:31 +0200 Subject: [PATCH 073/132] MultiSelect: Box-Select: added support for ImGuiMultiSelectFlags_BoxSelect. (v11) FIXME: broken on clipping demo. --- imgui.h | 13 ++++---- imgui_demo.cpp | 5 +-- imgui_internal.h | 16 +++++++-- imgui_widgets.cpp | 84 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 11 deletions(-) diff --git a/imgui.h b/imgui.h index 53a51c42ab91..701d995dd60b 100644 --- a/imgui.h +++ b/imgui.h @@ -2733,12 +2733,13 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_None = 0, ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to use same code/logic is desired, but may not be very useful. ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to set RequestSelectAll - ImGuiMultiSelectFlags_ClearOnEscape = 1 << 2, // Clear selection when pressing Escape while scope is focused. - ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 3, // Clear selection when clicking on empty location within scope. - ImGuiMultiSelectFlags_ScopeWindow = 1 << 4, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if (use if BeginMultiSelect() covers a whole window. - ImGuiMultiSelectFlags_ScopeRect = 1 << 5, // Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. Use if multiple BeginMultiSelect() are used in the same host window. - ImGuiMultiSelectFlags_SelectOnClick = 1 << 7, // Apply selection on mouse down when clicking on unselected item. (Default) - ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 8, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 4, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 5, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 6, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if (use if BeginMultiSelect() covers a whole window. + ImGuiMultiSelectFlags_ScopeRect = 1 << 7, // Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. Use if multiple BeginMultiSelect() are used in the same host window. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 8, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 9, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. }; // Multi-selection system diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 00b02eb08ef6..a97972648080 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3299,14 +3299,13 @@ static void ShowDemoWindowMultiSelect() for (int selection_scope_n = 0; selection_scope_n < SCOPES_COUNT; selection_scope_n++) { + ImGui::PushID(selection_scope_n); ExampleSelection* selection = &selections_data[selection_scope_n]; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection->ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); ImGui::SeparatorText("Selection scope"); ImGui::Text("Selection size: %d/%d", selection->GetSize(), ITEMS_COUNT); - ImGui::PushID(selection_scope_n); for (int n = 0; n < ITEMS_COUNT; n++) { @@ -3325,6 +3324,7 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } + // See ShowExampleAppAssetsBrowser() if (ImGui::TreeNode("Multi-Select (tiled assets browser)")) { ImGui::BulletText("See 'Examples->Assets Browser' in menu"); @@ -3361,6 +3361,7 @@ static void ShowDemoWindowMultiSelect() ImGui::Checkbox("Show color button", &show_color_button); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeWindow", &flags, ImGuiMultiSelectFlags_ScopeWindow) && (flags & ImGuiMultiSelectFlags_ScopeWindow)) diff --git a/imgui_internal.h b/imgui_internal.h index 6f930aec4e33..2ce16fb1a796 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1722,6 +1722,10 @@ struct IMGUI_API ImGuiMultiSelectTempData ImGuiMultiSelectFlags Flags; ImVec2 ScopeRectMin; ImVec2 BackupCursorMaxPos; + ImGuiID BoxSelectId; + ImRect BoxSelectRectCurr; // Selection rectangle in absolute coordinates (derived from Storage->BoxSelectStartPosRel + MousePos) + ImRect BoxSelectRectPrev; + ImGuiSelectionUserData BoxSelectLastitem; ImGuiKeyChord KeyMods; bool LoopRequestClear; bool LoopRequestSelectAll; @@ -1733,7 +1737,7 @@ struct IMGUI_API ImGuiMultiSelectTempData bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. ImGuiMultiSelectTempData() { Clear(); } - void Clear() { size_t io_sz = sizeof(IO); IO.Clear(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); } // Zero-clear except IO + void Clear() { size_t io_sz = sizeof(IO); IO.Clear(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); BoxSelectLastitem = -1; } // Zero-clear except IO }; // Persistent storage for multi-select (as long as selection is alive) @@ -1747,8 +1751,15 @@ struct IMGUI_API ImGuiMultiSelectState ImGuiSelectionUserData RangeSrcItem; // ImGuiSelectionUserData NavIdItem; // SetNextItemSelectionUserData() value for NavId (if part of submitted items) + bool BoxSelectActive; + bool BoxSelectStarting; + bool BoxSelectFromVoid; + ImGuiKeyChord BoxSelectKeyMods : 16; // Latched key-mods for box-select logic. + ImVec2 BoxSelectStartPosRel; // Start position in window-relative space (to support scrolling) + ImVec2 BoxSelectEndPosRel; // End position in window-relative space + ImGuiMultiSelectState() { Init(0); } - void Init(ImGuiID id) { Window = NULL; ID = id; LastFrameActive = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; } + void Init(ImGuiID id) { Window = NULL; ID = id; LastFrameActive = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; BoxSelectActive = BoxSelectStarting = BoxSelectFromVoid = false; BoxSelectKeyMods = 0; } }; #endif // #ifdef IMGUI_HAS_MULTI_SELECT @@ -3087,6 +3098,7 @@ namespace ImGui inline ImRect WindowRectAbsToRel(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x - off.x, r.Min.y - off.y, r.Max.x - off.x, r.Max.y - off.y); } inline ImRect WindowRectRelToAbs(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x + off.x, r.Min.y + off.y, r.Max.x + off.x, r.Max.y + off.y); } inline ImVec2 WindowPosRelToAbs(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x + off.x, p.y + off.y); } + inline ImVec2 WindowPosAbsToRel(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x - off.x, p.y - off.y); } // Windows: Display Order and Focus Order IMGUI_API void FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags = 0); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index dade5fb38baf..ec4f53744a2f 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7103,6 +7103,8 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) //------------------------------------------------------------------------- // [SECTION] Widgets: Multi-Select support //------------------------------------------------------------------------- +// - DebugLogMultiSelectRequests() [Internal] +// - BoxSelectStart() [Internal] // - BeginMultiSelect() // - EndMultiSelect() // - SetNextItemSelectionUserData() @@ -7122,6 +7124,15 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe } } +static void BoxSelectStart(ImGuiMultiSelectState* storage, ImGuiSelectionUserData clicked_item) +{ + ImGuiContext& g = *GImGui; + storage->BoxSelectStarting = true; // Consider starting box-select. + storage->BoxSelectFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid); + storage->BoxSelectKeyMods = g.IO.KeyMods; + storage->BoxSelectStartPosRel = storage->BoxSelectEndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); +} + // Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) { @@ -7193,6 +7204,38 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) request_select_all = true; } + // Box-select handling: update active state. + if (flags & ImGuiMultiSelectFlags_BoxSelect) + { + ms->BoxSelectId = GetID("##BoxSelect"); + KeepAliveID(ms->BoxSelectId); + + // BoxSelectStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. + if (storage->BoxSelectStarting && IsMouseDragPastThreshold(0)) + { + storage->BoxSelectActive = true; + storage->BoxSelectStarting = false; + SetActiveID(ms->BoxSelectId, window); + if (storage->BoxSelectFromVoid && (storage->BoxSelectKeyMods & ImGuiMod_Shift) == 0) + request_clear = true; + } + else if ((storage->BoxSelectStarting || storage->BoxSelectActive) && g.IO.MouseDown[0] == false) + { + storage->BoxSelectActive = storage->BoxSelectStarting = false; + if (g.ActiveId == ms->BoxSelectId) + ClearActiveID(); + } + if (storage->BoxSelectActive) + { + ImVec2 start_pos_abs = WindowPosRelToAbs(window, storage->BoxSelectStartPosRel); + ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, storage->BoxSelectEndPosRel); + ms->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); + ms->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); + ms->BoxSelectRectCurr.Min = ImMin(start_pos_abs, g.IO.MousePos); + ms->BoxSelectRectCurr.Max = ImMax(start_pos_abs, g.IO.MousePos); + } + } + if (request_clear || request_select_all) ms->IO.Requests.push_back(ImGuiSelectionRequest(request_select_all ? ImGuiSelectionRequestType_SelectAll : ImGuiSelectionRequestType_Clear)); ms->LoopRequestClear = request_clear; @@ -7228,6 +7271,15 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() storage->NavIdItem = ImGuiSelectionUserData_Invalid; storage->NavIdSelected = -1; } + + // Box-select: render selection rectangle + // FIXME-MULTISELECT: Scroll on box-select + if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && storage->BoxSelectActive) + { + ms->Storage->BoxSelectEndPosRel = WindowPosAbsToRel(window, g.IO.MousePos); + window->DrawList->AddRectFilled(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling + window->DrawList->AddRect(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, GetColorU32(ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling + } } if (ms->IsEndIO == false) @@ -7240,6 +7292,10 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! if (scope_hovered && g.HoveredId == 0) { + if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) + if (!storage->BoxSelectActive && !storage->BoxSelectStarting && g.IO.MouseClickedCount[0] == 1) + BoxSelectStart(storage, ImGuiSelectionUserData_Invalid); + if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { @@ -7385,6 +7441,26 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) selected = pressed = true; } + // Box-select handling + if (ms->Storage->BoxSelectActive) + { + const bool rect_overlap_curr = ms->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); + const bool rect_overlap_prev = ms->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); + if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) + { + selected = !selected; + ImGuiSelectionRequest req(ImGuiSelectionRequestType_SetRange); + req.RangeFirstItem = req.RangeLastItem = item_data; + req.RangeSelected = selected; + ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; + if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->RangeSelected == selected) + prev_req->RangeLastItem = item_data; // Merge span into same request + else + ms->IO.Requests.push_back(req); + } + ms->BoxSelectLastitem = item_data; + } + // Right-click handling: this could be moved at the Selectable() level. // FIXME-MULTISELECT: See https://github.com/ocornut/imgui/pull/5816 if (hovered && IsMouseClicked(1)) @@ -7407,6 +7483,12 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Alter selection if (pressed && (!enter_pressed || !selected)) { + // Box-select + ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) + if (selected == false && !storage->BoxSelectActive && !storage->BoxSelectStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) + BoxSelectStart(storage, item_data); + //---------------------------------------------------------------------------------------- // ACTION | Begin | Pressed/Activated | End //---------------------------------------------------------------------------------------- @@ -7424,7 +7506,6 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst //---------------------------------------------------------------------------------------- - const ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; bool request_clear = false; if (is_singleselect) request_clear = true; @@ -7492,6 +7573,7 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) return; Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected); Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected); + Text("BoxSelect Starting = %d, Active %d", storage->BoxSelectStarting, storage->BoxSelectActive); TreePop(); #else IM_UNUSED(storage); From aa4d64be925da3575bce51111114d5cb0ebe5433 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 26 Sep 2023 13:44:10 +0200 Subject: [PATCH 074/132] MultiSelect: Box-Select: added scroll support. --- imgui.h | 1 + imgui_demo.cpp | 3 ++- imgui_internal.h | 1 + imgui_widgets.cpp | 42 ++++++++++++++++++++++++++++++++++-------- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/imgui.h b/imgui.h index 701d995dd60b..bb6ab85fc016 100644 --- a/imgui.h +++ b/imgui.h @@ -2734,6 +2734,7 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to use same code/logic is desired, but may not be very useful. ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to set RequestSelectAll ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_NoBoxSelectScroll = 1 << 3, // Disable scrolling when box-selecting near edges of scope. ImGuiMultiSelectFlags_ClearOnEscape = 1 << 4, // Clear selection when pressing Escape while scope is focused. ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 5, // Clear selection when clicking on empty location within scope. ImGuiMultiSelectFlags_ScopeWindow = 1 << 6, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if (use if BeginMultiSelect() covers a whole window. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index a97972648080..46ccf7dd77e4 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3362,6 +3362,7 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoBoxSelectScroll", &flags, ImGuiMultiSelectFlags_NoBoxSelectScroll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeWindow", &flags, ImGuiMultiSelectFlags_ScopeWindow) && (flags & ImGuiMultiSelectFlags_ScopeWindow)) @@ -3372,7 +3373,7 @@ static void ShowDemoWindowMultiSelect() flags &= ~ImGuiMultiSelectFlags_SelectOnClickRelease; if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClickRelease", &flags, ImGuiMultiSelectFlags_SelectOnClickRelease) && (flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) flags &= ~ImGuiMultiSelectFlags_SelectOnClick; - ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); + ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); // Initialize default list with 1000 items. static ImVector items; diff --git a/imgui_internal.h b/imgui_internal.h index 2ce16fb1a796..27194656986b 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -490,6 +490,7 @@ static inline int ImModPositive(int a, int b) static inline float ImDot(const ImVec2& a, const ImVec2& b) { return a.x * b.x + a.y * b.y; } static inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a) { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); } static inline float ImLinearSweep(float current, float target, float speed) { if (current < target) return ImMin(current + speed, target); if (current > target) return ImMax(current - speed, target); return current; } +static inline float ImLinearRemapClamp(float s0, float s1, float d0, float d1, float x) { return ImSaturate((x - s0) / (s1 - s0)) * (d1 - d0) + d0; } static inline ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } static inline bool ImIsFloatAboveGuaranteedIntegerPrecision(float f) { return f <= -16777216 || f >= 16777216; } static inline float ImExponentialMovingAverage(float avg, float sample, int n) { avg -= avg / n; avg += sample / n; return avg; } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index ec4f53744a2f..b8b7032aaa37 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7105,6 +7105,7 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) //------------------------------------------------------------------------- // - DebugLogMultiSelectRequests() [Internal] // - BoxSelectStart() [Internal] +// - BoxSelectScrollWithMouseDrag() [Internal] // - BeginMultiSelect() // - EndMultiSelect() // - SetNextItemSelectionUserData() @@ -7133,6 +7134,23 @@ static void BoxSelectStart(ImGuiMultiSelectState* storage, ImGuiSelectionUserDat storage->BoxSelectStartPosRel = storage->BoxSelectEndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); } +static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& r) +{ + ImGuiContext& g = *GImGui; + for (int n = 0; n < 2; n++) + { + float dist = (g.IO.MousePos[n] > r.Max[n]) ? g.IO.MousePos[n] - r.Max[n] : (g.IO.MousePos[n] < r.Min[n]) ? g.IO.MousePos[n] - r.Min[n] : 0.0f; + if (dist == 0.0f || (dist < 0.0f && window->Scroll[n] < 0.0f) || (dist > 0.0f && window->Scroll[n] >= window->ScrollMax[n])) + continue; + float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance + float scroll_step = IM_ROUND(g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime); + if (n == 0) + ImGui::SetScrollX(window, window->Scroll[n] + scroll_step); + else + ImGui::SetScrollY(window, window->Scroll[n] + scroll_step); + } +} + // Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) { @@ -7142,6 +7160,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) IM_ASSERT(g.CurrentMultiSelect == NULL); // No recursion allowed yet (we could allow it if we deem it useful) IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that. g.CurrentMultiSelect = ms; + if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) + flags |= ImGuiMultiSelectFlags_ScopeWindow; // FIXME: BeginFocusScope() const ImGuiID id = window->IDStack.back(); @@ -7257,6 +7277,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() IM_ASSERT(ms->FocusScopeId == g.CurrentFocusScopeId); IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow); + const ImRect scope_rect = (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) ? ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin)) : window->InnerClipRect; if (ms->IsFocused) { // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here. @@ -7272,25 +7293,30 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() storage->NavIdSelected = -1; } - // Box-select: render selection rectangle - // FIXME-MULTISELECT: Scroll on box-select if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && storage->BoxSelectActive) { + // Box-select: render selection rectangle ms->Storage->BoxSelectEndPosRel = WindowPosAbsToRel(window, g.IO.MousePos); - window->DrawList->AddRectFilled(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling - window->DrawList->AddRect(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, GetColorU32(ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling + ImRect box_select_r = ms->BoxSelectRectCurr; + box_select_r.ClipWith(scope_rect); + window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling + window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling + + // Box-select: scroll + ImRect scroll_r = scope_rect; + scroll_r.Expand(g.Style.FramePadding); + if ((ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_NoBoxSelectScroll) == 0 && !scroll_r.Contains(g.IO.MousePos)) + BoxSelectScrollWithMouseDrag(window, scroll_r); } } if (ms->IsEndIO == false) ms->IO.Requests.resize(0); - const ImRect scope_rect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin)); - const bool scope_hovered = (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) ? IsMouseHoveringRect(scope_rect.Min, scope_rect.Max) : IsWindowHovered(); - // Clear selection when clicking void? // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! - if (scope_hovered && g.HoveredId == 0) + const bool scope_hovered = (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) ? scope_rect.Contains(g.IO.MousePos) : IsWindowHovered(); + if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) { if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) if (!storage->BoxSelectActive && !storage->BoxSelectStarting && g.IO.MouseClickedCount[0] == 1) From b747d6fe591452c5d3e5c06be0c9a3ef4e97b4bc Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 6 Oct 2023 17:10:52 +0200 Subject: [PATCH 075/132] MultiSelect: Demo: rework and move selection adapter inside ExampleSelection. --- imgui_demo.cpp | 157 ++++++++++++++++++++++++------------------------- 1 file changed, 76 insertions(+), 81 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 46ccf7dd77e4..5410393355eb 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2773,51 +2773,47 @@ static const char* ExampleNames[] = "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" }; -// Our multi-selection system doesn't make assumption about: -// - how you want to identify items in multi-selection API? Indices(*) / Custom Identifiers / Pointers ? -// - how you want to store persistent selection data? Indices / Custom Identifiers(*) / Pointers ? -// (*) This is the suggested solution: pass indices to API (because easy to iterate/interpolate) + persist your custom identifiers inside selection data. -// In this demo we: -// - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) -// - use a little extra indirection layer in order to abstract how persistent selection data is derived from an index. -// - in some cases we use Index as custom identifier, in some cases we read from some custom item data structure. -// Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. -// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ExampleSelectionAdapter' INDIRECTION LOGIC. -// In theory, for maximum abstraction, this class could contains IndexToUserData() and UserDataToIndex() functions, -// but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that for clarify. -struct ExampleSelectionAdapter -{ - void* Data = NULL; - ImGuiID (*IndexToStorage)(ExampleSelectionAdapter* self, int idx); // Function to convert item index to data stored in persistent selection - - ExampleSelectionAdapter() { SetupForDirectIndexes(); } - - // Example for the simplest case: Index==SelectionStorage (this adapter doesn't even need to use the item data field) - void SetupForDirectIndexes() { IndexToStorage = [](ExampleSelectionAdapter*, int idx) { return (ImGuiID)idx; }; } -}; - // [Advanced] Helper class to store multi-selection state, used by the BeginMultiSelect() demos. // Provide an abstraction layer for the purpose of the demo showcasing different forms of underlying selection data. // To store a single-selection: // - You only need a single variable and don't need any of this! // To store a multi-selection, in your real application you could: -// - A) Use external storage: e.g. std::set, std::set, std::vector, interval trees, etc. +// - A) Use external storage: e.g. std::set, std::vector, std::set, interval trees, etc. // are generally appropriate. Even a large array of bool might work for you... // This code here use ImGuiStorage (a simple key->value storage) as a std::set replacement to avoid external dependencies. // - B) Or use intrusively stored selection (e.g. 'bool IsSelected' inside your object). -// - It is simple, but it means you cannot have multiple simultaneous views over your objects. -// - Some of our features requires you to provide the selection _size_, which with this specific strategy require additional work: -// either you maintain it (which requires storage outside of objects) either you recompute (which may be costly for large sets). +// - That means you cannot have multiple simultaneous views over your objects. +// - Some of our features requires you to provide the selection _size_, which with this specific strategy require additional work. // - So we suggest using intrusive selection for multi-select is not really adequate. +// Our multi-selection system doesn't make assumption about: +// - how you want to identify items in multi-selection API? Indices(*) / Custom Identifiers / Pointers ? +// - how you want to store persistent selection data? Indices / Custom Identifiers(*) / Pointers ? +// (*) This is the suggested solution: pass indices to API (because easy to iterate/interpolate) + persist your custom identifiers inside selection data. +// In this demo we: +// - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) +// - use a little extra indirection layer in order to abstract how persistent selection data is derived from an index. +// - in some cases we use Index as custom identifier +// - in some cases we read an ID from some custom item data structure (this is closer to what you would do in your codebase) +// Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. +// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THE UNNECESSARY 'AdapterIndexToStorageId()' INDIRECTION LOGIC. +// In theory, for maximum abstraction, this class could contains AdapterIndexToUserData() and AdapterUserDataToIndex() functions as well, +// but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that for clarify. struct ExampleSelection { // Data - ImGuiStorage Storage; // Selection set + ImGuiStorage Storage; // Selection set (think of this as similar to e.g. std::set) int Size; // Number of selected items (== number of 1 in the Storage, maintained by this class). bool QueueDeletion; // Request deleting selected items + // Adapter to convert item index to item identifier + // e.g. + // selection.AdapterData = (void*)my_items; + // selection.AdapterIndexToStorageId = [](ExampleSelection* s, int idx) { return ((MyItems**)s->AdapterData)[idx]->ID; }; + void* AdapterData; + ImGuiID (*AdapterIndexToStorageId)(ExampleSelection* self, int idx); + // Functions - ExampleSelection() { Clear(); } + ExampleSelection() { Clear(); AdapterData = NULL; AdapterIndexToStorageId = [](ExampleSelection*, int idx) { return (ImGuiID)idx; };} void Clear() { Storage.Data.resize(0); Size = 0; QueueDeletion = false; } void Swap(ExampleSelection& rhs) { Storage.Data.swap(rhs.Storage.Data); } bool Contains(ImGuiID key) const { return Storage.GetInt(key, 0) != 0; } @@ -2828,8 +2824,8 @@ struct ExampleSelection 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(); } } // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). - // - Must be done in this order! Clear->SelectAll->SetRange. Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. - // - Honoring RequestSetRange requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. + // - 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. // - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection. // - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform // a lookup in order to have some way to iterate/interpolate between two items. @@ -2844,34 +2840,34 @@ struct ExampleSelection // if (req.Type == ImGuiSelectionRequestType_SelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } // if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } // } - void ApplyRequests(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, int items_count) + void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count) { - IM_ASSERT(adapter->IndexToStorage != NULL); + IM_ASSERT(AdapterIndexToStorageId != NULL); for (ImGuiSelectionRequest& req : ms_io->Requests) { - if (req.Type == ImGuiSelectionRequestType_Clear || req.Type == ImGuiSelectionRequestType_SelectAll) + if (req.Type == ImGuiSelectionRequestType_Clear) Clear(); if (req.Type == ImGuiSelectionRequestType_SelectAll) { + Storage.Data.resize(0); Storage.Data.reserve(items_count); for (int idx = 0; idx < items_count; idx++) - AddItem(adapter->IndexToStorage(adapter, idx)); + AddItem(AdapterIndexToStorageId(this, idx)); } if (req.Type == ImGuiSelectionRequestType_SetRange) for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) - UpdateItem(adapter->IndexToStorage(adapter, idx), req.RangeSelected); + UpdateItem(AdapterIndexToStorageId(this, idx), req.RangeSelected); } } // Find which item should be Focused after deletion. - // We output an index in the before-deletion-items list, that user will call SetKeyboardFocusHere() on. + // Call _before_ item submission. Retunr an index in the before-deletion item list, your item loop should call SetKeyboardFocusHere() on it. // The subsequent ApplyDeletionPostLoop() code will use it to apply Selection. // - We cannot provide this logic in core Dear ImGui because we don't have access to selection data. // - We don't actually manipulate the ImVector<> here, only in ApplyDeletionPostLoop(), but using similar API for consistency and flexibility. // - Important: Deletion only works if the underlying ImGuiID for your items are stable: aka not depend on their index, but on e.g. item id/ptr. // FIXME-MULTISELECT: Doesn't take account of the possibility focus target will be moved during deletion. Need refocus or offset. - template - int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, ImVector& items) + int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, int items_count) { QueueDeletion = false; @@ -2884,13 +2880,13 @@ struct ExampleSelection } // If focused item is selected: land on first unselected item after focused item. - for (int idx = focused_idx + 1; idx < items.Size; idx++) - if (!Contains(adapter->IndexToStorage(adapter, idx))) + for (int idx = focused_idx + 1; idx < items_count; idx++) + if (!Contains(AdapterIndexToStorageId(this, idx))) return idx; // If focused item is selected: otherwise return last unselected item before focused item. - for (int idx = IM_MIN(focused_idx, items.Size) - 1; idx >= 0; idx--) - if (!Contains(adapter->IndexToStorage(adapter, idx))) + for (int idx = IM_MIN(focused_idx, items_count) - 1; idx >= 0; idx--) + if (!Contains(AdapterIndexToStorageId(this, idx))) return idx; return -1; @@ -2900,7 +2896,7 @@ struct ExampleSelection // - Call after EndMultiSelect() // - We cannot provide this logic in core Dear ImGui because we don't have access to your items, nor to selection data. template - void ApplyDeletionPostLoop(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, ImVector& items, int item_curr_idx_to_select) + void ApplyDeletionPostLoop(ImGuiMultiSelectIO* ms_io, ImVector& items, int item_curr_idx_to_select) { // Rewrite item list (delete items) + convert old selection index (before deletion) to new selection index (after selection). // If NavId was not part of selection, we will stay on same item. @@ -2909,7 +2905,7 @@ struct ExampleSelection int item_next_idx_to_select = -1; for (int idx = 0; idx < items.Size; idx++) { - if (!Contains(adapter->IndexToStorage(adapter, idx))) + if (!Contains(AdapterIndexToStorageId(this, idx))) new_items.push_back(items[idx]); if (item_curr_idx_to_select == idx) item_next_idx_to_select = new_items.Size - 1; @@ -2919,7 +2915,7 @@ struct ExampleSelection // Update selection Clear(); if (item_next_idx_to_select != -1 && ms_io->NavIdSelected) - AddItem(adapter->IndexToStorage(adapter, item_next_idx_to_select)); + AddItem(AdapterIndexToStorageId(this, item_next_idx_to_select)); } }; @@ -2959,10 +2955,9 @@ struct ExampleDualListBox void ApplySelectionRequests(ImGuiMultiSelectIO* ms_io, int side) { // In this example we store item id in selection (instead of item index) - ExampleSelectionAdapter adapter; - adapter.Data = Items[side].Data; - adapter.IndexToStorage = [](ExampleSelectionAdapter* self, int idx) { return (ImGuiID)((ImGuiID*)self->Data)[idx]; }; - Selections[side].ApplyRequests(ms_io, &adapter, Items[side].Size); + Selections[side].AdapterData = Items[side].Data; + Selections[side].AdapterIndexToStorageId = [](ExampleSelection* self, int idx) { ImGuiID* items = (ImGuiID*)self->AdapterData; return items[idx]; }; + Selections[side].ApplyRequests(ms_io, Items[side].Size); } static int IMGUI_CDECL CompareItemsByValue(const void* lhs, const void* rhs) { @@ -3111,8 +3106,8 @@ static void ShowDemoWindowMultiSelect() IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select"); if (ImGui::TreeNode("Multi-Select")) { + // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection static ExampleSelection selection; - ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection ImGui::Text("Tips: Use 'Debug Log->Selection' to see selection requests as they happen."); @@ -3129,7 +3124,7 @@ static void ShowDemoWindowMultiSelect() { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - selection.ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); + selection.ApplyRequests(ms_io, ITEMS_COUNT); for (int n = 0; n < ITEMS_COUNT; n++) { @@ -3141,7 +3136,7 @@ static void ShowDemoWindowMultiSelect() } ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); + selection.ApplyRequests(ms_io, ITEMS_COUNT); ImGui::EndListBox(); } @@ -3152,8 +3147,8 @@ static void ShowDemoWindowMultiSelect() IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (with clipper)"); if (ImGui::TreeNode("Multi-Select (with clipper)")) { + // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection static ExampleSelection selection; - ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection ImGui::Text("Added features:"); ImGui::BulletText("Using ImGuiListClipper."); @@ -3164,7 +3159,7 @@ static void ShowDemoWindowMultiSelect() { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - selection.ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); + selection.ApplyRequests(ms_io, ITEMS_COUNT); ImGuiListClipper clipper; clipper.Begin(ITEMS_COUNT); @@ -3183,7 +3178,7 @@ static void ShowDemoWindowMultiSelect() } ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); + selection.ApplyRequests(ms_io, ITEMS_COUNT); ImGui::EndListBox(); } @@ -3203,9 +3198,9 @@ static void ShowDemoWindowMultiSelect() { // Intentionally separating items data from selection data! // But you may decide to store selection data inside your item (aka intrusive storage). + // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection static ImVector items; static ExampleSelection selection; - ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection ImGui::Text("Adding features:"); ImGui::BulletText("Dynamic list with Delete key support."); @@ -3230,7 +3225,7 @@ static void ShowDemoWindowMultiSelect() { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - selection.ApplyRequests(ms_io, &selection_adapter, items.Size); + selection.ApplyRequests(ms_io, items.Size); // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. @@ -3238,7 +3233,7 @@ static void ShowDemoWindowMultiSelect() const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); int item_curr_idx_to_focus = -1; if (want_delete) - item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, &selection_adapter, items); + item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, items.Size); for (int n = 0; n < items.Size; n++) { @@ -3255,9 +3250,9 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, &selection_adapter, items.Size); + selection.ApplyRequests(ms_io, items.Size); if (want_delete) - selection.ApplyDeletionPostLoop(ms_io, &selection_adapter, items, item_curr_idx_to_focus); + selection.ApplyDeletionPostLoop(ms_io, items, item_curr_idx_to_focus); ImGui::EndListBox(); } @@ -3284,10 +3279,10 @@ static void ShowDemoWindowMultiSelect() IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (multiple scopes)"); if (ImGui::TreeNode("Multi-Select (multiple scopes)")) { + // Use default select: Pass index to SetNextItemSelectionUserData(), store index in Selection const int SCOPES_COUNT = 3; const int ITEMS_COUNT = 8; // Per scope static ExampleSelection selections_data[SCOPES_COUNT]; - ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection // Use ImGuiMultiSelectFlags_ScopeRect to not affect other selections in same window. static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ScopeRect | ImGuiMultiSelectFlags_ClearOnEscape;// | ImGuiMultiSelectFlags_ClearOnClickVoid; @@ -3302,7 +3297,7 @@ static void ShowDemoWindowMultiSelect() ImGui::PushID(selection_scope_n); ExampleSelection* selection = &selections_data[selection_scope_n]; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - selection->ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); + selection->ApplyRequests(ms_io, ITEMS_COUNT); ImGui::SeparatorText("Selection scope"); ImGui::Text("Selection size: %d/%d", selection->GetSize(), ITEMS_COUNT); @@ -3318,7 +3313,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection->ApplyRequests(ms_io, &selection_adapter, ITEMS_COUNT); + selection->ApplyRequests(ms_io, ITEMS_COUNT); ImGui::PopID(); } ImGui::TreePop(); @@ -3376,11 +3371,11 @@ static void ShowDemoWindowMultiSelect() ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); // Initialize default list with 1000 items. + // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection static ImVector items; static int items_next_id = 0; if (items_next_id == 0) { for (int n = 0; n < 1000; n++) { items.push_back(items_next_id++); } } static ExampleSelection selection; - ExampleSelectionAdapter selection_adapter; // Use default: Pass index to SetNextItemSelectionUserData(), store index in Selection ImGui::Text("Selection size: %d/%d", selection.GetSize(), items.Size); @@ -3393,14 +3388,14 @@ static void ShowDemoWindowMultiSelect() ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - selection.ApplyRequests(ms_io, &selection_adapter, items.Size); + selection.ApplyRequests(ms_io, items.Size); // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to send a helper/optional "delete" signal. // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); int item_curr_idx_to_focus = -1; if (want_delete) - item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, &selection_adapter, items); + item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, items.Size); if (show_in_table) { @@ -3539,9 +3534,9 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, &selection_adapter, items.Size); + selection.ApplyRequests(ms_io, items.Size); if (want_delete) - selection.ApplyDeletionPostLoop(ms_io, &selection_adapter, items, item_curr_idx_to_focus); + selection.ApplyDeletionPostLoop(ms_io, items, item_curr_idx_to_focus); if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); @@ -9667,10 +9662,10 @@ void ShowExampleAppDocuments(bool* p_open) struct ExampleAsset { - int ID; + ImGuiID ID; int Type; - ExampleAsset(int id, int type) { ID = id; Type = type; } + ExampleAsset(ImGuiID id, int type) { ID = id; Type = type; } static const ImGuiTableSortSpecs* s_current_sort_specs; @@ -9692,7 +9687,7 @@ struct ExampleAsset const ImGuiTableColumnSortSpecs* sort_spec = &s_current_sort_specs->Specs[n]; int delta = 0; if (sort_spec->ColumnIndex == 0) - delta = (a->ID - b->ID); + delta = ((int)a->ID - (int)b->ID); else if (sort_spec->ColumnIndex == 1) delta = (a->Type - b->Type); if (delta > 0) @@ -9700,7 +9695,7 @@ struct ExampleAsset if (delta < 0) return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? -1 : +1; } - return (a->ID - b->ID); + return ((int)a->ID - (int)b->ID); } }; const ImGuiTableSortSpecs* ExampleAsset::s_current_sort_specs = NULL; @@ -9718,7 +9713,7 @@ struct ExampleAssetsBrowser // State ImVector Items; ExampleSelection Selection; - int NextItemId = 0; + ImGuiID NextItemId = 0; bool SortDirty = false; float ZoomWheelAccum = 0.0f; @@ -9847,12 +9842,12 @@ struct ExampleAssetsBrowser ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ClearOnClickVoid; if (AllowDragUnselected) ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags); - ExampleSelectionAdapter selection_adapter; - selection_adapter.Data = this; - selection_adapter.IndexToStorage = [](ExampleSelectionAdapter* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->Data; return (ImGuiID)self->Items[idx].ID; }; - Selection.ApplyRequests(ms_io, &selection_adapter, Items.Size); + + // Use custom selection adapter: store ID in selection (recommended) + Selection.AdapterData = this; + Selection.AdapterIndexToStorageId = [](ExampleSelection* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->AdapterData; return self->Items[idx].ID; }; + Selection.ApplyRequests(ms_io, Items.Size); // Altering ItemSpacing may seem unnecessary as we position every items using SetCursorScreenPos()... // But it is necessary for two reasons: @@ -9881,7 +9876,7 @@ struct ExampleAssetsBrowser for (int item_idx = item_min_idx_for_current_line; item_idx < item_max_idx_for_current_line; ++item_idx) { ExampleAsset* item_data = &Items[item_idx]; - ImGui::PushID(item_data->ID); + ImGui::PushID((int)item_data->ID); // Position item ImVec2 pos = ImVec2(start_pos.x + (item_idx % column_count) * (item_size.x + item_spacing), start_pos.y + (line_idx * line_height)); @@ -9946,7 +9941,7 @@ struct ExampleAssetsBrowser ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing ms_io = ImGui::EndMultiSelect(); - Selection.ApplyRequests(ms_io, &selection_adapter, Items.Size); + Selection.ApplyRequests(ms_io, Items.Size); // FIXME-MULTISELECT: Find a way to expose this in public API. This currently requires "imgui_internal.h" //ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); From 0af6fbb51d5797b9245e97ed3d061917a296302b Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 26 Oct 2023 17:20:33 +0200 Subject: [PATCH 076/132] MultiSelect: added support for nested/stacked BeginMultiSelect(). Mimicking table logic, reusing amortized buffers. --- imgui.cpp | 3 +++ imgui.h | 3 +++ imgui_internal.h | 6 ++++-- imgui_widgets.cpp | 14 +++++++++----- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index ab9cc59b6633..3128892f0411 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3819,6 +3819,7 @@ void ImGui::Shutdown() g.DrawChannelsTempMergeBuffer.clear(); g.MultiSelectStorage.Clear(); + g.MultiSelectTempData.clear_destruct(); g.ClipboardHandlerData.clear(); g.MenusIdSubmittedThisFrame.clear(); @@ -3930,6 +3931,8 @@ void ImGui::GcCompactTransientMiscBuffers() ImGuiContext& g = *GImGui; g.ItemFlagsStack.clear(); g.GroupStack.clear(); + g.MultiSelectTempDataStacked = 0; + g.MultiSelectTempData.clear_destruct(); TableGcCompactSettings(); } diff --git a/imgui.h b/imgui.h index bb6ab85fc016..31b6bcafa20f 100644 --- a/imgui.h +++ b/imgui.h @@ -2811,6 +2811,9 @@ struct ImGuiSelectionRequest ImGuiSelectionRequest(ImGuiSelectionRequestType type = ImGuiSelectionRequestType_None) { Type = type; RangeSelected = false; RangeFirstItem = RangeLastItem = (ImGuiSelectionUserData)-1; } }; +// Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). +// Read the large comments block above for details. +// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). struct ImGuiMultiSelectIO { ImVector Requests; // ms:w, app:r / ms:w app:r // Requests diff --git a/imgui_internal.h b/imgui_internal.h index 27194656986b..8062edad8967 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -2200,8 +2200,9 @@ struct ImGuiContext ImVector ShrinkWidthBuffer; // Multi-Select state - ImGuiMultiSelectTempData* CurrentMultiSelect; // FIXME-MULTISELECT: We currently don't support recursing/stacking multi-select - ImGuiMultiSelectTempData MultiSelectTempData[1]; + ImGuiMultiSelectTempData* CurrentMultiSelect; + int MultiSelectTempDataStacked; // Temporary multi-select data size (because we leave previous instances undestructed, we generally don't use MultiSelectTempData.Size) + ImVector MultiSelectTempData; ImPool MultiSelectStorage; // Hover Delay system @@ -2445,6 +2446,7 @@ struct ImGuiContext TablesTempDataStacked = 0; CurrentTabBar = NULL; CurrentMultiSelect = NULL; + MultiSelectTempDataStacked = 0; HoverItemDelayId = HoverItemDelayIdPreviousFrame = HoverItemUnlockedStationaryId = HoverWindowUnlockedStationaryId = 0; HoverItemDelayTimer = HoverItemDelayClearTimer = 0.0f; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index b8b7032aaa37..a20ca6b4bbcf 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7156,8 +7156,10 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[0]; - IM_ASSERT(g.CurrentMultiSelect == NULL); // No recursion allowed yet (we could allow it if we deem it useful) + + if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size) + g.MultiSelectTempData.resize(g.MultiSelectTempDataStacked, ImGuiMultiSelectTempData()); + ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1]; IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that. g.CurrentMultiSelect = ms; if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) @@ -7276,6 +7278,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() ImGuiWindow* window = g.CurrentWindow; IM_ASSERT(ms->FocusScopeId == g.CurrentFocusScopeId); IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow); + IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect); const ImRect scope_rect = (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) ? ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin)) : window->InnerClipRect; if (ms->IsFocused) @@ -7332,14 +7335,15 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() // Unwind window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos); - ms->FocusScopeId = 0; - ms->Flags = ImGuiMultiSelectFlags_None; PopFocusScope(); - g.CurrentMultiSelect = NULL; if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO); + ms->FocusScopeId = 0; + ms->Flags = ImGuiMultiSelectFlags_None; + g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL; + return &ms->IO; } From e0282347db70bf6817990dd114e9b20d80ebc658 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 1 Dec 2023 15:03:43 +0100 Subject: [PATCH 077/132] MultiSelect: remove ImGuiSelectionRequest/ImGuiMultiSelectIO details from public api to reduce confusion + comments. --- imgui.h | 56 +++++++++++++++++++++-------------------------- imgui_internal.h | 3 ++- imgui_widgets.cpp | 24 ++++++++++---------- 3 files changed, 40 insertions(+), 43 deletions(-) diff --git a/imgui.h b/imgui.h index 31b6bcafa20f..5f59d5ce9f50 100644 --- a/imgui.h +++ b/imgui.h @@ -2753,18 +2753,17 @@ enum ImGuiMultiSelectFlags_ // - In the spirit of Dear ImGui design, your code owns actual selection data. // This is designed to allow all kinds of selection storage you may use in your application: // e.g. set/map/hash (store only selected items), instructive selection (store a bool inside each object), -// external array (store an array in your view data, next to your objects), or other structures (store indices -// in an interval tree), etc. +// external array (store an array in your view data, next to your objects) etc. // - The work involved to deal with multi-selection differs whether you want to only submit visible items and // clip others, or submit all items regardless of their visibility. Clipping items is more efficient and will -// allow you to deal with large lists (1k~100k items) with no performance penalty, but requires a little more -// work on the code. -// For small selection set (<100 items), you might want to not bother with using the clipper, as the cost -// should be negligible (as least on Dear ImGui side). +// allow you to deal with large lists (1k~100k items). See "Usage flow" section below for details. // If you are not sure, always start without clipping! You can work your way to the optimized version afterwards. +// About ImGuiSelectionBasicStorage: +// - This is an optional helper to store a selection state and apply selection requests. +// - It is used by our demos and provided as a convenience if you want to quickly implement multi-selection. // About ImGuiSelectionUserData: // - This is your application-defined identifier in a selection set: -// - For each item is submitted by your calls to SetNextItemSelectionUserData(). +// - For each item is it submitted by your call to SetNextItemSelectionUserData(). // - In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO. // - Most applications will store an object INDEX, hence the chosen name and type. // Storing an integer index is the easiest thing to do, as RequestSetRange requests will give you two end-points @@ -2773,8 +2772,7 @@ enum ImGuiMultiSelectFlags_ // that you identify items by indices. It never attempt to iterate/interpolate between 2 ImGuiSelectionUserData values. // - As most users will want to cast this to integer, for convenience and to reduce confusion we use ImS64 instead // of void*, being syntactically easier to downcast. But feel free to reinterpret_cast a pointer into this. -// - You may store another type of (e.g. an data) but this may make your lookups and the interpolation between -// two values more cumbersome. +// - You may store another type as long as you can interpolate between two values. // - If you need to wrap this API for another language/framework, feel free to expose this as 'int' if simpler. // Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. @@ -2786,8 +2784,8 @@ enum ImGuiMultiSelectFlags_ // If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable()/TreeNode on a per-item basis. // However it is perfectly fine to honor all steps even if you don't use a clipper. // Advanced: -// - Deletion: If you need to handle items deletion a little more work if needed for post-deletion focus and scrolling to be correct. -// refer to 'Demo->Widgets->Selection State' for demos supporting deletion. +// - Deletion: If you need to handle items deletion: more work if needed for post-deletion focus and scrolling to be correct. +// Refer to 'Demo->Widgets->Selection State' for demos supporting deletion. enum ImGuiSelectionRequestType { @@ -2797,33 +2795,29 @@ enum ImGuiSelectionRequestType ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items based on 'bool RangeSelected'. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. }; -// List of requests stored in ImGuiMultiSelectIO -// - Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. -// - Some fields are only necessary if your list is dynamic and allows deletion (handling deletion and getting "post-deletion" state right is shown 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. struct ImGuiSelectionRequest { - ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r - bool RangeSelected; // / ms:w, app:r // Parameter for SetRange request (true = select range, false = unselect range) - ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) - ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) - - ImGuiSelectionRequest(ImGuiSelectionRequestType type = ImGuiSelectionRequestType_None) { Type = type; RangeSelected = false; RangeFirstItem = RangeLastItem = (ImGuiSelectionUserData)-1; } + //------------------------------------------// BeginMultiSelect / EndMultiSelect + ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r // Request type. You'll most often receive 1 Clear + 1 SetRange with a single-item range. + bool RangeSelected; // / ms:w, app:r // Parameter for SetRange request (true = select range, false = unselect range) + ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) + ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) }; // Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). -// Read the large comments block above for details. -// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). +// This mainly contains a list of selection requests. Read the large comments block above for details. +// - Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. +// - Some fields are only useful if your list is dynamic and allows deletion (handling deletion and getting "post-deletion" state right is shown 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. +// - Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). struct ImGuiMultiSelectIO { - ImVector Requests; // ms:w, app:r / ms:w app:r // Requests - ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! - ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). - bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). - bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). - - ImGuiMultiSelectIO() { Clear(); } - void Clear() { Requests.resize(0); RangeSrcItem = NavIdItem = (ImGuiSelectionUserData)-1; NavIdSelected = RangeSrcReset = false; } + //------------------------------------------// BeginMultiSelect / EndMultiSelect + ImVector Requests; // ms:w, app:r / ms:w app:r // Requests + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! + ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). + bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). }; //----------------------------------------------------------------------------- diff --git a/imgui_internal.h b/imgui_internal.h index 8062edad8967..65eb5eb897c5 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1738,7 +1738,8 @@ struct IMGUI_API ImGuiMultiSelectTempData bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. ImGuiMultiSelectTempData() { Clear(); } - void Clear() { size_t io_sz = sizeof(IO); IO.Clear(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); BoxSelectLastitem = -1; } // Zero-clear except IO + void Clear() { size_t io_sz = sizeof(IO); ClearIO(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); BoxSelectLastitem = -1; } // Zero-clear except IO as we preserve IO.Requests[] buffer allocation. + void ClearIO() { IO.Requests.resize(0); IO.RangeSrcItem = IO.NavIdItem = (ImGuiSelectionUserData)-1; IO.NavIdSelected = IO.RangeSrcReset = false; } }; // Persistent storage for multi-select (as long as selection is alive) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index a20ca6b4bbcf..677e34beda28 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7259,7 +7259,10 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) } if (request_clear || request_select_all) - ms->IO.Requests.push_back(ImGuiSelectionRequest(request_select_all ? ImGuiSelectionRequestType_SelectAll : ImGuiSelectionRequestType_Clear)); + { + ImGuiSelectionRequest req = { request_select_all ? ImGuiSelectionRequestType_SelectAll : ImGuiSelectionRequestType_Clear, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; + ms->IO.Requests.push_back(req); + } ms->LoopRequestClear = request_clear; ms->LoopRequestSelectAll = request_select_all; @@ -7328,8 +7331,9 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_Clear, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; ms->IO.Requests.resize(0); - ms->IO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); + ms->IO.Requests.push_back(req); } } @@ -7479,9 +7483,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) { selected = !selected; - ImGuiSelectionRequest req(ImGuiSelectionRequestType_SetRange); - req.RangeFirstItem = req.RangeLastItem = item_data; - req.RangeSelected = selected; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->RangeSelected == selected) prev_req->RangeLastItem = item_data; // Merge span into same request @@ -7545,12 +7547,13 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. if (request_clear) { + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_Clear, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; ms->IO.Requests.resize(0); - ms->IO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); + ms->IO.Requests.push_back(req); } int range_direction; - ImGuiSelectionRequest req(ImGuiSelectionRequestType_SetRange); + bool range_selected; if (is_shift && !is_singleselect) { // Shift+Arrow always select @@ -7558,7 +7561,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) storage->RangeSrcItem = item_data; - req.RangeSelected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; range_direction = ms->RangeSrcPassedBy ? +1 : -1; } else @@ -7566,12 +7569,11 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Ctrl inverts selection, otherwise always select selected = is_ctrl ? !selected : true; storage->RangeSrcItem = item_data; - req.RangeSelected = selected; + range_selected = selected; range_direction = +1; } ImGuiSelectionUserData range_dst_item = item_data; - req.RangeFirstItem = (range_direction > 0) ? storage->RangeSrcItem : range_dst_item; - req.RangeLastItem = (range_direction > 0) ? range_dst_item : storage->RangeSrcItem; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, range_selected, (range_direction > 0) ? storage->RangeSrcItem : range_dst_item, (range_direction > 0) ? range_dst_item : storage->RangeSrcItem }; ms->IO.Requests.push_back(req); } From 0f633c1d99b6097f63b2cb8f26622c7bcd0a556b Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 1 Dec 2023 17:49:47 +0100 Subject: [PATCH 078/132] MultiSelect: move demo's ExampleSelection to main api as a convenient ImGuiSelectionBasicStorage for basic users. --- imgui.h | 53 ++++++++++++++++++++- imgui_demo.cpp | 119 ++++++++-------------------------------------- imgui_widgets.cpp | 39 +++++++++++++++ 3 files changed, 109 insertions(+), 102 deletions(-) diff --git a/imgui.h b/imgui.h index 5f59d5ce9f50..1361e902b43d 100644 --- a/imgui.h +++ b/imgui.h @@ -44,7 +44,7 @@ Index of this file: // [SECTION] ImGuiIO // [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) // [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) -// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO, ImGuiSelectionBasicStorage) // [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) // [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) // [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) @@ -179,6 +179,7 @@ struct ImGuiMultiSelectIO; // Structure to interact with a BeginMultiSe struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame struct ImGuiPayload; // User data payload for drag and drop operations struct ImGuiPlatformImeData; // Platform IME data for io.PlatformSetImeDataFn() function. +struct ImGuiSelectionBasicStorage; // Helper struct to store multi-selection state + apply multi-selection requests. struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) struct ImGuiStorage; // Helper for key->value storage (container sorted by key) struct ImGuiStoragePair; // Helper for key->value storage (pair) @@ -2720,7 +2721,7 @@ struct ImColor }; //----------------------------------------------------------------------------- -// [SECTION] Multi-Select API flags & structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO, ImGuiSelectionBasicStorage) //----------------------------------------------------------------------------- #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. @@ -2820,6 +2821,54 @@ struct ImGuiMultiSelectIO bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). }; +// Helper struct to store multi-selection state + apply multi-selection requests. +// - Used by our demos and provided as a convenience if you want to quickly implement multi-selection. +// - Provide an abstraction layer for the purpose of the demo showcasing different forms of underlying selection data. +// - USING THIS IS NOT MANDATORY. This is only a helper and is not part of the main API. Advanced users are likely to implement their own. +// To store a single-selection, you only need a single variable and don't need any of this! +// To store a multi-selection, in your real application you could: +// - A) Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement. +// - B) Use your own external storage: e.g. std::set, std::vector, std::set, interval trees, etc. +// are generally appropriate. Even a large array of bool might work for you... +// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside your object). Not recommended because: +// - it means you cannot have multiple simultaneous views over your objects. +// - some of our features requires you to provide the selection _size_, which with this specific strategy require additional work. +// Our BeginMultiSelect() api/system doesn't make assumption about: +// - how you want to identify items in multi-selection API? Indices(*) / Custom Identifiers / Pointers ? +// - how you want to store persistent selection data? Indices / Custom Identifiers(*) / Pointers ? +// (*) This is the suggested solution: pass indices to API (because easy to iterate/interpolate) + persist your custom identifiers inside selection data. +// In ImGuiSelectionBasicStorage we: +// - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) +// - use a little extra indirection layer in order to abstract how persistent selection data is derived from an index. +// - in some cases we use Index as custom identifier (default, not ideal) +// - in some cases we read an ID from some custom item data structure (better, and closer to what you would do in your codebase) +// Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. +// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THE UNNECESSARY 'AdapterIndexToStorageId()' INDIRECTION LOGIC. +// In theory, for maximum abstraction, this class could contains AdapterIndexToUserData() and AdapterUserDataToIndex() functions as well, +// but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that for clarify. +struct ImGuiSelectionBasicStorage +{ + ImGuiStorage Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set + int Size; // Number of selected items (== number of 1 in the Storage, maintained by this class). + + // Adapter to convert item index to item identifier + void* AdapterData; // e.g. selection.AdapterData = (void*)my_items; + ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->AdapterData)[idx]->ID; }; + + // Selection storage + ImGuiSelectionBasicStorage() { Clear(); AdapterData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } + void Clear() { Storage.Data.resize(0); Size = 0; } + void Swap(ImGuiSelectionBasicStorage& r){ Storage.Data.swap(r.Storage.Data); } + bool Contains(ImGuiID key) const { return Storage.GetInt(key, 0) != 0; } + void AddItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int != 0) return; *p_int = 1; Size++; } + void RemoveItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int == 0) return; *p_int = 0; Size--; } + void UpdateItem(ImGuiID key, bool v) { if (v) { AddItem(key); } else { RemoveItem(key); } } + int GetSize() const { return Size; } + + // Request handling (apply requests coming from BeginMultiSelect() and EndMultiSelect() functions) + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count); +}; + //----------------------------------------------------------------------------- // [SECTION] Drawing API (ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) // Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 5410393355eb..291d50e8c01a 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2773,92 +2773,9 @@ static const char* ExampleNames[] = "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" }; -// [Advanced] Helper class to store multi-selection state, used by the BeginMultiSelect() demos. -// Provide an abstraction layer for the purpose of the demo showcasing different forms of underlying selection data. -// To store a single-selection: -// - You only need a single variable and don't need any of this! -// To store a multi-selection, in your real application you could: -// - A) Use external storage: e.g. std::set, std::vector, std::set, interval trees, etc. -// are generally appropriate. Even a large array of bool might work for you... -// This code here use ImGuiStorage (a simple key->value storage) as a std::set replacement to avoid external dependencies. -// - B) Or use intrusively stored selection (e.g. 'bool IsSelected' inside your object). -// - That means you cannot have multiple simultaneous views over your objects. -// - Some of our features requires you to provide the selection _size_, which with this specific strategy require additional work. -// - So we suggest using intrusive selection for multi-select is not really adequate. -// Our multi-selection system doesn't make assumption about: -// - how you want to identify items in multi-selection API? Indices(*) / Custom Identifiers / Pointers ? -// - how you want to store persistent selection data? Indices / Custom Identifiers(*) / Pointers ? -// (*) This is the suggested solution: pass indices to API (because easy to iterate/interpolate) + persist your custom identifiers inside selection data. -// In this demo we: -// - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) -// - use a little extra indirection layer in order to abstract how persistent selection data is derived from an index. -// - in some cases we use Index as custom identifier -// - in some cases we read an ID from some custom item data structure (this is closer to what you would do in your codebase) -// Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. -// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THE UNNECESSARY 'AdapterIndexToStorageId()' INDIRECTION LOGIC. -// In theory, for maximum abstraction, this class could contains AdapterIndexToUserData() and AdapterUserDataToIndex() functions as well, -// but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that for clarify. -struct ExampleSelection +struct ExampleSelectionStorageWithDeletion : ImGuiSelectionBasicStorage { - // Data - ImGuiStorage Storage; // Selection set (think of this as similar to e.g. std::set) - int Size; // Number of selected items (== number of 1 in the Storage, maintained by this class). - bool QueueDeletion; // Request deleting selected items - - // Adapter to convert item index to item identifier - // e.g. - // selection.AdapterData = (void*)my_items; - // selection.AdapterIndexToStorageId = [](ExampleSelection* s, int idx) { return ((MyItems**)s->AdapterData)[idx]->ID; }; - void* AdapterData; - ImGuiID (*AdapterIndexToStorageId)(ExampleSelection* self, int idx); - - // Functions - ExampleSelection() { Clear(); AdapterData = NULL; AdapterIndexToStorageId = [](ExampleSelection*, int idx) { return (ImGuiID)idx; };} - void Clear() { Storage.Data.resize(0); Size = 0; QueueDeletion = false; } - void Swap(ExampleSelection& rhs) { Storage.Data.swap(rhs.Storage.Data); } - bool Contains(ImGuiID key) const { return Storage.GetInt(key, 0) != 0; } - void AddItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int != 0) return; *p_int = 1; Size++; } - void RemoveItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int == 0) return; *p_int = 0; Size--; } - void UpdateItem(ImGuiID key, bool v){ if (v) AddItem(key); else RemoveItem(key); } - int GetSize() const { return Size; } - 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(); } } - - // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). - // - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. - // - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. - // - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection. - // - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform - // a lookup in order to have some way to iterate/interpolate between two items. - // - A full-featured application is likely to allow search/filtering which is likely to lead to using indices - // and constructing a view index <> object id/ptr data structure anyway. - // WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ExampleSelectionAdapter' INDIRECTION LOGIC. - // Notice that with the simplest adapter (using indices everywhere), all functions return their parameters. - // The most simple implementation (using indices everywhere) would look like: - // for (ImGuiSelectionRequest& req : ms_io->Requests) - // { - // if (req.Type == ImGuiSelectionRequestType_Clear) { Clear(); } - // if (req.Type == ImGuiSelectionRequestType_SelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } - // if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } - // } - void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count) - { - IM_ASSERT(AdapterIndexToStorageId != NULL); - for (ImGuiSelectionRequest& req : ms_io->Requests) - { - if (req.Type == ImGuiSelectionRequestType_Clear) - Clear(); - if (req.Type == ImGuiSelectionRequestType_SelectAll) - { - Storage.Data.resize(0); - Storage.Data.reserve(items_count); - for (int idx = 0; idx < items_count; idx++) - AddItem(AdapterIndexToStorageId(this, idx)); - } - if (req.Type == ImGuiSelectionRequestType_SetRange) - for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) - UpdateItem(AdapterIndexToStorageId(this, idx), req.RangeSelected); - } - } + bool QueueDeletion = false; // Track request deleting selected items // Find which item should be Focused after deletion. // Call _before_ item submission. Retunr an index in the before-deletion item list, your item loop should call SetKeyboardFocusHere() on it. @@ -2870,6 +2787,8 @@ struct ExampleSelection int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, int items_count) { QueueDeletion = false; + if (Size == 0) + return -1; // If focused item is not selected... const int focused_idx = (int)ms_io->NavIdItem; // Index of currently focused item @@ -2922,9 +2841,9 @@ struct ExampleSelection // Example: Implement dual list box storage and interface struct ExampleDualListBox { - ImVector Items[2]; // ID is index into ExampleName[] - ExampleSelection Selections[2]; // Store ExampleItemId into selection - bool OptKeepSorted = true; + ImVector Items[2]; // ID is index into ExampleName[] + ImGuiSelectionBasicStorage Selections[2]; // Store ExampleItemId into selection + bool OptKeepSorted = true; void MoveAll(int src, int dst) { @@ -2956,7 +2875,7 @@ struct ExampleDualListBox { // In this example we store item id in selection (instead of item index) Selections[side].AdapterData = Items[side].Data; - Selections[side].AdapterIndexToStorageId = [](ExampleSelection* self, int idx) { ImGuiID* items = (ImGuiID*)self->AdapterData; return items[idx]; }; + Selections[side].AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { ImGuiID* items = (ImGuiID*)self->AdapterData; return items[idx]; }; Selections[side].ApplyRequests(ms_io, Items[side].Size); } static int IMGUI_CDECL CompareItemsByValue(const void* lhs, const void* rhs) @@ -2986,7 +2905,7 @@ struct ExampleDualListBox // FIXME-MULTISELECT: Dual List Box: Add context menus // FIXME-NAV: Using ImGuiWindowFlags_NavFlattened exhibit many issues. ImVector& items = Items[side]; - ExampleSelection& selection = Selections[side]; + ImGuiSelectionBasicStorage& selection = Selections[side]; ImGui::TableSetColumnIndex((side == 0) ? 0 : 2); ImGui::Text("%s (%d)", (side == 0) ? "Available" : "Basket", items.Size); @@ -3107,7 +3026,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::TreeNode("Multi-Select")) { // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection - static ExampleSelection selection; + static ImGuiSelectionBasicStorage selection; ImGui::Text("Tips: Use 'Debug Log->Selection' to see selection requests as they happen."); @@ -3148,7 +3067,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::TreeNode("Multi-Select (with clipper)")) { // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection - static ExampleSelection selection; + static ImGuiSelectionBasicStorage selection; ImGui::Text("Added features:"); ImGui::BulletText("Using ImGuiListClipper."); @@ -3200,13 +3119,13 @@ static void ShowDemoWindowMultiSelect() // But you may decide to store selection data inside your item (aka intrusive storage). // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection static ImVector items; - static ExampleSelection selection; + static ExampleSelectionStorageWithDeletion selection; 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(); + //if (ImGui::IsItemHovered() && selection.GetSize() > 0) + // selection.DebugTooltip(); // Initialize default list with 50 items + button to add/remove items. static int items_next_id = 0; @@ -3282,7 +3201,7 @@ static void ShowDemoWindowMultiSelect() // Use default select: Pass index to SetNextItemSelectionUserData(), store index in Selection const int SCOPES_COUNT = 3; const int ITEMS_COUNT = 8; // Per scope - static ExampleSelection selections_data[SCOPES_COUNT]; + static ImGuiSelectionBasicStorage selections_data[SCOPES_COUNT]; // Use ImGuiMultiSelectFlags_ScopeRect to not affect other selections in same window. static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ScopeRect | ImGuiMultiSelectFlags_ClearOnEscape;// | ImGuiMultiSelectFlags_ClearOnClickVoid; @@ -3295,7 +3214,7 @@ static void ShowDemoWindowMultiSelect() for (int selection_scope_n = 0; selection_scope_n < SCOPES_COUNT; selection_scope_n++) { ImGui::PushID(selection_scope_n); - ExampleSelection* selection = &selections_data[selection_scope_n]; + ImGuiSelectionBasicStorage* selection = &selections_data[selection_scope_n]; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection->ApplyRequests(ms_io, ITEMS_COUNT); @@ -3375,7 +3294,7 @@ static void ShowDemoWindowMultiSelect() static ImVector items; static int items_next_id = 0; if (items_next_id == 0) { for (int n = 0; n < 1000; n++) { items.push_back(items_next_id++); } } - static ExampleSelection selection; + static ExampleSelectionStorageWithDeletion selection; ImGui::Text("Selection size: %d/%d", selection.GetSize(), items.Size); @@ -9712,7 +9631,7 @@ struct ExampleAssetsBrowser // State ImVector Items; - ExampleSelection Selection; + ImGuiSelectionBasicStorage Selection; ImGuiID NextItemId = 0; bool SortDirty = false; float ZoomWheelAccum = 0.0f; @@ -9846,7 +9765,7 @@ struct ExampleAssetsBrowser // Use custom selection adapter: store ID in selection (recommended) Selection.AdapterData = this; - Selection.AdapterIndexToStorageId = [](ExampleSelection* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->AdapterData; return self->Items[idx].ID; }; + Selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->AdapterData; return self->Items[idx].ID; }; Selection.ApplyRequests(ms_io, Items.Size); // Altering ItemSpacing may seem unnecessary as we position every items using SetCursorScreenPos()... diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 677e34beda28..11ae828c9988 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7112,6 +7112,7 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) // - MultiSelectItemHeader() [Internal] // - MultiSelectItemFooter() [Internal] // - DebugNodeMultiSelectState() [Internal] +// - ImGuiSelectionBasicStorage //------------------------------------------------------------------------- static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) @@ -7612,6 +7613,44 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) #endif } +// Apply requests coming from BeginMultiSelect() and EndMultiSelect(). +// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. +// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. +// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection. +// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform +// a lookup in order to have some way to iterate/interpolate between two items. +// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices +// and constructing a view index <> object id/ptr data structure anyway. +// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC. +// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters. +// The most simple implementation (using indices everywhere) would look like: +// for (ImGuiSelectionRequest& req : ms_io->Requests) +// { +// if (req.Type == ImGuiSelectionRequestType_Clear) { Clear(); } +// if (req.Type == ImGuiSelectionRequestType_SelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } +// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } +// } +void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count) +{ + IM_ASSERT(AdapterIndexToStorageId != NULL); + for (ImGuiSelectionRequest& req : ms_io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_Clear) + Clear(); + if (req.Type == ImGuiSelectionRequestType_SelectAll) + { + Storage.Data.resize(0); + Storage.Data.reserve(items_count); + for (int idx = 0; idx < items_count; idx++) + AddItem(AdapterIndexToStorageId(this, idx)); + } + if (req.Type == ImGuiSelectionRequestType_SetRange) + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) + UpdateItem(AdapterIndexToStorageId(this, idx), req.RangeSelected); + } +} + + //------------------------------------------------------------------------- // [SECTION] Widgets: ListBox //------------------------------------------------------------------------- From 51fe0bfcf642e7317548bd3b8f62a19dc5e13551 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 5 Dec 2023 18:36:00 +0100 Subject: [PATCH 079/132] MultiSelect: reworked comments in imgui.h now that we have our own section. --- imgui.h | 142 ++++++++++++++++++++++++---------------------- imgui_demo.cpp | 31 +++++----- imgui_widgets.cpp | 4 +- 3 files changed, 91 insertions(+), 86 deletions(-) diff --git a/imgui.h b/imgui.h index 1361e902b43d..0e7a63ce6712 100644 --- a/imgui.h +++ b/imgui.h @@ -673,7 +673,7 @@ namespace ImGui // Multi-selection system for Selectable() and TreeNode() functions. // - 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. // - ImGuiSelectionUserData is often used to store your item index. - // - Read comments near ImGuiMultiSelectIO for instructions/details and see 'Demo->Widgets->Selection State' for demo. + // - Read comments near ImGuiMultiSelectIO for instructions/details and see 'Demo->Widgets->Selection State & Multi-Select' for demo. IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags); IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); @@ -2726,46 +2726,24 @@ 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(). -// (we provide 'ImGuiMultiSelectFlags_SingleSelect' for consistency and flexiblity to allow a single-selection to use same code/logic, but it essentially disable the biggest purpose of BeginMultiSelect(). -// If you use 'ImGuiMultiSelectFlags_SingleSelect' you can handle single-selection in a simpler way by just calling Selectable()/TreeNode() and reacting on clicks). -enum ImGuiMultiSelectFlags_ -{ - ImGuiMultiSelectFlags_None = 0, - ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to use same code/logic is desired, but may not be very useful. - ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to set RequestSelectAll - ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_NoBoxSelectScroll = 1 << 3, // Disable scrolling when box-selecting near edges of scope. - ImGuiMultiSelectFlags_ClearOnEscape = 1 << 4, // Clear selection when pressing Escape while scope is focused. - ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 5, // Clear selection when clicking on empty location within scope. - ImGuiMultiSelectFlags_ScopeWindow = 1 << 6, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if (use if BeginMultiSelect() covers a whole window. - ImGuiMultiSelectFlags_ScopeRect = 1 << 7, // Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. Use if multiple BeginMultiSelect() are used in the same host window. - ImGuiMultiSelectFlags_SelectOnClick = 1 << 8, // Apply selection on mouse down when clicking on unselected item. (Default) - ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 9, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. -}; - // Multi-selection system -// - Refer to 'Demo->Widgets->Selection State' for references using this. +// - Refer to 'Demo->Widgets->Selection State & Multi-Select' for references using this. // - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) -// and supports a clipper being used. Handling this manually and correctly i tricky, this is why we provide +// and supports a clipper being used. Handling this manually and correctly is tricky, this is why we provide // the functionality. If you don't need SHIFT+Mouse/Keyboard range-select + clipping, you can implement a // simple form of multi-selection yourself, by reacting to click/presses on Selectable() items. // - TreeNode() and Selectable() are supported but custom widgets may use it as well. // - In the spirit of Dear ImGui design, your code owns actual selection data. // This is designed to allow all kinds of selection storage you may use in your application: -// e.g. set/map/hash (store only selected items), instructive selection (store a bool inside each object), -// external array (store an array in your view data, next to your objects) etc. +// e.g. set/map/hash (store selected items), instructive selection (store a bool inside each object), etc. // - The work involved to deal with multi-selection differs whether you want to only submit visible items and // clip others, or submit all items regardless of their visibility. Clipping items is more efficient and will // allow you to deal with large lists (1k~100k items). See "Usage flow" section below for details. // If you are not sure, always start without clipping! You can work your way to the optimized version afterwards. -// About ImGuiSelectionBasicStorage: -// - This is an optional helper to store a selection state and apply selection requests. -// - It is used by our demos and provided as a convenience if you want to quickly implement multi-selection. // About ImGuiSelectionUserData: -// - This is your application-defined identifier in a selection set: -// - For each item is it submitted by your call to SetNextItemSelectionUserData(). -// - In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO. +// - This can store an application-defined identifier (e.g. index or pointer). +// - For each item is it submitted by your call to SetNextItemSelectionUserData(). +// - In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO. // - Most applications will store an object INDEX, hence the chosen name and type. // Storing an integer index is the easiest thing to do, as RequestSetRange requests will give you two end-points // and you will need to iterate/interpolate between them to honor range selection. @@ -2778,15 +2756,33 @@ enum ImGuiMultiSelectFlags_ // Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. // - (2) [If using clipper] Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. Same code as Step 6. -// - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeIndex(). If already using indices in ImGuiSelectionUserData, it is as simple as clipper.IncludeIndex((int)ms_io->RangeSrcItem); +// - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeItemByIndex(). If storing indices in ImGuiSelectionUserData, a simple clipper.IncludeItemByIndex(ms_io->RangeSrcItem) call will work. // LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. // - (6) Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. 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. -// However it is perfectly fine to honor all steps even if you don't use a clipper. +// If you submit all items (no clipper), Step 2 and 3 are optional and will be handled by each item themselves. It is perfectly fine if you honor those steps without a clipper. +// About ImGuiSelectionBasicStorage: +// - This is an optional helper to store a selection state and apply selection requests. +// - It is used by our demos and provided as a convenience if you want to quickly implement multi-selection. // Advanced: // - Deletion: If you need to handle items deletion: more work if needed for post-deletion focus and scrolling to be correct. -// Refer to 'Demo->Widgets->Selection State' for demos supporting deletion. +// Refer to 'Demo->Widgets->Selection State & Multi-Select' for demos supporting deletion. + +// Flags for BeginMultiSelect(). +enum ImGuiMultiSelectFlags_ +{ + ImGuiMultiSelectFlags_None = 0, + ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! + ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut sending a SelectAll request. + ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_NoBoxSelectScroll = 1 << 3, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 4, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 5, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 6, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if BeginMultiSelect() covers a whole window. + ImGuiMultiSelectFlags_ScopeRect = 1 << 7, // Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. Use if multiple BeginMultiSelect() are used in the same host window. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 8, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 9, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. +}; enum ImGuiSelectionRequestType { @@ -2814,58 +2810,70 @@ struct ImGuiSelectionRequest struct ImGuiMultiSelectIO { //------------------------------------------// BeginMultiSelect / EndMultiSelect - ImVector Requests; // ms:w, app:r / ms:w app:r // Requests + ImVector Requests; // ms:w, app:r / ms:w app:r // Requests to apply to your selection data. ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). }; -// Helper struct to store multi-selection state + apply multi-selection requests. -// - Used by our demos and provided as a convenience if you want to quickly implement multi-selection. -// - Provide an abstraction layer for the purpose of the demo showcasing different forms of underlying selection data. -// - USING THIS IS NOT MANDATORY. This is only a helper and is not part of the main API. Advanced users are likely to implement their own. -// To store a single-selection, you only need a single variable and don't need any of this! +// Optional helper struct to store multi-selection state + apply multi-selection requests. +// - Used by our demos and provided as a convenience to easily implement basic multi-selection. +// - USING THIS IS NOT MANDATORY. This is only a helper and not a required API. Advanced users are likely to implement their own. // To store a multi-selection, in your real application you could: // - A) Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement. -// - B) Use your own external storage: e.g. std::set, std::vector, std::set, interval trees, etc. -// are generally appropriate. Even a large array of bool might work for you... -// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside your object). Not recommended because: -// - it means you cannot have multiple simultaneous views over your objects. -// - some of our features requires you to provide the selection _size_, which with this specific strategy require additional work. +// - B) Use your own external storage: e.g. std::set, std::vector, interval trees, etc. +// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Not recommended because you can't have multiple views +// over same objects. Also some features requires to provide selection _size_, which with this strategy requires additional work. // Our BeginMultiSelect() api/system doesn't make assumption about: -// - how you want to identify items in multi-selection API? Indices(*) / Custom Identifiers / Pointers ? -// - how you want to store persistent selection data? Indices / Custom Identifiers(*) / Pointers ? -// (*) This is the suggested solution: pass indices to API (because easy to iterate/interpolate) + persist your custom identifiers inside selection data. +// - how you want to identify items in multi-selection API? Indices(*) or Custom Ids or Pointers -> Indices is better (easy to iterate/interpolate) +// - how you want to store persistent selection data? Indices or Custom Ids(*) or Pointers -> Custom ids is better (as selection can persist) // In ImGuiSelectionBasicStorage we: // - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) -// - use a little extra indirection layer in order to abstract how persistent selection data is derived from an index. -// - in some cases we use Index as custom identifier (default, not ideal) +// - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index. +// - in some cases we use Index as custom identifier (default implementation returns Index casted as Identifier): only valid for a never changing item list. // - in some cases we read an ID from some custom item data structure (better, and closer to what you would do in your codebase) // Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. -// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THE UNNECESSARY 'AdapterIndexToStorageId()' INDIRECTION LOGIC. +// When your application settles on a choice, you may want to get rid of this indirection layer and do your own thing. +// Minimum pseudo-code example using this helper: +// { +// static vector items; // Your items +// static ImGuiSelectionBasicStorage selection; // Your selection +// selection.AdapterData = (void*)&items; // Setup adapter so selection.ApplyRequests() function can convert indexes to identifiers. +// selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((vector*)self->AdapterData))[idx].ID; }; +// +// ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None); +// selection.ApplyRequests(ms_io, items.Size); +// for (int idx = 0; idx < items.Size; idx++) +// { +// bool item_is_selected = selection.Contains(items[idx].ID); +// ImGui::SetNextItemSelectionUserData(idx); +// ImGui::Selectable(label, item_is_selected); +// } +// ms_io = ImGui::EndMultiSelect(); +// selection.ApplyRequests(ms_io, items.Size); +// } // In theory, for maximum abstraction, this class could contains AdapterIndexToUserData() and AdapterUserDataToIndex() functions as well, -// but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that for clarify. +// but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that indirection for clarity. struct ImGuiSelectionBasicStorage { - ImGuiStorage Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set - int Size; // Number of selected items (== number of 1 in the Storage, maintained by this class). - - // Adapter to convert item index to item identifier - void* AdapterData; // e.g. selection.AdapterData = (void*)my_items; + // Members + ImGuiStorage Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set + int Size; // Number of selected items (== number of 1 in the Storage, maintained by this class). + void* AdapterData; // Adapter to convert item index to item identifier // e.g. selection.AdapterData = (void*)my_items; ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->AdapterData)[idx]->ID; }; - // Selection storage - ImGuiSelectionBasicStorage() { Clear(); AdapterData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } - void Clear() { Storage.Data.resize(0); Size = 0; } - void Swap(ImGuiSelectionBasicStorage& r){ Storage.Data.swap(r.Storage.Data); } - bool Contains(ImGuiID key) const { return Storage.GetInt(key, 0) != 0; } - void AddItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int != 0) return; *p_int = 1; Size++; } - void RemoveItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int == 0) return; *p_int = 0; Size--; } - void UpdateItem(ImGuiID key, bool v) { if (v) { AddItem(key); } else { RemoveItem(key); } } - int GetSize() const { return Size; } - - // Request handling (apply requests coming from BeginMultiSelect() and EndMultiSelect() functions) + // Methods: selection storage + ImGuiSelectionBasicStorage() { Clear(); AdapterData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } + void Clear() { Storage.Data.resize(0); Size = 0; } + void Swap(ImGuiSelectionBasicStorage& r) { Storage.Data.swap(r.Storage.Data); } + bool Contains(ImGuiID key) const { return Storage.GetInt(key, 0) != 0; } + void AddItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int != 0) return; *p_int = 1; Size++; } + void RemoveItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int == 0) return; *p_int = 0; Size--; } + void UpdateItem(ImGuiID key, bool v) { if (v) { AddItem(key); } else { RemoveItem(key); } } + int GetSize() const { return Size; } + + // Methods: apply selection requests (that are coming from BeginMultiSelect() and EndMultiSelect() functions) IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count); }; diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 291d50e8c01a..58cc4ee579b3 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2783,7 +2783,7 @@ struct ExampleSelectionStorageWithDeletion : ImGuiSelectionBasicStorage // - We cannot provide this logic in core Dear ImGui because we don't have access to selection data. // - We don't actually manipulate the ImVector<> here, only in ApplyDeletionPostLoop(), but using similar API for consistency and flexibility. // - Important: Deletion only works if the underlying ImGuiID for your items are stable: aka not depend on their index, but on e.g. item id/ptr. - // FIXME-MULTISELECT: Doesn't take account of the possibility focus target will be moved during deletion. Need refocus or offset. + // FIXME-MULTISELECT: Doesn't take account of the possibility focus target will be moved during deletion. Need refocus or scroll offset. int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, int items_count) { QueueDeletion = false; @@ -2890,7 +2890,7 @@ struct ExampleDualListBox } void Show() { - ImGui::Checkbox("Sorted", &OptKeepSorted); + //ImGui::Checkbox("Sorted", &OptKeepSorted); if (ImGui::BeginTable("split", 3, ImGuiTableFlags_None)) { ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); // Left side @@ -2964,13 +2964,15 @@ struct ExampleDualListBox if (request_move_selected != -1) MoveSelected(request_move_selected, request_move_selected ^ 1); - // FIXME-MULTISELECT: action from outside + // FIXME-MULTISELECT: Support action from outside + /* if (OptKeepSorted == false) { ImGui::NewLine(); if (ImGui::ArrowButton("MoveUp", ImGuiDir_Up)) {} if (ImGui::ArrowButton("MoveDown", ImGuiDir_Down)) {} } + */ ImGui::EndTable(); } @@ -3025,20 +3027,19 @@ static void ShowDemoWindowMultiSelect() IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select"); if (ImGui::TreeNode("Multi-Select")) { - // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection - static ImGuiSelectionBasicStorage selection; - - ImGui::Text("Tips: Use 'Debug Log->Selection' to see selection requests as they happen."); - ImGui::Text("Supported features:"); ImGui::BulletText("Keyboard navigation (arrows, page up/down, home/end, space)."); ImGui::BulletText("Ctrl modifier to preserve and toggle selection."); ImGui::BulletText("Shift modifier for range selection."); ImGui::BulletText("CTRL+A to select all."); + ImGui::Text("Tip: Use 'Debug Log->Selection' to see selection requests as they happen."); - // The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling region). + // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection const int ITEMS_COUNT = 50; + static ImGuiSelectionBasicStorage selection; ImGui::Text("Selection: %d/%d", selection.GetSize(), ITEMS_COUNT); + + // The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling region). if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; @@ -3124,8 +3125,6 @@ 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/remove items. static int items_next_id = 0; @@ -3146,10 +3145,9 @@ static void ShowDemoWindowMultiSelect() 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. - // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. + // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to turn into 'ms_io->RequestDelete' signal -> need HasSelection passed. // FIXME-MULTISELECT: If pressing Delete + another key we have ambiguous behavior. - const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); + const bool want_delete = selection.QueueDeletion || ((selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); int item_curr_idx_to_focus = -1; if (want_delete) item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, items.Size); @@ -3309,9 +3307,8 @@ static void ShowDemoWindowMultiSelect() 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. - // FIXME-MULTISELECT: may turn into 'ms_io->RequestDelete' -> need HasSelection passed. - const bool want_delete = selection.QueueDeletion || ((selection.GetSize() > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); + // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to turn into 'ms_io->RequestDelete' signal -> need HasSelection passed. + const bool want_delete = selection.QueueDeletion || ((selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); int item_curr_idx_to_focus = -1; if (want_delete) item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, items.Size); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 11ae828c9988..e0b32ac866f0 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6858,7 +6858,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl // Render if (hovered || selected) { - // FIXME-MULTISELECT, FIXME-STYLE: Color for 'selected' elements? ImGuiCol_HeaderSelected + // FIXME-MULTISELECT: Styling: Color for 'selected' elements? ImGuiCol_HeaderSelected ImU32 col; if (selected && !hovered) col = GetColorU32(ImLerp(GetStyleColorVec4(ImGuiCol_Header), GetStyleColorVec4(ImGuiCol_HeaderHovered), 0.5f)); @@ -7558,7 +7558,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) if (is_shift && !is_singleselect) { // Shift+Arrow always select - // Ctrl+Shift+Arrow copy source selection state (alrady stored by BeginMultiSelect() in RangeSelected) + // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected) //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) storage->RangeSrcItem = item_data; From 750e23998f528f1490992a6cb61373e251961d1d Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 6 Dec 2023 16:55:55 +0100 Subject: [PATCH 080/132] MultiSelect: Demo: Assets Browser: added deletion support. Store ID in selection. Moved QueueDeletion to local var to emphasis that this is a user extension. --- imgui.h | 1 - imgui_demo.cpp | 170 +++++++++++++++++++++++++++---------------------- 2 files changed, 95 insertions(+), 76 deletions(-) diff --git a/imgui.h b/imgui.h index 0e7a63ce6712..6de5cec7f736 100644 --- a/imgui.h +++ b/imgui.h @@ -2871,7 +2871,6 @@ struct ImGuiSelectionBasicStorage void AddItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int != 0) return; *p_int = 1; Size++; } void RemoveItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int == 0) return; *p_int = 0; Size--; } void UpdateItem(ImGuiID key, bool v) { if (v) { AddItem(key); } else { RemoveItem(key); } } - int GetSize() const { return Size; } // Methods: apply selection requests (that are coming from BeginMultiSelect() and EndMultiSelect() functions) IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count); diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 58cc4ee579b3..aa6ecf01070a 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2773,10 +2773,9 @@ static const char* ExampleNames[] = "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" }; -struct ExampleSelectionStorageWithDeletion : ImGuiSelectionBasicStorage +// Extra functions to add deletion support to ImGuiSelectionBasicStorage +struct ExampleSelectionWithDeletion : ImGuiSelectionBasicStorage { - bool QueueDeletion = false; // Track request deleting selected items - // Find which item should be Focused after deletion. // Call _before_ item submission. Retunr an index in the before-deletion item list, your item loop should call SetKeyboardFocusHere() on it. // The subsequent ApplyDeletionPostLoop() code will use it to apply Selection. @@ -2786,7 +2785,6 @@ struct ExampleSelectionStorageWithDeletion : ImGuiSelectionBasicStorage // FIXME-MULTISELECT: Doesn't take account of the possibility focus target will be moved during deletion. Need refocus or scroll offset. int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, int items_count) { - QueueDeletion = false; if (Size == 0) return -1; @@ -3032,12 +3030,13 @@ static void ShowDemoWindowMultiSelect() ImGui::BulletText("Ctrl modifier to preserve and toggle selection."); ImGui::BulletText("Shift modifier for range selection."); ImGui::BulletText("CTRL+A to select all."); - ImGui::Text("Tip: Use 'Debug Log->Selection' to see selection requests as they happen."); + ImGui::BulletText("Escape to clear selection."); + ImGui::Text("Tip: Use 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen."); // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection const int ITEMS_COUNT = 50; static ImGuiSelectionBasicStorage selection; - ImGui::Text("Selection: %d/%d", selection.GetSize(), ITEMS_COUNT); + ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT); // The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling region). if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) @@ -3074,7 +3073,7 @@ static void ShowDemoWindowMultiSelect() ImGui::BulletText("Using ImGuiListClipper."); const int ITEMS_COUNT = 10000; - ImGui::Text("Selection: %d/%d", selection.GetSize(), ITEMS_COUNT); + ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT); if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; @@ -3116,24 +3115,26 @@ static void ShowDemoWindowMultiSelect() IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (with deletion)"); if (ImGui::TreeNode("Multi-Select (with deletion)")) { - // Intentionally separating items data from selection data! - // But you may decide to store selection data inside your item (aka intrusive storage). - // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection - static ImVector items; - static ExampleSelectionStorageWithDeletion selection; + // Storing items data separately from selection data. + // (you may decide to store selection data inside your item (aka intrusive storage) if you don't need multiple views over same items) + // Use a custom selection.Adapter: store item identifier in Selection (instead of index) + static ImVector items; + static ExampleSelectionWithDeletion selection; + selection.AdapterData = (void*)&items; + selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { ImVector* p_items = (ImVector*)self->AdapterData; return (*p_items)[idx]; }; // Index -> ID - ImGui::Text("Adding features:"); + ImGui::Text("Added features:"); ImGui::BulletText("Dynamic list with Delete key support."); - ImGui::Text("Selection size: %d/%d", selection.GetSize(), items.Size); + ImGui::Text("Selection size: %d/%d", selection.Size, items.Size); // Initialize default list with 50 items + button to add/remove items. - static int items_next_id = 0; + static ImGuiID items_next_id = 0; if (items_next_id == 0) - for (int n = 0; n < 50; n++) + for (ImGuiID n = 0; n < 50; n++) items.push_back(items_next_id++); if (ImGui::SmallButton("Add 20 items")) { for (int n = 0; n < 20; n++) { items.push_back(items_next_id++); } } ImGui::SameLine(); - if (ImGui::SmallButton("Remove 20 items")) { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.RemoveItem((ImGuiID)(items.Size - 1)); items.pop_back(); } } // This is to test + if (ImGui::SmallButton("Remove 20 items")) { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.RemoveItem(items.back()); items.pop_back(); } } // (1) Extra to support deletion: Submit scrolling range to avoid glitches on deletion const float items_height = ImGui::GetTextLineHeightWithSpacing(); @@ -3147,18 +3148,16 @@ static void ShowDemoWindowMultiSelect() // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to turn into 'ms_io->RequestDelete' signal -> need HasSelection passed. // FIXME-MULTISELECT: If pressing Delete + another key we have ambiguous behavior. - const bool want_delete = selection.QueueDeletion || ((selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); - int item_curr_idx_to_focus = -1; - if (want_delete) - item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, items.Size); + const bool want_delete = (selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete); + const int item_curr_idx_to_focus = want_delete ? selection.ApplyDeletionPreLoop(ms_io, items.Size) : -1; for (int n = 0; n < items.Size; n++) { - const int item_id = items[n]; + const ImGuiID item_id = items[n]; char label[64]; - sprintf(label, "Object %05d: %s", item_id, ExampleNames[item_id % IM_ARRAYSIZE(ExampleNames)]); + sprintf(label, "Object %05u: %s", item_id, ExampleNames[item_id % IM_ARRAYSIZE(ExampleNames)]); - bool item_is_selected = selection.Contains((ImGuiID)n); + bool item_is_selected = selection.Contains(item_id); ImGui::SetNextItemSelectionUserData(n); ImGui::Selectable(label, item_is_selected); if (item_curr_idx_to_focus == n) @@ -3217,7 +3216,7 @@ static void ShowDemoWindowMultiSelect() selection->ApplyRequests(ms_io, ITEMS_COUNT); ImGui::SeparatorText("Selection scope"); - ImGui::Text("Selection size: %d/%d", selection->GetSize(), ITEMS_COUNT); + ImGui::Text("Selection size: %d/%d", selection->Size, ITEMS_COUNT); for (int n = 0; n < ITEMS_COUNT; n++) { @@ -3292,9 +3291,10 @@ static void ShowDemoWindowMultiSelect() static ImVector items; static int items_next_id = 0; if (items_next_id == 0) { for (int n = 0; n < 1000; n++) { items.push_back(items_next_id++); } } - static ExampleSelectionStorageWithDeletion selection; + static ExampleSelectionWithDeletion selection; + static bool request_deletion_from_menu = false; // Queue deletion triggered from context menu - ImGui::Text("Selection size: %d/%d", selection.GetSize(), items.Size); + ImGui::Text("Selection size: %d/%d", selection.Size, items.Size); const float items_height = (widget_type == WidgetType_TreeNode) ? ImGui::GetTextLineHeight() : ImGui::GetTextLineHeightWithSpacing(); ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); @@ -3308,10 +3308,9 @@ static void ShowDemoWindowMultiSelect() selection.ApplyRequests(ms_io, items.Size); // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to turn into 'ms_io->RequestDelete' signal -> need HasSelection passed. - const bool want_delete = selection.QueueDeletion || ((selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); - int item_curr_idx_to_focus = -1; - if (want_delete) - item_curr_idx_to_focus = selection.ApplyDeletionPreLoop(ms_io, items.Size); + const bool want_delete = request_deletion_from_menu || ((selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); + const int item_curr_idx_to_focus = want_delete ? selection.ApplyDeletionPreLoop(ms_io, items.Size) : -1; + request_deletion_from_menu = false; if (show_in_table) { @@ -3328,7 +3327,7 @@ static void ShowDemoWindowMultiSelect() { clipper.Begin(items.Size); if (item_curr_idx_to_focus != -1) - clipper.IncludeItemByIndex(item_curr_idx_to_focus); // Ensure focused item is not clipped + clipper.IncludeItemByIndex(item_curr_idx_to_focus); // Ensure focused item is not clipped. if (ms_io->RangeSrcItem > 0) clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. } @@ -3417,9 +3416,10 @@ static void ShowDemoWindowMultiSelect() // Right-click: context menu if (ImGui::BeginPopupContextItem()) { - ImGui::BeginDisabled(!use_deletion || selection.GetSize() == 0); - sprintf(label, "Delete %d item(s)###DeleteSelected", selection.GetSize()); - selection.QueueDeletion |= ImGui::Selectable(label); + ImGui::BeginDisabled(!use_deletion || selection.Size == 0); + sprintf(label, "Delete %d item(s)###DeleteSelected", selection.Size); + if (ImGui::Selectable(label)) + request_deletion_from_menu = true; ImGui::EndDisabled(); ImGui::Selectable("Close"); ImGui::EndPopup(); @@ -9619,19 +9619,20 @@ const ImGuiTableSortSpecs* ExampleAsset::s_current_sort_specs = NULL; struct ExampleAssetsBrowser { // Options - bool ShowTypeOverlay = true; - bool AllowDragUnselected = false; - float IconSize = 32.0f; - int IconSpacing = 10; - int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer. - bool StretchSpacing = true; + bool ShowTypeOverlay = true; + bool AllowDragUnselected = false; + float IconSize = 32.0f; + int IconSpacing = 10; + int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer. + bool StretchSpacing = true; // State - ImVector Items; - ImGuiSelectionBasicStorage Selection; - ImGuiID NextItemId = 0; - bool SortDirty = false; - float ZoomWheelAccum = 0.0f; + ImVector Items; // Our items + ExampleSelectionWithDeletion Selection; // Our selection (ImGuiSelectionBasicStorage + helper funcs to handle deletion) + ImGuiID NextItemId = 0; // Unique identifier when creating new items + bool RequestDelete = false; // Deferred deletion request + bool RequestSort = false; // Deferred sort request + float ZoomWheelAccum = 0.0f; // Mouse wheel accumulator to handle smooth wheels better // Functions ExampleAssetsBrowser() @@ -9645,7 +9646,7 @@ struct ExampleAssetsBrowser Items.reserve(Items.Size + count); for (int n = 0; n < count; n++, NextItemId++) Items.push_back(ExampleAsset(NextItemId, (NextItemId % 20) < 15 ? 0 : (NextItemId % 20) < 18 ? 1 : 2)); - SortDirty = true; + RequestSort = true; } void ClearItems() { @@ -9676,6 +9677,12 @@ struct ExampleAssetsBrowser *p_open = false; ImGui::EndMenu(); } + if (ImGui::BeginMenu("Edit")) + { + if (ImGui::MenuItem("Delete", "Del", false, Selection.Size > 0)) + RequestDelete = true; + ImGui::EndMenu(); + } if (ImGui::BeginMenu("Options")) { ImGui::PushItemWidth(ImGui::GetFontSize() * 10); @@ -9697,22 +9704,6 @@ struct ExampleAssetsBrowser ImGui::EndMenuBar(); } - // Zooming with CTRL+Wheel - // FIXME-MULTISELECT: Try to maintain scroll. - ImGuiIO& io = ImGui::GetIO(); - if (ImGui::IsWindowAppearing()) - ZoomWheelAccum = 0.0f; - if (io.MouseWheel != 0.0f && ImGui::IsKeyDown(ImGuiMod_Ctrl) && ImGui::IsAnyItemActive() == false) - { - ZoomWheelAccum += io.MouseWheel; - if (fabsf(ZoomWheelAccum) >= 1.0f) - { - IconSize *= powf(1.1f, (float)(int)ZoomWheelAccum); - IconSize = IM_CLAMP(IconSize, 16.0f, 128.0f); - ZoomWheelAccum -= (int)ZoomWheelAccum; - } - } - // Show a table with ONLY one header row to showcase the idea/possibility of using this to provide a sorting UI ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); ImGuiTableFlags table_flags_for_sort_specs = ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Borders; @@ -9722,10 +9713,10 @@ struct ExampleAssetsBrowser ImGui::TableSetupColumn("Type"); ImGui::TableHeadersRow(); if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs()) - if (sort_specs->SpecsDirty || SortDirty) + if (sort_specs->SpecsDirty || RequestSort) { ExampleAsset::SortWithSortSpecs(sort_specs, Items.Data, Items.Size); - sort_specs->SpecsDirty = SortDirty = false; + sort_specs->SpecsDirty = RequestSort = false; } ImGui::EndTable(); } @@ -9765,6 +9756,10 @@ struct ExampleAssetsBrowser Selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->AdapterData; return self->Items[idx].ID; }; Selection.ApplyRequests(ms_io, Items.Size); + const bool want_delete = RequestDelete || ((Selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); + const int item_curr_idx_to_focus = want_delete ? Selection.ApplyDeletionPreLoop(ms_io, Items.Size) : -1; + RequestDelete = false; + // Altering ItemSpacing may seem unnecessary as we position every items using SetCursorScreenPos()... // But it is necessary for two reasons: // - Selectables uses it by default to visually fill the space between two items. @@ -9781,8 +9776,10 @@ struct ExampleAssetsBrowser const float line_height = item_size.y + item_spacing; ImGuiListClipper clipper; clipper.Begin(line_count, line_height); + if (item_curr_idx_to_focus != -1) + clipper.IncludeItemByIndex(item_curr_idx_to_focus / column_count); // Ensure focused item line is not clipped. if (ms_io->RangeSrcItem != -1) - clipper.IncludeItemByIndex((int)(ms_io->RangeSrcItem / column_count)); + clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem / column_count); // Ensure RangeSrc item line is not clipped. while (clipper.Step()) { for (int line_idx = clipper.DisplayStart; line_idx < clipper.DisplayEnd; line_idx++) @@ -9812,6 +9809,10 @@ struct ExampleAssetsBrowser if (ImGui::IsItemToggledSelection()) item_is_selected = !item_is_selected; + // Focus (for after deletion) + if (item_curr_idx_to_focus == item_idx) + ImGui::SetKeyboardFocusHere(-1); + // Drag and drop if (ImGui::BeginDragDropSource()) { @@ -9825,15 +9826,6 @@ struct ExampleAssetsBrowser ImGui::EndDragDropSource(); } - // Popup menu - if (ImGui::BeginPopupContextItem()) - { - ImGui::Text("Selection: %d items", Selection.Size); - if (ImGui::Button("Close")) - ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - } - // A real app would likely display an image/thumbnail here. draw_list->AddRectFilled(box_min, box_max, icon_bg_color); if (ShowTypeOverlay && item_data->Type != 0) @@ -9856,13 +9848,41 @@ struct ExampleAssetsBrowser clipper.End(); ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing + // Context menu + if (ImGui::BeginPopupContextWindow()) + { + ImGui::Text("Selection: %d items", Selection.Size); + ImGui::Separator(); + if (ImGui::MenuItem("Delete", "Del", false, Selection.Size > 0)) + RequestDelete = true; + ImGui::EndPopup(); + } + ms_io = ImGui::EndMultiSelect(); Selection.ApplyRequests(ms_io, Items.Size); + if (want_delete) + Selection.ApplyDeletionPostLoop(ms_io, Items, item_curr_idx_to_focus); // FIXME-MULTISELECT: Find a way to expose this in public API. This currently requires "imgui_internal.h" //ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); } + // Zooming with CTRL+Wheel + // FIXME-MULTISELECT: Try to maintain scroll. + ImGuiIO& io = ImGui::GetIO(); + if (ImGui::IsWindowAppearing()) + ZoomWheelAccum = 0.0f; + if (ImGui::IsWindowHovered() && io.MouseWheel != 0.0f && ImGui::IsKeyDown(ImGuiMod_Ctrl) && ImGui::IsAnyItemActive() == false) + { + ZoomWheelAccum += io.MouseWheel; + if (fabsf(ZoomWheelAccum) >= 1.0f) + { + IconSize *= powf(1.1f, (float)(int)ZoomWheelAccum); + IconSize = IM_CLAMP(IconSize, 16.0f, 128.0f); + ZoomWheelAccum -= (int)ZoomWheelAccum; + } + } + ImGui::EndChild(); ImGui::Text("Selected: %d/%d items", Selection.Size, Items.Size); ImGui::End(); From 7546a2d345c10e3ccfdb6a08afdee2aa2e52c97c Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 12 Dec 2023 17:26:02 +0100 Subject: [PATCH 081/132] MultiSelect: Demo: Assets Browser: track scrolling target so we can roughly land on hovered item. It's impossible to do this perfectly without some form of locking on item because as the hovered item X position changes it's easy to drift. --- imgui_demo.cpp | 114 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 38 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index aa6ecf01070a..45b7584366a5 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -9634,6 +9634,15 @@ struct ExampleAssetsBrowser bool RequestSort = false; // Deferred sort request float ZoomWheelAccum = 0.0f; // Mouse wheel accumulator to handle smooth wheels better + // Calculated sizes for layout, output of UpdateLayoutSizes(). Could be locals but our code is simpler this way. + ImVec2 LayoutItemSize; + ImVec2 LayoutItemStep; // == LayoutItemSize + LayoutItemSpacing + float LayoutItemSpacing = 0.0f; + float LayoutSelectableSpacing = 0.0f; + float LayoutOuterPadding = 0.0f; + int LayoutColumnCount = 0; + int LayoutLineCount = 0; + // Functions ExampleAssetsBrowser() { @@ -9654,6 +9663,29 @@ struct ExampleAssetsBrowser Selection.Clear(); } + // Logic would be written in the main code BeginChild() and outputing to local variables. + // We extracted it into a function so we can call it easily from multiple places. + void UpdateLayoutSizes(float avail_width) + { + // Layout: when not stretching: allow extending into right-most spacing. + LayoutItemSpacing = (float)IconSpacing; + if (StretchSpacing == false) + avail_width += floorf(LayoutItemSpacing * 0.5f); + + // Layout: calculate number of icon per line and number of lines + LayoutItemSize = ImVec2(floorf(IconSize), floorf(IconSize)); + LayoutColumnCount = IM_MAX((int)(avail_width / (LayoutItemSize.x + LayoutItemSpacing)), 1); + LayoutLineCount = (Items.Size + LayoutColumnCount - 1) / LayoutColumnCount; + + // Layout: when stretching: allocate remaining space to more spacing. Round before division, so item_spacing may be non-integer. + if (StretchSpacing && LayoutColumnCount > 1) + LayoutItemSpacing = floorf(avail_width - LayoutItemSize.x * LayoutColumnCount) / LayoutColumnCount; + + LayoutItemStep = ImVec2(LayoutItemSize.x + LayoutItemSpacing, LayoutItemSize.y + LayoutItemSpacing); + LayoutSelectableSpacing = IM_MAX(floorf(LayoutItemSpacing) - IconHitSpacing, 0.0f); + LayoutOuterPadding = floorf(LayoutItemSpacing * 0.5f); + } + void Draw(const char* title, bool* p_open) { ImGui::SetNextWindowSize(ImVec2(IconSize * 25, IconSize * 15), ImGuiCond_FirstUseEver); @@ -9722,27 +9754,18 @@ struct ExampleAssetsBrowser } ImGui::PopStyleVar(); - if (ImGui::BeginChild("Assets", ImVec2(0, -ImGui::GetTextLineHeightWithSpacing()), true, ImGuiWindowFlags_NoMove)) + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowContentSize(ImVec2(0.0f, LayoutOuterPadding + LayoutLineCount * (LayoutItemSize.x + LayoutItemSpacing))); + if (ImGui::BeginChild("Assets", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing()), ImGuiChildFlags_Border, ImGuiWindowFlags_NoMove)) { ImDrawList* draw_list = ImGui::GetWindowDrawList(); - const ImVec2 item_size(floorf(IconSize), floorf(IconSize)); - - // Layout: when not stretching: allow extending into right-most spacing. - float item_spacing = (float)IconSpacing; - const float avail_width = ImGui::GetContentRegionAvail().x + (StretchSpacing ? 0.0f : floorf(item_spacing * 0.5f)); - // Layout: calculate number of icon per line and number of lines - const int column_count = IM_MAX((int)(avail_width / (item_size.x + IconSpacing)), 1); - const int line_count = (Items.Size + column_count - 1) / column_count; - - // Layout: when stretching: allocate remaining space to more spacing. Round before division, so item_spacing may be non-integer. - if (StretchSpacing && column_count > 1) - item_spacing = floorf(avail_width - item_size.x * column_count) / column_count; + const float avail_width = ImGui::GetContentRegionAvail().x; + UpdateLayoutSizes(avail_width); // Calculate and store start position. - const float outer_padding = floorf(item_spacing * 0.5f); ImVec2 start_pos = ImGui::GetCursorScreenPos(); - start_pos = ImVec2(start_pos.x + outer_padding, start_pos.y + outer_padding); + start_pos = ImVec2(start_pos.x + LayoutOuterPadding, start_pos.y + LayoutOuterPadding); ImGui::SetCursorScreenPos(start_pos); // Multi-select @@ -9760,22 +9783,22 @@ struct ExampleAssetsBrowser const int item_curr_idx_to_focus = want_delete ? Selection.ApplyDeletionPreLoop(ms_io, Items.Size) : -1; RequestDelete = false; - // Altering ItemSpacing may seem unnecessary as we position every items using SetCursorScreenPos()... + // Push LayoutSelectableSpacing (which is LayoutItemSpacing minus hit-spacing, if we decide to have hit gaps between items) + // Altering style ItemSpacing may seem unnecessary as we position every items using SetCursorScreenPos()... // But it is necessary for two reasons: // - Selectables uses it by default to visually fill the space between two items. // - The vertical spacing would be measured by Clipper to calculate line height if we didn't provide it explicitly (here we do). - const float selectable_spacing = IM_MAX(floorf(item_spacing) - IconHitSpacing, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(selectable_spacing, selectable_spacing)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(LayoutSelectableSpacing, LayoutSelectableSpacing)); // Rendering parameters const ImU32 icon_bg_color = IM_COL32(48, 48, 48, 128); const ImU32 icon_type_overlay_colors[3] = { 0, IM_COL32(200, 70, 70, 255), IM_COL32(70, 170, 70, 255) }; const ImVec2 icon_type_overlay_size = ImVec2(4.0f, 4.0f); - const bool display_label = (item_size.x >= ImGui::CalcTextSize("999").x); + const bool display_label = (LayoutItemSize.x >= ImGui::CalcTextSize("999").x); - const float line_height = item_size.y + item_spacing; + const int column_count = LayoutColumnCount; ImGuiListClipper clipper; - clipper.Begin(line_count, line_height); + clipper.Begin(LayoutLineCount, LayoutItemStep.y); if (item_curr_idx_to_focus != -1) clipper.IncludeItemByIndex(item_curr_idx_to_focus / column_count); // Ensure focused item line is not clipped. if (ms_io->RangeSrcItem != -1) @@ -9792,17 +9815,17 @@ struct ExampleAssetsBrowser ImGui::PushID((int)item_data->ID); // Position item - ImVec2 pos = ImVec2(start_pos.x + (item_idx % column_count) * (item_size.x + item_spacing), start_pos.y + (line_idx * line_height)); + ImVec2 pos = ImVec2(start_pos.x + (item_idx % column_count) * LayoutItemStep.x, start_pos.y + line_idx * LayoutItemStep.y); ImGui::SetCursorScreenPos(pos); // Draw box ImVec2 box_min(pos.x - 1, pos.y - 1); - ImVec2 box_max(box_min.x + item_size.x + 2, box_min.y + item_size.y + 2); + ImVec2 box_max(box_min.x + LayoutItemSize.x + 2, box_min.y + LayoutItemSize.y + 2); draw_list->AddRect(box_min, box_max, IM_COL32(90, 90, 90, 255)); ImGui::SetNextItemSelectionUserData(item_idx); bool item_is_selected = Selection.Contains((ImGuiID)item_data->ID); - ImGui::Selectable("", item_is_selected, ImGuiSelectableFlags_None, item_size); + ImGui::Selectable("", item_is_selected, ImGuiSelectableFlags_None, LayoutItemSize); // Update our selection state immediately (without waiting for EndMultiSelect() requests) // because we use this to alter the color of our text/icon. @@ -9865,25 +9888,40 @@ struct ExampleAssetsBrowser // FIXME-MULTISELECT: Find a way to expose this in public API. This currently requires "imgui_internal.h" //ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); - } - // Zooming with CTRL+Wheel - // FIXME-MULTISELECT: Try to maintain scroll. - ImGuiIO& io = ImGui::GetIO(); - if (ImGui::IsWindowAppearing()) - ZoomWheelAccum = 0.0f; - if (ImGui::IsWindowHovered() && io.MouseWheel != 0.0f && ImGui::IsKeyDown(ImGuiMod_Ctrl) && ImGui::IsAnyItemActive() == false) - { - ZoomWheelAccum += io.MouseWheel; - if (fabsf(ZoomWheelAccum) >= 1.0f) + // Zooming with CTRL+Wheel + if (ImGui::IsWindowAppearing()) + ZoomWheelAccum = 0.0f; + if (ImGui::IsWindowHovered() && io.MouseWheel != 0.0f && ImGui::IsKeyDown(ImGuiMod_Ctrl) && ImGui::IsAnyItemActive() == false) { - IconSize *= powf(1.1f, (float)(int)ZoomWheelAccum); - IconSize = IM_CLAMP(IconSize, 16.0f, 128.0f); - ZoomWheelAccum -= (int)ZoomWheelAccum; + ZoomWheelAccum += io.MouseWheel; + if (fabsf(ZoomWheelAccum) >= 1.0f) + { + // Calculate hovered item index from mouse location + // FIXME: Locking aiming on 'hovered_item_idx' (with a cool-down timer) would ensure zoom keeps on it. + const float hovered_item_nx = (io.MousePos.x - start_pos.x + LayoutItemSpacing * 0.5f) / LayoutItemStep.x; + const float hovered_item_ny = (io.MousePos.y - start_pos.y + LayoutItemSpacing * 0.5f) / LayoutItemStep.y; + const int hovered_item_idx = ((int)hovered_item_ny * LayoutColumnCount) + (int)hovered_item_nx; + //ImGui::SetTooltip("%f,%f -> item %d", hovered_item_nx, hovered_item_ny, hovered_item_idx); // Move those 4 lines in block above for easy debugging + + // Zoom + IconSize *= powf(1.1f, (float)(int)ZoomWheelAccum); + IconSize = IM_CLAMP(IconSize, 16.0f, 128.0f); + ZoomWheelAccum -= (int)ZoomWheelAccum; + UpdateLayoutSizes(avail_width); + + // Manipulate scroll to that we will land at the same Y location of currently hovered item. + // - Calculate next frame position of item under mouse + // - Set new scroll position to be used in next ImGui::BeginChild() call. + float hovered_item_rel_pos_y = ((float)(hovered_item_idx / LayoutColumnCount) + fmodf(hovered_item_ny, 1.0f)) * LayoutItemStep.y; + hovered_item_rel_pos_y += ImGui::GetStyle().WindowPadding.y; + float mouse_local_y = io.MousePos.y - ImGui::GetWindowPos().y; + ImGui::SetScrollY(hovered_item_rel_pos_y - mouse_local_y); + } } } - ImGui::EndChild(); + ImGui::Text("Selected: %d/%d items", Selection.Size, Items.Size); ImGui::End(); } From 1ac469b50f5c93087d3449754bc5bcbb60584037 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 20 Dec 2023 11:34:25 +0100 Subject: [PATCH 082/132] MultiSelect: Box-Select: Fixed holes when using with clipper (in 1D list.) Clipper accounts for Selectable() layout oddity as BoxSelect is sensitive to it. Also tweaked scroll triggering region inward. Rename ImGuiMultiSelectFlags_NoBoxSelectScroll to ImGuiMultiSelectFlags_BoxSelectNoScroll. Fixed use with ImGuiMultiSelectFlags_SinglaSelect. --- imgui.cpp | 18 ++++++++++++++++-- imgui.h | 4 ++-- imgui_demo.cpp | 17 ++++++++++++----- imgui_internal.h | 4 ++-- imgui_widgets.cpp | 42 ++++++++++++++++++++++++++++-------------- 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 3128892f0411..bee410a72fc2 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3079,9 +3079,23 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) data->Ranges.push_back(ImGuiListClipperRange::FromPositions(nav_rect_abs.Min.y, nav_rect_abs.Max.y, 0, 0)); // Add visible range + float min_y = window->ClipRect.Min.y; + float max_y = window->ClipRect.Max.y; + + // Add box selection range + if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect) + if (ms->Storage->Window == window && ms->Storage->BoxSelectActive) + { + // FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos. + // RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that. + // As a workaround we currently half ItemSpacing worth on each side. + min_y -= g.Style.ItemSpacing.y; + max_y += g.Style.ItemSpacing.y; + } + const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0; const int off_max = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Down) ? 1 : 0; - data->Ranges.push_back(ImGuiListClipperRange::FromPositions(window->ClipRect.Min.y, window->ClipRect.Max.y, off_min, off_max)); + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(min_y, max_y, off_min, off_max)); } // Convert position ranges to item index ranges @@ -13438,7 +13452,7 @@ bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() g.DragDropTargetRect = bb; - g.DragDropTargetClipRect = window->ClipRect; // May want to be overriden by user depending on use case? + g.DragDropTargetClipRect = window->ClipRect; // May want to be overridden by user depending on use case? g.DragDropTargetId = id; g.DragDropWithinTarget = true; return true; diff --git a/imgui.h b/imgui.h index 6de5cec7f736..63159a14b67a 100644 --- a/imgui.h +++ b/imgui.h @@ -2774,8 +2774,8 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_None = 0, ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut sending a SelectAll request. - ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_NoBoxSelectScroll = 1 << 3, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection. Box-selection + clipper is currently only supported for 1D list (not with 2D grid). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 3, // Disable scrolling when box-selecting near edges of scope. ImGuiMultiSelectFlags_ClearOnEscape = 1 << 4, // Clear selection when pressing Escape while scope is focused. ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 5, // Clear selection when clicking on empty location within scope. ImGuiMultiSelectFlags_ScopeWindow = 1 << 6, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if BeginMultiSelect() covers a whole window. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 45b7584366a5..004f89ca1ad8 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3031,6 +3031,7 @@ static void ShowDemoWindowMultiSelect() ImGui::BulletText("Shift modifier for range selection."); ImGui::BulletText("CTRL+A to select all."); ImGui::BulletText("Escape to clear selection."); + ImGui::BulletText("Click and drag to box-select."); ImGui::Text("Tip: Use 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen."); // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection @@ -3041,7 +3042,7 @@ static void ShowDemoWindowMultiSelect() // The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling region). if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection.ApplyRequests(ms_io, ITEMS_COUNT); @@ -3076,7 +3077,7 @@ static void ShowDemoWindowMultiSelect() ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT); if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection.ApplyRequests(ms_io, ITEMS_COUNT); @@ -3142,7 +3143,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) { - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape; + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection.ApplyRequests(ms_io, items.Size); @@ -3259,7 +3260,7 @@ static void ShowDemoWindowMultiSelect() static bool use_drag_drop = true; static bool show_in_table = false; static bool show_color_button = false; - static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; + static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; static WidgetType widget_type = WidgetType_Selectable; if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } @@ -3273,7 +3274,7 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoBoxSelectScroll", &flags, ImGuiMultiSelectFlags_NoBoxSelectScroll); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeWindow", &flags, ImGuiMultiSelectFlags_ScopeWindow) && (flags & ImGuiMultiSelectFlags_ScopeWindow)) @@ -9621,6 +9622,7 @@ struct ExampleAssetsBrowser // Options bool ShowTypeOverlay = true; bool AllowDragUnselected = false; + bool AllowBoxSelect = false; // Unsupported for 2D selection for now. float IconSize = 32.0f; int IconSpacing = 10; int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer. @@ -9724,6 +9726,9 @@ struct ExampleAssetsBrowser ImGui::SeparatorText("Selection Behavior"); ImGui::Checkbox("Allow dragging unselected item", &AllowDragUnselected); + ImGui::BeginDisabled(); // Unsupported for 2D selection for now. + ImGui::Checkbox("Allow box-selection", &AllowBoxSelect); + ImGui::EndDisabled(); ImGui::SeparatorText("Layout"); ImGui::SliderFloat("Icon Size", &IconSize, 16.0f, 128.0f, "%.0f"); @@ -9772,6 +9777,8 @@ struct ExampleAssetsBrowser ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ClearOnClickVoid; if (AllowDragUnselected) ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. + if (AllowBoxSelect) + ms_flags |= ImGuiMultiSelectFlags_BoxSelect; // FIXME-MULTISELECT: Box-select not yet supported for 2D selection when using clipper. ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags); // Use custom selection adapter: store ID in selection (recommended) diff --git a/imgui_internal.h b/imgui_internal.h index 65eb5eb897c5..0b05404e18bb 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1724,8 +1724,8 @@ struct IMGUI_API ImGuiMultiSelectTempData ImVec2 ScopeRectMin; ImVec2 BackupCursorMaxPos; ImGuiID BoxSelectId; - ImRect BoxSelectRectCurr; // Selection rectangle in absolute coordinates (derived from Storage->BoxSelectStartPosRel + MousePos) ImRect BoxSelectRectPrev; + ImRect BoxSelectRectCurr; // Selection rectangle in absolute coordinates (derived every frame from Storage->BoxSelectStartPosRel + MousePos) ImGuiSelectionUserData BoxSelectLastitem; ImGuiKeyChord KeyMods; bool LoopRequestClear; @@ -3102,7 +3102,7 @@ namespace ImGui inline ImRect WindowRectAbsToRel(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x - off.x, r.Min.y - off.y, r.Max.x - off.x, r.Max.y - off.y); } inline ImRect WindowRectRelToAbs(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x + off.x, r.Min.y + off.y, r.Max.x + off.x, r.Max.y + off.y); } inline ImVec2 WindowPosRelToAbs(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x + off.x, p.y + off.y); } - inline ImVec2 WindowPosAbsToRel(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x - off.x, p.y - off.y); } + inline ImVec2 WindowPosAbsToRel(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x - off.x, p.y - off.y); } // Windows: Display Order and Focus Order IMGUI_API void FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags = 0); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index e0b32ac866f0..52a342e5ad4b 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6749,6 +6749,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl const ImVec2 text_max(min_x + size.x, pos.y + size.y); // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable. + // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currenty doesn't allow offsetting CursorPos. ImRect bb(min_x, pos.y, text_max.x, text_max.y); if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0) { @@ -7135,20 +7136,22 @@ static void BoxSelectStart(ImGuiMultiSelectState* storage, ImGuiSelectionUserDat storage->BoxSelectStartPosRel = storage->BoxSelectEndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); } -static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& r) +static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& inner_r) { ImGuiContext& g = *GImGui; - for (int n = 0; n < 2; n++) + for (int n = 0; n < 2; n++) // each axis { - float dist = (g.IO.MousePos[n] > r.Max[n]) ? g.IO.MousePos[n] - r.Max[n] : (g.IO.MousePos[n] < r.Min[n]) ? g.IO.MousePos[n] - r.Min[n] : 0.0f; - if (dist == 0.0f || (dist < 0.0f && window->Scroll[n] < 0.0f) || (dist > 0.0f && window->Scroll[n] >= window->ScrollMax[n])) + const float mouse_pos = g.IO.MousePos[n]; + const float dist = (mouse_pos > inner_r.Max[n]) ? mouse_pos - inner_r.Max[n] : (mouse_pos < inner_r.Min[n]) ? mouse_pos - inner_r.Min[n] : 0.0f; + const float scroll_curr = window->Scroll[n]; + if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n])) continue; - float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance - float scroll_step = IM_ROUND(g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime); + const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance + const float scroll_step = IM_ROUND(g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime); if (n == 0) - ImGui::SetScrollX(window, window->Scroll[n] + scroll_step); + ImGui::SetScrollX(window, scroll_curr + scroll_step); else - ImGui::SetScrollY(window, window->Scroll[n] + scroll_step); + ImGui::SetScrollY(window, scroll_curr + scroll_step); } } @@ -7165,6 +7168,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) g.CurrentMultiSelect = ms; if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) flags |= ImGuiMultiSelectFlags_ScopeWindow; + if (flags & ImGuiMultiSelectFlags_SingleSelect) + flags &= ~ImGuiMultiSelectFlags_BoxSelect; // FIXME: BeginFocusScope() const ImGuiID id = window->IDStack.back(); @@ -7250,12 +7255,20 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) } if (storage->BoxSelectActive) { + // Current frame absolute prev/current rectangles are used to toggle selection. + // They are derived from positions relative to scrolling space. + const ImRect scope_rect = window->InnerClipRect; ImVec2 start_pos_abs = WindowPosRelToAbs(window, storage->BoxSelectStartPosRel); - ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, storage->BoxSelectEndPosRel); + ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, storage->BoxSelectEndPosRel); // Clamped already + ImVec2 curr_end_pos_abs = g.IO.MousePos; + if (ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow + curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max); ms->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); ms->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); - ms->BoxSelectRectCurr.Min = ImMin(start_pos_abs, g.IO.MousePos); - ms->BoxSelectRectCurr.Max = ImMax(start_pos_abs, g.IO.MousePos); + ms->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); + ms->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); + //GetForegroundDrawList()->AddRect(ms->BoxSelectRectPrev.Min, ms->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); + //GetForegroundDrawList()->AddRect(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); } } @@ -7303,7 +7316,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && storage->BoxSelectActive) { // Box-select: render selection rectangle - ms->Storage->BoxSelectEndPosRel = WindowPosAbsToRel(window, g.IO.MousePos); + ms->Storage->BoxSelectEndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view ImRect box_select_r = ms->BoxSelectRectCurr; box_select_r.ClipWith(scope_rect); window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling @@ -7311,8 +7324,9 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() // Box-select: scroll ImRect scroll_r = scope_rect; - scroll_r.Expand(g.Style.FramePadding); - if ((ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_NoBoxSelectScroll) == 0 && !scroll_r.Contains(g.IO.MousePos)) + scroll_r.Expand(-g.FontSize); + //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255)); + if ((ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0 && !scroll_r.Contains(g.IO.MousePos)) BoxSelectScrollWithMouseDrag(window, scroll_r); } } From 15391762ddbd4ad03c11dc1746f0603c75036edc Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 20 Dec 2023 21:14:09 +0100 Subject: [PATCH 083/132] MultiSelect: Box-Select: Added ImGuiMultiSelectFlags_BoxSelect2d support. Enabled in Asset Browser. Selectable() supports it. --- imgui.cpp | 4 +++ imgui.h | 17 +++++++------ imgui_demo.cpp | 47 +++++++++++++++++----------------- imgui_internal.h | 2 ++ imgui_widgets.cpp | 65 ++++++++++++++++++++++++++++++++--------------- 5 files changed, 84 insertions(+), 51 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index bee410a72fc2..8f9a8fbc99cd 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3091,6 +3091,10 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) // As a workaround we currently half ItemSpacing worth on each side. min_y -= g.Style.ItemSpacing.y; max_y += g.Style.ItemSpacing.y; + + // Box-select on 2D area requires different clipping. + if (ms->BoxSelectUnclipMode) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(ms->BoxSelectUnclipRect.Min.y, ms->BoxSelectUnclipRect.Max.y, 0, 0)); } const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0; diff --git a/imgui.h b/imgui.h index 63159a14b67a..c4b8afa69a0c 100644 --- a/imgui.h +++ b/imgui.h @@ -2774,14 +2774,15 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_None = 0, ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut sending a SelectAll request. - ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection. Box-selection + clipper is currently only supported for 1D list (not with 2D grid). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 3, // Disable scrolling when box-selecting near edges of scope. - ImGuiMultiSelectFlags_ClearOnEscape = 1 << 4, // Clear selection when pressing Escape while scope is focused. - ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 5, // Clear selection when clicking on empty location within scope. - ImGuiMultiSelectFlags_ScopeWindow = 1 << 6, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if BeginMultiSelect() covers a whole window. - ImGuiMultiSelectFlags_ScopeRect = 1 << 7, // Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. Use if multiple BeginMultiSelect() are used in the same host window. - ImGuiMultiSelectFlags_SelectOnClick = 1 << 8, // Apply selection on mouse down when clicking on unselected item. (Default) - ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 9, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection (only supporting 1D list when using clipper, not 2D grids). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 3, // Enable box-selection with 2D layout/grid support. This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 4, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 5, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 6, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 7, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if BeginMultiSelect() covers a whole window. + ImGuiMultiSelectFlags_ScopeRect = 1 << 8, // Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. Use if multiple BeginMultiSelect() are used in the same host window. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 9, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 10, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. }; enum ImGuiSelectionRequestType diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 004f89ca1ad8..b84a0ca9f5a3 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -9622,7 +9622,7 @@ struct ExampleAssetsBrowser // Options bool ShowTypeOverlay = true; bool AllowDragUnselected = false; - bool AllowBoxSelect = false; // Unsupported for 2D selection for now. + bool AllowBoxSelect = true; float IconSize = 32.0f; int IconSpacing = 10; int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer. @@ -9726,9 +9726,7 @@ struct ExampleAssetsBrowser ImGui::SeparatorText("Selection Behavior"); ImGui::Checkbox("Allow dragging unselected item", &AllowDragUnselected); - ImGui::BeginDisabled(); // Unsupported for 2D selection for now. ImGui::Checkbox("Allow box-selection", &AllowBoxSelect); - ImGui::EndDisabled(); ImGui::SeparatorText("Layout"); ImGui::SliderFloat("Icon Size", &IconSize, 16.0f, 128.0f, "%.0f"); @@ -9778,7 +9776,7 @@ struct ExampleAssetsBrowser if (AllowDragUnselected) ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. if (AllowBoxSelect) - ms_flags |= ImGuiMultiSelectFlags_BoxSelect; // FIXME-MULTISELECT: Box-select not yet supported for 2D selection when using clipper. + ms_flags |= ImGuiMultiSelectFlags_BoxSelect2d; // Enable box-select in 2D mode. ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags); // Use custom selection adapter: store ID in selection (recommended) @@ -9798,7 +9796,6 @@ struct ExampleAssetsBrowser ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(LayoutSelectableSpacing, LayoutSelectableSpacing)); // Rendering parameters - const ImU32 icon_bg_color = IM_COL32(48, 48, 48, 128); const ImU32 icon_type_overlay_colors[3] = { 0, IM_COL32(200, 70, 70, 255), IM_COL32(70, 170, 70, 255) }; const ImVec2 icon_type_overlay_size = ImVec2(4.0f, 4.0f); const bool display_label = (LayoutItemSize.x >= ImGui::CalcTextSize("999").x); @@ -9825,13 +9822,9 @@ struct ExampleAssetsBrowser ImVec2 pos = ImVec2(start_pos.x + (item_idx % column_count) * LayoutItemStep.x, start_pos.y + line_idx * LayoutItemStep.y); ImGui::SetCursorScreenPos(pos); - // Draw box - ImVec2 box_min(pos.x - 1, pos.y - 1); - ImVec2 box_max(box_min.x + LayoutItemSize.x + 2, box_min.y + LayoutItemSize.y + 2); - draw_list->AddRect(box_min, box_max, IM_COL32(90, 90, 90, 255)); - ImGui::SetNextItemSelectionUserData(item_idx); bool item_is_selected = Selection.Contains((ImGuiID)item_data->ID); + bool item_is_visible = ImGui::IsRectVisible(LayoutItemSize); ImGui::Selectable("", item_is_selected, ImGuiSelectableFlags_None, LayoutItemSize); // Update our selection state immediately (without waiting for EndMultiSelect() requests) @@ -9856,19 +9849,26 @@ struct ExampleAssetsBrowser ImGui::EndDragDropSource(); } - // A real app would likely display an image/thumbnail here. - draw_list->AddRectFilled(box_min, box_max, icon_bg_color); - if (ShowTypeOverlay && item_data->Type != 0) - { - ImU32 type_col = icon_type_overlay_colors[item_data->Type % IM_ARRAYSIZE(icon_type_overlay_colors)]; - draw_list->AddRectFilled(ImVec2(box_max.x - 2 - icon_type_overlay_size.x, box_min.y + 2), ImVec2(box_max.x - 2, box_min.y + 2 + icon_type_overlay_size.y), type_col); - } - if (display_label) + // Render icon (a real app would likely display an image/thumbnail here) + // Because we use ImGuiMultiSelectFlags_BoxSelect2d mode, + // clipping vertical range may occasionally be larger so we coarse-clip our rendering. + if (item_is_visible) { - ImU32 label_col = item_is_selected ? IM_COL32(255, 255, 255, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled); - char label[32]; - sprintf(label, "%d", item_data->ID); - draw_list->AddText(ImVec2(box_min.x, box_max.y - ImGui::GetFontSize()), label_col, label); + ImVec2 box_min(pos.x - 1, pos.y - 1); + ImVec2 box_max(box_min.x + LayoutItemSize.x + 2, box_min.y + LayoutItemSize.y + 2); // Dubious + draw_list->AddRectFilled(box_min, box_max, IM_COL32(48, 48, 48, 200)); // Background color + if (ShowTypeOverlay && item_data->Type != 0) + { + ImU32 type_col = icon_type_overlay_colors[item_data->Type % IM_ARRAYSIZE(icon_type_overlay_colors)]; + draw_list->AddRectFilled(ImVec2(box_max.x - 2 - icon_type_overlay_size.x, box_min.y + 2), ImVec2(box_max.x - 2, box_min.y + 2 + icon_type_overlay_size.y), type_col); + } + if (display_label) + { + ImU32 label_col = item_is_selected ? IM_COL32(255, 255, 255, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled); + char label[32]; + sprintf(label, "%d", item_data->ID); + draw_list->AddText(ImVec2(box_min.x, box_max.y - ImGui::GetFontSize()), label_col, label); + } } ImGui::PopID(); @@ -9893,7 +9893,8 @@ struct ExampleAssetsBrowser if (want_delete) Selection.ApplyDeletionPostLoop(ms_io, Items, item_curr_idx_to_focus); - // FIXME-MULTISELECT: Find a way to expose this in public API. This currently requires "imgui_internal.h" + // Keyboard/Gamepad Wrapping + // FIXME-MULTISELECT: Currently an imgui_internal.h API. Find a design/way to expose this in public API. //ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); // Zooming with CTRL+Wheel diff --git a/imgui_internal.h b/imgui_internal.h index 0b05404e18bb..968a9413fdfd 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1726,8 +1726,10 @@ struct IMGUI_API ImGuiMultiSelectTempData ImGuiID BoxSelectId; ImRect BoxSelectRectPrev; ImRect BoxSelectRectCurr; // Selection rectangle in absolute coordinates (derived every frame from Storage->BoxSelectStartPosRel + MousePos) + ImRect BoxSelectUnclipRect;// Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. ImGuiSelectionUserData BoxSelectLastitem; ImGuiKeyChord KeyMods; + bool BoxSelectUnclipMode; bool LoopRequestClear; bool LoopRequestSelectAll; bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 52a342e5ad4b..050136a8821d 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6775,7 +6775,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0; const bool is_multi_select = (g.NextItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; // Before ItemAdd() - const bool item_add = ItemAdd(bb, id, NULL, disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None); + const bool is_visible = ItemAdd(bb, id, NULL, disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None); if (span_all_columns) { @@ -6783,8 +6783,14 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl window->ClipRect.Max.x = backup_clip_rect_max_x; } - if (!item_add) - return false; + if (!is_visible) + { + if (!is_multi_select) + return false; + // Extra layer of "no logic clip" for box-select support + if (!g.CurrentMultiSelect->BoxSelectUnclipMode || !g.CurrentMultiSelect->BoxSelectUnclipRect.Overlaps(bb)) + return false; + } const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; if (disabled_item && !disabled_global) // Only testing this as an optimization @@ -6857,22 +6863,25 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; // Render - if (hovered || selected) - { - // FIXME-MULTISELECT: Styling: Color for 'selected' elements? ImGuiCol_HeaderSelected - ImU32 col; - if (selected && !hovered) - col = GetColorU32(ImLerp(GetStyleColorVec4(ImGuiCol_Header), GetStyleColorVec4(ImGuiCol_HeaderHovered), 0.5f)); - else - col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); - RenderFrame(bb.Min, bb.Max, col, false, 0.0f); - } - if (g.NavId == id) + if (is_visible) { - ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding; - if (is_multi_select) - nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle - RenderNavHighlight(bb, id, nav_highlight_flags); + if (hovered || selected) + { + // FIXME-MULTISELECT: Styling: Color for 'selected' elements? ImGuiCol_HeaderSelected + ImU32 col; + if (selected && !hovered) + col = GetColorU32(ImLerp(GetStyleColorVec4(ImGuiCol_Header), GetStyleColorVec4(ImGuiCol_HeaderHovered), 0.5f)); + else + col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(bb.Min, bb.Max, col, false, 0.0f); + } + if (g.NavId == id) + { + ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding; + if (is_multi_select) + nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle + RenderNavHighlight(bb, id, nav_highlight_flags); + } } if (span_all_columns) @@ -6883,7 +6892,8 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl PopColumnsBackground(); } - RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb); + if (is_visible) + RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb); // Automatically close popups if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.InFlags & ImGuiItemFlags_AutoClosePopups)) @@ -7169,7 +7179,9 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) flags |= ImGuiMultiSelectFlags_ScopeWindow; if (flags & ImGuiMultiSelectFlags_SingleSelect) - flags &= ~ImGuiMultiSelectFlags_BoxSelect; + flags &= ~(ImGuiMultiSelectFlags_BoxSelect | ImGuiMultiSelectFlags_BoxSelect2d); + if (flags & ImGuiMultiSelectFlags_BoxSelect2d) + flags |= ImGuiMultiSelectFlags_BoxSelect; // FIXME: BeginFocusScope() const ImGuiID id = window->IDStack.back(); @@ -7233,6 +7245,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) } // Box-select handling: update active state. + ms->BoxSelectUnclipMode = false; if (flags & ImGuiMultiSelectFlags_BoxSelect) { ms->BoxSelectId = GetID("##BoxSelect"); @@ -7267,6 +7280,18 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ms->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); ms->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); ms->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); + + // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) + // Storing an extra rect used by widgets supporting box-select. + if (flags & ImGuiMultiSelectFlags_BoxSelect2d) + if (ms->BoxSelectRectPrev.Min.x != ms->BoxSelectRectCurr.Min.x || ms->BoxSelectRectPrev.Max.x != ms->BoxSelectRectCurr.Max.x) + { + ms->BoxSelectUnclipRect = ms->BoxSelectRectPrev; + ms->BoxSelectUnclipRect.Add(ms->BoxSelectRectCurr); + ms->BoxSelectUnclipMode = true; + } + + //GetForegroundDrawList()->AddRect(ms->BoxSelectNoClipRect.Min, ms->BoxSelectNoClipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); //GetForegroundDrawList()->AddRect(ms->BoxSelectRectPrev.Min, ms->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); //GetForegroundDrawList()->AddRect(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); } From 75bac1aac67570109d0ca4fb87bd03a73ec30fb2 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 20 Dec 2023 22:07:28 +0100 Subject: [PATCH 084/132] MultiSelect: Box-Select: Refactor into its own structure, designed for single-instance but closer to being reusable outside Multi-Select. Kept same member names. --- imgui.cpp | 10 ++-- imgui_internal.h | 47 ++++++++++------ imgui_widgets.cpp | 133 ++++++++++++++++++++++++++-------------------- 3 files changed, 113 insertions(+), 77 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 8f9a8fbc99cd..d82d48c51572 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3083,8 +3083,8 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) float max_y = window->ClipRect.Max.y; // Add box selection range - if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect) - if (ms->Storage->Window == window && ms->Storage->BoxSelectActive) + if (ImGuiBoxSelectState* bs = &g.BoxSelectState) + if (bs->BoxSelectActive && bs->BoxSelectWindow == window) { // FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos. // RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that. @@ -3093,8 +3093,8 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) max_y += g.Style.ItemSpacing.y; // Box-select on 2D area requires different clipping. - if (ms->BoxSelectUnclipMode) - data->Ranges.push_back(ImGuiListClipperRange::FromPositions(ms->BoxSelectUnclipRect.Min.y, ms->BoxSelectUnclipRect.Max.y, 0, 0)); + if (bs->BoxSelectUnclipMode) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->BoxSelectUnclipRect.Min.y, bs->BoxSelectUnclipRect.Max.y, 0, 0)); } const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0; @@ -14997,6 +14997,8 @@ void ImGui::ShowMetricsWindow(bool* p_open) // Details for MultiSelect if (TreeNode("MultiSelect", "MultiSelect (%d)", g.MultiSelectStorage.GetAliveCount())) { + ImGuiBoxSelectState* ms = &g.BoxSelectState; + Text("BoxSelect ID=0x%08X, Starting = %d, Active %d", ms->BoxSelectId, ms->BoxSelectStarting, ms->BoxSelectActive); for (int n = 0; n < g.MultiSelectStorage.GetMapSize(); n++) if (ImGuiMultiSelectState* state = g.MultiSelectStorage.TryGetMapData(n)) DebugNodeMultiSelectState(state); diff --git a/imgui_internal.h b/imgui_internal.h index 968a9413fdfd..165e3c058aec 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -22,6 +22,7 @@ Index of this file: // [SECTION] Navigation support // [SECTION] Typing-select support // [SECTION] Columns support +// [SECTION] Box-select support // [SECTION] Multi-select support // [SECTION] Docking support // [SECTION] Viewport support @@ -123,6 +124,7 @@ struct ImBitVector; // Store 1-bit per value struct ImRect; // An axis-aligned rectangle (2 points) struct ImDrawDataBuilder; // Helper to build a ImDrawData instance struct ImDrawListSharedData; // Data shared between all ImDrawList instances +struct ImGuiBoxSelectState; // Box-selection state (currently used by multi-selection, could potentially be used by others) struct ImGuiColorMod; // Stacked color modifier, backup of modified data so we can restore it struct ImGuiContext; // Main Dear ImGui context struct ImGuiContextHook; // Hook for extensions like ImGuiTestEngine @@ -1705,6 +1707,32 @@ struct ImGuiOldColumns ImGuiOldColumns() { memset(this, 0, sizeof(*this)); } }; +//----------------------------------------------------------------------------- +// [SECTION] Box-select support +//----------------------------------------------------------------------------- + +struct ImGuiBoxSelectState +{ + // Active box-selection data (persistent, 1 active at a time) + ImGuiID BoxSelectId; + bool BoxSelectActive; + bool BoxSelectStarting; + bool BoxSelectFromVoid; + ImGuiKeyChord BoxSelectKeyMods : 16; // Latched key-mods for box-select logic. + ImVec2 BoxSelectStartPosRel; // Start position in window-relative space (to support scrolling) + ImVec2 BoxSelectEndPosRel; // End position in window-relative space + ImGuiWindow* BoxSelectWindow; + + // Temporary/Transient data + bool BoxSelectUnclipMode; // Set/cleared by the BeginMultiSelect()/EndMultiSelect() owning active box-select. + ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos) + ImRect BoxSelectRectCurr; + ImRect BoxSelectUnclipRect; // Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. + ImGuiSelectionUserData BoxSelectLastitem; + + ImGuiBoxSelectState() { memset(this, 0, sizeof(*this)); } +}; + //----------------------------------------------------------------------------- // [SECTION] Multi-select support //----------------------------------------------------------------------------- @@ -1724,12 +1752,7 @@ struct IMGUI_API ImGuiMultiSelectTempData ImVec2 ScopeRectMin; ImVec2 BackupCursorMaxPos; ImGuiID BoxSelectId; - ImRect BoxSelectRectPrev; - ImRect BoxSelectRectCurr; // Selection rectangle in absolute coordinates (derived every frame from Storage->BoxSelectStartPosRel + MousePos) - ImRect BoxSelectUnclipRect;// Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. - ImGuiSelectionUserData BoxSelectLastitem; ImGuiKeyChord KeyMods; - bool BoxSelectUnclipMode; bool LoopRequestClear; bool LoopRequestSelectAll; bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. @@ -1740,7 +1763,7 @@ struct IMGUI_API ImGuiMultiSelectTempData bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. ImGuiMultiSelectTempData() { Clear(); } - void Clear() { size_t io_sz = sizeof(IO); ClearIO(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); BoxSelectLastitem = -1; } // Zero-clear except IO as we preserve IO.Requests[] buffer allocation. + void Clear() { size_t io_sz = sizeof(IO); ClearIO(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); } // Zero-clear except IO as we preserve IO.Requests[] buffer allocation. void ClearIO() { IO.Requests.resize(0); IO.RangeSrcItem = IO.NavIdItem = (ImGuiSelectionUserData)-1; IO.NavIdSelected = IO.RangeSrcReset = false; } }; @@ -1755,15 +1778,7 @@ struct IMGUI_API ImGuiMultiSelectState ImGuiSelectionUserData RangeSrcItem; // ImGuiSelectionUserData NavIdItem; // SetNextItemSelectionUserData() value for NavId (if part of submitted items) - bool BoxSelectActive; - bool BoxSelectStarting; - bool BoxSelectFromVoid; - ImGuiKeyChord BoxSelectKeyMods : 16; // Latched key-mods for box-select logic. - ImVec2 BoxSelectStartPosRel; // Start position in window-relative space (to support scrolling) - ImVec2 BoxSelectEndPosRel; // End position in window-relative space - - ImGuiMultiSelectState() { Init(0); } - void Init(ImGuiID id) { Window = NULL; ID = id; LastFrameActive = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; BoxSelectActive = BoxSelectStarting = BoxSelectFromVoid = false; BoxSelectKeyMods = 0; } + ImGuiMultiSelectState() { Window = NULL; ID = 0; LastFrameActive = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; } }; #endif // #ifdef IMGUI_HAS_MULTI_SELECT @@ -2203,6 +2218,7 @@ struct ImGuiContext ImVector ShrinkWidthBuffer; // Multi-Select state + ImGuiBoxSelectState BoxSelectState; ImGuiMultiSelectTempData* CurrentMultiSelect; int MultiSelectTempDataStacked; // Temporary multi-select data size (because we leave previous instances undestructed, we generally don't use MultiSelectTempData.Size) ImVector MultiSelectTempData; @@ -3389,6 +3405,7 @@ namespace ImGui // Multi-Select API IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags); IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); + inline ImGuiBoxSelectState* GetBoxSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.BoxSelectState.BoxSelectId == id && g.BoxSelectState.BoxSelectActive) ? &g.BoxSelectState : NULL; } // Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API) IMGUI_API void SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 050136a8821d..7470b607e683 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -19,6 +19,7 @@ Index of this file: // [SECTION] Widgets: TreeNode, CollapsingHeader, etc. // [SECTION] Widgets: Selectable // [SECTION] Widgets: Typing-Select support +// [SECTION] Widgets: Box-Select support // [SECTION] Widgets: Multi-Select support // [SECTION] Widgets: ListBox // [SECTION] Widgets: PlotLines, PlotHistogram @@ -6788,7 +6789,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (!is_multi_select) return false; // Extra layer of "no logic clip" for box-select support - if (!g.CurrentMultiSelect->BoxSelectUnclipMode || !g.CurrentMultiSelect->BoxSelectUnclipRect.Overlaps(bb)) + if (!g.BoxSelectState.BoxSelectUnclipMode || !g.BoxSelectState.BoxSelectUnclipRect.Overlaps(bb)) return false; } @@ -7110,40 +7111,22 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) #endif } - //------------------------------------------------------------------------- -// [SECTION] Widgets: Multi-Select support +// [SECTION] Widgets: Box-Select support //------------------------------------------------------------------------- -// - DebugLogMultiSelectRequests() [Internal] // - BoxSelectStart() [Internal] // - BoxSelectScrollWithMouseDrag() [Internal] -// - BeginMultiSelect() -// - EndMultiSelect() -// - SetNextItemSelectionUserData() -// - MultiSelectItemHeader() [Internal] -// - MultiSelectItemFooter() [Internal] -// - DebugNodeMultiSelectState() [Internal] -// - ImGuiSelectionBasicStorage //------------------------------------------------------------------------- -static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) +static void BoxSelectStart(ImGuiID id, ImGuiSelectionUserData clicked_item) { ImGuiContext& g = *GImGui; - for (const ImGuiSelectionRequest& req : io->Requests) - { - if (req.Type == ImGuiSelectionRequestType_Clear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: Clear\n", function); - if (req.Type == ImGuiSelectionRequestType_SelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SelectAll\n", function); - if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.RangeSelected); - } -} - -static void BoxSelectStart(ImGuiMultiSelectState* storage, ImGuiSelectionUserData clicked_item) -{ - ImGuiContext& g = *GImGui; - storage->BoxSelectStarting = true; // Consider starting box-select. - storage->BoxSelectFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid); - storage->BoxSelectKeyMods = g.IO.KeyMods; - storage->BoxSelectStartPosRel = storage->BoxSelectEndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); + ImGuiBoxSelectState* bs = &g.BoxSelectState; + bs->BoxSelectId = id; + bs->BoxSelectStarting = true; // Consider starting box-select. + bs->BoxSelectFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid); + bs->BoxSelectKeyMods = g.IO.KeyMods; + bs->BoxSelectStartPosRel = bs->BoxSelectEndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); } static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& inner_r) @@ -7165,6 +7148,31 @@ static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& inne } } + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Multi-Select support +//------------------------------------------------------------------------- +// - DebugLogMultiSelectRequests() [Internal] +// - BeginMultiSelect() +// - EndMultiSelect() +// - SetNextItemSelectionUserData() +// - MultiSelectItemHeader() [Internal] +// - MultiSelectItemFooter() [Internal] +// - DebugNodeMultiSelectState() [Internal] +// - ImGuiSelectionBasicStorage +//------------------------------------------------------------------------- + +static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) +{ + ImGuiContext& g = *GImGui; + for (const ImGuiSelectionRequest& req : io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_Clear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: Clear\n", function); + if (req.Type == ImGuiSelectionRequestType_SelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SelectAll\n", function); + if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.RangeSelected); + } +} + // Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) { @@ -7245,50 +7253,57 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) } // Box-select handling: update active state. - ms->BoxSelectUnclipMode = false; + ImGuiBoxSelectState* bs = &g.BoxSelectState; if (flags & ImGuiMultiSelectFlags_BoxSelect) { ms->BoxSelectId = GetID("##BoxSelect"); KeepAliveID(ms->BoxSelectId); + } + if ((flags & ImGuiMultiSelectFlags_BoxSelect) && ms->BoxSelectId == bs->BoxSelectId) + { + bs->BoxSelectUnclipMode = false; // BoxSelectStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. - if (storage->BoxSelectStarting && IsMouseDragPastThreshold(0)) + if (bs->BoxSelectStarting && IsMouseDragPastThreshold(0)) { - storage->BoxSelectActive = true; - storage->BoxSelectStarting = false; + bs->BoxSelectActive = true; + bs->BoxSelectWindow = ms->Storage->Window; + bs->BoxSelectStarting = false; SetActiveID(ms->BoxSelectId, window); - if (storage->BoxSelectFromVoid && (storage->BoxSelectKeyMods & ImGuiMod_Shift) == 0) + if (bs->BoxSelectFromVoid && (bs->BoxSelectKeyMods & ImGuiMod_Shift) == 0) request_clear = true; } - else if ((storage->BoxSelectStarting || storage->BoxSelectActive) && g.IO.MouseDown[0] == false) + else if ((bs->BoxSelectStarting || bs->BoxSelectActive) && g.IO.MouseDown[0] == false) { - storage->BoxSelectActive = storage->BoxSelectStarting = false; + bs->BoxSelectId = 0; + bs->BoxSelectActive = bs->BoxSelectStarting = false; if (g.ActiveId == ms->BoxSelectId) ClearActiveID(); } - if (storage->BoxSelectActive) + if (bs->BoxSelectActive) { // Current frame absolute prev/current rectangles are used to toggle selection. // They are derived from positions relative to scrolling space. const ImRect scope_rect = window->InnerClipRect; - ImVec2 start_pos_abs = WindowPosRelToAbs(window, storage->BoxSelectStartPosRel); - ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, storage->BoxSelectEndPosRel); // Clamped already + ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->BoxSelectStartPosRel); + ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->BoxSelectEndPosRel); // Clamped already ImVec2 curr_end_pos_abs = g.IO.MousePos; if (ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max); - ms->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); - ms->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); - ms->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); - ms->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); + bs->BoxSelectLastitem = -1; + bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); + bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); + bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); + bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) // Storing an extra rect used by widgets supporting box-select. if (flags & ImGuiMultiSelectFlags_BoxSelect2d) - if (ms->BoxSelectRectPrev.Min.x != ms->BoxSelectRectCurr.Min.x || ms->BoxSelectRectPrev.Max.x != ms->BoxSelectRectCurr.Max.x) + if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) { - ms->BoxSelectUnclipRect = ms->BoxSelectRectPrev; - ms->BoxSelectUnclipRect.Add(ms->BoxSelectRectCurr); - ms->BoxSelectUnclipMode = true; + bs->BoxSelectUnclipRect = bs->BoxSelectRectPrev; + bs->BoxSelectUnclipRect.Add(bs->BoxSelectRectCurr); + bs->BoxSelectUnclipMode = true; } //GetForegroundDrawList()->AddRect(ms->BoxSelectNoClipRect.Min, ms->BoxSelectNoClipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); @@ -7338,11 +7353,12 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() storage->NavIdSelected = -1; } - if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && storage->BoxSelectActive) + ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId); + if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && bs != NULL) { // Box-select: render selection rectangle - ms->Storage->BoxSelectEndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view - ImRect box_select_r = ms->BoxSelectRectCurr; + bs->BoxSelectEndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view + ImRect box_select_r = bs->BoxSelectRectCurr; box_select_r.ClipWith(scope_rect); window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling @@ -7353,6 +7369,8 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255)); if ((ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0 && !scroll_r.Contains(g.IO.MousePos)) BoxSelectScrollWithMouseDrag(window, scroll_r); + + bs->BoxSelectUnclipMode = false; } } @@ -7365,8 +7383,8 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) { if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) - if (!storage->BoxSelectActive && !storage->BoxSelectStarting && g.IO.MouseClickedCount[0] == 1) - BoxSelectStart(storage, ImGuiSelectionUserData_Invalid); + if (!g.BoxSelectState.BoxSelectActive && !g.BoxSelectState.BoxSelectStarting && g.IO.MouseClickedCount[0] == 1) + BoxSelectStart(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) @@ -7516,21 +7534,21 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) } // Box-select handling - if (ms->Storage->BoxSelectActive) + if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId)) { - const bool rect_overlap_curr = ms->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); - const bool rect_overlap_prev = ms->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); + const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); + const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) { selected = !selected; ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; - if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->RangeSelected == selected) + if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == bs->BoxSelectLastitem && prev_req->RangeSelected == selected) prev_req->RangeLastItem = item_data; // Merge span into same request else ms->IO.Requests.push_back(req); } - ms->BoxSelectLastitem = item_data; + bs->BoxSelectLastitem = item_data; } // Right-click handling: this could be moved at the Selectable() level. @@ -7558,8 +7576,8 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Box-select ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) - if (selected == false && !storage->BoxSelectActive && !storage->BoxSelectStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) - BoxSelectStart(storage, item_data); + if (selected == false && !g.BoxSelectState.BoxSelectActive && !g.BoxSelectState.BoxSelectStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) + BoxSelectStart(ms->BoxSelectId, item_data); //---------------------------------------------------------------------------------------- // ACTION | Begin | Pressed/Activated | End @@ -7645,7 +7663,6 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) return; Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected); Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected); - Text("BoxSelect Starting = %d, Active %d", storage->BoxSelectStarting, storage->BoxSelectActive); TreePop(); #else IM_UNUSED(storage); From 5d9de14493c39ec1ca00eed1be4b1e02761857e4 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 20 Dec 2023 22:19:55 +0100 Subject: [PATCH 085/132] MultiSelect: Box-Select: Refactor: Renames. Split into two commits to facilite looking into previous one if needed. --- imgui.cpp | 28 +++++++++++------------ imgui_internal.h | 26 ++++++++++----------- imgui_widgets.cpp | 58 +++++++++++++++++++++++------------------------ 3 files changed, 56 insertions(+), 56 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index d82d48c51572..ffbebea7ca1e 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3083,19 +3083,19 @@ static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) float max_y = window->ClipRect.Max.y; // Add box selection range - if (ImGuiBoxSelectState* bs = &g.BoxSelectState) - if (bs->BoxSelectActive && bs->BoxSelectWindow == window) - { - // FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos. - // RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that. - // As a workaround we currently half ItemSpacing worth on each side. - min_y -= g.Style.ItemSpacing.y; - max_y += g.Style.ItemSpacing.y; - - // Box-select on 2D area requires different clipping. - if (bs->BoxSelectUnclipMode) - data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->BoxSelectUnclipRect.Min.y, bs->BoxSelectUnclipRect.Max.y, 0, 0)); - } + ImGuiBoxSelectState* bs = &g.BoxSelectState; + if (bs->IsActive && bs->Window == window) + { + // FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos. + // RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that. + // As a workaround we currently half ItemSpacing worth on each side. + min_y -= g.Style.ItemSpacing.y; + max_y += g.Style.ItemSpacing.y; + + // Box-select on 2D area requires different clipping. + if (bs->UnclipMode) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->UnclipRect.Min.y, bs->UnclipRect.Max.y, 0, 0)); + } const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0; const int off_max = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Down) ? 1 : 0; @@ -14998,7 +14998,7 @@ void ImGui::ShowMetricsWindow(bool* p_open) if (TreeNode("MultiSelect", "MultiSelect (%d)", g.MultiSelectStorage.GetAliveCount())) { ImGuiBoxSelectState* ms = &g.BoxSelectState; - Text("BoxSelect ID=0x%08X, Starting = %d, Active %d", ms->BoxSelectId, ms->BoxSelectStarting, ms->BoxSelectActive); + Text("BoxSelect ID=0x%08X, Starting = %d, Active %d", ms->ID, ms->IsStarting, ms->IsActive); for (int n = 0; n < g.MultiSelectStorage.GetMapSize(); n++) if (ImGuiMultiSelectState* state = g.MultiSelectStorage.TryGetMapData(n)) DebugNodeMultiSelectState(state); diff --git a/imgui_internal.h b/imgui_internal.h index 165e3c058aec..8e71683f4f3f 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1714,21 +1714,21 @@ struct ImGuiOldColumns struct ImGuiBoxSelectState { // Active box-selection data (persistent, 1 active at a time) - ImGuiID BoxSelectId; - bool BoxSelectActive; - bool BoxSelectStarting; - bool BoxSelectFromVoid; - ImGuiKeyChord BoxSelectKeyMods : 16; // Latched key-mods for box-select logic. - ImVec2 BoxSelectStartPosRel; // Start position in window-relative space (to support scrolling) - ImVec2 BoxSelectEndPosRel; // End position in window-relative space - ImGuiWindow* BoxSelectWindow; + ImGuiID ID; + bool IsActive; + bool IsStarting; + bool IsStartedFromVoid; // Starting click was not from an item. + ImGuiKeyChord KeyMods : 16; // Latched key-mods for box-select logic. + ImVec2 StartPosRel; // Start position in window-contents relative space (to support scrolling) + ImVec2 EndPosRel; // End position in window-contents relative space + ImGuiWindow* Window; // Temporary/Transient data - bool BoxSelectUnclipMode; // Set/cleared by the BeginMultiSelect()/EndMultiSelect() owning active box-select. - ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos) + bool UnclipMode; // (Temp/Transient, here in hot area). Set/cleared by the BeginMultiSelect()/EndMultiSelect() owning active box-select. + ImRect UnclipRect; // Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. + ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos) ImRect BoxSelectRectCurr; - ImRect BoxSelectUnclipRect; // Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. - ImGuiSelectionUserData BoxSelectLastitem; + ImGuiSelectionUserData LastSubmittedItem; // Copy of last submitted item data, used to merge output ranges. ImGuiBoxSelectState() { memset(this, 0, sizeof(*this)); } }; @@ -3405,7 +3405,7 @@ namespace ImGui // Multi-Select API IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags); IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); - inline ImGuiBoxSelectState* GetBoxSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.BoxSelectState.BoxSelectId == id && g.BoxSelectState.BoxSelectActive) ? &g.BoxSelectState : NULL; } + inline ImGuiBoxSelectState* GetBoxSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.BoxSelectState.ID == id && g.BoxSelectState.IsActive) ? &g.BoxSelectState : NULL; } // Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API) IMGUI_API void SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 7470b607e683..7f3e529cad8a 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6789,7 +6789,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (!is_multi_select) return false; // Extra layer of "no logic clip" for box-select support - if (!g.BoxSelectState.BoxSelectUnclipMode || !g.BoxSelectState.BoxSelectUnclipRect.Overlaps(bb)) + if (!g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(bb)) return false; } @@ -7122,11 +7122,11 @@ static void BoxSelectStart(ImGuiID id, ImGuiSelectionUserData clicked_item) { ImGuiContext& g = *GImGui; ImGuiBoxSelectState* bs = &g.BoxSelectState; - bs->BoxSelectId = id; - bs->BoxSelectStarting = true; // Consider starting box-select. - bs->BoxSelectFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid); - bs->BoxSelectKeyMods = g.IO.KeyMods; - bs->BoxSelectStartPosRel = bs->BoxSelectEndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); + bs->ID = id; + bs->IsStarting = true; // Consider starting box-select. + bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid); + bs->KeyMods = g.IO.KeyMods; + bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); } static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& inner_r) @@ -7259,38 +7259,38 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ms->BoxSelectId = GetID("##BoxSelect"); KeepAliveID(ms->BoxSelectId); } - if ((flags & ImGuiMultiSelectFlags_BoxSelect) && ms->BoxSelectId == bs->BoxSelectId) + if ((flags & ImGuiMultiSelectFlags_BoxSelect) && ms->BoxSelectId == bs->ID) { - bs->BoxSelectUnclipMode = false; + bs->UnclipMode = false; // BoxSelectStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. - if (bs->BoxSelectStarting && IsMouseDragPastThreshold(0)) + if (bs->IsStarting && IsMouseDragPastThreshold(0)) { - bs->BoxSelectActive = true; - bs->BoxSelectWindow = ms->Storage->Window; - bs->BoxSelectStarting = false; + bs->IsActive = true; + bs->Window = ms->Storage->Window; + bs->IsStarting = false; SetActiveID(ms->BoxSelectId, window); - if (bs->BoxSelectFromVoid && (bs->BoxSelectKeyMods & ImGuiMod_Shift) == 0) + if (bs->IsStartedFromVoid && (bs->KeyMods & ImGuiMod_Shift) == 0) request_clear = true; } - else if ((bs->BoxSelectStarting || bs->BoxSelectActive) && g.IO.MouseDown[0] == false) + else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false) { - bs->BoxSelectId = 0; - bs->BoxSelectActive = bs->BoxSelectStarting = false; + bs->ID = 0; + bs->IsActive = bs->IsStarting = false; if (g.ActiveId == ms->BoxSelectId) ClearActiveID(); } - if (bs->BoxSelectActive) + if (bs->IsActive) { // Current frame absolute prev/current rectangles are used to toggle selection. // They are derived from positions relative to scrolling space. const ImRect scope_rect = window->InnerClipRect; - ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->BoxSelectStartPosRel); - ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->BoxSelectEndPosRel); // Clamped already + ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel); + ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already ImVec2 curr_end_pos_abs = g.IO.MousePos; if (ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max); - bs->BoxSelectLastitem = -1; + bs->LastSubmittedItem = ImGuiSelectionUserData_Invalid; bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); @@ -7301,9 +7301,9 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if (flags & ImGuiMultiSelectFlags_BoxSelect2d) if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) { - bs->BoxSelectUnclipRect = bs->BoxSelectRectPrev; - bs->BoxSelectUnclipRect.Add(bs->BoxSelectRectCurr); - bs->BoxSelectUnclipMode = true; + bs->UnclipRect = bs->BoxSelectRectPrev; + bs->UnclipRect.Add(bs->BoxSelectRectCurr); + bs->UnclipMode = true; } //GetForegroundDrawList()->AddRect(ms->BoxSelectNoClipRect.Min, ms->BoxSelectNoClipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); @@ -7357,7 +7357,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && bs != NULL) { // Box-select: render selection rectangle - bs->BoxSelectEndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view + bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view ImRect box_select_r = bs->BoxSelectRectCurr; box_select_r.ClipWith(scope_rect); window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling @@ -7370,7 +7370,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if ((ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0 && !scroll_r.Contains(g.IO.MousePos)) BoxSelectScrollWithMouseDrag(window, scroll_r); - bs->BoxSelectUnclipMode = false; + bs->UnclipMode = false; } } @@ -7383,7 +7383,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) { if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) - if (!g.BoxSelectState.BoxSelectActive && !g.BoxSelectState.BoxSelectStarting && g.IO.MouseClickedCount[0] == 1) + if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1) BoxSelectStart(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) @@ -7543,12 +7543,12 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) selected = !selected; ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; - if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == bs->BoxSelectLastitem && prev_req->RangeSelected == selected) + if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == bs->LastSubmittedItem && prev_req->RangeSelected == selected) prev_req->RangeLastItem = item_data; // Merge span into same request else ms->IO.Requests.push_back(req); } - bs->BoxSelectLastitem = item_data; + bs->LastSubmittedItem = item_data; } // Right-click handling: this could be moved at the Selectable() level. @@ -7576,7 +7576,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Box-select ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) - if (selected == false && !g.BoxSelectState.BoxSelectActive && !g.BoxSelectState.BoxSelectStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) + if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) BoxSelectStart(ms->BoxSelectId, item_data); //---------------------------------------------------------------------------------------- From 907268a4305b77920a94453214a35637e981b0ab Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 20 Dec 2023 22:31:07 +0100 Subject: [PATCH 086/132] MultiSelect: Box-Select: Fixed scrolling on high framerates. --- imgui_internal.h | 1 + imgui_widgets.cpp | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 8e71683f4f3f..7c0c6aa22cca 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1721,6 +1721,7 @@ struct ImGuiBoxSelectState ImGuiKeyChord KeyMods : 16; // Latched key-mods for box-select logic. ImVec2 StartPosRel; // Start position in window-contents relative space (to support scrolling) ImVec2 EndPosRel; // End position in window-contents relative space + ImVec2 ScrollAccum; // Scrolling accumulator (to behave at high-frame spaces) ImGuiWindow* Window; // Temporary/Transient data diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 7f3e529cad8a..14d4cac64a7a 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7127,11 +7127,14 @@ static void BoxSelectStart(ImGuiID id, ImGuiSelectionUserData clicked_item) bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid); bs->KeyMods = g.IO.KeyMods; bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); + bs->ScrollAccum = ImVec2(0.0f, 0.0f); } static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& inner_r) { ImGuiContext& g = *GImGui; + ImGuiBoxSelectState* bs = &g.BoxSelectState; + IM_ASSERT(bs->Window == window); for (int n = 0; n < 2; n++) // each axis { const float mouse_pos = g.IO.MousePos[n]; @@ -7139,12 +7142,20 @@ static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& inne const float scroll_curr = window->Scroll[n]; if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n])) continue; + const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance - const float scroll_step = IM_ROUND(g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime); + const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime; + bs->ScrollAccum[n] += scroll_step; + + // Accumulate into a stored value so we can handle high-framerate + const float scroll_step_i = ImFloor(bs->ScrollAccum[n]); + if (scroll_step_i == 0.0f) + continue; if (n == 0) - ImGui::SetScrollX(window, scroll_curr + scroll_step); + ImGui::SetScrollX(window, scroll_curr + scroll_step_i); else - ImGui::SetScrollY(window, scroll_curr + scroll_step); + ImGui::SetScrollY(window, scroll_curr + scroll_step_i); + bs->ScrollAccum[n] -= scroll_step_i; } } From f3d77d8e71bdb72c9daa62942bab4ebb751222ec Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 20 Dec 2023 22:34:50 +0100 Subject: [PATCH 087/132] MultiSelect: Box-Select: Further refactor to extra mode code away from multi-select function into box-select funcitons. --- imgui.cpp | 4 +- imgui_internal.h | 7 +- imgui_widgets.cpp | 178 ++++++++++++++++++++++++++-------------------- 3 files changed, 109 insertions(+), 80 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index ffbebea7ca1e..291863e6f487 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -14997,8 +14997,8 @@ void ImGui::ShowMetricsWindow(bool* p_open) // Details for MultiSelect if (TreeNode("MultiSelect", "MultiSelect (%d)", g.MultiSelectStorage.GetAliveCount())) { - ImGuiBoxSelectState* ms = &g.BoxSelectState; - Text("BoxSelect ID=0x%08X, Starting = %d, Active %d", ms->ID, ms->IsStarting, ms->IsActive); + ImGuiBoxSelectState* bs = &g.BoxSelectState; + BulletText("BoxSelect ID=0x%08X, Starting = %d, Active %d", bs->ID, bs->IsStarting, bs->IsActive); for (int n = 0; n < g.MultiSelectStorage.GetMapSize(); n++) if (ImGuiMultiSelectState* state = g.MultiSelectStorage.TryGetMapData(n)) DebugNodeMultiSelectState(state); diff --git a/imgui_internal.h b/imgui_internal.h index 7c0c6aa22cca..3659bf8cc07b 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1718,6 +1718,7 @@ struct ImGuiBoxSelectState bool IsActive; bool IsStarting; bool IsStartedFromVoid; // Starting click was not from an item. + bool RequestClear; ImGuiKeyChord KeyMods : 16; // Latched key-mods for box-select logic. ImVec2 StartPosRel; // Start position in window-contents relative space (to support scrolling) ImVec2 EndPosRel; // End position in window-contents relative space @@ -1729,7 +1730,6 @@ struct ImGuiBoxSelectState ImRect UnclipRect; // Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos) ImRect BoxSelectRectCurr; - ImGuiSelectionUserData LastSubmittedItem; // Copy of last submitted item data, used to merge output ranges. ImGuiBoxSelectState() { memset(this, 0, sizeof(*this)); } }; @@ -1762,6 +1762,7 @@ struct IMGUI_API ImGuiMultiSelectTempData bool NavIdPassedBy; bool RangeSrcPassedBy; // Set by the item that matches RangeSrcItem. bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. + ImGuiSelectionUserData BoxSelectLastitem; // Copy of last submitted item data, used to merge output ranges. ImGuiMultiSelectTempData() { Clear(); } void Clear() { size_t io_sz = sizeof(IO); ClearIO(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); } // Zero-clear except IO as we preserve IO.Requests[] buffer allocation. @@ -3403,6 +3404,10 @@ namespace ImGui IMGUI_API int TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); IMGUI_API int TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data); + // Box-Select API + IMGUI_API bool BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags); + IMGUI_API void EndBoxSelect(const ImRect& scope_rect, bool enable_scroll); + // Multi-Select API IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags); IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 14d4cac64a7a..f9dfa143ef02 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7113,12 +7113,15 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) //------------------------------------------------------------------------- // [SECTION] Widgets: Box-Select support +// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet. //------------------------------------------------------------------------- -// - BoxSelectStart() [Internal] +// - BoxSelectStartDrag() [Internal] // - BoxSelectScrollWithMouseDrag() [Internal] +// - BeginBoxSelect() [Internal] +// - EndBoxSelect() [Internal] //------------------------------------------------------------------------- -static void BoxSelectStart(ImGuiID id, ImGuiSelectionUserData clicked_item) +static void BoxSelectStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item) { ImGuiContext& g = *GImGui; ImGuiBoxSelectState* bs = &g.BoxSelectState; @@ -7159,6 +7162,91 @@ static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& inne } } +bool ImGui::BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiBoxSelectState* bs = &g.BoxSelectState; + KeepAliveID(box_select_id); + if (bs->ID != box_select_id) + return false; + + bs->UnclipMode = false; + bs->RequestClear = false; + + // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. + if (bs->IsStarting && IsMouseDragPastThreshold(0)) + { + bs->IsActive = true; + bs->Window = window; + bs->IsStarting = false; + SetActiveID(bs->ID, window); + if (bs->IsStartedFromVoid && (bs->KeyMods & ImGuiMod_Shift) == 0) + bs->RequestClear = true; + } + else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false) + { + bs->IsActive = bs->IsStarting = false; + if (g.ActiveId == bs->ID) + ClearActiveID(); + bs->ID = 0; + } + if (!bs->IsActive) + return false; + + // Current frame absolute prev/current rectangles are used to toggle selection. + // They are derived from positions relative to scrolling space. + const ImRect scope_rect = window->InnerClipRect; + ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel); + ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already + ImVec2 curr_end_pos_abs = g.IO.MousePos; + if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow + curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max); + bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); + bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); + bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); + bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); + + // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) + // Storing an extra rect used by widgets supporting box-select. + if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) + if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) + { + bs->UnclipRect = bs->BoxSelectRectPrev; + bs->UnclipRect.Add(bs->BoxSelectRectCurr); + bs->UnclipMode = true; + } + + //GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); + //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); + //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); + return true; +} + +void ImGui::EndBoxSelect(const ImRect& scope_rect, bool enable_scroll) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiBoxSelectState* bs = &g.BoxSelectState; + IM_ASSERT(bs->IsActive); + bs->UnclipMode = false; + + // Render selection rectangle + bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view + ImRect box_select_r = bs->BoxSelectRectCurr; + box_select_r.ClipWith(scope_rect); + window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling + window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling + + // Scroll + if (enable_scroll) + { + ImRect scroll_r = scope_rect; + scroll_r.Expand(-g.FontSize); + //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255)); + if (!scroll_r.Contains(g.IO.MousePos)) + BoxSelectScrollWithMouseDrag(window, scroll_r); + } +} //------------------------------------------------------------------------- // [SECTION] Widgets: Multi-Select support @@ -7268,59 +7356,9 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if (flags & ImGuiMultiSelectFlags_BoxSelect) { ms->BoxSelectId = GetID("##BoxSelect"); - KeepAliveID(ms->BoxSelectId); - } - if ((flags & ImGuiMultiSelectFlags_BoxSelect) && ms->BoxSelectId == bs->ID) - { - bs->UnclipMode = false; - - // BoxSelectStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. - if (bs->IsStarting && IsMouseDragPastThreshold(0)) - { - bs->IsActive = true; - bs->Window = ms->Storage->Window; - bs->IsStarting = false; - SetActiveID(ms->BoxSelectId, window); - if (bs->IsStartedFromVoid && (bs->KeyMods & ImGuiMod_Shift) == 0) - request_clear = true; - } - else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false) - { - bs->ID = 0; - bs->IsActive = bs->IsStarting = false; - if (g.ActiveId == ms->BoxSelectId) - ClearActiveID(); - } - if (bs->IsActive) - { - // Current frame absolute prev/current rectangles are used to toggle selection. - // They are derived from positions relative to scrolling space. - const ImRect scope_rect = window->InnerClipRect; - ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel); - ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already - ImVec2 curr_end_pos_abs = g.IO.MousePos; - if (ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow - curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max); - bs->LastSubmittedItem = ImGuiSelectionUserData_Invalid; - bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); - bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); - bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); - bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); - - // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) - // Storing an extra rect used by widgets supporting box-select. - if (flags & ImGuiMultiSelectFlags_BoxSelect2d) - if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) - { - bs->UnclipRect = bs->BoxSelectRectPrev; - bs->UnclipRect.Add(bs->BoxSelectRectCurr); - bs->UnclipMode = true; - } - - //GetForegroundDrawList()->AddRect(ms->BoxSelectNoClipRect.Min, ms->BoxSelectNoClipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); - //GetForegroundDrawList()->AddRect(ms->BoxSelectRectPrev.Min, ms->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); - //GetForegroundDrawList()->AddRect(ms->BoxSelectRectCurr.Min, ms->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); - } + ms->BoxSelectLastitem = ImGuiSelectionUserData_Invalid; + if (BeginBoxSelect(window, ms->BoxSelectId, flags)) + request_clear |= bs->RequestClear; } if (request_clear || request_select_all) @@ -7364,24 +7402,10 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() storage->NavIdSelected = -1; } - ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId); - if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && bs != NULL) + if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && GetBoxSelectState(ms->BoxSelectId)) { - // Box-select: render selection rectangle - bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view - ImRect box_select_r = bs->BoxSelectRectCurr; - box_select_r.ClipWith(scope_rect); - window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling - window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling - - // Box-select: scroll - ImRect scroll_r = scope_rect; - scroll_r.Expand(-g.FontSize); - //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255)); - if ((ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0 && !scroll_r.Contains(g.IO.MousePos)) - BoxSelectScrollWithMouseDrag(window, scroll_r); - - bs->UnclipMode = false; + bool enable_scroll = (ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0; + EndBoxSelect(scope_rect, enable_scroll); } } @@ -7395,7 +7419,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() { if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1) - BoxSelectStart(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); + BoxSelectStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) @@ -7544,7 +7568,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) selected = pressed = true; } - // Box-select handling + // Box-select toggle handling if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId)) { const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); @@ -7554,12 +7578,12 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) selected = !selected; ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; - if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == bs->LastSubmittedItem && prev_req->RangeSelected == selected) + if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->RangeSelected == selected) prev_req->RangeLastItem = item_data; // Merge span into same request else ms->IO.Requests.push_back(req); } - bs->LastSubmittedItem = item_data; + ms->BoxSelectLastitem = item_data; } // Right-click handling: this could be moved at the Selectable() level. @@ -7588,7 +7612,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) - BoxSelectStart(ms->BoxSelectId, item_data); + BoxSelectStartDrag(ms->BoxSelectId, item_data); //---------------------------------------------------------------------------------------- // ACTION | Begin | Pressed/Activated | End From 6c4bf8e56eca5900b925aed1c57ca4179c450482 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 21 Dec 2023 14:29:01 +0100 Subject: [PATCH 088/132] MultiSelect: Fixed ImGuiSelectionBasicStorage::ApplyRequests() incorrectly maintaining selection size on SelectAll. --- imgui_widgets.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index f9dfa143ef02..05ced948e1e0 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7730,7 +7730,7 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io, int it Clear(); if (req.Type == ImGuiSelectionRequestType_SelectAll) { - Storage.Data.resize(0); + Clear(); Storage.Data.reserve(items_count); for (int idx = 0; idx < items_count; idx++) AddItem(AdapterIndexToStorageId(this, idx)); From d439f590ab54e1a60ea62e0dfb55f5e70ea6f2e5 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 21 Dec 2023 15:12:17 +0100 Subject: [PATCH 089/132] MultiSelect: Comments + Assets Browser : Tweak colors. --- imgui.h | 122 +++++++++++++++++++--------------------------- imgui_demo.cpp | 67 +++++++++++++------------ imgui_widgets.cpp | 6 ++- 3 files changed, 92 insertions(+), 103 deletions(-) diff --git a/imgui.h b/imgui.h index c4b8afa69a0c..be79eb665e01 100644 --- a/imgui.h +++ b/imgui.h @@ -180,6 +180,7 @@ struct ImGuiOnceUponAFrame; // Helper for running a block of code not mo struct ImGuiPayload; // User data payload for drag and drop operations struct ImGuiPlatformImeData; // Platform IME data for io.PlatformSetImeDataFn() function. struct ImGuiSelectionBasicStorage; // Helper struct to store multi-selection state + apply multi-selection requests. +struct ImGuiSelectionRequest; // A selection request (stored in ImGuiMultiSelectIO) struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) struct ImGuiStorage; // Helper for key->value storage (container sorted by key) struct ImGuiStoragePair; // Helper for key->value storage (pair) @@ -268,7 +269,7 @@ typedef ImWchar16 ImWchar; // Multi-Selection item index or identifier when using BeginMultiSelect() // - Used by SetNextItemSelectionUserData() + and inside ImGuiMultiSelectIO structure. -// - Most users are likely to use this store an item INDEX but this may be used to store a POINTER as well. Read comments near ImGuiMultiSelectIO for details. +// - Most users are likely to use this store an item INDEX but this may be used to store a POINTER/ID as well. Read comments near ImGuiMultiSelectIO for details. typedef ImS64 ImGuiSelectionUserData; // Callback and functions types @@ -2727,48 +2728,45 @@ 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. // Multi-selection system -// - Refer to 'Demo->Widgets->Selection State & Multi-Select' for references using this. +// Also read: https://github.com/ocornut/imgui/wiki/Multi-Select +// - Refer to 'Demo->Widgets->Selection State & Multi-Select' for demos using this. // - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) -// and supports a clipper being used. Handling this manually and correctly is tricky, this is why we provide -// the functionality. If you don't need SHIFT+Mouse/Keyboard range-select + clipping, you can implement a -// simple form of multi-selection yourself, by reacting to click/presses on Selectable() items. +// with support for clipper (skipping non-visible items), box-select and many other details. // - TreeNode() and Selectable() are supported but custom widgets may use it as well. // - In the spirit of Dear ImGui design, your code owns actual selection data. -// This is designed to allow all kinds of selection storage you may use in your application: -// e.g. set/map/hash (store selected items), instructive selection (store a bool inside each object), etc. +// This is designed to allow all kinds of selection storage you may use in your application e.g. set/map/hash. // - The work involved to deal with multi-selection differs whether you want to only submit visible items and // clip others, or submit all items regardless of their visibility. Clipping items is more efficient and will // allow you to deal with large lists (1k~100k items). See "Usage flow" section below for details. // If you are not sure, always start without clipping! You can work your way to the optimized version afterwards. +// TL;DR; +// - Identify submitted items with SetNextItemSelectionUserData(), most likely using an index into your current data-set. +// - Store and maintain actual selection data using persistent object identifiers. +// - Usage flow: +// BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. +// - (2) [If using clipper] Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. Same code as Step 6. +// - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeItemByIndex(). If storing indices in ImGuiSelectionUserData, a simple clipper.IncludeItemByIndex(ms_io->RangeSrcItem) call will work. +// LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. +// END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. +// - (6) Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. Same code as Step 2. +// If you submit all items (no clipper), Step 2 and 3 are optional and will be handled by each item themselves. It is fine to always honor those steps. // About ImGuiSelectionUserData: -// - This can store an application-defined identifier (e.g. index or pointer). // - For each item is it submitted by your call to SetNextItemSelectionUserData(). +// - This can store an application-defined identifier (e.g. index or pointer). // - In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO. // - Most applications will store an object INDEX, hence the chosen name and type. -// Storing an integer index is the easiest thing to do, as RequestSetRange requests will give you two end-points -// and you will need to iterate/interpolate between them to honor range selection. -// - However it is perfectly possible to store a POINTER inside this value! The multi-selection system never assume -// that you identify items by indices. It never attempt to iterate/interpolate between 2 ImGuiSelectionUserData values. -// - As most users will want to cast this to integer, for convenience and to reduce confusion we use ImS64 instead -// of void*, being syntactically easier to downcast. But feel free to reinterpret_cast a pointer into this. -// - You may store another type as long as you can interpolate between two values. +// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end-points +// and you will need to iterate/interpolate between them to update your selection. +// - However it is perfectly possible to store a POINTER or another IDENTIFIER inside this value! +// Our system never assume that you identify items by indices, it never attempts to interpolate between two values. +// - As most users will want to store an index, for convenience and to reduce confusion we use ImS64 instead of void*, +// being syntactically easier to downcast. Feel free to reinterpret_cast and store a pointer inside. // - If you need to wrap this API for another language/framework, feel free to expose this as 'int' if simpler. -// Usage flow: -// BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (2) [If using clipper] Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. Same code as Step 6. -// - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeItemByIndex(). If storing indices in ImGuiSelectionUserData, a simple clipper.IncludeItemByIndex(ms_io->RangeSrcItem) call will work. -// LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. -// END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (6) Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. Same code as Step 2. -// If you submit all items (no clipper), Step 2 and 3 are optional and will be handled by each item themselves. It is perfectly fine if you honor those steps without a clipper. // About ImGuiSelectionBasicStorage: // - This is an optional helper to store a selection state and apply selection requests. // - It is used by our demos and provided as a convenience if you want to quickly implement multi-selection. -// Advanced: -// - Deletion: If you need to handle items deletion: more work if needed for post-deletion focus and scrolling to be correct. -// Refer to 'Demo->Widgets->Selection State & Multi-Select' for demos supporting deletion. -// Flags for BeginMultiSelect(). +// Flags for BeginMultiSelect() enum ImGuiMultiSelectFlags_ { ImGuiMultiSelectFlags_None = 0, @@ -2785,6 +2783,22 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 10, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. }; +// Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). +// This mainly contains a list of selection requests. +// - Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. +// - Some fields are only useful if your list is dynamic and allows deletion (getting post-deletion focus/state right is shown in the demo) +// - Below: who reads/writes each fields? 'r'=read, 'w'=write, 'ms'=multi-select code, 'app'=application/user code. +struct ImGuiMultiSelectIO +{ + //------------------------------------------// BeginMultiSelect / EndMultiSelect + ImVector Requests; // ms:w, app:r / ms:w app:r // Requests to apply to your selection data. + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! + ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). + bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). +}; + +// Selection request type enum ImGuiSelectionRequestType { ImGuiSelectionRequestType_None = 0, @@ -2793,6 +2807,7 @@ enum ImGuiSelectionRequestType ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items based on 'bool RangeSelected'. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. }; +// Selection request item struct ImGuiSelectionRequest { //------------------------------------------// BeginMultiSelect / EndMultiSelect @@ -2802,65 +2817,30 @@ struct ImGuiSelectionRequest ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) }; -// Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). -// This mainly contains a list of selection requests. Read the large comments block above for details. -// - Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. -// - Some fields are only useful if your list is dynamic and allows deletion (handling deletion and getting "post-deletion" state right is shown 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. -// - Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). -struct ImGuiMultiSelectIO -{ - //------------------------------------------// BeginMultiSelect / EndMultiSelect - ImVector Requests; // ms:w, app:r / ms:w app:r // Requests to apply to your selection data. - ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! - ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). - bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). - bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). -}; - -// Optional helper struct 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. // - USING THIS IS NOT MANDATORY. This is only a helper and not a required API. Advanced users are likely to implement their own. -// To store a multi-selection, in your real 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 replacement. // - B) Use your own external storage: e.g. std::set, std::vector, interval trees, etc. // - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Not recommended because you can't have multiple views // over same objects. Also some features requires to provide selection _size_, which with this strategy requires additional work. -// Our BeginMultiSelect() api/system doesn't make assumption about: -// - how you want to identify items in multi-selection API? Indices(*) or Custom Ids or Pointers -> Indices is better (easy to iterate/interpolate) -// - how you want to store persistent selection data? Indices or Custom Ids(*) or Pointers -> Custom ids is better (as selection can persist) // In ImGuiSelectionBasicStorage we: // - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) // - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index. -// - in some cases we use Index as custom identifier (default implementation returns Index casted as Identifier): only valid for a never changing item list. -// - in some cases we read an ID from some custom item data structure (better, and closer to what you would do in your codebase) +// - so this helper can be used regardless of your object storage/types, and without using templates or virtual functions. +// - in some cases we read an ID from some custom item data structure (similar to what you would do in your codebase) +// - in some cases we use Index as custom identifier (default implementation returns Index cast as Identifier): only OK for a never changing item list. // Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. // When your application settles on a choice, you may want to get rid of this indirection layer and do your own thing. -// Minimum pseudo-code example using this helper: -// { -// static vector items; // Your items -// static ImGuiSelectionBasicStorage selection; // Your selection -// selection.AdapterData = (void*)&items; // Setup adapter so selection.ApplyRequests() function can convert indexes to identifiers. -// selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((vector*)self->AdapterData))[idx].ID; }; -// -// ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None); -// selection.ApplyRequests(ms_io, items.Size); -// for (int idx = 0; idx < items.Size; idx++) -// { -// bool item_is_selected = selection.Contains(items[idx].ID); -// ImGui::SetNextItemSelectionUserData(idx); -// ImGui::Selectable(label, item_is_selected); -// } -// ms_io = ImGui::EndMultiSelect(); -// selection.ApplyRequests(ms_io, items.Size); -// } -// In theory, for maximum abstraction, this class could contains AdapterIndexToUserData() and AdapterUserDataToIndex() functions as well, -// but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that indirection for clarity. +// See https://github.com/ocornut/imgui/wiki/Multi-Select for minimum pseudo-code example using this helper. +// (In theory, for maximum abstraction, this class could contains AdapterIndexToUserData() and AdapterUserDataToIndex() functions as well, +// but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that indirection for clarity.) struct ImGuiSelectionBasicStorage { // Members ImGuiStorage Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set - int Size; // Number of selected items (== number of 1 in the Storage, maintained by this class). + int Size; // Number of selected items (== number of 1 in the Storage), maintained by this helper. void* AdapterData; // Adapter to convert item index to item identifier // e.g. selection.AdapterData = (void*)my_items; ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->AdapterData)[idx]->ID; }; diff --git a/imgui_demo.cpp b/imgui_demo.cpp index b84a0ca9f5a3..0df16adb8e2e 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2977,14 +2977,16 @@ struct ExampleDualListBox } }; - +// Multi-selection demos +// Also read: https://github.com/ocornut/imgui/wiki/Multi-Select static void ShowDemoWindowMultiSelect() { IMGUI_DEMO_MARKER("Widgets/Selection State & Multi-Select"); if (ImGui::TreeNode("Selection State & Multi-Select")) { - HelpMarker("Selections can be built under Selectable(), TreeNode() or other widgets. Selection state is owned by application code/data."); + HelpMarker("Selections can be built using Selectable(), TreeNode() or other widgets. Selection state is owned by application code/data."); + // Without any fancy API: manage single-selection yourself. IMGUI_DEMO_MARKER("Widgets/Selection State/Single-Select"); if (ImGui::TreeNode("Single-Select")) { @@ -3020,8 +3022,9 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } - // Demonstrate holding/updating multi-selection data using the BeginMultiSelect/EndMultiSelect API. + // Demonstrate handling proper multi-selection using the BeginMultiSelect/EndMultiSelect API. // SHIFT+Click w/ CTRL and other standard features are supported. + // We use the ImGuiSelectionBasicStorage helper which you may freely reimplement. IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select"); if (ImGui::TreeNode("Multi-Select")) { @@ -3105,8 +3108,7 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } - // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API + support dynamic item list and deletion. - // SHIFT+Click w/ CTRL and other standard features are supported. + // Demonstrate dynamic item list + deletion support using the BeginMultiSelect/EndMultiSelect API. // In order to support Deletion without any glitches you need to: // - (1) If items are submitted in their own scrolling area, submit contents size SetNextWindowContentSize() ahead of time to prevent one-frame readjustment of scrolling. // - (2) Items needs to have persistent ID Stack identifier = ID needs to not depends on their index. PushID(index) = KO. PushID(item_id) = OK. This is in order to focus items reliably after a selection. @@ -3263,29 +3265,33 @@ static void ShowDemoWindowMultiSelect() static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; static WidgetType widget_type = WidgetType_Selectable; - if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } - ImGui::SameLine(); - if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } - ImGui::Checkbox("Enable clipper", &use_clipper); - ImGui::Checkbox("Enable deletion", &use_deletion); - ImGui::Checkbox("Enable drag & drop", &use_drag_drop); - ImGui::Checkbox("Show in a table", &show_in_table); - ImGui::Checkbox("Show color button", &show_color_button); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); - if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeWindow", &flags, ImGuiMultiSelectFlags_ScopeWindow) && (flags & ImGuiMultiSelectFlags_ScopeWindow)) - flags &= ~ImGuiMultiSelectFlags_ScopeRect; - if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeRect", &flags, ImGuiMultiSelectFlags_ScopeRect) && (flags & ImGuiMultiSelectFlags_ScopeRect)) - flags &= ~ImGuiMultiSelectFlags_ScopeWindow; - if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClick", &flags, ImGuiMultiSelectFlags_SelectOnClick) && (flags & ImGuiMultiSelectFlags_SelectOnClick)) - flags &= ~ImGuiMultiSelectFlags_SelectOnClickRelease; - if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClickRelease", &flags, ImGuiMultiSelectFlags_SelectOnClickRelease) && (flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) - flags &= ~ImGuiMultiSelectFlags_SelectOnClick; - ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); + if (ImGui::TreeNode("Options")) + { + if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } + ImGui::SameLine(); + if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } + ImGui::Checkbox("Enable clipper", &use_clipper); + ImGui::Checkbox("Enable deletion", &use_deletion); + ImGui::Checkbox("Enable drag & drop", &use_drag_drop); + ImGui::Checkbox("Show in a table", &show_in_table); + ImGui::Checkbox("Show color button", &show_color_button); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeWindow", &flags, ImGuiMultiSelectFlags_ScopeWindow) && (flags & ImGuiMultiSelectFlags_ScopeWindow)) + flags &= ~ImGuiMultiSelectFlags_ScopeRect; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeRect", &flags, ImGuiMultiSelectFlags_ScopeRect) && (flags & ImGuiMultiSelectFlags_ScopeRect)) + flags &= ~ImGuiMultiSelectFlags_ScopeWindow; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClick", &flags, ImGuiMultiSelectFlags_SelectOnClick) && (flags & ImGuiMultiSelectFlags_SelectOnClick)) + flags &= ~ImGuiMultiSelectFlags_SelectOnClickRelease; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClickRelease", &flags, ImGuiMultiSelectFlags_SelectOnClickRelease) && (flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) + flags &= ~ImGuiMultiSelectFlags_SelectOnClick; + ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); + ImGui::TreePop(); + } // Initialize default list with 1000 items. // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection @@ -9797,6 +9803,7 @@ struct ExampleAssetsBrowser // Rendering parameters const ImU32 icon_type_overlay_colors[3] = { 0, IM_COL32(200, 70, 70, 255), IM_COL32(70, 170, 70, 255) }; + const ImU32 icon_bg_color = ImGui::GetColorU32(ImGuiCol_MenuBarBg); const ImVec2 icon_type_overlay_size = ImVec2(4.0f, 4.0f); const bool display_label = (LayoutItemSize.x >= ImGui::CalcTextSize("999").x); @@ -9856,7 +9863,7 @@ struct ExampleAssetsBrowser { ImVec2 box_min(pos.x - 1, pos.y - 1); ImVec2 box_max(box_min.x + LayoutItemSize.x + 2, box_min.y + LayoutItemSize.y + 2); // Dubious - draw_list->AddRectFilled(box_min, box_max, IM_COL32(48, 48, 48, 200)); // Background color + draw_list->AddRectFilled(box_min, box_max, icon_bg_color); // Background color if (ShowTypeOverlay && item_data->Type != 0) { ImU32 type_col = icon_type_overlay_colors[item_data->Type % IM_ARRAYSIZE(icon_type_overlay_colors)]; @@ -9864,7 +9871,7 @@ struct ExampleAssetsBrowser } if (display_label) { - ImU32 label_col = item_is_selected ? IM_COL32(255, 255, 255, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled); + ImU32 label_col = ImGui::GetColorU32(item_is_selected ? ImGuiCol_Text : ImGuiCol_TextDisabled); char label[32]; sprintf(label, "%d", item_data->ID); draw_list->AddText(ImVec2(box_min.x, box_max.y - ImGui::GetFontSize()), label_col, label); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 05ced948e1e0..98569ead2091 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7272,7 +7272,8 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe } } -// Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). +// Return ImGuiMultiSelectIO structure. +// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) { ImGuiContext& g = *GImGui; @@ -7375,7 +7376,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) return &ms->IO; } -// Return updated ImGuiMultiSelectIO structure. Lifetime: until EndFrame() or next BeginMultiSelect() call. +// Return updated ImGuiMultiSelectIO structure. +// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). ImGuiMultiSelectIO* ImGui::EndMultiSelect() { ImGuiContext& g = *GImGui; From 8312c75fef0a174fa277b5538f39e4f4ef317f5f Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 4 Jan 2024 18:41:20 +0100 Subject: [PATCH 090/132] MultiSelect: Added ImGuiMultiSelectFlags_NoRangeSelect. Fixed ImGuiMultiSelectFlags_ScopeRect not querying proper window hover. --- imgui.h | 19 ++++++++++--------- imgui_demo.cpp | 1 + imgui_widgets.cpp | 6 +++++- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/imgui.h b/imgui.h index be79eb665e01..54c4b0264cc6 100644 --- a/imgui.h +++ b/imgui.h @@ -2772,15 +2772,16 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_None = 0, ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut sending a SelectAll request. - ImGuiMultiSelectFlags_BoxSelect = 1 << 2, // Enable box-selection (only supporting 1D list when using clipper, not 2D grids). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_BoxSelect2d = 1 << 3, // Enable box-selection with 2D layout/grid support. This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. - ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 4, // Disable scrolling when box-selecting near edges of scope. - ImGuiMultiSelectFlags_ClearOnEscape = 1 << 5, // Clear selection when pressing Escape while scope is focused. - ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 6, // Clear selection when clicking on empty location within scope. - ImGuiMultiSelectFlags_ScopeWindow = 1 << 7, // Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). Use if BeginMultiSelect() covers a whole window. - ImGuiMultiSelectFlags_ScopeRect = 1 << 8, // Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. Use if multiple BeginMultiSelect() are used in the same host window. - ImGuiMultiSelectFlags_SelectOnClick = 1 << 9, // Apply selection on mouse down when clicking on unselected item. (Default) - ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 10, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+Click/Shift+Keyboard handling (useful for unordered 2D selection). + ImGuiMultiSelectFlags_BoxSelect = 1 << 3, // Enable box-selection (only supporting 1D list when using clipper, not 2D grids). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 4, // Enable box-selection with 2D layout/grid support. This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 5, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 6, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 7, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 8, // Use if BeginMultiSelect() covers a whole window (Default): Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). + ImGuiMultiSelectFlags_ScopeRect = 1 << 9, // Use if multiple BeginMultiSelect() are used in the same host window: Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 10, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 11, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. }; // Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 0df16adb8e2e..9f8a80f8df25 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3277,6 +3277,7 @@ static void ShowDemoWindowMultiSelect() ImGui::Checkbox("Show color button", &show_color_button); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoRangeSelect", &flags, ImGuiMultiSelectFlags_NoRangeSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 98569ead2091..4ca36d17e061 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7303,6 +7303,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) // 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; + if (flags & ImGuiMultiSelectFlags_NoRangeSelect) + ms->KeyMods &= ~ImGuiMod_Shift; // Bind storage ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id); @@ -7416,7 +7418,9 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() // Clear selection when clicking void? // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! - const bool scope_hovered = (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) ? scope_rect.Contains(g.IO.MousePos) : IsWindowHovered(); + bool scope_hovered = IsWindowHovered(); + if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)) + scope_hovered &= scope_rect.Contains(g.IO.MousePos); if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) { if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) From 3141d87ef810a7478c078f956ff3865152af0a78 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 4 Jan 2024 19:13:42 +0100 Subject: [PATCH 091/132] MultiSelect: Box-Select: Fixed CTRL+drag from void clearing items. --- imgui_demo.cpp | 1 + imgui_widgets.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 9f8a80f8df25..32e6e8f2fb2e 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -9737,6 +9737,7 @@ struct ExampleAssetsBrowser ImGui::SeparatorText("Layout"); ImGui::SliderFloat("Icon Size", &IconSize, 16.0f, 128.0f, "%.0f"); + ImGui::SameLine(); HelpMarker("Use CTRL+Wheel to zoom"); ImGui::SliderInt("Icon Spacing", &IconSpacing, 0, 32); ImGui::SliderInt("Icon Hit Spacing", &IconHitSpacing, 0, 32); ImGui::Checkbox("Stretch Spacing", &StretchSpacing); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 4ca36d17e061..f7be141602b8 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7180,7 +7180,7 @@ bool ImGui::BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMult bs->Window = window; bs->IsStarting = false; SetActiveID(bs->ID, window); - if (bs->IsStartedFromVoid && (bs->KeyMods & ImGuiMod_Shift) == 0) + if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) bs->RequestClear = true; } else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false) From 9337151a0132c8be7020f36df86a4f6adc6d2c74 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 4 Jan 2024 20:05:05 +0100 Subject: [PATCH 092/132] MultiSelect: Box-Select: Fixed initial drag from not claiming hovered id, preventing window behind to move for a frame. --- imgui_demo.cpp | 5 +++-- imgui_widgets.cpp | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 32e6e8f2fb2e..79634d8204be 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3086,7 +3086,7 @@ static void ShowDemoWindowMultiSelect() ImGuiListClipper clipper; clipper.Begin(ITEMS_COUNT); - if (ms_io->RangeSrcItem > 0) + if (ms_io->RangeSrcItem != -1) clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. while (clipper.Step()) { @@ -3210,6 +3210,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeRect", &flags, ImGuiMultiSelectFlags_ScopeRect) && (flags & ImGuiMultiSelectFlags_ScopeRect)) flags &= ~ImGuiMultiSelectFlags_ScopeWindow; ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); for (int selection_scope_n = 0; selection_scope_n < SCOPES_COUNT; selection_scope_n++) { @@ -3336,7 +3337,7 @@ static void ShowDemoWindowMultiSelect() clipper.Begin(items.Size); if (item_curr_idx_to_focus != -1) clipper.IncludeItemByIndex(item_curr_idx_to_focus); // Ensure focused item is not clipped. - if (ms_io->RangeSrcItem > 0) + if (ms_io->RangeSrcItem != -1) clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index f7be141602b8..646a72852c6e 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7176,6 +7176,7 @@ bool ImGui::BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMult // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. if (bs->IsStarting && IsMouseDragPastThreshold(0)) { + IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Started.\n", box_select_id); bs->IsActive = true; bs->Window = window; bs->IsStarting = false; @@ -7187,7 +7188,10 @@ bool ImGui::BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMult { bs->IsActive = bs->IsStarting = false; if (g.ActiveId == bs->ID) + { + IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Ended.\n", box_select_id); ClearActiveID(); + } bs->ID = 0; } if (!bs->IsActive) @@ -7424,8 +7428,13 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) { if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) + { if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1) BoxSelectStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); + SetHoveredID(ms->BoxSelectId); + if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) + SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from outside to box-select. + } if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) From b13a78e6b2d19773d5cf6f1006de7bc0f4bed42c Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 12 Jan 2024 12:05:03 +0100 Subject: [PATCH 093/132] MultiSelect: Fixed ImGuiMultiSelectFlags_SelectOnClickRelease over tree node arrow. --- imgui_widgets.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 646a72852c6e..fecfbc4fb5bb 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6471,6 +6471,8 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags { // Handle multi-select + alter button flags for it MultiSelectItemHeader(id, &selected, &button_flags); + if (is_mouse_x_over_arrow) + button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; // We absolutely need to distinguish open vs select so comes by default flags |= ImGuiTreeNodeFlags_OpenOnArrow; From f36a03c317a97da95fba9287afff64c8548f3620 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 6 Mar 2024 14:22:38 +0100 Subject: [PATCH 094/132] MultiSelect: (Breaking) merge ImGuiSelectionRequestType_Clear and ImGuiSelectionRequestType_SelectAll into ImGuiSelectionRequestType_SetAll., rename ImGuiSelectionRequest::RangeSelected to Selected. The reasoning is that it makes it easier/faster to write an adhoc ImGuiMultiSelectIO handler (e.g. trying to apply multi-select to checkboxes) --- imgui.h | 13 ++++++------- imgui_internal.h | 3 +-- imgui_widgets.cpp | 38 ++++++++++++++++---------------------- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/imgui.h b/imgui.h index 54c4b0264cc6..44990f68bd94 100644 --- a/imgui.h +++ b/imgui.h @@ -2744,11 +2744,11 @@ struct ImColor // - Store and maintain actual selection data using persistent object identifiers. // - Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (2) [If using clipper] Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. Same code as Step 6. +// - (2) [If using clipper] Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 6. // - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeItemByIndex(). If storing indices in ImGuiSelectionUserData, a simple clipper.IncludeItemByIndex(ms_io->RangeSrcItem) call will work. // LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (6) Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. Same code as Step 2. +// - (6) Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 2. // If you submit all items (no clipper), Step 2 and 3 are optional and will be handled by each item themselves. It is fine to always honor those steps. // About ImGuiSelectionUserData: // - For each item is it submitted by your call to SetNextItemSelectionUserData(). @@ -2771,7 +2771,7 @@ enum ImGuiMultiSelectFlags_ { ImGuiMultiSelectFlags_None = 0, ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! - ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut sending a SelectAll request. + ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to select all. ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+Click/Shift+Keyboard handling (useful for unordered 2D selection). ImGuiMultiSelectFlags_BoxSelect = 1 << 3, // Enable box-selection (only supporting 1D list when using clipper, not 2D grids). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. ImGuiMultiSelectFlags_BoxSelect2d = 1 << 4, // Enable box-selection with 2D layout/grid support. This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. @@ -2803,9 +2803,8 @@ struct ImGuiMultiSelectIO enum ImGuiSelectionRequestType { ImGuiSelectionRequestType_None = 0, - ImGuiSelectionRequestType_Clear, // Request app to clear selection. - ImGuiSelectionRequestType_SelectAll, // Request app to select all. - ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items based on 'bool RangeSelected'. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. + ImGuiSelectionRequestType_SetAll, // Request app to clear selection (if Selected==false) or select all items (if Selected==true) + ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items (inclusive) based on value of Selected. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. }; // Selection request item @@ -2813,7 +2812,7 @@ struct ImGuiSelectionRequest { //------------------------------------------// BeginMultiSelect / EndMultiSelect ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r // Request type. You'll most often receive 1 Clear + 1 SetRange with a single-item range. - bool RangeSelected; // / ms:w, app:r // Parameter for SetRange request (true = select range, false = unselect range) + bool Selected; // / ms:w, app:r // Parameter for SetAll/SetRange requests (true = select, false = unselect) ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) }; diff --git a/imgui_internal.h b/imgui_internal.h index 3659bf8cc07b..1e97b71f4771 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1754,8 +1754,7 @@ struct IMGUI_API ImGuiMultiSelectTempData ImVec2 BackupCursorMaxPos; ImGuiID BoxSelectId; ImGuiKeyChord KeyMods; - bool LoopRequestClear; - bool LoopRequestSelectAll; + ImS8 LoopRequestSetAll; // -1: no operation, 0: clear all, 1: select all. bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. 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. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index fecfbc4fb5bb..e71f32ddd5ad 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7272,9 +7272,8 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe ImGuiContext& g = *GImGui; for (const ImGuiSelectionRequest& req : io->Requests) { - if (req.Type == ImGuiSelectionRequestType_Clear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: Clear\n", function); - if (req.Type == ImGuiSelectionRequestType_SelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SelectAll\n", function); - if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.RangeSelected); + if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear"); + if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.Selected); } } @@ -7372,11 +7371,10 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if (request_clear || request_select_all) { - ImGuiSelectionRequest req = { request_select_all ? ImGuiSelectionRequestType_SelectAll : ImGuiSelectionRequestType_Clear, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, request_select_all, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; ms->IO.Requests.push_back(req); } - ms->LoopRequestClear = request_clear; - ms->LoopRequestSelectAll = request_select_all; + ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1; if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO); @@ -7441,7 +7439,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_Clear, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; ms->IO.Requests.resize(0); ms->IO.Requests.push_back(req); } @@ -7494,13 +7492,11 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); - // Apply Clear/SelectAll requests requested by BeginMultiSelect(). + // Apply SetAll (Clear/SelectAll )requests requested by BeginMultiSelect(). // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. - // If you are using a clipper (aka not submitting every element of the list) you need to process the Clear/SelectAll request after calling BeginMultiSelect() - if (ms->LoopRequestClear) - selected = false; - else if (ms->LoopRequestSelectAll) - selected = true; + // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect() + if (ms->LoopRequestSetAll != -1) + selected = (ms->LoopRequestSetAll == 1); // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function) @@ -7595,7 +7591,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) selected = !selected; ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; - if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->RangeSelected == selected) + if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->Selected == selected) prev_req->RangeLastItem = item_data; // Merge span into same request else ms->IO.Requests.push_back(req); @@ -7657,7 +7653,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. if (request_clear) { - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_Clear, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; ms->IO.Requests.resize(0); ms->IO.Requests.push_back(req); } @@ -7734,27 +7730,25 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) // The most simple implementation (using indices everywhere) would look like: // for (ImGuiSelectionRequest& req : ms_io->Requests) // { -// if (req.Type == ImGuiSelectionRequestType_Clear) { Clear(); } -// if (req.Type == ImGuiSelectionRequestType_SelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } -// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } +// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { AddItem(n); } } +// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->Selected); } } // } void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count) { IM_ASSERT(AdapterIndexToStorageId != NULL); for (ImGuiSelectionRequest& req : ms_io->Requests) { - if (req.Type == ImGuiSelectionRequestType_Clear) + if (req.Type == ImGuiSelectionRequestType_SetAll) Clear(); - if (req.Type == ImGuiSelectionRequestType_SelectAll) + if (req.Type == ImGuiSelectionRequestType_SetAll && req.Selected) { - Clear(); Storage.Data.reserve(items_count); for (int idx = 0; idx < items_count; idx++) AddItem(AdapterIndexToStorageId(this, idx)); } if (req.Type == ImGuiSelectionRequestType_SetRange) for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) - UpdateItem(AdapterIndexToStorageId(this, idx), req.RangeSelected); + UpdateItem(AdapterIndexToStorageId(this, idx), req.Selected); } } From dbc67bbf23fcf57ad8271f9c2a4ea1cc4641410e Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 6 Mar 2024 15:04:03 +0100 Subject: [PATCH 095/132] MultiSelect: Simplified ImGuiSelectionBasicStorage by using a single SetItemSelected() entry point. --- imgui.h | 22 ++++++++++------------ imgui_demo.cpp | 4 ++-- imgui_widgets.cpp | 4 ++-- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/imgui.h b/imgui.h index 44990f68bd94..a5a3d9265935 100644 --- a/imgui.h +++ b/imgui.h @@ -2823,16 +2823,16 @@ struct ImGuiSelectionRequest // 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 replacement. // - B) Use your own external storage: e.g. std::set, std::vector, interval trees, etc. -// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Not recommended because you can't have multiple views -// over same objects. Also some features requires to provide selection _size_, which with this strategy requires additional work. +// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Doing that, you can't have multiple views over +// your objects. Also, some features requires to provide selection _size_, which with this strategy requires additional work. // In ImGuiSelectionBasicStorage we: // - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) -// - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index. -// - so this helper can be used regardless of your object storage/types, and without using templates or virtual functions. +// - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index, +// so this helper can be used regardless of your object storage/types (it is analogous to using a virtual function): // - in some cases we read an ID from some custom item data structure (similar to what you would do in your codebase) // - in some cases we use Index as custom identifier (default implementation returns Index cast as Identifier): only OK for a never changing item list. // Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. -// When your application settles on a choice, you may want to get rid of this indirection layer and do your 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 minimum pseudo-code example using this helper. // (In theory, for maximum abstraction, this class could contains AdapterIndexToUserData() and AdapterUserDataToIndex() functions as well, // but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that indirection for clarity.) @@ -2844,17 +2844,15 @@ struct ImGuiSelectionBasicStorage void* AdapterData; // Adapter to convert item index to item identifier // e.g. selection.AdapterData = (void*)my_items; ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->AdapterData)[idx]->ID; }; + // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count); + // Methods: selection storage ImGuiSelectionBasicStorage() { Clear(); AdapterData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } void Clear() { Storage.Data.resize(0); Size = 0; } void Swap(ImGuiSelectionBasicStorage& r) { Storage.Data.swap(r.Storage.Data); } - bool Contains(ImGuiID key) const { return Storage.GetInt(key, 0) != 0; } - void AddItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int != 0) return; *p_int = 1; Size++; } - void RemoveItem(ImGuiID key) { int* p_int = Storage.GetIntRef(key, 0); if (*p_int == 0) return; *p_int = 0; Size--; } - void UpdateItem(ImGuiID key, bool v) { if (v) { AddItem(key); } else { RemoveItem(key); } } - - // Methods: apply selection requests (that are coming from BeginMultiSelect() and EndMultiSelect() functions) - IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count); + 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--; } } }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 79634d8204be..7c03f5302e4a 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2832,7 +2832,7 @@ struct ExampleSelectionWithDeletion : ImGuiSelectionBasicStorage // Update selection Clear(); if (item_next_idx_to_select != -1 && ms_io->NavIdSelected) - AddItem(AdapterIndexToStorageId(this, item_next_idx_to_select)); + SetItemSelected(AdapterIndexToStorageId(this, item_next_idx_to_select), true); } }; @@ -3137,7 +3137,7 @@ static void ShowDemoWindowMultiSelect() items.push_back(items_next_id++); if (ImGui::SmallButton("Add 20 items")) { for (int n = 0; n < 20; n++) { items.push_back(items_next_id++); } } ImGui::SameLine(); - if (ImGui::SmallButton("Remove 20 items")) { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.RemoveItem(items.back()); items.pop_back(); } } + if (ImGui::SmallButton("Remove 20 items")) { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.SetItemSelected(items.back(), false); items.pop_back(); } } // (1) Extra to support deletion: Submit scrolling range to avoid glitches on deletion const float items_height = ImGui::GetTextLineHeightWithSpacing(); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index e71f32ddd5ad..444fadc8bac8 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7744,11 +7744,11 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io, int it { Storage.Data.reserve(items_count); for (int idx = 0; idx < items_count; idx++) - AddItem(AdapterIndexToStorageId(this, idx)); + SetItemSelected(AdapterIndexToStorageId(this, idx), true); } if (req.Type == ImGuiSelectionRequestType_SetRange) for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) - UpdateItem(AdapterIndexToStorageId(this, idx), req.Selected); + SetItemSelected(AdapterIndexToStorageId(this, idx), req.Selected); } } From 2111e3597bc6cc16bea20040dbfd05e38d3a4202 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 6 Mar 2024 15:33:50 +0100 Subject: [PATCH 096/132] MultiSelect: Comments + tweaked location for widgets to test ImGuiItemFlags_IsMultiSelect to avoid misleading into thinking doing it before ItemAdd() is necessary. --- imgui_internal.h | 4 ++-- imgui_widgets.cpp | 47 ++++++++++++++++++++++++++++------------------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 1e97b71f4771..5fb2d568d8e6 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1757,7 +1757,7 @@ struct IMGUI_API ImGuiMultiSelectTempData ImS8 LoopRequestSetAll; // -1: no operation, 0: clear all, 1: select all. bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. 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 IsKeyboardSetRange; // 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 RangeSrcPassedBy; // Set by the item that matches RangeSrcItem. bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. @@ -1765,7 +1765,7 @@ struct IMGUI_API ImGuiMultiSelectTempData ImGuiMultiSelectTempData() { Clear(); } void Clear() { size_t io_sz = sizeof(IO); ClearIO(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); } // Zero-clear except IO as we preserve IO.Requests[] buffer allocation. - void ClearIO() { IO.Requests.resize(0); IO.RangeSrcItem = IO.NavIdItem = (ImGuiSelectionUserData)-1; IO.NavIdSelected = IO.RangeSrcReset = false; } + void ClearIO() { IO.Requests.resize(0); IO.RangeSrcItem = IO.NavIdItem = ImGuiSelectionUserData_Invalid; IO.NavIdSelected = IO.RangeSrcReset = false; } }; // Persistent storage for multi-select (as long as selection is alive) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 444fadc8bac8..eef613071c4f 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6392,7 +6392,6 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags } // Compute open and multi-select states before ItemAdd() as it clear NextItem data. - const bool is_multi_select = (g.NextItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; // Before ItemAdd() bool is_open = TreeNodeUpdateNextOpen(storage_id, flags); bool item_add = ItemAdd(interact_bb, id); g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; @@ -6467,6 +6466,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags const bool was_selected = selected; // Multi-selection support (header) + const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0; if (is_multi_select) { // Handle multi-select + alter button flags for it @@ -6777,7 +6777,6 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl } const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0; - const bool is_multi_select = (g.NextItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; // Before ItemAdd() const bool is_visible = ItemAdd(bb, id, NULL, disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None); if (span_all_columns) @@ -6786,11 +6785,12 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl window->ClipRect.Max.x = backup_clip_rect_max_x; } + const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0; if (!is_visible) { + // Extra layer of "no logic clip" for box-select support if (!is_multi_select) return false; - // Extra layer of "no logic clip" for box-select support if (!g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(bb)) return false; } @@ -7323,16 +7323,15 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; ms->IO.Requests.resize(0); - bool request_clear = false; - bool request_select_all = false; - // Clear when using Navigation to move within the scope // (we compare FocusScopeId so it possible to use multiple selections inside a same window) + bool request_clear = false; + bool request_select_all = false; if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) { if (ms->KeyMods & ImGuiMod_Shift) - ms->IsSetRange = true; - if (ms->IsSetRange) + ms->IsKeyboardSetRange = true; + if (ms->IsKeyboardSetRange) IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear? if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) request_clear = true; @@ -7371,7 +7370,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if (request_clear || request_select_all) { - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, request_select_all, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, request_select_all, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; ms->IO.Requests.push_back(req); } ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1; @@ -7439,7 +7438,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; ms->IO.Requests.resize(0); ms->IO.Requests.push_back(req); } @@ -7480,6 +7479,10 @@ void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_d } } +// In charge of: +// - Applying SetAll for submitted items. +// - Applying SetRange for submitted items and record end points. +// - Altering button behavior flags to facilitate use with drag and drop. void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags) { ImGuiContext& g = *GImGui; @@ -7500,18 +7503,16 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function) - if (ms->IsSetRange) + if (ms->IsKeyboardSetRange) { IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0); 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 == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst - { - storage->RangeSrcItem = item_data; - storage->RangeSelected = selected ? 1 : 0; - } + if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // 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->RangeSrcPassedBy != ms->RangeDstPassedBy) @@ -7537,6 +7538,13 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags *p_button_flags = button_flags; } +// In charge of: +// - Auto-select on navigation. +// - Box-select toggle handling. +// - Right-click handling. +// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse. +// - Record current selection state for RangeSrc +// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite. void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) { ImGuiContext& g = *GImGui; @@ -7557,7 +7565,8 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; - const bool is_singleselect = (ms->Flags & ImGuiMultiSelectFlags_SingleSelect) != 0; + ImGuiMultiSelectFlags flags = ms->Flags; + const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0; bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0; bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0; @@ -7623,7 +7632,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) { // Box-select ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; - if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) + if (flags & ImGuiMultiSelectFlags_BoxSelect) if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) BoxSelectStartDrag(ms->BoxSelectId, item_data); From a639346fbaf29a662a10c622956a6c66c06242ba Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 7 Mar 2024 15:52:30 +0100 Subject: [PATCH 097/132] MultiSelect: Demo: make various child windows resizable, with synched heights for the dual list box demo. --- imgui_demo.cpp | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 7c03f5302e4a..9d23d4661b83 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2898,6 +2898,7 @@ struct ExampleDualListBox int request_move_selected = -1; int request_move_all = -1; + float child_height_0 = 0.0f; for (int side = 0; side < 2; side++) { // FIXME-MULTISELECT: Dual List Box: Add context menus @@ -2912,7 +2913,20 @@ struct ExampleDualListBox const float items_height = ImGui::GetTextLineHeightWithSpacing(); ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); - if (ImGui::BeginChild(ImGui::GetID(side ? "1" : "0"), ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle)) + bool child_visible; + if (side == 0) + { + // Left child is resizable + ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, ImGui::GetFrameHeightWithSpacing() * 4), ImVec2(FLT_MAX, FLT_MAX)); + child_visible = ImGui::BeginChild("0", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY); + child_height_0 = ImGui::GetWindowSize().y; + } + else + { + // Right child use same height as left one + child_visible = ImGui::BeginChild("1", ImVec2(-FLT_MIN, child_height_0), ImGuiChildFlags_FrameStyle); + } + if (child_visible) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); @@ -3042,8 +3056,8 @@ static void ShowDemoWindowMultiSelect() static ImGuiSelectionBasicStorage selection; ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT); - // The BeginListBox() has no actual purpose for selection logic (other that offering a scrolling region). - if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) + // The BeginChild() has no purpose for selection logic, other that offering a scrolling region. + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); @@ -3060,9 +3074,8 @@ static void ShowDemoWindowMultiSelect() ms_io = ImGui::EndMultiSelect(); selection.ApplyRequests(ms_io, ITEMS_COUNT); - - ImGui::EndListBox(); } + ImGui::EndChild(); ImGui::TreePop(); } @@ -3078,7 +3091,7 @@ static void ShowDemoWindowMultiSelect() const int ITEMS_COUNT = 10000; ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT); - if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); @@ -3102,9 +3115,8 @@ static void ShowDemoWindowMultiSelect() ms_io = ImGui::EndMultiSelect(); selection.ApplyRequests(ms_io, ITEMS_COUNT); - - ImGui::EndListBox(); } + ImGui::EndChild(); ImGui::TreePop(); } @@ -3143,7 +3155,7 @@ static void ShowDemoWindowMultiSelect() const float items_height = ImGui::GetTextLineHeightWithSpacing(); ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); - if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); @@ -3172,9 +3184,8 @@ static void ShowDemoWindowMultiSelect() selection.ApplyRequests(ms_io, items.Size); if (want_delete) selection.ApplyDeletionPostLoop(ms_io, items, item_curr_idx_to_focus); - - ImGui::EndListBox(); } + ImGui::EndChild(); ImGui::TreePop(); } @@ -3307,7 +3318,7 @@ static void ShowDemoWindowMultiSelect() const float items_height = (widget_type == WidgetType_TreeNode) ? ImGui::GetTextLineHeight() : ImGui::GetTextLineHeightWithSpacing(); ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); - if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20))) + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { ImVec2 color_button_sz(ImGui::GetFontSize(), ImGui::GetFontSize()); if (widget_type == WidgetType_TreeNode) @@ -3465,9 +3476,8 @@ static void ShowDemoWindowMultiSelect() if (widget_type == WidgetType_TreeNode) ImGui::PopStyleVar(); - ImGui::EndListBox(); } - + ImGui::EndChild(); ImGui::TreePop(); } ImGui::TreePop(); From e7a734f78d1d87afcb845148eaa93177406d933e Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 7 Mar 2024 16:26:22 +0100 Subject: [PATCH 098/132] MultiSelect: added ImGuiMultiSelectFlags_NoAutoSelect, ImGuiMultiSelectFlags_NoAutoClear features + added Checkbox Demo Refer to "widgets_multiselect_checkboxes" in imgui_test_suite. --- imgui.cpp | 2 +- imgui.h | 24 +++++---- imgui_demo.cpp | 51 ++++++++++++++++++ imgui_widgets.cpp | 131 ++++++++++++++++++++++++++++++++-------------- 4 files changed, 158 insertions(+), 50 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 291863e6f487..ee9462a9a52b 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -4075,7 +4075,7 @@ void ImGui::MarkItemEdited(ImGuiID id) // We accept a MarkItemEdited() on drag and drop targets (see https://github.com/ocornut/imgui/issues/1875#issuecomment-978243343) // We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714) - IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id); + IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive)); //IM_ASSERT(g.CurrentWindow->DC.LastItemId == id); g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited; diff --git a/imgui.h b/imgui.h index a5a3d9265935..88445d5b7ac2 100644 --- a/imgui.h +++ b/imgui.h @@ -2744,7 +2744,7 @@ struct ImColor // - Store and maintain actual selection data using persistent object identifiers. // - Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (2) [If using clipper] Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 6. +// - (2) Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 6. // - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeItemByIndex(). If storing indices in ImGuiSelectionUserData, a simple clipper.IncludeItemByIndex(ms_io->RangeSrcItem) call will work. // LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. @@ -2773,15 +2773,17 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to select all. ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+Click/Shift+Keyboard handling (useful for unordered 2D selection). - ImGuiMultiSelectFlags_BoxSelect = 1 << 3, // Enable box-selection (only supporting 1D list when using clipper, not 2D grids). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_BoxSelect2d = 1 << 4, // Enable box-selection with 2D layout/grid support. This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. - ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 5, // Disable scrolling when box-selecting near edges of scope. - ImGuiMultiSelectFlags_ClearOnEscape = 1 << 6, // Clear selection when pressing Escape while scope is focused. - ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 7, // Clear selection when clicking on empty location within scope. - ImGuiMultiSelectFlags_ScopeWindow = 1 << 8, // Use if BeginMultiSelect() covers a whole window (Default): Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). - ImGuiMultiSelectFlags_ScopeRect = 1 << 9, // Use if multiple BeginMultiSelect() are used in the same host window: Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. - ImGuiMultiSelectFlags_SelectOnClick = 1 << 10, // Apply selection on mouse down when clicking on unselected item. (Default) - ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 11, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes) + ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing other items when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes) + ImGuiMultiSelectFlags_BoxSelect = 1 << 5, // Enable box-selection (only supporting 1D list when using clipper, not 2D grids). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 6, // Enable box-selection with 2D layout/grid support. This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 7, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 8, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 9, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 10, // Use if BeginMultiSelect() covers a whole window (Default): Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). + ImGuiMultiSelectFlags_ScopeRect = 1 << 11, // Use if multiple BeginMultiSelect() are used in the same host window: Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 12, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 13, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. }; // Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). @@ -2803,7 +2805,7 @@ struct ImGuiMultiSelectIO enum ImGuiSelectionRequestType { ImGuiSelectionRequestType_None = 0, - ImGuiSelectionRequestType_SetAll, // Request app to clear selection (if Selected==false) or select all items (if Selected==true) + ImGuiSelectionRequestType_SetAll, // Request app to clear selection (if Selected==false) or select all items (if Selected==true). We cannot set RangeFirstItem/RangeLastItem as its contents is entirely up to user (not necessarily an index) ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items (inclusive) based on value of Selected. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. }; diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 9d23d4661b83..f5cfa53e143b 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3205,6 +3205,55 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (checkboxes)"); + if (ImGui::TreeNode("Multi-Select (checkboxes)")) + { + ImGui::Text("In a list of checkboxes (not selectable):"); + ImGui::BulletText("Using _NoAutoSelect + _NoAutoClear flags."); + ImGui::BulletText("Shift+Click to check multiple boxes."); + ImGui::BulletText("Shift+Keyboard to copy current value to other boxes."); + + static bool values[20] = {}; + static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_ClearOnEscape; + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); + + struct Funcs + { + static void ApplyMultiSelectRequestsToBoolArray(ImGuiMultiSelectIO* ms_io, bool items[], int items_count) + { + for (ImGuiSelectionRequest& req : ms_io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_SetAll) + for (int n = 0; n < items_count; n++) + items[n] = req.Selected; + else if (req.Type == ImGuiSelectionRequestType_SetRange) + for (int n = (int)req.RangeFirstItem; n <= (int)req.RangeLastItem; n++) + items[n] = req.Selected; + } + } + }; + + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY)) + { + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + Funcs::ApplyMultiSelectRequestsToBoolArray(ms_io, values, IM_ARRAYSIZE(values)); //// By specs, it could be optional to apply requests from BeginMultiSelect() if not using a clipper. + for (int n = 0; n < 20; n++) + { + char label[32]; + sprintf(label, "Item %d", n); + ImGui::SetNextItemSelectionUserData(n); + ImGui::Checkbox(label, &values[n]); + } + ms_io = ImGui::EndMultiSelect(); + Funcs::ApplyMultiSelectRequestsToBoolArray(ms_io, values, IM_ARRAYSIZE(values)); + } + ImGui::EndChild(); + + ImGui::TreePop(); + } + // Demonstrate individual selection scopes in same window IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (multiple scopes)"); if (ImGui::TreeNode("Multi-Select (multiple scopes)")) @@ -3290,6 +3339,8 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoRangeSelect", &flags, ImGuiMultiSelectFlags_NoRangeSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index eef613071c4f..2eefe8feac00 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -1141,11 +1141,25 @@ bool ImGui::Checkbox(const char* label, bool* v) return false; } + // Range-Selection/Multi-selection support (header) + bool checked = *v; + const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0; + if (is_multi_select) + MultiSelectItemHeader(id, &checked, NULL); + bool hovered, held; bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); - if (pressed) + + // Range-Selection/Multi-selection support (footer) + if (is_multi_select) + MultiSelectItemFooter(id, &checked, &pressed); + else if (pressed) + checked = !checked; + + if (*v != checked) { - *v = !(*v); + *v = checked; + pressed = true; // return value MarkItemEdited(id); } @@ -7333,13 +7347,13 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ms->IsKeyboardSetRange = true; if (ms->IsKeyboardSetRange) IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear? - if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) + if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) request_clear = true; } else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId) { // Also clear on leaving scope (may be optional?) - if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) + if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) request_clear = true; } @@ -7517,11 +7531,15 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags const bool is_range_src = storage->RangeSrcItem == item_data; if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy) { + // Apply range-select value to visible items IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1); selected = (storage->RangeSelected != 0); } - else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0) + else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0) + { + // Clear other items selected = false; + } } *p_selected = selected; } @@ -7529,13 +7547,16 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags // Alter button behavior flags // To handle drag and drop of multiple items we need to avoid clearing selection on click. // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items. - ImGuiButtonFlags button_flags = *p_button_flags; - button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; - if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) - button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; - else - button_flags |= ImGuiButtonFlags_PressedOnClickRelease; - *p_button_flags = button_flags; + if (p_button_flags != NULL) + { + ImGuiButtonFlags button_flags = *p_button_flags; + button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; + if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) + button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + *p_button_flags = button_flags; + } } // In charge of: @@ -7570,11 +7591,10 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0; bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0; + bool apply_to_range_src = false; + if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) - { - storage->RangeSrcItem = item_data; - storage->RangeSelected = selected; // Will be updated at the end of this function anyway. - } + apply_to_range_src = true; if (ms->IsEndIO == false) { ms->IO.Requests.resize(0); @@ -7584,10 +7604,27 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Auto-select as you navigate a list if (g.NavJustMovedToId == id) { - if (is_ctrl && is_shift) - pressed = true; - else if (!is_ctrl) - selected = pressed = true; + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + { + if (is_ctrl && is_shift) + pressed = true; + else if (!is_ctrl) + selected = pressed = true; + } + else + { + // With NoAutoSelect, using Shift+keyboard performs a write/copy + if (is_shift) + pressed = true; + else if (!is_ctrl) + apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this + } + } + + if (apply_to_range_src) + { + storage->RangeSrcItem = item_data; + storage->RangeSelected = selected; // Will be updated at the end of this function anyway. } // Box-select toggle handling @@ -7608,9 +7645,9 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ms->BoxSelectLastitem = item_data; } - // Right-click handling: this could be moved at the Selectable() level. - // FIXME-MULTISELECT: See https://github.com/ocornut/imgui/pull/5816 - if (hovered && IsMouseClicked(1)) + // Right-click handling. + // FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816 + if (hovered && IsMouseClicked(1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) { if (g.ActiveId != 0 && g.ActiveId != id) ClearActiveID(); @@ -7653,36 +7690,54 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst //---------------------------------------------------------------------------------------- - bool request_clear = false; - if (is_singleselect) - request_clear = true; - else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) - request_clear = true; - else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) - request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. - if (request_clear) + if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0) { - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; - ms->IO.Requests.resize(0); - ms->IO.Requests.push_back(req); + bool request_clear = false; + if (is_singleselect) + request_clear = true; + else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) + request_clear = true; + else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) + request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. + if (request_clear) + { + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; + ms->IO.Requests.resize(0); + ms->IO.Requests.push_back(req); + } } int range_direction; bool range_selected; if (is_shift && !is_singleselect) { - // Shift+Arrow always select - // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected) //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) storage->RangeSrcItem = item_data; - range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + { + // Shift+Arrow always select + // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected) + range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + } + else + { + // Shift+Arrow copy source selection state + // Shift+Click always copy from target selection state + if (ms->IsKeyboardSetRange) + range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + else + range_selected = !selected; + } range_direction = ms->RangeSrcPassedBy ? +1 : -1; } else { // Ctrl inverts selection, otherwise always select - selected = is_ctrl ? !selected : true; + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + selected = is_ctrl ? !selected : true; + else + selected = !selected; storage->RangeSrcItem = item_data; range_selected = selected; range_direction = +1; From 0be238ec587e98ee00a50c67ad826c3e8f0420bc Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 18 Apr 2024 15:35:29 +0200 Subject: [PATCH 099/132] MultiSelect: Box-Select: fix preventing focus. amend determination of scope_hovered for decorated/non-child windows + avoid stealing NavId. (#7424) --- imgui_widgets.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 2eefe8feac00..1f024d0ce87c 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7435,7 +7435,8 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() // Clear selection when clicking void? // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! - bool scope_hovered = IsWindowHovered(); + // The InnerRect test is necessary for non-child/decorated windows. + bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(g.IO.MousePos); if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)) scope_hovered &= scope_rect.Contains(g.IO.MousePos); if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) @@ -7443,10 +7444,13 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) { if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1) + { BoxSelectStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); - SetHoveredID(ms->BoxSelectId); - if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) - SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from outside to box-select. + FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal); + SetHoveredID(ms->BoxSelectId); + if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) + SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select. + } } if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) From 955210ae5bf4aeeb7ca2a6bc15521f6481016624 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 23 May 2024 18:56:31 +0200 Subject: [PATCH 100/132] MultiSelect: Demo: use Shortcut(). Got rid of suggestion to move Delete signal processing to BeginMultiSelect(), seems unnecessary. --- imgui.h | 2 +- imgui_demo.cpp | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/imgui.h b/imgui.h index 88445d5b7ac2..89f016dfc810 100644 --- a/imgui.h +++ b/imgui.h @@ -2814,7 +2814,7 @@ struct ImGuiSelectionRequest { //------------------------------------------// BeginMultiSelect / EndMultiSelect ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r // Request type. You'll most often receive 1 Clear + 1 SetRange with a single-item range. - bool Selected; // / ms:w, app:r // Parameter for SetAll/SetRange requests (true = select, false = unselect) + bool Selected; // ms:w, app:r / ms:w, app:r // Parameter for SetAll/SetRange requests (true = select, false = unselect) ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) }; diff --git a/imgui_demo.cpp b/imgui_demo.cpp index f5cfa53e143b..a8759933d9b8 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3161,9 +3161,7 @@ static void ShowDemoWindowMultiSelect() ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection.ApplyRequests(ms_io, items.Size); - // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to turn into 'ms_io->RequestDelete' signal -> need HasSelection passed. - // FIXME-MULTISELECT: If pressing Delete + another key we have ambiguous behavior. - const bool want_delete = (selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete); + const bool want_delete = ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (selection.Size > 0); const int item_curr_idx_to_focus = want_delete ? selection.ApplyDeletionPreLoop(ms_io, items.Size) : -1; for (int n = 0; n < items.Size; n++) @@ -3378,8 +3376,7 @@ static void ShowDemoWindowMultiSelect() ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); selection.ApplyRequests(ms_io, items.Size); - // FIXME-MULTISELECT: Shortcut(). Hard to demo this? May be helpful to turn into 'ms_io->RequestDelete' signal -> need HasSelection passed. - const bool want_delete = request_deletion_from_menu || ((selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); + const bool want_delete = (ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (selection.Size > 0)) || request_deletion_from_menu; const int item_curr_idx_to_focus = want_delete ? selection.ApplyDeletionPreLoop(ms_io, items.Size) : -1; request_deletion_from_menu = false; @@ -9854,7 +9851,7 @@ struct ExampleAssetsBrowser Selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->AdapterData; return self->Items[idx].ID; }; Selection.ApplyRequests(ms_io, Items.Size); - const bool want_delete = RequestDelete || ((Selection.Size > 0) && ImGui::IsWindowFocused() && ImGui::IsKeyPressed(ImGuiKey_Delete)); + const bool want_delete = (ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (Selection.Size > 0)) || RequestDelete; const int item_curr_idx_to_focus = want_delete ? Selection.ApplyDeletionPreLoop(ms_io, Items.Size) : -1; RequestDelete = false; From 9435a3185affdb72a405d7257d2dc63f662c7c93 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 19 Dec 2023 14:06:58 +0100 Subject: [PATCH 101/132] RangeSelect/MultiSelect: (Breaking) Added current_selection_size to BeginMultiSelect(). Required for shortcut routing so we can e.g. have Escape be used to clear selection THEN to exit child window. --- imgui.h | 2 +- imgui_demo.cpp | 14 +++++++------- imgui_widgets.cpp | 10 ++++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/imgui.h b/imgui.h index 89f016dfc810..d5a3bda523d0 100644 --- a/imgui.h +++ b/imgui.h @@ -675,7 +675,7 @@ namespace ImGui // - 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. // - ImGuiSelectionUserData is often used to store your item index. // - Read comments near ImGuiMultiSelectIO for instructions/details and see 'Demo->Widgets->Selection State & Multi-Select' for demo. - IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags); + IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags, int current_selection_size = -1); IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); IMGUI_API bool IsItemToggledSelection(); // Was the last item selection state toggled? Useful if you need the per-item information _before_ reaching EndMultiSelect(). We only returns toggle _event_ in order to handle clipping correctly. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index a8759933d9b8..c984ee40f354 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2929,7 +2929,7 @@ struct ExampleDualListBox if (child_visible) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); ApplySelectionRequests(ms_io, side); for (int item_n = 0; item_n < items.Size; item_n++) @@ -3060,7 +3060,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); selection.ApplyRequests(ms_io, ITEMS_COUNT); for (int n = 0; n < ITEMS_COUNT; n++) @@ -3094,7 +3094,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); selection.ApplyRequests(ms_io, ITEMS_COUNT); ImGuiListClipper clipper; @@ -3158,7 +3158,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); selection.ApplyRequests(ms_io, items.Size); const bool want_delete = ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (selection.Size > 0); @@ -3274,7 +3274,7 @@ static void ShowDemoWindowMultiSelect() { ImGui::PushID(selection_scope_n); ImGuiSelectionBasicStorage* selection = &selections_data[selection_scope_n]; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection->Size); selection->ApplyRequests(ms_io, ITEMS_COUNT); ImGui::SeparatorText("Selection scope"); @@ -3373,7 +3373,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); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); selection.ApplyRequests(ms_io, items.Size); const bool want_delete = (ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (selection.Size > 0)) || request_deletion_from_menu; @@ -9844,7 +9844,7 @@ struct ExampleAssetsBrowser ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. if (AllowBoxSelect) ms_flags |= ImGuiMultiSelectFlags_BoxSelect2d; // Enable box-select in 2D mode. - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags, Selection.Size); // Use custom selection adapter: store ID in selection (recommended) Selection.AdapterData = this; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 1f024d0ce87c..a85776200e66 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7293,7 +7293,10 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe // Return ImGuiMultiSelectIO structure. // Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). -ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) +// Passing 'current_selection_size' is currently optional: +// - it is useful for shortcut routing with ImGuiMultiSelectFlags_ClearOnEscape: so we can have Escape be used to clear selection THEN to exit child window. +// - if it is costly for you to compute, but can easily tell if your selection is empty or not, you may alter the ImGuiMultiSelectFlags_ClearOnEscape flag based on that. +ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int current_selection_size) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; @@ -7360,9 +7363,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if (ms->IsFocused) { // 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 (flags & ImGuiMultiSelectFlags_ClearOnEscape) + // Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window. + if ((flags & ImGuiMultiSelectFlags_ClearOnEscape) && (current_selection_size != 0)) if (Shortcut(ImGuiKey_Escape)) request_clear = true; From 65ebc0513b2dd0eb8eec86075060fb62c43b488a Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 29 May 2024 14:33:41 +0200 Subject: [PATCH 102/132] MultiSelect: Box-Select: minor refactor, tidying up. --- imgui_widgets.cpp | 94 ++++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 41 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index a85776200e66..f83fda9e2c7e 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7131,13 +7131,18 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) // [SECTION] Widgets: Box-Select support // This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet. //------------------------------------------------------------------------- -// - BoxSelectStartDrag() [Internal] +// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step() +//------------------------------------------------------------------------- +// - BoxSelectPreStartDrag() [Internal] +// - BoxSelectActivateDrag() [Internal] +// - BoxSelectDeactivateDrag() [Internal] // - BoxSelectScrollWithMouseDrag() [Internal] // - BeginBoxSelect() [Internal] // - EndBoxSelect() [Internal] //------------------------------------------------------------------------- -static void BoxSelectStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item) +// Call on the initial click. +static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item) { ImGuiContext& g = *GImGui; ImGuiBoxSelectState* bs = &g.BoxSelectState; @@ -7149,10 +7154,33 @@ static void BoxSelectStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item) bs->ScrollAccum = ImVec2(0.0f, 0.0f); } -static void BoxSelectScrollWithMouseDrag(ImGuiWindow* window, const ImRect& inner_r) +static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID); + bs->IsActive = true; + bs->Window = window; + bs->IsStarting = false; + ImGui::SetActiveID(bs->ID, window); + if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) + bs->RequestClear = true; +} + +static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs) +{ + ImGuiContext& g = *GImGui; + bs->IsActive = bs->IsStarting = false; + if (g.ActiveId == bs->ID) + { + IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID); + ImGui::ClearActiveID(); + } + bs->ID = 0; +} + +static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r) { ImGuiContext& g = *GImGui; - ImGuiBoxSelectState* bs = &g.BoxSelectState; IM_ASSERT(bs->Window == window); for (int n = 0; n < 2; n++) // each axis { @@ -7186,30 +7214,13 @@ bool ImGui::BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMult if (bs->ID != box_select_id) return false; + // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. bs->UnclipMode = false; bs->RequestClear = false; - - // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. if (bs->IsStarting && IsMouseDragPastThreshold(0)) - { - IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Started.\n", box_select_id); - bs->IsActive = true; - bs->Window = window; - bs->IsStarting = false; - SetActiveID(bs->ID, window); - if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) - bs->RequestClear = true; - } + BoxSelectActivateDrag(bs, window); else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false) - { - bs->IsActive = bs->IsStarting = false; - if (g.ActiveId == bs->ID) - { - IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Ended.\n", box_select_id); - ClearActiveID(); - } - bs->ID = 0; - } + BoxSelectDeactivateDrag(bs); if (!bs->IsActive) return false; @@ -7264,7 +7275,7 @@ void ImGui::EndBoxSelect(const ImRect& scope_rect, bool enable_scroll) scroll_r.Expand(-g.FontSize); //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255)); if (!scroll_r.Contains(g.IO.MousePos)) - BoxSelectScrollWithMouseDrag(window, scroll_r); + BoxSelectScrollWithMouseDrag(bs, window, scroll_r); } } @@ -7447,7 +7458,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() { if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1) { - BoxSelectStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); + BoxSelectPreStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal); SetHoveredID(ms->BoxSelectId); if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) @@ -7634,22 +7645,23 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) } // Box-select toggle handling - if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId)) - { - const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); - const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); - if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) + if (ms->BoxSelectId != 0) + if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId)) { - selected = !selected; - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; - ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; - if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->Selected == selected) - prev_req->RangeLastItem = item_data; // Merge span into same request - else - ms->IO.Requests.push_back(req); + const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); + const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); + if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) + { + selected = !selected; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; + ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; + if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->Selected == selected) + prev_req->RangeLastItem = item_data; // Merge span into same request + else + ms->IO.Requests.push_back(req); + } + ms->BoxSelectLastitem = item_data; } - ms->BoxSelectLastitem = item_data; - } // Right-click handling. // FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816 @@ -7677,7 +7689,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; if (flags & ImGuiMultiSelectFlags_BoxSelect) if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) - BoxSelectStartDrag(ms->BoxSelectId, item_data); + BoxSelectPreStartDrag(ms->BoxSelectId, item_data); //---------------------------------------------------------------------------------------- // ACTION | Begin | Pressed/Activated | End From dc0a1682e3b5d74c685a31649ece5d2dabedbd37 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 29 May 2024 15:01:52 +0200 Subject: [PATCH 103/132] MultiSelect: Box-Select: when dragging from void, first hit item sets NavId by simulating a press, so navigation can resume from that spot. --- imgui_internal.h | 1 + imgui_widgets.cpp | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/imgui_internal.h b/imgui_internal.h index 5fb2d568d8e6..53b0c85c47bf 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1758,6 +1758,7 @@ struct IMGUI_API ImGuiMultiSelectTempData bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. 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 IsKeyboardSetRange; // Set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. + bool IsSelectionEmpty; // Set by BeginMultiSelect() based on optional info provided by user. May be false positive, never false negative. bool NavIdPassedBy; bool RangeSrcPassedBy; // Set by the item that matches RangeSrcItem. bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index f83fda9e2c7e..9eaadc2a8f1d 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7330,6 +7330,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int cur ms->FocusScopeId = id; ms->Flags = flags; ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId); + ms->IsSelectionEmpty = (current_selection_size == 0); ms->BackupCursorMaxPos = window->DC.CursorMaxPos; ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos; PushFocusScope(ms->FocusScopeId); @@ -7398,6 +7399,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int cur if (request_clear || request_select_all) { ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, request_select_all, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; + if (!request_select_all) + ms->IsSelectionEmpty = true; ms->IO.Requests.push_back(req); } ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1; @@ -7655,10 +7658,13 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) selected = !selected; ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; - if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->Selected == selected) + if (ms->IsSelectionEmpty && bs->IsStartedFromVoid) + pressed = true; // First item act as a pressed: code below will emit selection request and set NavId (whatever we emit here will be overriden anyway) + else if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->Selected == selected) prev_req->RangeLastItem = item_data; // Merge span into same request else ms->IO.Requests.push_back(req); + ms->IsSelectionEmpty = false; } ms->BoxSelectLastitem = item_data; } From 81548cb6bf3a0b3f50d7c0fbb65be1529aba68c9 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 29 May 2024 15:22:32 +0200 Subject: [PATCH 104/132] MultiSelect: added GetMultiSelectState() + store LastSelectionSize as provided by user, convenient for quick debugging and testing. --- imgui_internal.h | 7 ++++--- imgui_widgets.cpp | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 53b0c85c47bf..908c8f426fe1 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1758,7 +1758,6 @@ struct IMGUI_API ImGuiMultiSelectTempData bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. 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 IsKeyboardSetRange; // Set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. - bool IsSelectionEmpty; // Set by BeginMultiSelect() based on optional info provided by user. May be false positive, never false negative. bool NavIdPassedBy; bool RangeSrcPassedBy; // Set by the item that matches RangeSrcItem. bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. @@ -1775,12 +1774,13 @@ struct IMGUI_API ImGuiMultiSelectState ImGuiWindow* Window; ImGuiID ID; int LastFrameActive; // Last used frame-count, for GC. + int LastSelectionSize; // Set by BeginMultiSelect() based on optional info provided by user. May be -1 if unknown. ImS8 RangeSelected; // -1 (don't have) or true/false ImS8 NavIdSelected; // -1 (don't have) or true/false ImGuiSelectionUserData RangeSrcItem; // ImGuiSelectionUserData NavIdItem; // SetNextItemSelectionUserData() value for NavId (if part of submitted items) - ImGuiMultiSelectState() { Window = NULL; ID = 0; LastFrameActive = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; } + ImGuiMultiSelectState() { Window = NULL; ID = 0; LastFrameActive = LastSelectionSize = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; } }; #endif // #ifdef IMGUI_HAS_MULTI_SELECT @@ -3411,7 +3411,8 @@ namespace ImGui // Multi-Select API IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags); IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); - inline ImGuiBoxSelectState* GetBoxSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.BoxSelectState.ID == id && g.BoxSelectState.IsActive) ? &g.BoxSelectState : NULL; } + inline ImGuiBoxSelectState* GetBoxSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.BoxSelectState.ID == id && g.BoxSelectState.IsActive) ? &g.BoxSelectState : NULL; } + inline ImGuiMultiSelectState* GetMultiSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return g.MultiSelectStorage.GetByKey(id); } // Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API) IMGUI_API void SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 9eaadc2a8f1d..d0adf2a9c505 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7330,7 +7330,6 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int cur ms->FocusScopeId = id; ms->Flags = flags; ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId); - ms->IsSelectionEmpty = (current_selection_size == 0); ms->BackupCursorMaxPos = window->DC.CursorMaxPos; ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos; PushFocusScope(ms->FocusScopeId); @@ -7344,6 +7343,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int cur ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id); storage->ID = id; storage->LastFrameActive = g.FrameCount; + storage->LastSelectionSize = current_selection_size; storage->Window = window; ms->Storage = storage; @@ -7400,7 +7400,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int cur { ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, request_select_all, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; if (!request_select_all) - ms->IsSelectionEmpty = true; + storage->LastSelectionSize = 0; ms->IO.Requests.push_back(req); } ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1; @@ -7658,13 +7658,13 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) selected = !selected; ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; - if (ms->IsSelectionEmpty && bs->IsStartedFromVoid) + if (storage->LastSelectionSize == 0 && bs->IsStartedFromVoid) pressed = true; // First item act as a pressed: code below will emit selection request and set NavId (whatever we emit here will be overriden anyway) else if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->Selected == selected) prev_req->RangeLastItem = item_data; // Merge span into same request else ms->IO.Requests.push_back(req); - ms->IsSelectionEmpty = false; + storage->LastSelectionSize++; } ms->BoxSelectLastitem = item_data; } @@ -7799,6 +7799,7 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) return; Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected); Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected); + Text("LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user TreePop(); #else IM_UNUSED(storage); From 1113f13f8384d9b61919ec34e209a6a7dedab7ff Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 31 May 2024 20:18:57 +0200 Subject: [PATCH 105/132] MultiSelect: Box-Select: fixed "when dragging from void" implementation messing with calling BeginMultiSelect() without a selection size. --- imgui_widgets.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index d0adf2a9c505..062b36452938 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7529,7 +7529,7 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); - // Apply SetAll (Clear/SelectAll )requests requested by BeginMultiSelect(). + // Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect(). // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect() if (ms->LoopRequestSetAll != -1) @@ -7655,16 +7655,21 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) { - selected = !selected; - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; - ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; - if (storage->LastSelectionSize == 0 && bs->IsStartedFromVoid) - pressed = true; // First item act as a pressed: code below will emit selection request and set NavId (whatever we emit here will be overriden anyway) - else if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->Selected == selected) - prev_req->RangeLastItem = item_data; // Merge span into same request + if (storage->LastSelectionSize <= 0 && bs->IsStartedFromVoid) + { + pressed = true; // First item act as a pressed: code below will emit selection request and set NavId (whatever we emit here will be overridden anyway) + } else - ms->IO.Requests.push_back(req); - storage->LastSelectionSize++; + { + selected = !selected; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; + ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; + if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->Selected == selected) + prev_req->RangeLastItem = item_data; // Merge span into same request + else + ms->IO.Requests.push_back(req); + } + storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1); } ms->BoxSelectLastitem = item_data; } From 2f56df483981985b60dffc4577b868375711541a Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 30 May 2024 18:44:57 +0200 Subject: [PATCH 106/132] MultiSelect: (breaking) renamed ImGuiSelectionBasicStorage::AdapterData to UserData. --- imgui.h | 4 ++-- imgui_demo.cpp | 12 ++++++------ imgui_widgets.cpp | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/imgui.h b/imgui.h index d5a3bda523d0..58409f6113e2 100644 --- a/imgui.h +++ b/imgui.h @@ -2843,14 +2843,14 @@ struct ImGuiSelectionBasicStorage // Members ImGuiStorage Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set int Size; // Number of selected items (== number of 1 in the Storage), maintained by this helper. - void* AdapterData; // Adapter to convert item index to item identifier // e.g. selection.AdapterData = (void*)my_items; ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->AdapterData)[idx]->ID; }; + void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count); // Methods: selection storage - ImGuiSelectionBasicStorage() { Clear(); AdapterData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } + ImGuiSelectionBasicStorage() { Clear(); UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } void Clear() { Storage.Data.resize(0); Size = 0; } void Swap(ImGuiSelectionBasicStorage& r) { Storage.Data.swap(r.Storage.Data); } bool Contains(ImGuiID id) const { return Storage.GetInt(id, 0) != 0; } diff --git a/imgui_demo.cpp b/imgui_demo.cpp index c984ee40f354..efaf116dd120 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2872,8 +2872,8 @@ struct ExampleDualListBox void ApplySelectionRequests(ImGuiMultiSelectIO* ms_io, int side) { // In this example we store item id in selection (instead of item index) - Selections[side].AdapterData = Items[side].Data; - Selections[side].AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { ImGuiID* items = (ImGuiID*)self->AdapterData; return items[idx]; }; + Selections[side].UserData = Items[side].Data; + Selections[side].AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { ImGuiID* items = (ImGuiID*)self->UserData; return items[idx]; }; Selections[side].ApplyRequests(ms_io, Items[side].Size); } static int IMGUI_CDECL CompareItemsByValue(const void* lhs, const void* rhs) @@ -3135,8 +3135,8 @@ static void ShowDemoWindowMultiSelect() // Use a custom selection.Adapter: store item identifier in Selection (instead of index) static ImVector items; static ExampleSelectionWithDeletion selection; - selection.AdapterData = (void*)&items; - selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { ImVector* p_items = (ImVector*)self->AdapterData; return (*p_items)[idx]; }; // Index -> ID + selection.UserData = (void*)&items; + selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { ImVector* p_items = (ImVector*)self->UserData; return (*p_items)[idx]; }; // Index -> ID ImGui::Text("Added features:"); ImGui::BulletText("Dynamic list with Delete key support."); @@ -9847,8 +9847,8 @@ struct ExampleAssetsBrowser ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags, Selection.Size); // Use custom selection adapter: store ID in selection (recommended) - Selection.AdapterData = this; - Selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->AdapterData; return self->Items[idx].ID; }; + Selection.UserData = this; + Selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->UserData; return self->Items[idx].ID; }; Selection.ApplyRequests(ms_io, Items.Size); const bool want_delete = (ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (Selection.Size > 0)) || RequestDelete; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 062b36452938..7602b2cd1525 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7306,7 +7306,7 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe // Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). // Passing 'current_selection_size' is currently optional: // - it is useful for shortcut routing with ImGuiMultiSelectFlags_ClearOnEscape: so we can have Escape be used to clear selection THEN to exit child window. -// - if it is costly for you to compute, but can easily tell if your selection is empty or not, you may alter the ImGuiMultiSelectFlags_ClearOnEscape flag based on that. +// - if it is costly for you to compute, but can easily tell if your selection is empty or not, you may pass 1, or use the ImGuiMultiSelectFlags_ClearOnEscape flag dynamically. ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int current_selection_size) { ImGuiContext& g = *GImGui; @@ -7347,6 +7347,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int cur storage->Window = window; ms->Storage = storage; + // Output to user ms->IO.RangeSrcItem = storage->RangeSrcItem; ms->IO.NavIdItem = storage->NavIdItem; ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; From 7bbbbea20045ce933b4c3c2f5dd683bb03e08930 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 31 May 2024 21:37:45 +0200 Subject: [PATCH 107/132] MultiSelect: Box-Select: fixes for checkboxes support. Comments. --- imgui.h | 4 ++-- imgui_demo.cpp | 2 +- imgui_widgets.cpp | 20 ++++++++------------ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/imgui.h b/imgui.h index 58409f6113e2..40ac55587d95 100644 --- a/imgui.h +++ b/imgui.h @@ -2775,8 +2775,8 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+Click/Shift+Keyboard handling (useful for unordered 2D selection). ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes) ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing other items when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes) - ImGuiMultiSelectFlags_BoxSelect = 1 << 5, // Enable box-selection (only supporting 1D list when using clipper, not 2D grids). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_BoxSelect2d = 1 << 6, // Enable box-selection with 2D layout/grid support. This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. + ImGuiMultiSelectFlags_BoxSelect = 1 << 5, // Enable box-selection with same width and same x pos items. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 6, // Enable box-selection with varying width or varying x pos items (e.g. different width labels, or 2D layout/grid). This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 7, // Disable scrolling when box-selecting near edges of scope. ImGuiMultiSelectFlags_ClearOnEscape = 1 << 8, // Clear selection when pressing Escape while scope is focused. ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 9, // Clear selection when clicking on empty location within scope. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index efaf116dd120..ec15e279129c 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3215,7 +3215,7 @@ static void ShowDemoWindowMultiSelect() static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_ClearOnEscape; ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect2d", &flags, ImGuiMultiSelectFlags_BoxSelect2d); // Use 2D version as checkboxes are not same width. struct Funcs { diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 7602b2cd1525..c5e8c1f606d0 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -1136,10 +1136,11 @@ bool ImGui::Checkbox(const char* label, bool* v) ItemSize(total_bb, style.FramePadding.y); const bool is_visible = ItemAdd(total_bb, id); if (!is_visible) - { - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); - return false; - } + if (!g.BoxSelectState.UnclipMode || (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) == 0 || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support + { + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); + return false; + } // Range-Selection/Multi-selection support (header) bool checked = *v; @@ -6801,13 +6802,8 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0; if (!is_visible) - { - // Extra layer of "no logic clip" for box-select support - if (!is_multi_select) - return false; - if (!g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(bb)) + if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(bb)) // Extra layer of "no logic clip" for box-select support (would be more overhead to add to ItemAdd) return false; - } const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; if (disabled_item && !disabled_global) // Only testing this as an optimization @@ -7242,9 +7238,9 @@ bool ImGui::BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMult if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) { - bs->UnclipRect = bs->BoxSelectRectPrev; - bs->UnclipRect.Add(bs->BoxSelectRectCurr); bs->UnclipMode = true; + bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis. + bs->UnclipRect.Add(bs->BoxSelectRectCurr); } //GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); From f6b5caf82c718297a61127ae474290ea2e88be1a Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 31 May 2024 21:57:10 +0200 Subject: [PATCH 108/132] MultiSelect: (breaking) renamed ImGuiMultiSelectFlags_BoxSelect -> ImGuiMultiSelectFlags_BoxSelect1d, ImGuiMultiSelectFlags_BoxSelect2d -> ImGuiMultiSelectFlags_BoxSelect. ImGuiMultiSelectFlags_BoxSelect1d being an optimization it is the optional flag. --- imgui.h | 4 ++-- imgui_demo.cpp | 17 +++++++++-------- imgui_widgets.cpp | 6 +++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/imgui.h b/imgui.h index 40ac55587d95..4be6e6467485 100644 --- a/imgui.h +++ b/imgui.h @@ -2775,8 +2775,8 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+Click/Shift+Keyboard handling (useful for unordered 2D selection). ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes) ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing other items when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes) - ImGuiMultiSelectFlags_BoxSelect = 1 << 5, // Enable box-selection with same width and same x pos items. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_BoxSelect2d = 1 << 6, // Enable box-selection with varying width or varying x pos items (e.g. different width labels, or 2D layout/grid). This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. + ImGuiMultiSelectFlags_BoxSelect = 1 << 5, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect1d = 1 << 6, // Enable box-selection with same width and same x pos items (e.g. only full row Selectable()). Small optimization. ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 7, // Disable scrolling when box-selecting near edges of scope. ImGuiMultiSelectFlags_ClearOnEscape = 1 << 8, // Clear selection when pressing Escape while scope is focused. ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 9, // Clear selection when clicking on empty location within scope. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index ec15e279129c..58ceb68b3618 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3059,7 +3059,7 @@ static void ShowDemoWindowMultiSelect() // The BeginChild() has no purpose for selection logic, other that offering a scrolling region. if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); selection.ApplyRequests(ms_io, ITEMS_COUNT); @@ -3093,7 +3093,7 @@ static void ShowDemoWindowMultiSelect() ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT); if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); selection.ApplyRequests(ms_io, ITEMS_COUNT); @@ -3157,7 +3157,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { - ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); selection.ApplyRequests(ms_io, items.Size); @@ -3215,7 +3215,7 @@ static void ShowDemoWindowMultiSelect() static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_ClearOnEscape; ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect2d", &flags, ImGuiMultiSelectFlags_BoxSelect2d); // Use 2D version as checkboxes are not same width. + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); // Cannot use ImGuiMultiSelectFlags_BoxSelect1d as checkboxes are varying width. struct Funcs { @@ -3268,7 +3268,7 @@ static void ShowDemoWindowMultiSelect() if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeRect", &flags, ImGuiMultiSelectFlags_ScopeRect) && (flags & ImGuiMultiSelectFlags_ScopeRect)) flags &= ~ImGuiMultiSelectFlags_ScopeWindow; ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect1d", &flags, ImGuiMultiSelectFlags_BoxSelect1d); for (int selection_scope_n = 0; selection_scope_n < SCOPES_COUNT; selection_scope_n++) { @@ -3321,7 +3321,7 @@ static void ShowDemoWindowMultiSelect() static bool use_drag_drop = true; static bool show_in_table = false; static bool show_color_button = false; - static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect; + static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; static WidgetType widget_type = WidgetType_Selectable; if (ImGui::TreeNode("Options")) @@ -3340,6 +3340,7 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect1d", &flags, ImGuiMultiSelectFlags_BoxSelect1d); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); @@ -9843,7 +9844,7 @@ struct ExampleAssetsBrowser if (AllowDragUnselected) ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. if (AllowBoxSelect) - ms_flags |= ImGuiMultiSelectFlags_BoxSelect2d; // Enable box-select in 2D mode. + ms_flags |= ImGuiMultiSelectFlags_BoxSelect; // Enable box-select in 2D mode. ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags, Selection.Size); // Use custom selection adapter: store ID in selection (recommended) @@ -9918,7 +9919,7 @@ struct ExampleAssetsBrowser } // Render icon (a real app would likely display an image/thumbnail here) - // Because we use ImGuiMultiSelectFlags_BoxSelect2d mode, + // Because we use ImGuiMultiSelectFlags_BoxSelect (without ImGuiMultiSelectFlags_BoxSelect1d flag), // clipping vertical range may occasionally be larger so we coarse-clip our rendering. if (item_is_visible) { diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index c5e8c1f606d0..bd25e1adb9bc 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7235,7 +7235,7 @@ bool ImGui::BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMult // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) // Storing an extra rect used by widgets supporting box-select. - if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) + if ((ms_flags & ImGuiMultiSelectFlags_BoxSelect) && !(ms_flags & ImGuiMultiSelectFlags_BoxSelect1d)) if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) { bs->UnclipMode = true; @@ -7316,8 +7316,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int cur if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) flags |= ImGuiMultiSelectFlags_ScopeWindow; if (flags & ImGuiMultiSelectFlags_SingleSelect) - flags &= ~(ImGuiMultiSelectFlags_BoxSelect | ImGuiMultiSelectFlags_BoxSelect2d); - if (flags & ImGuiMultiSelectFlags_BoxSelect2d) + flags &= ~(ImGuiMultiSelectFlags_BoxSelect | ImGuiMultiSelectFlags_BoxSelect1d); + if (flags & ImGuiMultiSelectFlags_BoxSelect1d) flags |= ImGuiMultiSelectFlags_BoxSelect; // FIXME: BeginFocusScope() From 443b034895cc334dfb58fe7e0f8c5df5044adb66 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 4 Jun 2024 15:10:07 +0200 Subject: [PATCH 109/132] MultiSelect: mark parent child window as navigable into, with highlight. Assume user will always submit interactive items. --- imgui_demo.cpp | 1 + imgui_widgets.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 58ceb68b3618..12f873b506dd 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3211,6 +3211,7 @@ static void ShowDemoWindowMultiSelect() ImGui::BulletText("Shift+Click to check multiple boxes."); ImGui::BulletText("Shift+Keyboard to copy current value to other boxes."); + // If you have an array of checkboxes, you may want to use NoAutoSelect + NoAutoClear and the ImGuiSelectionExternalStorage helper. static bool values[20] = {}; static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_ClearOnEscape; ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index bd25e1adb9bc..b60e73a497a6 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7329,6 +7329,8 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int cur ms->BackupCursorMaxPos = window->DC.CursorMaxPos; ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos; PushFocusScope(ms->FocusScopeId); + if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items. + window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main; // 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; From ab995d3d4f39315ba405beb2a49861b53a8edf58 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 5 Jun 2024 17:04:55 +0200 Subject: [PATCH 110/132] MultiSelect: (breaking) Added 'items_count' parameter to BeginMultiSelect(). Will enable extra features, and remove equivalent param from ImGuiSelectionBasicStorage::ApplyRequests(. --- imgui.h | 12 +++++++----- imgui_demo.cpp | 40 ++++++++++++++++++++-------------------- imgui_widgets.cpp | 29 +++++++++++++++++++---------- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/imgui.h b/imgui.h index 4be6e6467485..e358e1aebf4a 100644 --- a/imgui.h +++ b/imgui.h @@ -179,7 +179,7 @@ struct ImGuiMultiSelectIO; // Structure to interact with a BeginMultiSe struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame struct ImGuiPayload; // User data payload for drag and drop operations struct ImGuiPlatformImeData; // Platform IME data for io.PlatformSetImeDataFn() function. -struct ImGuiSelectionBasicStorage; // Helper struct to store multi-selection state + apply multi-selection requests. +struct ImGuiSelectionBasicStorage; // Optional helper to store multi-selection state + apply multi-selection requests. struct ImGuiSelectionRequest; // A selection request (stored in ImGuiMultiSelectIO) struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) struct ImGuiStorage; // Helper for key->value storage (container sorted by key) @@ -673,9 +673,10 @@ namespace ImGui // Multi-selection system for Selectable() and TreeNode() functions. // - 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. - // - ImGuiSelectionUserData is often used to store your item index. + // - ImGuiSelectionUserData is often used to store your item index within the current view (but may store something else). // - Read comments near ImGuiMultiSelectIO for instructions/details and see 'Demo->Widgets->Selection State & Multi-Select' for demo. - IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags, int current_selection_size = -1); + // - 'selection_size' and 'items_count' parameters are optional and used by a few features. If they are costly for you to compute, you may avoid them. + IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size = -1, int items_count = -1); IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); IMGUI_API bool IsItemToggledSelection(); // Was the last item selection state toggled? Useful if you need the per-item information _before_ reaching EndMultiSelect(). We only returns toggle _event_ in order to handle clipping correctly. @@ -2799,6 +2800,7 @@ struct ImGuiMultiSelectIO ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). + int ItemsCount; // ms:w, app:r / app:r // 'int items_count' parameter to BeginMultiSelect() is copied here for convenience, allowing simpler calls to your ApplyRequests handler. Not used internally. }; // Selection request type @@ -2846,8 +2848,8 @@ struct ImGuiSelectionBasicStorage ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->AdapterData)[idx]->ID; }; void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; - // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions - IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count); + // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. Uses 'items_count' based to BeginMultiSelect() + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Methods: selection storage ImGuiSelectionBasicStorage() { Clear(); UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 12f873b506dd..7d4f04cdff4c 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2874,7 +2874,7 @@ struct ExampleDualListBox // In this example we store item id in selection (instead of item index) Selections[side].UserData = Items[side].Data; Selections[side].AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { ImGuiID* items = (ImGuiID*)self->UserData; return items[idx]; }; - Selections[side].ApplyRequests(ms_io, Items[side].Size); + Selections[side].ApplyRequests(ms_io); } static int IMGUI_CDECL CompareItemsByValue(const void* lhs, const void* rhs) { @@ -2929,7 +2929,7 @@ struct ExampleDualListBox if (child_visible) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, items.Size); ApplySelectionRequests(ms_io, side); for (int item_n = 0; item_n < items.Size; item_n++) @@ -3060,8 +3060,8 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); - selection.ApplyRequests(ms_io, ITEMS_COUNT); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, ITEMS_COUNT); + selection.ApplyRequests(ms_io); for (int n = 0; n < ITEMS_COUNT; n++) { @@ -3073,7 +3073,7 @@ static void ShowDemoWindowMultiSelect() } ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, ITEMS_COUNT); + selection.ApplyRequests(ms_io); } ImGui::EndChild(); ImGui::TreePop(); @@ -3094,8 +3094,8 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); - selection.ApplyRequests(ms_io, ITEMS_COUNT); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, ITEMS_COUNT); + selection.ApplyRequests(ms_io); ImGuiListClipper clipper; clipper.Begin(ITEMS_COUNT); @@ -3114,7 +3114,7 @@ static void ShowDemoWindowMultiSelect() } ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, ITEMS_COUNT); + selection.ApplyRequests(ms_io); } ImGui::EndChild(); ImGui::TreePop(); @@ -3158,8 +3158,8 @@ static void ShowDemoWindowMultiSelect() if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) { ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size); - selection.ApplyRequests(ms_io, items.Size); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, items.Size); + selection.ApplyRequests(ms_io); const bool want_delete = ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (selection.Size > 0); const int item_curr_idx_to_focus = want_delete ? selection.ApplyDeletionPreLoop(ms_io, items.Size) : -1; @@ -3179,7 +3179,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, items.Size); + selection.ApplyRequests(ms_io); if (want_delete) selection.ApplyDeletionPostLoop(ms_io, items, item_curr_idx_to_focus); } @@ -3275,8 +3275,8 @@ static void ShowDemoWindowMultiSelect() { ImGui::PushID(selection_scope_n); ImGuiSelectionBasicStorage* selection = &selections_data[selection_scope_n]; - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection->Size); - selection->ApplyRequests(ms_io, ITEMS_COUNT); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection->Size, ITEMS_COUNT); + selection->ApplyRequests(ms_io); ImGui::SeparatorText("Selection scope"); ImGui::Text("Selection size: %d/%d", selection->Size, ITEMS_COUNT); @@ -3292,7 +3292,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection->ApplyRequests(ms_io, ITEMS_COUNT); + selection->ApplyRequests(ms_io); ImGui::PopID(); } ImGui::TreePop(); @@ -3375,8 +3375,8 @@ 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, selection.Size); - selection.ApplyRequests(ms_io, items.Size); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, items.Size); + selection.ApplyRequests(ms_io); const bool want_delete = (ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (selection.Size > 0)) || request_deletion_from_menu; const int item_curr_idx_to_focus = want_delete ? selection.ApplyDeletionPreLoop(ms_io, items.Size) : -1; @@ -3520,7 +3520,7 @@ static void ShowDemoWindowMultiSelect() // Apply multi-select requests ms_io = ImGui::EndMultiSelect(); - selection.ApplyRequests(ms_io, items.Size); + selection.ApplyRequests(ms_io); if (want_delete) selection.ApplyDeletionPostLoop(ms_io, items, item_curr_idx_to_focus); @@ -9846,12 +9846,12 @@ struct ExampleAssetsBrowser ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. if (AllowBoxSelect) ms_flags |= ImGuiMultiSelectFlags_BoxSelect; // Enable box-select in 2D mode. - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags, Selection.Size); + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags, Selection.Size, Items.Size); // Use custom selection adapter: store ID in selection (recommended) Selection.UserData = this; Selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->UserData; return self->Items[idx].ID; }; - Selection.ApplyRequests(ms_io, Items.Size); + Selection.ApplyRequests(ms_io); const bool want_delete = (ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (Selection.Size > 0)) || RequestDelete; const int item_curr_idx_to_focus = want_delete ? Selection.ApplyDeletionPreLoop(ms_io, Items.Size) : -1; @@ -9959,7 +9959,7 @@ struct ExampleAssetsBrowser } ms_io = ImGui::EndMultiSelect(); - Selection.ApplyRequests(ms_io, Items.Size); + Selection.ApplyRequests(ms_io); if (want_delete) Selection.ApplyDeletionPostLoop(ms_io, Items, item_curr_idx_to_focus); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index b60e73a497a6..67ccebeae4ad 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7300,10 +7300,13 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe // Return ImGuiMultiSelectIO structure. // Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). -// Passing 'current_selection_size' is currently optional: -// - it is useful for shortcut routing with ImGuiMultiSelectFlags_ClearOnEscape: so we can have Escape be used to clear selection THEN to exit child window. -// - if it is costly for you to compute, but can easily tell if your selection is empty or not, you may pass 1, or use the ImGuiMultiSelectFlags_ClearOnEscape flag dynamically. -ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int current_selection_size) +// Passing 'selection_size' and 'items_count' parameters is currently optional. +// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0, +// allowing a first press to clear selection THEN the second press to leave child window and return to parent. +// - 'items_count' is stored in ImGuiMultiSelectIO which makes it a convenient way to pass the information to your ApplyRequest() handler (but you may pass it differently). +// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1. +// - If you can easily tell if your selection is empty or not, you may pass 0/1, or you may enable ImGuiMultiSelectFlags_ClearOnEscape flag dynamically. +ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; @@ -7341,15 +7344,16 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int cur ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id); storage->ID = id; storage->LastFrameActive = g.FrameCount; - storage->LastSelectionSize = current_selection_size; + storage->LastSelectionSize = selection_size; storage->Window = window; ms->Storage = storage; // Output to user + ms->IO.Requests.resize(0); ms->IO.RangeSrcItem = storage->RangeSrcItem; ms->IO.NavIdItem = storage->NavIdItem; ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; - ms->IO.Requests.resize(0); + ms->IO.ItemsCount = items_count; // Clear when using Navigation to move within the scope // (we compare FocusScopeId so it possible to use multiple selections inside a same window) @@ -7375,7 +7379,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int cur { // Shortcut: Clear selection (Escape) // Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window. - if ((flags & ImGuiMultiSelectFlags_ClearOnEscape) && (current_selection_size != 0)) + if ((flags & ImGuiMultiSelectFlags_ClearOnEscape) && (selection_size != 0)) if (Shortcut(ImGuiKey_Escape)) request_clear = true; @@ -7826,17 +7830,22 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) // if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { AddItem(n); } } // if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->Selected); } } // } -void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io, int items_count) +void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) { + // For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional. + // It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect(). + // Other scheme may handle SetAll differently. + IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!"); IM_ASSERT(AdapterIndexToStorageId != NULL); + for (ImGuiSelectionRequest& req : ms_io->Requests) { if (req.Type == ImGuiSelectionRequestType_SetAll) Clear(); if (req.Type == ImGuiSelectionRequestType_SetAll && req.Selected) { - Storage.Data.reserve(items_count); - for (int idx = 0; idx < items_count; idx++) + Storage.Data.reserve(ms_io->ItemsCount); + for (int idx = 0; idx < ms_io->ItemsCount; idx++) SetItemSelected(AdapterIndexToStorageId(this, idx), true); } if (req.Type == ImGuiSelectionRequestType_SetRange) From c94cf6f01fe85f475a6a02206b772743a856a55e Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 5 Jun 2024 17:16:06 +0200 Subject: [PATCH 111/132] MultiSelect: added ImGuiSelectionBasicStorage::GetStorageIdFromIndex() indirection to be easier on the reader. Tempting to make it a virtual. --- imgui.h | 5 +++-- imgui_demo.cpp | 8 ++++---- imgui_widgets.cpp | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/imgui.h b/imgui.h index e358e1aebf4a..9f441ed09980 100644 --- a/imgui.h +++ b/imgui.h @@ -2823,7 +2823,7 @@ struct ImGuiSelectionRequest // Optional helper to store multi-selection state + apply multi-selection requests. // - Used by our demos and provided as a convenience to easily implement basic multi-selection. -// - USING THIS IS NOT MANDATORY. This is only a helper and not a required API. Advanced users are likely to implement their own. +// - USING THIS IS NOT MANDATORY. This is only a helper and not a required API. // To store a multi-selection, in your application you could: // - A) Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement. // - B) Use your own external storage: e.g. std::set, std::vector, interval trees, etc. @@ -2845,7 +2845,7 @@ struct ImGuiSelectionBasicStorage // Members ImGuiStorage Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set int Size; // Number of selected items (== number of 1 in the Storage), maintained by this helper. - ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->AdapterData)[idx]->ID; }; + ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. Uses 'items_count' based to BeginMultiSelect() @@ -2857,6 +2857,7 @@ struct ImGuiSelectionBasicStorage void Swap(ImGuiSelectionBasicStorage& r) { Storage.Data.swap(r.Storage.Data); } bool Contains(ImGuiID id) const { return Storage.GetInt(id, 0) != 0; } void SetItemSelected(ImGuiID id, bool v) { int* p_int = Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } } + ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 7d4f04cdff4c..b3dc5ef54f8e 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2798,12 +2798,12 @@ struct ExampleSelectionWithDeletion : ImGuiSelectionBasicStorage // If focused item is selected: land on first unselected item after focused item. for (int idx = focused_idx + 1; idx < items_count; idx++) - if (!Contains(AdapterIndexToStorageId(this, idx))) + if (!Contains(GetStorageIdFromIndex(idx))) return idx; // If focused item is selected: otherwise return last unselected item before focused item. for (int idx = IM_MIN(focused_idx, items_count) - 1; idx >= 0; idx--) - if (!Contains(AdapterIndexToStorageId(this, idx))) + if (!Contains(GetStorageIdFromIndex(idx))) return idx; return -1; @@ -2822,7 +2822,7 @@ struct ExampleSelectionWithDeletion : ImGuiSelectionBasicStorage int item_next_idx_to_select = -1; for (int idx = 0; idx < items.Size; idx++) { - if (!Contains(AdapterIndexToStorageId(this, idx))) + if (!Contains(GetStorageIdFromIndex(idx))) new_items.push_back(items[idx]); if (item_curr_idx_to_select == idx) item_next_idx_to_select = new_items.Size - 1; @@ -2832,7 +2832,7 @@ struct ExampleSelectionWithDeletion : ImGuiSelectionBasicStorage // Update selection Clear(); if (item_next_idx_to_select != -1 && ms_io->NavIdSelected) - SetItemSelected(AdapterIndexToStorageId(this, item_next_idx_to_select), true); + SetItemSelected(GetStorageIdFromIndex(item_next_idx_to_select), true); } }; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 67ccebeae4ad..1ebabe4d1931 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7846,11 +7846,11 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) { Storage.Data.reserve(ms_io->ItemsCount); for (int idx = 0; idx < ms_io->ItemsCount; idx++) - SetItemSelected(AdapterIndexToStorageId(this, idx), true); + SetItemSelected(GetStorageIdFromIndex(idx), true); } if (req.Type == ImGuiSelectionRequestType_SetRange) for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) - SetItemSelected(AdapterIndexToStorageId(this, idx), req.Selected); + SetItemSelected(GetStorageIdFromIndex(idx), req.Selected); } } From f9caf4447a65b3a686051e0c449fc4b3b93a4ae5 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 5 Jun 2024 18:32:59 +0200 Subject: [PATCH 112/132] MultiSelect: fixed ImGuiSelectionBasicStorage::Swap() helper. --- imgui.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/imgui.h b/imgui.h index 9f441ed09980..c0c2d0b2ca84 100644 --- a/imgui.h +++ b/imgui.h @@ -44,7 +44,7 @@ Index of this file: // [SECTION] ImGuiIO // [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) // [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) -// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO, ImGuiSelectionBasicStorage) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO, ImGuiSelectionRequest, ImGuiSelectionBasicStorage) // [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) // [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) // [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) @@ -2817,8 +2817,8 @@ struct ImGuiSelectionRequest //------------------------------------------// BeginMultiSelect / EndMultiSelect ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r // Request type. You'll most often receive 1 Clear + 1 SetRange with a single-item range. bool Selected; // ms:w, app:r / ms:w, app:r // Parameter for SetAll/SetRange requests (true = select, false = unselect) - ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) - ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) + ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom). + ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top). Inclusive! }; // Optional helper to store multi-selection state + apply multi-selection requests. @@ -2848,16 +2848,16 @@ struct ImGuiSelectionBasicStorage ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; - // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. Uses 'items_count' based to BeginMultiSelect() - IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); + // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. Uses 'items_count' passed to BeginMultiSelect() + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Methods: selection storage - ImGuiSelectionBasicStorage() { Clear(); UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } - void Clear() { Storage.Data.resize(0); Size = 0; } - void Swap(ImGuiSelectionBasicStorage& r) { Storage.Data.swap(r.Storage.Data); } - bool Contains(ImGuiID id) const { return Storage.GetInt(id, 0) != 0; } - void SetItemSelected(ImGuiID id, bool v) { int* p_int = Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } } - ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } + ImGuiSelectionBasicStorage() { Clear(); UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } + void Clear() { Storage.Data.resize(0); Size = 0; } + void Swap(ImGuiSelectionBasicStorage& r) { Storage.Data.swap(r.Storage.Data); int lhs_size = Size; Size = r.Size; r.Size = lhs_size; } + bool Contains(ImGuiID id) const { return Storage.GetInt(id, 0) != 0; } + void SetItemSelected(ImGuiID id, bool v) { int* p_int = Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } } + ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } }; //----------------------------------------------------------------------------- From db4898cb913552af14b37099ab06b60eb17a8149 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 5 Jun 2024 19:00:13 +0200 Subject: [PATCH 113/132] MultiSelect: added ImGuiSelectionExternalStorage helper. Simplify bool demo. --- imgui.h | 16 +++++++++++++++- imgui_demo.cpp | 29 ++++++++--------------------- imgui_widgets.cpp | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/imgui.h b/imgui.h index c0c2d0b2ca84..050c8bb06323 100644 --- a/imgui.h +++ b/imgui.h @@ -44,7 +44,7 @@ Index of this file: // [SECTION] ImGuiIO // [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) // [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) -// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO, ImGuiSelectionRequest, ImGuiSelectionBasicStorage) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO, ImGuiSelectionRequest, ImGuiSelectionBasicStorage, ImGuiSelectionExternalStorage) // [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) // [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) // [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) @@ -180,6 +180,7 @@ struct ImGuiOnceUponAFrame; // Helper for running a block of code not mo struct ImGuiPayload; // User data payload for drag and drop operations struct ImGuiPlatformImeData; // Platform IME data for io.PlatformSetImeDataFn() function. struct ImGuiSelectionBasicStorage; // Optional helper to store multi-selection state + apply multi-selection requests. +struct ImGuiSelectionExternalStorage;//Optional helper to apply multi-selection requests to existing randomly accessible storage. struct ImGuiSelectionRequest; // A selection request (stored in ImGuiMultiSelectIO) struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) struct ImGuiStorage; // Helper for key->value storage (container sorted by key) @@ -2860,6 +2861,19 @@ struct ImGuiSelectionBasicStorage ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } }; +// Optional helper to apply multi-selection requests to existing randomly accessible storage. +// Convenient if you want to quickly wire multi-select API on e.g. an array of bool or items storing their own selection state. +struct ImGuiSelectionExternalStorage +{ + // Members + void (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; } + void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; + + // Methods + ImGuiSelectionExternalStorage() { memset(this, 0, sizeof(*this)); } + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Generic function, using AdapterSetItemSelected() +}; + //----------------------------------------------------------------------------- // [SECTION] Drawing API (ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) // Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index b3dc5ef54f8e..3cd2046a1c7d 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3212,41 +3212,28 @@ static void ShowDemoWindowMultiSelect() ImGui::BulletText("Shift+Keyboard to copy current value to other boxes."); // If you have an array of checkboxes, you may want to use NoAutoSelect + NoAutoClear and the ImGuiSelectionExternalStorage helper. - static bool values[20] = {}; + static bool items[20] = {}; static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_ClearOnEscape; ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); // Cannot use ImGuiMultiSelectFlags_BoxSelect1d as checkboxes are varying width. - struct Funcs - { - static void ApplyMultiSelectRequestsToBoolArray(ImGuiMultiSelectIO* ms_io, bool items[], int items_count) - { - for (ImGuiSelectionRequest& req : ms_io->Requests) - { - if (req.Type == ImGuiSelectionRequestType_SetAll) - for (int n = 0; n < items_count; n++) - items[n] = req.Selected; - else if (req.Type == ImGuiSelectionRequestType_SetRange) - for (int n = (int)req.RangeFirstItem; n <= (int)req.RangeLastItem; n++) - items[n] = req.Selected; - } - } - }; - if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY)) { - ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); - Funcs::ApplyMultiSelectRequestsToBoolArray(ms_io, values, IM_ARRAYSIZE(values)); //// By specs, it could be optional to apply requests from BeginMultiSelect() if not using a clipper. + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, -1, IM_ARRAYSIZE(items)); + ImGuiSelectionExternalStorage storage_wrapper; + storage_wrapper.UserData = (void*)items; + storage_wrapper.AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int n, bool selected) { bool* array = (bool*)self->UserData; array[n] = selected; }; + storage_wrapper.ApplyRequests(ms_io); for (int n = 0; n < 20; n++) { char label[32]; sprintf(label, "Item %d", n); ImGui::SetNextItemSelectionUserData(n); - ImGui::Checkbox(label, &values[n]); + ImGui::Checkbox(label, &items[n]); } ms_io = ImGui::EndMultiSelect(); - Funcs::ApplyMultiSelectRequestsToBoolArray(ms_io, values, IM_ARRAYSIZE(values)); + storage_wrapper.ApplyRequests(ms_io); } ImGui::EndChild(); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 1ebabe4d1931..c85b6ff9a090 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7854,6 +7854,22 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) } } +// Apply requests coming from BeginMultiSelect() and EndMultiSelect(). +// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage +// This makes no assumption about underlying storage. +void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) +{ + IM_ASSERT(AdapterSetItemSelected); + for (ImGuiSelectionRequest& req : ms_io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_SetAll) + for (int idx = 0; idx < ms_io->ItemsCount; idx++) + AdapterSetItemSelected(this, idx, req.Selected); + if (req.Type == ImGuiSelectionRequestType_SetRange) + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) + AdapterSetItemSelected(this, idx, req.Selected); + } +} //------------------------------------------------------------------------- // [SECTION] Widgets: ListBox From c3d7aa252b39a5a4a61c02b99dd5bc5e6b7fdf1f Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 6 Jun 2024 14:40:57 +0200 Subject: [PATCH 114/132] MultiSelect: comments, header tweaks., simplication (some of it on wiki). --- imgui.h | 49 +++++++++++++++++------------------------------ imgui_widgets.cpp | 27 +++++++++++++++++--------- 2 files changed, 36 insertions(+), 40 deletions(-) diff --git a/imgui.h b/imgui.h index 050c8bb06323..bfaa0182e0c3 100644 --- a/imgui.h +++ b/imgui.h @@ -2730,18 +2730,17 @@ 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. // Multi-selection system -// Also read: https://github.com/ocornut/imgui/wiki/Multi-Select +// Documentation at: https://github.com/ocornut/imgui/wiki/Multi-Select // - Refer to 'Demo->Widgets->Selection State & Multi-Select' for demos using this. // - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) // with support for clipper (skipping non-visible items), box-select and many other details. -// - TreeNode() and Selectable() are supported but custom widgets may use it as well. +// - TreeNode(), Selectable(), Checkbox() are supported but custom widgets may use it as well. // - In the spirit of Dear ImGui design, your code owns actual selection data. // This is designed to allow all kinds of selection storage you may use in your application e.g. set/map/hash. -// - The work involved to deal with multi-selection differs whether you want to only submit visible items and -// clip others, or submit all items regardless of their visibility. Clipping items is more efficient and will -// allow you to deal with large lists (1k~100k items). See "Usage flow" section below for details. -// If you are not sure, always start without clipping! You can work your way to the optimized version afterwards. -// TL;DR; +// About ImGuiSelectionBasicStorage: +// - This is an optional helper to store a selection state and apply selection requests. +// - It is used by our demos and provided as a convenience to quickly implement multi-selection. +// Usage: // - Identify submitted items with SetNextItemSelectionUserData(), most likely using an index into your current data-set. // - Store and maintain actual selection data using persistent object identifiers. // - Usage flow: @@ -2753,20 +2752,14 @@ struct ImColor // - (6) Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 2. // If you submit all items (no clipper), Step 2 and 3 are optional and will be handled by each item themselves. It is fine to always honor those steps. // About ImGuiSelectionUserData: -// - For each item is it submitted by your call to SetNextItemSelectionUserData(). -// - This can store an application-defined identifier (e.g. index or pointer). +// - This can store an application-defined identifier (e.g. index or pointer) submitted via SetNextItemSelectionUserData(). // - In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO. -// - Most applications will store an object INDEX, hence the chosen name and type. -// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end-points -// and you will need to iterate/interpolate between them to update your selection. -// - However it is perfectly possible to store a POINTER or another IDENTIFIER inside this value! +// - Most applications will store an object INDEX, hence the chosen name and type. Storing an index is natural, because +// SetRange requests will give you two end-points and you will need to iterate/interpolate between them to update your selection. +// - However it is perfectly possible to store a POINTER or another IDENTIFIER inside ImGuiSelectionUserData. // Our system never assume that you identify items by indices, it never attempts to interpolate between two values. // - As most users will want to store an index, for convenience and to reduce confusion we use ImS64 instead of void*, // being syntactically easier to downcast. Feel free to reinterpret_cast and store a pointer inside. -// - If you need to wrap this API for another language/framework, feel free to expose this as 'int' if simpler. -// About ImGuiSelectionBasicStorage: -// - This is an optional helper to store a selection state and apply selection requests. -// - It is used by our demos and provided as a convenience if you want to quickly implement multi-selection. // Flags for BeginMultiSelect() enum ImGuiMultiSelectFlags_ @@ -2774,7 +2767,7 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_None = 0, ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to select all. - ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+Click/Shift+Keyboard handling (useful for unordered 2D selection). + ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+selection mouse/keyboard support (useful for unordered 2D selection). ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes) ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing other items when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes) ImGuiMultiSelectFlags_BoxSelect = 1 << 5, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. @@ -2828,32 +2821,26 @@ struct ImGuiSelectionRequest // 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 replacement. // - B) Use your own external storage: e.g. std::set, std::vector, interval trees, etc. -// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Doing that, you can't have multiple views over -// your objects. Also, some features requires to provide selection _size_, which with this strategy requires additional work. +// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Cannot have multiple views over same objects. // In ImGuiSelectionBasicStorage we: // - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) -// - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index, -// so this helper can be used regardless of your object storage/types (it is analogous to using a virtual function): -// - in some cases we read an ID from some custom item data structure (similar to what you would do in your codebase) -// - in some cases we use Index as custom identifier (default implementation returns Index cast as Identifier): only OK for a never changing item list. +// - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index. // Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. // Large applications are likely to eventually want to get rid of this indirection layer and do their own thing. -// See https://github.com/ocornut/imgui/wiki/Multi-Select for minimum pseudo-code example using this helper. -// (In theory, for maximum abstraction, this class could contains AdapterIndexToUserData() and AdapterUserDataToIndex() functions as well, -// but because we always use indices in SetNextItemSelectionUserData() in the demo, we omit that indirection for clarity.) +// See https://github.com/ocornut/imgui/wiki/Multi-Select for details and pseudo-code using this helper. struct ImGuiSelectionBasicStorage { // Members ImGuiStorage Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set int Size; // Number of selected items (== number of 1 in the Storage), maintained by this helper. - ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; + ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. Uses 'items_count' passed to BeginMultiSelect() IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Methods: selection storage - ImGuiSelectionBasicStorage() { Clear(); 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 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; } @@ -2866,11 +2853,11 @@ struct ImGuiSelectionBasicStorage struct ImGuiSelectionExternalStorage { // Members - void (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; } void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; + void (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; } // Methods - ImGuiSelectionExternalStorage() { memset(this, 0, sizeof(*this)); } + ImGuiSelectionExternalStorage() { UserData = NULL; AdapterSetItemSelected = NULL; } IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Generic function, using AdapterSetItemSelected() }; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index c85b6ff9a090..be02fe1402a8 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -21,6 +21,7 @@ Index of this file: // [SECTION] Widgets: Typing-Select support // [SECTION] Widgets: Box-Select support // [SECTION] Widgets: Multi-Select support +// [SECTION] Widgets: Multi-Select helpers // [SECTION] Widgets: ListBox // [SECTION] Widgets: PlotLines, PlotHistogram // [SECTION] Widgets: Value helpers @@ -7285,7 +7286,6 @@ void ImGui::EndBoxSelect(const ImRect& scope_rect, bool enable_scroll) // - MultiSelectItemHeader() [Internal] // - MultiSelectItemFooter() [Internal] // - DebugNodeMultiSelectState() [Internal] -// - ImGuiSelectionBasicStorage //------------------------------------------------------------------------- static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) @@ -7814,6 +7814,13 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) #endif } +//------------------------------------------------------------------------- +// [SECTION] Widgets: Multi-Select helpers +//------------------------------------------------------------------------- +// - ImGuiSelectionBasicStorage +// - ImGuiSelectionExternalStorage +//------------------------------------------------------------------------- + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). // - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. // - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. @@ -7827,8 +7834,8 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) // The most simple implementation (using indices everywhere) would look like: // for (ImGuiSelectionRequest& req : ms_io->Requests) // { -// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { AddItem(n); } } -// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->Selected); } } +// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } } +// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } } // } void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) { @@ -7841,14 +7848,16 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) for (ImGuiSelectionRequest& req : ms_io->Requests) { if (req.Type == ImGuiSelectionRequestType_SetAll) - Clear(); - if (req.Type == ImGuiSelectionRequestType_SetAll && req.Selected) { - Storage.Data.reserve(ms_io->ItemsCount); - for (int idx = 0; idx < ms_io->ItemsCount; idx++) - SetItemSelected(GetStorageIdFromIndex(idx), true); + Clear(); + if (req.Selected) + { + Storage.Data.reserve(ms_io->ItemsCount); + for (int idx = 0; idx < ms_io->ItemsCount; idx++) + SetItemSelected(GetStorageIdFromIndex(idx), true); + } } - if (req.Type == ImGuiSelectionRequestType_SetRange) + else if (req.Type == ImGuiSelectionRequestType_SetRange) for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) SetItemSelected(GetStorageIdFromIndex(idx), req.Selected); } From e1fd25051e1c4d4fef53eca80412668505a6908f Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 11 Jun 2024 17:14:00 +0200 Subject: [PATCH 115/132] MultiSelect: ImGuiSelectionBasicStorage: added GetNextSelectedItem() to abstract selection storage from user. Amend Assets Browser demo to handle drag and drop correctly. --- imgui.h | 34 +++++++++++++++++++--------------- imgui_demo.cpp | 31 ++++++++++++++++++++++--------- imgui_widgets.cpp | 21 ++++++++++++++++++++- 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/imgui.h b/imgui.h index bfaa0182e0c3..93ecaf77b9f6 100644 --- a/imgui.h +++ b/imgui.h @@ -2817,35 +2817,39 @@ struct ImGuiSelectionRequest // Optional helper to store multi-selection state + apply multi-selection requests. // - Used by our demos and provided as a convenience to easily implement basic multi-selection. +// - Iterate selection with 'void* it = NULL; while (ImGuiId id = selection.GetNextSelectedItem(&it)) { ... }' +// Or you can check 'if (Contains(id)) { ... }' for each possible object if their number is not too high to iterate. // - USING THIS IS NOT MANDATORY. This is only a helper and not a required API. // To store a multi-selection, in your application you could: -// - A) Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement. -// - B) Use your own external storage: e.g. std::set, std::vector, interval trees, etc. -// - C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Cannot have multiple views over same objects. +// - Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement. +// - Use your own external storage: e.g. std::set, std::vector, interval trees, intrusively stored selection etc. // In ImGuiSelectionBasicStorage we: // - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) // - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index. +// - use decently optimized logic to allow queries and insertion of very large selection sets. +// - do not preserve selection order. // Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. // Large applications are likely to eventually want to get rid of this indirection layer and do their own thing. // See https://github.com/ocornut/imgui/wiki/Multi-Select for details and pseudo-code using this helper. struct ImGuiSelectionBasicStorage { // Members - ImGuiStorage Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set + ImGuiStorage _Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set. Prefer not accessing directly: iterate with GetNextSelectedItem(). int Size; // Number of selected items (== number of 1 in the Storage), maintained by this helper. void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; - // Methods: apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. Uses 'items_count' passed to BeginMultiSelect() - IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); + // Methods + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io);// Apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. It uses 'items_count' passed to BeginMultiSelect() + IMGUI_API ImGuiID GetNextSelectedItem(void** opaque_it); // Iterate selection with 'void* it = NULL; while (ImGuiId id = selection.GetNextSelectedItem(&it)) { ... }' + bool Contains(ImGuiID id) const { return _Storage.GetInt(id, 0) != 0; } // Query if an item id is in selection. + ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } // Convert index to item id based on provided adapter. - // Methods: selection storage - ImGuiSelectionBasicStorage() { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } - void Clear() { Storage.Data.resize(0); Size = 0; } - void Swap(ImGuiSelectionBasicStorage& r) { Storage.Data.swap(r.Storage.Data); int lhs_size = Size; Size = r.Size; r.Size = lhs_size; } - bool Contains(ImGuiID id) const { return Storage.GetInt(id, 0) != 0; } - void SetItemSelected(ImGuiID id, bool v) { int* p_int = Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } } - ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } + // [Internal, rarely called directly by end-user] + ImGuiSelectionBasicStorage() { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } + void Clear() { _Storage.Data.resize(0); Size = 0; } + void Swap(ImGuiSelectionBasicStorage& r) { _Storage.Data.swap(r._Storage.Data); int lhs_size = Size; Size = r.Size; r.Size = lhs_size; } + void SetItemSelected(ImGuiID id, bool v) { int* p_int = _Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } } }; // Optional helper to apply multi-selection requests to existing randomly accessible storage. @@ -2857,8 +2861,8 @@ struct ImGuiSelectionExternalStorage void (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; } // Methods - ImGuiSelectionExternalStorage() { UserData = NULL; AdapterSetItemSelected = NULL; } - IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Generic function, using AdapterSetItemSelected() + ImGuiSelectionExternalStorage() { UserData = NULL; AdapterSetItemSelected = NULL; } + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Generic function, using AdapterSetItemSelected() }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 3cd2046a1c7d..d5ff2972d64a 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3441,24 +3441,24 @@ static void ShowDemoWindowMultiSelect() // Drag and Drop if (use_drag_drop && ImGui::BeginDragDropSource()) { - // Consider payload to be full selection OR single unselected item. + // Create payload with full selection OR single unselected item. // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease) if (ImGui::GetDragDropPayload() == NULL) { ImVector payload_items; + void* it = NULL; if (!item_is_selected) payload_items.push_back(item_id); else - for (const ImGuiStoragePair& pair : selection.Storage.Data) - if (pair.val_i) - payload_items.push_back((int)pair.key); + while (int id = (int)selection.GetNextSelectedItem(&it)) + payload_items.push_back(id); ImGui::SetDragDropPayload("MULTISELECT_DEMO_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes()); } // Display payload content in tooltip const ImGuiPayload* payload = ImGui::GetDragDropPayload(); const int* payload_items = (int*)payload->Data; - const int payload_count = (int)payload->DataSize / (int)sizeof(payload_items[0]); + const int payload_count = (int)payload->DataSize / (int)sizeof(int); if (payload_count == 1) ImGui::Text("Object %05d: %s", payload_items[0], ExampleNames[payload_items[0] % IM_ARRAYSIZE(ExampleNames)]); else @@ -9896,13 +9896,26 @@ struct ExampleAssetsBrowser // Drag and drop if (ImGui::BeginDragDropSource()) { - // Consider payload to be full selection OR single unselected item + // Create payload with full selection OR single unselected item. // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease) - int payload_size = item_is_selected ? Selection.Size : 1; if (ImGui::GetDragDropPayload() == NULL) - ImGui::SetDragDropPayload("ASSETS_BROWSER_ITEMS", "Dummy", 5); // Dummy payload + { + ImVector 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(); } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index be02fe1402a8..bf9c9139a68b 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7821,6 +7821,23 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) // - ImGuiSelectionExternalStorage //------------------------------------------------------------------------- +// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user. +// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector instead of ImGuiStorage...) +ImGuiID ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it) +{ + ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it; + ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size; + if (it == NULL) + it = _Storage.Data.Data; + IM_ASSERT(it >= _Storage.Data.Data && it <= it_end); + if (it != it_end) + while (it->val_i == 0 && it < it_end) + it++; + const bool has_more = (it != it_end); + *opaque_it = has_more ? (void**)(it + 1) : (void**)(it); + return has_more ? it->key : 0; +} + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). // - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. // - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. @@ -7852,7 +7869,7 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) Clear(); if (req.Selected) { - Storage.Data.reserve(ms_io->ItemsCount); + _Storage.Data.reserve(ms_io->ItemsCount); for (int idx = 0; idx < ms_io->ItemsCount; idx++) SetItemSelected(GetStorageIdFromIndex(idx), true); } @@ -7863,6 +7880,8 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) } } +//------------------------------------------------------------------------- + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). // We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage // This makes no assumption about underlying storage. From e61612a6873788c9237c076c480b6182dc0e1547 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 11 Jun 2024 18:26:22 +0200 Subject: [PATCH 116/132] MultiSelect: ImGuiSelectionBasicStorage: rework to accept massive selections requests without flinching. Batch modification + storage only keeps selected items. --- imgui.h | 4 +-- imgui_widgets.cpp | 71 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/imgui.h b/imgui.h index 93ecaf77b9f6..a4162b88665e 100644 --- a/imgui.h +++ b/imgui.h @@ -2835,7 +2835,7 @@ struct ImGuiSelectionBasicStorage { // Members ImGuiStorage _Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set. 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, maintained by this helper. void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; @@ -2849,7 +2849,7 @@ struct ImGuiSelectionBasicStorage ImGuiSelectionBasicStorage() { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } void Clear() { _Storage.Data.resize(0); Size = 0; } void Swap(ImGuiSelectionBasicStorage& r) { _Storage.Data.swap(r._Storage.Data); int lhs_size = Size; Size = r.Size; r.Size = lhs_size; } - void SetItemSelected(ImGuiID id, bool v) { int* p_int = _Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } } + IMGUI_API void SetItemSelected(ImGuiID id, bool selected); }; // Optional helper to apply multi-selection requests to existing randomly accessible storage. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index bf9c9139a68b..82d90c1b2d95 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7830,14 +7830,65 @@ ImGuiID ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it) 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; } +// Basic function to update selection (for very large amounts of changes, see what ApplyRequests is doing) +void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected) +{ + ImGuiStoragePair* it = ImLowerBound(_Storage.Data.Data, _Storage.Data.Data + _Storage.Data.Size, id); + if (selected == (it != _Storage.Data.Data + _Storage.Data.Size) && (it->key == id)) + return; + if (selected) + _Storage.Data.insert(it, ImGuiStoragePair(id, 1)); + else + _Storage.Data.erase(it); + Size = _Storage.Data.Size; +} + +// Optimized for batch edits (with same value of 'selected') +static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends) +{ + ImGuiStorage* storage = &selection->_Storage; + ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id); + if (selected == (it != storage->Data.Data + size_before_amends) && (it->key == id)) + return; + if (selected) + storage->Data.push_back(ImGuiStoragePair(id, 1)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish() + else + it->val_i = 0; // Clear in-place, will be removed in SelectionMultiAmendsFinish() + selection->Size += selected ? +1 : -1; +} + +static void ImGuiSelectionBasicStorage_Compact(ImGuiSelectionBasicStorage* selection) +{ + ImGuiStorage* storage = &selection->_Storage; + ImGuiStoragePair* p_out = storage->Data.Data; + ImGuiStoragePair* p_end = storage->Data.Data + storage->Data.Size; + for (ImGuiStoragePair* p_in = p_out; p_in < p_end; p_in++) + if (p_in->val_i != 0) + { + if (p_out != p_in) + *p_out = *p_in; + p_out++; + } + storage->Data.Size = (int)(p_out - storage->Data.Data); +} + +static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends) +{ + ImGuiStorage* storage = &selection->_Storage; + if (selection->Size == size_before_amends) + return; + if (selected) + storage->BuildSortByKey(); // When done selecting: sort everything + else + ImGuiSelectionBasicStorage_Compact(selection); // When done unselecting: compact by removing all zero values (might be done lazily when iterating selection?) + IM_ASSERT(selection->Size == storage->Data.Size); +} + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). // - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. // - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. @@ -7862,6 +7913,10 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!"); IM_ASSERT(AdapterIndexToStorageId != NULL); + // This is optimized/specialized to cope nicely with very large selections (e.g. 1 million items) + // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish(). + // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass. + // - (A more optimal version wouldn't even use ImGuiStorage but directly a ImVector to reduce bandwidth, but this is a reasonable trade off to reuse code) for (ImGuiSelectionRequest& req : ms_io->Requests) { if (req.Type == ImGuiSelectionRequestType_SetAll) @@ -7870,13 +7925,19 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) if (req.Selected) { _Storage.Data.reserve(ms_io->ItemsCount); + const int size_before_amends = _Storage.Data.Size; for (int idx = 0; idx < ms_io->ItemsCount; idx++) - SetItemSelected(GetStorageIdFromIndex(idx), true); + ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends); + ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); } } else if (req.Type == ImGuiSelectionRequestType_SetRange) + { + const int size_before_amends = _Storage.Data.Size; for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) - SetItemSelected(GetStorageIdFromIndex(idx), req.Selected); + ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends); + ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); + } } } From 2af3b2ac81578b3e586e8de577910df495bc3e7c Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 11 Jun 2024 18:50:30 +0200 Subject: [PATCH 117/132] MultiSelect: ImGuiSelectionBasicStorage: simplify by removing compacting code (compacting may be opt-in?). GetNextSelectedItem() wrapper gives us more flexibility to work on this kind of stuff now. --- imgui.h | 4 ++-- imgui_widgets.cpp | 47 +++++++++-------------------------------------- 2 files changed, 11 insertions(+), 40 deletions(-) diff --git a/imgui.h b/imgui.h index a4162b88665e..93ecaf77b9f6 100644 --- a/imgui.h +++ b/imgui.h @@ -2835,7 +2835,7 @@ struct ImGuiSelectionBasicStorage { // Members ImGuiStorage _Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set. Prefer not accessing directly: iterate with GetNextSelectedItem(). - int Size; // Number of selected items, 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; ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; @@ -2849,7 +2849,7 @@ struct ImGuiSelectionBasicStorage ImGuiSelectionBasicStorage() { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } void Clear() { _Storage.Data.resize(0); Size = 0; } void Swap(ImGuiSelectionBasicStorage& r) { _Storage.Data.swap(r._Storage.Data); int lhs_size = Size; Size = r.Size; r.Size = lhs_size; } - IMGUI_API void SetItemSelected(ImGuiID id, bool selected); + void SetItemSelected(ImGuiID id, bool v) { int* p_int = _Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } } }; // Optional helper to apply multi-selection requests to existing randomly accessible storage. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 82d90c1b2d95..bcfa921945f5 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7830,63 +7830,34 @@ ImGuiID ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it) 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; } -// Basic function to update selection (for very large amounts of changes, see what ApplyRequests is doing) -void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected) -{ - ImGuiStoragePair* it = ImLowerBound(_Storage.Data.Data, _Storage.Data.Data + _Storage.Data.Size, id); - if (selected == (it != _Storage.Data.Data + _Storage.Data.Size) && (it->key == id)) - return; - if (selected) - _Storage.Data.insert(it, ImGuiStoragePair(id, 1)); - else - _Storage.Data.erase(it); - Size = _Storage.Data.Size; -} - // Optimized for batch edits (with same value of 'selected') static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends) { ImGuiStorage* storage = &selection->_Storage; ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id); - if (selected == (it != storage->Data.Data + size_before_amends) && (it->key == id)) + const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id); + if (selected == is_contained && it->val_i == 1) return; - if (selected) + if (selected && !is_contained) storage->Data.push_back(ImGuiStoragePair(id, 1)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish() - else - it->val_i = 0; // Clear in-place, will be removed in SelectionMultiAmendsFinish() + else if (is_contained) + it->val_i = selected ? 1 : 0; // Modify in-place. selection->Size += selected ? +1 : -1; } -static void ImGuiSelectionBasicStorage_Compact(ImGuiSelectionBasicStorage* selection) -{ - ImGuiStorage* storage = &selection->_Storage; - ImGuiStoragePair* p_out = storage->Data.Data; - ImGuiStoragePair* p_end = storage->Data.Data + storage->Data.Size; - for (ImGuiStoragePair* p_in = p_out; p_in < p_end; p_in++) - if (p_in->val_i != 0) - { - if (p_out != p_in) - *p_out = *p_in; - p_out++; - } - storage->Data.Size = (int)(p_out - storage->Data.Data); -} - static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends) { ImGuiStorage* storage = &selection->_Storage; - if (selection->Size == size_before_amends) - return; - if (selected) + if (selected && selection->Size != size_before_amends) storage->BuildSortByKey(); // When done selecting: sort everything - else - ImGuiSelectionBasicStorage_Compact(selection); // When done unselecting: compact by removing all zero values (might be done lazily when iterating selection?) - IM_ASSERT(selection->Size == storage->Data.Size); } // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). From c07864f64ab1c8db0682c6b752765d7d5db959e6 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 11 Jun 2024 19:45:21 +0200 Subject: [PATCH 118/132] MultiSelect: ImGuiSelectionBasicStorage: move function bodies to cpp file. + make ImGuiStorage::BuildSortByKey() less affected by msvc debug mode. --- imgui.cpp | 2 ++ imgui.h | 22 ++++++++++------------ imgui_widgets.cpp | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index ee9462a9a52b..ffcdce5f6375 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -2567,10 +2567,12 @@ static int IMGUI_CDECL PairComparerByID(const void* lhs, const void* rhs) } // For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. +IM_MSVC_RUNTIME_CHECKS_OFF void ImGuiStorage::BuildSortByKey() { ImQsort(Data.Data, (size_t)Data.Size, sizeof(ImGuiStoragePair), PairComparerByID); } +IM_MSVC_RUNTIME_CHECKS_RESTORE int ImGuiStorage::GetInt(ImGuiID key, int default_val) const { diff --git a/imgui.h b/imgui.h index 93ecaf77b9f6..6e86b85c0ea8 100644 --- a/imgui.h +++ b/imgui.h @@ -2840,16 +2840,14 @@ struct ImGuiSelectionBasicStorage ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; // Methods - IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io);// Apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. It uses 'items_count' passed to BeginMultiSelect() - IMGUI_API ImGuiID GetNextSelectedItem(void** opaque_it); // Iterate selection with 'void* it = NULL; while (ImGuiId id = selection.GetNextSelectedItem(&it)) { ... }' - bool Contains(ImGuiID id) const { return _Storage.GetInt(id, 0) != 0; } // Query if an item id is in selection. - ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } // Convert index to item id based on provided adapter. - - // [Internal, rarely called directly by end-user] - ImGuiSelectionBasicStorage() { Size = 0; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; } - void Clear() { _Storage.Data.resize(0); Size = 0; } - void Swap(ImGuiSelectionBasicStorage& r) { _Storage.Data.swap(r._Storage.Data); int lhs_size = Size; Size = r.Size; r.Size = lhs_size; } - void SetItemSelected(ImGuiID id, bool v) { int* p_int = _Storage.GetIntRef(id, 0); if (v && *p_int == 0) { *p_int = 1; Size++; } else if (!v && *p_int != 0) { *p_int = 0; Size--; } } + ImGuiSelectionBasicStorage(); + 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 bool Contains(ImGuiID id) const; // Query if an item id is in selection. + IMGUI_API void Clear(); // Clear selection + IMGUI_API void Swap(ImGuiSelectionBasicStorage& r); // Swap two selections + IMGUI_API void SetItemSelected(ImGuiID id, bool selected); // Add/remove an item from selection (generally done by ApplyRequests() function) + IMGUI_API ImGuiID GetNextSelectedItem(void** opaque_it); // Iterate selection with 'void* it = NULL; while (ImGuiId id = selection.GetNextSelectedItem(&it)) { ... }' + inline ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } // Convert index to item id based on provided adapter. }; // Optional helper to apply multi-selection requests to existing randomly accessible storage. @@ -2861,8 +2859,8 @@ struct ImGuiSelectionExternalStorage void (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; } // Methods - ImGuiSelectionExternalStorage() { UserData = NULL; AdapterSetItemSelected = NULL; } - IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Generic function, using AdapterSetItemSelected() + IMGUI_API ImGuiSelectionExternalStorage(); + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Apply selection requests by using AdapterSetItemSelected() calls }; //----------------------------------------------------------------------------- diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index bcfa921945f5..54b1b1f6896d 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7821,6 +7821,30 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) // - ImGuiSelectionExternalStorage //------------------------------------------------------------------------- +ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage() +{ + Size = 0; + UserData = NULL; + AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; +} + +void ImGuiSelectionBasicStorage::Clear() +{ + Size = 0; + _Storage.Data.resize(0); +} + +void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r) +{ + ImSwap(Size, r.Size); + _Storage.Data.swap(r._Storage.Data); +} + +bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const +{ + return _Storage.GetInt(id, 0) != 0; +} + // 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 instead of ImGuiStorage...) ImGuiID ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it) @@ -7838,6 +7862,13 @@ ImGuiID ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it) return has_more ? it->key : 0; } +void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected) +{ + int* p_int = _Storage.GetIntRef(id, 0); + if (selected && *p_int == 0) { *p_int = 1; Size++; } + else if (!selected && *p_int != 0) { *p_int = 0; Size--; } +} + // Optimized for batch edits (with same value of 'selected') static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends) { @@ -7914,6 +7945,12 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) //------------------------------------------------------------------------- +ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage() +{ + UserData = NULL; + AdapterSetItemSelected = NULL; +} + // Apply requests coming from BeginMultiSelect() and EndMultiSelect(). // We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage // This makes no assumption about underlying storage. From f472f17054030eab44df2ad6c7aba28c47dcf1fc Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 11 Jun 2024 20:56:41 +0200 Subject: [PATCH 119/132] Demo: Assets Browser: added a way to disable sorting and hide sorting options. This is mostly designed to showcase that on very large sets (e.g. 1 million) most of the time is likely spent on sorting. --- imgui_demo.cpp | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/imgui_demo.cpp b/imgui_demo.cpp index d5ff2972d64a..ec5037297cf2 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -9676,6 +9676,7 @@ struct ExampleAssetsBrowser { // Options bool ShowTypeOverlay = true; + bool AllowSorting = true; bool AllowDragUnselected = false; bool AllowBoxSelect = true; float IconSize = 32.0f; @@ -9778,6 +9779,7 @@ struct ExampleAssetsBrowser ImGui::SeparatorText("Contents"); ImGui::Checkbox("Show Type Overlay", &ShowTypeOverlay); + ImGui::Checkbox("Allow Sorting", &AllowSorting); ImGui::SeparatorText("Selection Behavior"); ImGui::Checkbox("Allow dragging unselected item", &AllowDragUnselected); @@ -9796,22 +9798,25 @@ struct ExampleAssetsBrowser } // Show a table with ONLY one header row to showcase the idea/possibility of using this to provide a sorting UI - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - ImGuiTableFlags table_flags_for_sort_specs = ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Borders; - if (ImGui::BeginTable("for_sort_specs_only", 2, table_flags_for_sort_specs, ImVec2(0.0f, ImGui::GetFrameHeight()))) + if (AllowSorting) { - ImGui::TableSetupColumn("Index"); - ImGui::TableSetupColumn("Type"); - ImGui::TableHeadersRow(); - if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs()) - if (sort_specs->SpecsDirty || RequestSort) - { - ExampleAsset::SortWithSortSpecs(sort_specs, Items.Data, Items.Size); - sort_specs->SpecsDirty = RequestSort = false; - } - ImGui::EndTable(); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGuiTableFlags table_flags_for_sort_specs = ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Borders; + if (ImGui::BeginTable("for_sort_specs_only", 2, table_flags_for_sort_specs, ImVec2(0.0f, ImGui::GetFrameHeight()))) + { + ImGui::TableSetupColumn("Index"); + ImGui::TableSetupColumn("Type"); + ImGui::TableHeadersRow(); + if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs()) + if (sort_specs->SpecsDirty || RequestSort) + { + ExampleAsset::SortWithSortSpecs(sort_specs, Items.Data, Items.Size); + sort_specs->SpecsDirty = RequestSort = false; + } + ImGui::EndTable(); + } + ImGui::PopStyleVar(); } - ImGui::PopStyleVar(); ImGuiIO& io = ImGui::GetIO(); ImGui::SetNextWindowContentSize(ImVec2(0.0f, LayoutOuterPadding + LayoutLineCount * (LayoutItemSize.x + LayoutItemSpacing))); From 3ac367ff41b33eb7e7e6013f0132441b5cbfed35 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 26 Jun 2024 17:00:52 +0200 Subject: [PATCH 120/132] MultiSelect: ImGuiSelectionBasicStorage: (breaking) rework GetNextSelectedItem() api to avoid ambiguity/failure when user uses a zero id. --- imgui.h | 6 +++--- imgui_demo.cpp | 8 +++++--- imgui_widgets.cpp | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/imgui.h b/imgui.h index 6e86b85c0ea8..a66e8d84cb89 100644 --- a/imgui.h +++ b/imgui.h @@ -2817,7 +2817,7 @@ struct ImGuiSelectionRequest // Optional helper to store multi-selection state + apply multi-selection requests. // - Used by our demos and provided as a convenience to easily implement basic multi-selection. -// - Iterate selection with 'void* it = NULL; while (ImGuiId id = selection.GetNextSelectedItem(&it)) { ... }' +// - Iterate selection with 'void* it = NULL; ImGuiID id; while (selection.GetNextSelectedItem(&it, &id)) { ... }' // Or you can check 'if (Contains(id)) { ... }' for each possible object if their number is not too high to iterate. // - USING THIS IS NOT MANDATORY. This is only a helper and not a required API. // To store a multi-selection, in your application you could: @@ -2834,10 +2834,10 @@ struct ImGuiSelectionRequest struct ImGuiSelectionBasicStorage { // Members - ImGuiStorage _Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set. Prefer not accessing directly: iterate with GetNextSelectedItem(). int Size; // Number of selected items (== number of 1 in the Storage), maintained by this helper. void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; + ImGuiStorage _Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set. Prefer not accessing directly: iterate with GetNextSelectedItem(). // Methods ImGuiSelectionBasicStorage(); @@ -2846,7 +2846,7 @@ struct ImGuiSelectionBasicStorage IMGUI_API void Clear(); // Clear selection IMGUI_API void Swap(ImGuiSelectionBasicStorage& r); // Swap two selections IMGUI_API void SetItemSelected(ImGuiID id, bool selected); // Add/remove an item from selection (generally done by ApplyRequests() function) - IMGUI_API ImGuiID GetNextSelectedItem(void** opaque_it); // Iterate selection with 'void* it = NULL; while (ImGuiId id = selection.GetNextSelectedItem(&it)) { ... }' + IMGUI_API bool GetNextSelectedItem(void** opaque_it, ImGuiID* out_id); // Iterate selection with 'void* it = NULL; ImGuiId id; while (selection.GetNextSelectedItem(&it, &id)) { ... }' inline ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } // Convert index to item id based on provided adapter. }; diff --git a/imgui_demo.cpp b/imgui_demo.cpp index ec5037297cf2..690c79fda452 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3447,11 +3447,12 @@ static void ShowDemoWindowMultiSelect() { ImVector payload_items; void* it = NULL; + ImGuiID id = 0; if (!item_is_selected) payload_items.push_back(item_id); else - while (int id = (int)selection.GetNextSelectedItem(&it)) - payload_items.push_back(id); + while (selection.GetNextSelectedItem(&it, &id)) + payload_items.push_back((int)id); ImGui::SetDragDropPayload("MULTISELECT_DEMO_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes()); } @@ -9907,10 +9908,11 @@ struct ExampleAssetsBrowser { ImVector payload_items; void* it = NULL; + ImGuiID id = 0; if (!item_is_selected) payload_items.push_back(item_data->ID); else - while (ImGuiID id = Selection.GetNextSelectedItem(&it)) + while (Selection.GetNextSelectedItem(&it, &id)) payload_items.push_back(id); ImGui::SetDragDropPayload("ASSETS_BROWSER_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes()); } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 54b1b1f6896d..fccc15d141ff 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7847,7 +7847,7 @@ bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const // 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 instead of ImGuiStorage...) -ImGuiID ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it) +bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id) { ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it; ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size; @@ -7859,7 +7859,8 @@ ImGuiID ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it) it++; const bool has_more = (it != it_end); *opaque_it = has_more ? (void**)(it + 1) : (void**)(it); - return has_more ? it->key : 0; + *out_id = has_more ? it->key : 0; + return has_more; } void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected) From df664329cb57cac10e82f7b1970ca3bf1b59d907 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 26 Jun 2024 18:29:30 +0200 Subject: [PATCH 121/132] MultiSelect: provide RangeDirection to allow selection handler to handler backward shift+click. --- imgui.h | 1 + imgui_widgets.cpp | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/imgui.h b/imgui.h index a66e8d84cb89..6c1aac5705ff 100644 --- a/imgui.h +++ b/imgui.h @@ -2811,6 +2811,7 @@ struct ImGuiSelectionRequest //------------------------------------------// BeginMultiSelect / EndMultiSelect ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r // Request type. You'll most often receive 1 Clear + 1 SetRange with a single-item range. bool Selected; // ms:w, app:r / ms:w, app:r // Parameter for SetAll/SetRange requests (true = select, false = unselect) + ImS8 RangeDirection; // / ms:w app:r // Parameter for SetRange request: +1 when RangeFirstItem comes before RangeLastItem, -1 otherwise. Useful if you want to preserve selection order on a backward Shift+Click. ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom). ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top). Inclusive! }; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index fccc15d141ff..80b1c06fa4ae 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7294,7 +7294,7 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe for (const ImGuiSelectionRequest& req : io->Requests) { if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear"); - if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.Selected); + if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d (dir %d)\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.Selected, req.RangeDirection); } } @@ -7401,7 +7401,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel if (request_clear || request_select_all) { - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, request_select_all, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, request_select_all, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; if (!request_select_all) storage->LastSelectionSize = 0; ms->IO.Requests.push_back(req); @@ -7475,7 +7475,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; ms->IO.Requests.resize(0); ms->IO.Requests.push_back(req); } @@ -7665,7 +7665,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) else { selected = !selected; - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, item_data, item_data }; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, +1, item_data, item_data }; ImGuiSelectionRequest* prev_req = (ms->IO.Requests.Size > 0) ? &ms->IO.Requests.Data[ms->IO.Requests.Size - 1] : NULL; if (prev_req && prev_req->Type == ImGuiSelectionRequestType_SetRange && prev_req->RangeLastItem == ms->BoxSelectLastitem && prev_req->Selected == selected) prev_req->RangeLastItem = item_data; // Merge span into same request @@ -7733,7 +7733,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. if (request_clear) { - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; ms->IO.Requests.resize(0); ms->IO.Requests.push_back(req); } @@ -7775,7 +7775,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) range_direction = +1; } ImGuiSelectionUserData range_dst_item = item_data; - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, range_selected, (range_direction > 0) ? storage->RangeSrcItem : range_dst_item, (range_direction > 0) ? range_dst_item : storage->RangeSrcItem }; + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, range_selected, (ImS8)range_direction, (range_direction > 0) ? storage->RangeSrcItem : range_dst_item, (range_direction > 0) ? range_dst_item : storage->RangeSrcItem }; ms->IO.Requests.push_back(req); } @@ -7876,7 +7876,7 @@ static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicS ImGuiStorage* storage = &selection->_Storage; ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id); const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id); - if (selected == is_contained && it->val_i == 1) + if (selected == is_contained && it->val_i != 0) return; if (selected && !is_contained) storage->Data.push_back(ImGuiStoragePair(id, 1)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish() From c52346850d7766727b6ba5c55ae0583672033420 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 26 Jun 2024 18:41:21 +0200 Subject: [PATCH 122/132] MultiSelect: ImGuiSelectionBasicStorage: added PreserveOrder, maintain implicit order data in storage. Little tested but provided for completeness. --- imgui.h | 8 +++++--- imgui_widgets.cpp | 34 ++++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/imgui.h b/imgui.h index 6c1aac5705ff..d20c0dc696f6 100644 --- a/imgui.h +++ b/imgui.h @@ -2835,9 +2835,11 @@ struct ImGuiSelectionRequest struct ImGuiSelectionBasicStorage { // Members - int Size; // Number of selected items (== number of 1 in the Storage), maintained by this helper. - void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; - ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; + int Size; // // Number of selected items, maintained by this helper. + bool PreserveOrder; // = false // GetNextSelectedItem() will return ordered selection (currently implemented by two additional sorts of selection. Could be improved) + void* UserData; // = NULL // 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; }; + int _SelectionOrder;// [Internal] Increasing counter to store selection order ImGuiStorage _Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set. Prefer not accessing directly: iterate with GetNextSelectedItem(). // Methods diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 80b1c06fa4ae..d842d5d8fa67 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7824,19 +7824,23 @@ void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage() { Size = 0; + PreserveOrder = false; UserData = NULL; AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; + _SelectionOrder = 1; // Always >0 } void ImGuiSelectionBasicStorage::Clear() { Size = 0; + _SelectionOrder = 1; // Always >0 _Storage.Data.resize(0); } void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r) { ImSwap(Size, r.Size); + ImSwap(_SelectionOrder, r._SelectionOrder); _Storage.Data.swap(r._Storage.Data); } @@ -7845,12 +7849,21 @@ bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const return _Storage.GetInt(id, 0) != 0; } +static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs) +{ + int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i; + int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i; + return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0); +} + // 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 instead of ImGuiStorage...) bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id) { ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it; ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size; + if (PreserveOrder && it == NULL && it_end != NULL) + ImQsort(_Storage.Data.Data, (size_t)_Storage.Data.Size, sizeof(ImGuiStoragePair), PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt() if (it == NULL) it = _Storage.Data.Data; IM_ASSERT(it >= _Storage.Data.Data && it <= it_end); @@ -7860,18 +7873,20 @@ bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* const bool has_more = (it != it_end); *opaque_it = has_more ? (void**)(it + 1) : (void**)(it); *out_id = has_more ? it->key : 0; + if (PreserveOrder && !has_more) + _Storage.BuildSortByKey(); return has_more; } void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected) { int* p_int = _Storage.GetIntRef(id, 0); - if (selected && *p_int == 0) { *p_int = 1; Size++; } + if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; } else if (!selected && *p_int != 0) { *p_int = 0; Size--; } } // Optimized for batch edits (with same value of 'selected') -static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends) +static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order) { ImGuiStorage* storage = &selection->_Storage; ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id); @@ -7879,9 +7894,9 @@ static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicS if (selected == is_contained && it->val_i != 0) return; if (selected && !is_contained) - storage->Data.push_back(ImGuiStoragePair(id, 1)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish() + storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish() else if (is_contained) - it->val_i = selected ? 1 : 0; // Modify in-place. + it->val_i = selected ? selection_order : 0; // Modify in-place. selection->Size += selected ? +1 : -1; } @@ -7929,16 +7944,19 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) { _Storage.Data.reserve(ms_io->ItemsCount); const int size_before_amends = _Storage.Data.Size; - for (int idx = 0; idx < ms_io->ItemsCount; idx++) - ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends); + for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++) + ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, _SelectionOrder); ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); } } else if (req.Type == ImGuiSelectionRequestType_SetRange) { + // Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1 const int size_before_amends = _Storage.Data.Size; - for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) - ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends); + int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? (int)req.RangeLastItem - (int)req.RangeFirstItem : 0); + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection) + ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order); + _SelectionOrder += (int)req.RangeLastItem - (int)req.RangeFirstItem + 1; ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); } } From a8a1f29512667545da4ba4700f2e86070d54f477 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 26 Jun 2024 20:26:06 +0200 Subject: [PATCH 123/132] MultiSelect: (breaking) renamed ImGuiMultiSelectFlags_BoxSelect -> ImGuiMultiSelectFlags_BoxSelect2d. Which include not assuming one flag imply the other. Amend 2024/05/31 commit. --- imgui.h | 4 ++-- imgui_demo.cpp | 9 ++++----- imgui_widgets.cpp | 16 ++++++++-------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/imgui.h b/imgui.h index d20c0dc696f6..08464802a3d1 100644 --- a/imgui.h +++ b/imgui.h @@ -2770,8 +2770,8 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+selection mouse/keyboard support (useful for unordered 2D selection). ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes) ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing other items when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes) - ImGuiMultiSelectFlags_BoxSelect = 1 << 5, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_BoxSelect1d = 1 << 6, // Enable box-selection with same width and same x pos items (e.g. only full row Selectable()). Small optimization. + ImGuiMultiSelectFlags_BoxSelect1d = 1 << 5, // Enable box-selection with same width and same x pos items (e.g. only full row Selectable()). Small optimization. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 6, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 7, // Disable scrolling when box-selecting near edges of scope. ImGuiMultiSelectFlags_ClearOnEscape = 1 << 8, // Clear selection when pressing Escape while scope is focused. ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 9, // Clear selection when clicking on empty location within scope. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 690c79fda452..007a6449094f 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3216,7 +3216,7 @@ static void ShowDemoWindowMultiSelect() static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_ClearOnEscape; ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); // Cannot use ImGuiMultiSelectFlags_BoxSelect1d as checkboxes are varying width. + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect2d", &flags, ImGuiMultiSelectFlags_BoxSelect2d); // Cannot use ImGuiMultiSelectFlags_BoxSelect1d as checkboxes are varying width. if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY)) { @@ -3327,8 +3327,8 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoRangeSelect", &flags, ImGuiMultiSelectFlags_NoRangeSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); - ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect1d", &flags, ImGuiMultiSelectFlags_BoxSelect1d); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect2d", &flags, ImGuiMultiSelectFlags_BoxSelect2d); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); @@ -9838,7 +9838,7 @@ struct ExampleAssetsBrowser if (AllowDragUnselected) ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. if (AllowBoxSelect) - ms_flags |= ImGuiMultiSelectFlags_BoxSelect; // Enable box-select in 2D mode. + ms_flags |= ImGuiMultiSelectFlags_BoxSelect2d; // Enable box-select in 2D mode. ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags, Selection.Size, Items.Size); // Use custom selection adapter: store ID in selection (recommended) @@ -9927,8 +9927,7 @@ struct ExampleAssetsBrowser } // Render icon (a real app would likely display an image/thumbnail here) - // Because we use ImGuiMultiSelectFlags_BoxSelect (without ImGuiMultiSelectFlags_BoxSelect1d flag), - // clipping vertical range may occasionally be larger so we coarse-clip our rendering. + // Because we use ImGuiMultiSelectFlags_BoxSelect2d, clipping vertical may occasionally be larger, so we coarse-clip our rendering as well. if (item_is_visible) { ImVec2 box_min(pos.x - 1, pos.y - 1); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index d842d5d8fa67..25b9a8e1f467 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7236,7 +7236,7 @@ bool ImGui::BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMult // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) // Storing an extra rect used by widgets supporting box-select. - if ((ms_flags & ImGuiMultiSelectFlags_BoxSelect) && !(ms_flags & ImGuiMultiSelectFlags_BoxSelect1d)) + if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) { bs->UnclipMode = true; @@ -7319,9 +7319,9 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) flags |= ImGuiMultiSelectFlags_ScopeWindow; if (flags & ImGuiMultiSelectFlags_SingleSelect) - flags &= ~(ImGuiMultiSelectFlags_BoxSelect | ImGuiMultiSelectFlags_BoxSelect1d); - if (flags & ImGuiMultiSelectFlags_BoxSelect1d) - flags |= ImGuiMultiSelectFlags_BoxSelect; + flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d); + if (flags & ImGuiMultiSelectFlags_BoxSelect2d) + flags &= ~ImGuiMultiSelectFlags_BoxSelect1d; // FIXME: BeginFocusScope() const ImGuiID id = window->IDStack.back(); @@ -7391,7 +7391,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel // Box-select handling: update active state. ImGuiBoxSelectState* bs = &g.BoxSelectState; - if (flags & ImGuiMultiSelectFlags_BoxSelect) + if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) { ms->BoxSelectId = GetID("##BoxSelect"); ms->BoxSelectLastitem = ImGuiSelectionUserData_Invalid; @@ -7442,7 +7442,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() storage->NavIdSelected = -1; } - if ((ms->Flags & ImGuiMultiSelectFlags_BoxSelect) && GetBoxSelectState(ms->BoxSelectId)) + if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId)) { bool enable_scroll = (ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0; EndBoxSelect(scope_rect, enable_scroll); @@ -7460,7 +7460,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() scope_hovered &= scope_rect.Contains(g.IO.MousePos); if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) { - if (ms->Flags & ImGuiMultiSelectFlags_BoxSelect) + if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) { if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1) { @@ -7701,7 +7701,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) { // Box-select ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; - if (flags & ImGuiMultiSelectFlags_BoxSelect) + if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) BoxSelectPreStartDrag(ms->BoxSelectId, item_data); From 529c73ba2182da1bff839cea798efe93673a368c Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 27 Jun 2024 19:13:19 +0200 Subject: [PATCH 124/132] MultiSelect: Shift+Tab doesn't enable Shift select on landing item. --- imgui_widgets.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 25b9a8e1f467..910d8dacf1be 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7336,7 +7336,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main; // 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; + ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods; if (flags & ImGuiMultiSelectFlags_NoRangeSelect) ms->KeyMods &= ~ImGuiMod_Shift; From 3f34c83bc6e822769ac61cbd399dd1b39e750fcc Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 28 Jun 2024 11:55:22 +0200 Subject: [PATCH 125/132] MultiSelect: added ImGuiMultiSelectFlags_NoAutoClearOnReselect + tweak flags comments. (#7424) --- imgui.h | 21 +++++++++++---------- imgui_demo.cpp | 1 + imgui_widgets.cpp | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/imgui.h b/imgui.h index 08464802a3d1..5692817290c6 100644 --- a/imgui.h +++ b/imgui.h @@ -2769,16 +2769,17 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to select all. ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+selection mouse/keyboard support (useful for unordered 2D selection). ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes) - ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing other items when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes) - ImGuiMultiSelectFlags_BoxSelect1d = 1 << 5, // Enable box-selection with same width and same x pos items (e.g. only full row Selectable()). Small optimization. - ImGuiMultiSelectFlags_BoxSelect2d = 1 << 6, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 7, // Disable scrolling when box-selecting near edges of scope. - ImGuiMultiSelectFlags_ClearOnEscape = 1 << 8, // Clear selection when pressing Escape while scope is focused. - ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 9, // Clear selection when clicking on empty location within scope. - ImGuiMultiSelectFlags_ScopeWindow = 1 << 10, // Use if BeginMultiSelect() covers a whole window (Default): Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). - ImGuiMultiSelectFlags_ScopeRect = 1 << 11, // Use if multiple BeginMultiSelect() are used in the same host window: Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. - ImGuiMultiSelectFlags_SelectOnClick = 1 << 12, // Apply selection on mouse down when clicking on unselected item. (Default) - ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 13, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing selection when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes) + ImGuiMultiSelectFlags_NoAutoClearOnReselect = 1 << 5, // Disable clearing selection when clicking/selecting an already selected item + ImGuiMultiSelectFlags_BoxSelect1d = 1 << 6, // Enable box-selection with same width and same x pos items (e.g. only full row Selectable()). Small optimization. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 7, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 8, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 9, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 10, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 11, // Scope for _BoxSelect and _ClearOnClickVoid is whole window (Default). Use if BeginMultiSelect() covers a whole window or used a single time in same window. + ImGuiMultiSelectFlags_ScopeRect = 1 << 12, // Scope for _BoxSelect and _ClearOnClickVoid is rectangle encompassing BeginMultiSelect()/EndMultiSelect(). Use if BeginMultiSelect() is called multiple times in same window. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 13, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 14, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. }; // Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 007a6449094f..ebfb5ddf59bb 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3327,6 +3327,7 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoRangeSelect", &flags, ImGuiMultiSelectFlags_NoRangeSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClearOnReselect", &flags, ImGuiMultiSelectFlags_NoAutoClearOnReselect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect1d", &flags, ImGuiMultiSelectFlags_BoxSelect1d); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect2d", &flags, ImGuiMultiSelectFlags_BoxSelect2d); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 910d8dacf1be..5b2e400d1eb8 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7728,7 +7728,7 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) if (is_singleselect) request_clear = true; else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) - request_clear = true; + request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true; else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. if (request_clear) From d411c9054ad93e8e5b17cc3f451a94483b897f16 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 28 Jun 2024 18:36:53 +0200 Subject: [PATCH 126/132] MultiSelect: minor tidying up. Checkbox() was reworked in master effectively fixing render clipping when culled by BoxSelect2d's UnclipMode. --- imgui.h | 1 + imgui_internal.h | 2 +- imgui_widgets.cpp | 12 +++++------- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/imgui.h b/imgui.h index 5692817290c6..b086caa2744f 100644 --- a/imgui.h +++ b/imgui.h @@ -2780,6 +2780,7 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_ScopeRect = 1 << 12, // Scope for _BoxSelect and _ClearOnClickVoid is rectangle encompassing BeginMultiSelect()/EndMultiSelect(). Use if BeginMultiSelect() is called multiple times in same window. ImGuiMultiSelectFlags_SelectOnClick = 1 << 13, // Apply selection on mouse down when clicking on unselected item. (Default) ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 14, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + //ImGuiMultiSelectFlags_RangeSelect2d = 1 << 15, // Shift+Selection uses 2d geometry instead of linear sequence, so possible to use Shift+up/down to select vertically in grid. Analogous to what BoxSelect does. }; // Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). diff --git a/imgui_internal.h b/imgui_internal.h index 908c8f426fe1..ae3375e28b07 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -3406,7 +3406,7 @@ namespace ImGui // Box-Select API IMGUI_API bool BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags); - IMGUI_API void EndBoxSelect(const ImRect& scope_rect, bool enable_scroll); + IMGUI_API void EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags); // Multi-Select API IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 5b2e400d1eb8..135273597800 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -1136,8 +1136,9 @@ bool ImGui::Checkbox(const char* label, bool* v) const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f)); ItemSize(total_bb, style.FramePadding.y); const bool is_visible = ItemAdd(total_bb, id); + const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0; if (!is_visible) - if (!g.BoxSelectState.UnclipMode || (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) == 0 || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support + if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support { IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); return false; @@ -1145,7 +1146,6 @@ bool ImGui::Checkbox(const char* label, bool* v) // Range-Selection/Multi-selection support (header) bool checked = *v; - const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0; if (is_multi_select) MultiSelectItemHeader(id, &checked, NULL); @@ -7250,7 +7250,7 @@ bool ImGui::BeginBoxSelect(ImGuiWindow* window, ImGuiID box_select_id, ImGuiMult return true; } -void ImGui::EndBoxSelect(const ImRect& scope_rect, bool enable_scroll) +void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; @@ -7266,6 +7266,7 @@ void ImGui::EndBoxSelect(const ImRect& scope_rect, bool enable_scroll) window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavHighlight)); // FIXME-MULTISELECT: Styling // Scroll + const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0; if (enable_scroll) { ImRect scroll_r = scope_rect; @@ -7443,10 +7444,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() } if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId)) - { - bool enable_scroll = (ms->Flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms->Flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0; - EndBoxSelect(scope_rect, enable_scroll); - } + EndBoxSelect(scope_rect, ms->Flags); } if (ms->IsEndIO == false) From 7d4de84ee3ccc4c144fbe48236d59d7ceabdb058 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 28 Jun 2024 19:01:18 +0200 Subject: [PATCH 127/132] MultiSelect: added courtesy ImGuiMultiSelectFlags_NavWrapX flag so we can demo this until a nav api is designed. --- imgui.h | 1 + imgui_demo.cpp | 21 ++++++++++++++------- imgui_widgets.cpp | 7 +++++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/imgui.h b/imgui.h index b086caa2744f..3aaa0981c76b 100644 --- a/imgui.h +++ b/imgui.h @@ -2781,6 +2781,7 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_SelectOnClick = 1 << 13, // Apply selection on mouse down when clicking on unselected item. (Default) ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 14, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. //ImGuiMultiSelectFlags_RangeSelect2d = 1 << 15, // Shift+Selection uses 2d geometry instead of linear sequence, so possible to use Shift+up/down to select vertically in grid. Analogous to what BoxSelect does. + ImGuiMultiSelectFlags_NavWrapX = 1 << 16, // [Temporary] Enable navigation wrapping on X axis. Provided as a convenience because we don't have a design for the general Nav API for this yet. When the more general feature be public we may obsolete this flag in favor of new one. }; // Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). diff --git a/imgui_demo.cpp b/imgui_demo.cpp index ebfb5ddf59bb..ab2ae6b08912 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -9836,10 +9836,21 @@ struct ExampleAssetsBrowser // Multi-select ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ClearOnClickVoid; - if (AllowDragUnselected) - ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; // To allow dragging an unselected item without altering selection. + + // - Enable box-select (in 2D mode, so that changing box-select rectangle X1/X2 boundaries will affect clipped items) if (AllowBoxSelect) - ms_flags |= ImGuiMultiSelectFlags_BoxSelect2d; // Enable box-select in 2D mode. + ms_flags |= ImGuiMultiSelectFlags_BoxSelect2d; + + // - This feature allows dragging an unselected item without selecting it (rarely used) + if (AllowDragUnselected) + ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; + + // - Enable keyboard wrapping on X axis + // (FIXME-MULTISELECT: We haven't designed/exposed a general nav wrapping api yet, so this flag is provided as a courtesy to avoid doing: + // ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); + // When we finish implementing a more general API for this, we will obsolete this flag in favor of the new system) + ms_flags |= ImGuiMultiSelectFlags_NavWrapX; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags, Selection.Size, Items.Size); // Use custom selection adapter: store ID in selection (recommended) @@ -9970,10 +9981,6 @@ struct ExampleAssetsBrowser if (want_delete) Selection.ApplyDeletionPostLoop(ms_io, Items, item_curr_idx_to_focus); - // Keyboard/Gamepad Wrapping - // FIXME-MULTISELECT: Currently an imgui_internal.h API. Find a design/way to expose this in public API. - //ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); - // Zooming with CTRL+Wheel if (ImGui::IsWindowAppearing()) ZoomWheelAccum = 0.0f; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 135273597800..703dcdb7af9f 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7479,6 +7479,13 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() } } + // Courtesy nav wrapping helper flag + if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX) + { + IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope + ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); + } + // Unwind window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos); PopFocusScope(); From 2697cfe35463aeeecfa9e7ec983f187b38fb3893 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 28 Jun 2024 19:11:05 +0200 Subject: [PATCH 128/132] MultiSelect: Box-Select: uses SetActiveIdUsingAllKeyboardKeys() to avoid nav interference, much like most drag operations. --- imgui.cpp | 3 ++- imgui_internal.h | 2 +- imgui_widgets.cpp | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index ffcdce5f6375..d4a4d47f9e96 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -5539,7 +5539,8 @@ void ImGui::SetItemAllowOverlap() } #endif -// FIXME: It might be undesirable that this will likely disable KeyOwner-aware shortcuts systems. Consider a more fine-tuned version for the two users of this function. +// This is a shortcut for not taking ownership of 100+ keys, frequently used by drag operations. +// FIXME: It might be undesirable that this will likely disable KeyOwner-aware shortcuts systems. Consider a more fine-tuned version if needed? void ImGui::SetActiveIdUsingAllKeyboardKeys() { ImGuiContext& g = *GImGui; diff --git a/imgui_internal.h b/imgui_internal.h index ae3375e28b07..c03839a8fb1b 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -2081,7 +2081,7 @@ struct ImGuiContext ImGuiKeyOwnerData KeysOwnerData[ImGuiKey_NamedKey_COUNT]; ImGuiKeyRoutingTable KeysRoutingTable; ImU32 ActiveIdUsingNavDirMask; // Active widget will want to read those nav move requests (e.g. can activate a button and move away from it) - bool ActiveIdUsingAllKeyboardKeys; // Active widget will want to read all keyboard keys inputs. (FIXME: This is a shortcut for not taking ownership of 100+ keys but perhaps best to not have the inconsistency) + bool ActiveIdUsingAllKeyboardKeys; // Active widget will want to read all keyboard keys inputs. (this is a shortcut for not taking ownership of 100+ keys, frequently used by drag operations) ImGuiKeyChord DebugBreakInShortcutRouting; // Set to break in SetShortcutRouting()/Shortcut() calls. #ifndef IMGUI_DISABLE_OBSOLETE_KEYIO ImU32 ActiveIdUsingNavInputMask; // If you used this. Since (IMGUI_VERSION_NUM >= 18804) : 'g.ActiveIdUsingNavInputMask |= (1 << ImGuiNavInput_Cancel);' becomes 'SetKeyOwner(ImGuiKey_Escape, g.ActiveId) and/or SetKeyOwner(ImGuiKey_NavGamepadCancel, g.ActiveId);' diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 703dcdb7af9f..d04c1dd4434b 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7159,6 +7159,7 @@ static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window) bs->Window = window; bs->IsStarting = false; ImGui::SetActiveID(bs->ID, window); + ImGui::SetActiveIdUsingAllKeyboardKeys(); if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) bs->RequestClear = true; } From 1b6352244659149898579575fabedbc4d27fd6bd Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 28 Jun 2024 19:35:24 +0200 Subject: [PATCH 129/132] MultiSelect: Box-Select: handle Esc to disable box-select. This avoid remove a one-frame delay when finishing box-select, where Esc wouldn't be routed to selection but to child. --- imgui_widgets.cpp | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index d04c1dd4434b..442ce2e50517 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7377,20 +7377,6 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel request_clear = true; } - if (ms->IsFocused) - { - // Shortcut: Clear selection (Escape) - // Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window. - if ((flags & ImGuiMultiSelectFlags_ClearOnEscape) && (selection_size != 0)) - if (Shortcut(ImGuiKey_Escape)) - request_clear = true; - - // Shortcut: Select all (CTRL+A) - if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) - if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) - request_select_all = true; - } - // Box-select handling: update active state. ImGuiBoxSelectState* bs = &g.BoxSelectState; if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) @@ -7401,6 +7387,28 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel request_clear |= bs->RequestClear; } + if (ms->IsFocused) + { + // Shortcut: Clear selection (Escape) + // - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window. + // - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock. + if (flags & ImGuiMultiSelectFlags_ClearOnEscape) + { + if (selection_size != 0 || bs->IsActive) + if (Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, bs->IsActive ? bs->ID : 0)) + { + request_clear = true; + if (bs->IsActive) + BoxSelectDeactivateDrag(bs); + } + } + + // Shortcut: Select all (CTRL+A) + if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) + if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) + request_select_all = true; + } + if (request_clear || request_select_all) { ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, request_select_all, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; From 7814518049e84860bc1b9ebd2f907dad2fca67c1 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 1 Jul 2024 19:54:35 +0200 Subject: [PATCH 130/132] MultiSelect: ImGuiSelectionBasicStorage: optimized for smaller insertion amounts in larger sets + fix caling batch select with same value. --- imgui.cpp | 2 +- imgui.h | 4 ++-- imgui_widgets.cpp | 39 +++++++++++++++++++++++++++++---------- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index d4a4d47f9e96..0fb4e34ca11b 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -2558,6 +2558,7 @@ ImGuiStoragePair* ImLowerBound(ImGuiStoragePair* in_begin, ImGuiStoragePair* in_ return in_p; } +IM_MSVC_RUNTIME_CHECKS_OFF static int IMGUI_CDECL PairComparerByID(const void* lhs, const void* rhs) { // We can't just do a subtraction because qsort uses signed integers and subtracting our ID doesn't play well with that. @@ -2567,7 +2568,6 @@ static int IMGUI_CDECL PairComparerByID(const void* lhs, const void* rhs) } // For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. -IM_MSVC_RUNTIME_CHECKS_OFF void ImGuiStorage::BuildSortByKey() { ImQsort(Data.Data, (size_t)Data.Size, sizeof(ImGuiStoragePair), PairComparerByID); diff --git a/imgui.h b/imgui.h index 3aaa0981c76b..0ecbcbdefcf5 100644 --- a/imgui.h +++ b/imgui.h @@ -2771,8 +2771,8 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes) ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing selection when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes) ImGuiMultiSelectFlags_NoAutoClearOnReselect = 1 << 5, // Disable clearing selection when clicking/selecting an already selected item - ImGuiMultiSelectFlags_BoxSelect1d = 1 << 6, // Enable box-selection with same width and same x pos items (e.g. only full row Selectable()). Small optimization. - ImGuiMultiSelectFlags_BoxSelect2d = 1 << 7, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect1d = 1 << 6, // Enable box-selection with same width and same x pos items (e.g. only full row Selectable()). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 7, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This is slower: alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 8, // Disable scrolling when box-selecting near edges of scope. ImGuiMultiSelectFlags_ClearOnEscape = 1 << 9, // Clear selection when pressing Escape while scope is focused. ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 10, // Clear selection when clicking on empty location within scope. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 442ce2e50517..637295e7ae2c 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7905,7 +7905,7 @@ static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicS ImGuiStorage* storage = &selection->_Storage; ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id); const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id); - if (selected == is_contained && it->val_i != 0) + if (selected == (is_contained && it->val_i != 0)) return; if (selected && !is_contained) storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish() @@ -7945,10 +7945,15 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!"); IM_ASSERT(AdapterIndexToStorageId != NULL); - // This is optimized/specialized to cope nicely with very large selections (e.g. 1 million items) + // This is optimized/specialized to cope with very large selections (e.g. 100k+ items) // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish(). // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass. - // - (A more optimal version wouldn't even use ImGuiStorage but directly a ImVector to reduce bandwidth, but this is a reasonable trade off to reuse code) + // - A more optimal version wouldn't even use ImGuiStorage but directly a ImVector to reduce bandwidth, but this is a reasonable trade off to reuse code. + // - There are many ways this could be better optimized. The worse case scenario being: using BoxSelect2d in a grid, box-select scrolling down while wiggling + // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.) + // FIXME-OPT: For each block of consecutive SetRange request: + // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage. + // - rewrite sorted storage a single time. for (ImGuiSelectionRequest& req : ms_io->Requests) { if (req.Type == ImGuiSelectionRequestType_SetAll) @@ -7965,13 +7970,27 @@ void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) } else if (req.Type == ImGuiSelectionRequestType_SetRange) { - // Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1 - const int size_before_amends = _Storage.Data.Size; - int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? (int)req.RangeLastItem - (int)req.RangeFirstItem : 0); - for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection) - ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order); - _SelectionOrder += (int)req.RangeLastItem - (int)req.RangeFirstItem + 1; - ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); + const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1; + //ImGuiContext& g = *GImGui; IMGUI_DEBUG_LOG_SELECTION("Req %d/%d: set %d to %d\n", ms_io->Requests.index_from_ptr(&req), ms_io->Requests.Size, selection_changes, req.Selected); + if (selection_changes == 1 || (selection_changes < Size / 100)) + { + // Multiple sorted insertion + copy likely to be faster. + // Technically we could do a single copy with a little more work (sort sequential SetRange requests) + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) + SetItemSelected(GetStorageIdFromIndex(idx), req.Selected); + } + else + { + // Append insertion + single sort likely be faster. + // Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1 + const int size_before_amends = _Storage.Data.Size; + int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0); + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection) + ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order); + if (req.Selected) + _SelectionOrder += selection_changes; + ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); + } } } } From 2688562fd2dc4dd9dfd4ac2c41fe97943bbe20a7 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 18 Jul 2024 18:05:45 +0200 Subject: [PATCH 131/132] MultiSelect: Better document how TreeNode() is not trivially usable yet. Will revert when the time is right. --- imgui.h | 8 ++++++-- imgui_demo.cpp | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/imgui.h b/imgui.h index 0ecbcbdefcf5..5fb60300e61d 100644 --- a/imgui.h +++ b/imgui.h @@ -672,10 +672,12 @@ namespace ImGui IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height 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. + // Multi-selection system for Selectable(), Checkbox() functions* // - 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. // - ImGuiSelectionUserData is often used to store your item index within the current view (but may store something else). // - Read comments near ImGuiMultiSelectIO for instructions/details and see 'Demo->Widgets->Selection State & Multi-Select' for demo. + // - (*) TreeNode() is technically supported but... using this correctly is more complicate: you need some sort of linear/random access to your tree, + // which is suited to advanced trees setups already implementing filters and clipper. We will work toward simplifying and demoing this. // - 'selection_size' and 'items_count' parameters are optional and used by a few features. If they are costly for you to compute, you may avoid them. IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size = -1, int items_count = -1); IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); @@ -2734,7 +2736,9 @@ struct ImColor // - Refer to 'Demo->Widgets->Selection State & Multi-Select' for demos using this. // - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) // with support for clipper (skipping non-visible items), box-select and many other details. -// - TreeNode(), Selectable(), Checkbox() are supported but custom widgets may use it as well. +// - Selectable(), Checkbox() are supported but custom widgets may use it as well. +// - TreeNode() is technically supported but... using this correctly is more complicated: you need some sort of linear/random access to your tree, +// which is suited to advanced trees setups also implementing filters and clipper. We will work toward simplifying and demoing it. // - In the spirit of Dear ImGui design, your code owns actual selection data. // This is designed to allow all kinds of selection storage you may use in your application e.g. set/map/hash. // About ImGuiSelectionBasicStorage: diff --git a/imgui_demo.cpp b/imgui_demo.cpp index ab2ae6b08912..88ccce1f0af6 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3317,6 +3317,8 @@ static void ShowDemoWindowMultiSelect() if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } ImGui::SameLine(); if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } + ImGui::SameLine(); + HelpMarker("TreeNode() is technically supported but... using this correctly is more complicated (you need some sort of linear/random access to your tree, which is suited to advanced trees setups already implementing filters and clipper. We will work toward simplifying and demoing this.\n\nFor now the tree demo is actually a little bit meaningless because it is an empty tree with only root nodes."); ImGui::Checkbox("Enable clipper", &use_clipper); ImGui::Checkbox("Enable deletion", &use_deletion); ImGui::Checkbox("Enable drag & drop", &use_drag_drop); From 02c31a8dd1f38b86f28b3a683cd55ebc4d8a839f Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 1 Jul 2024 19:15:18 +0200 Subject: [PATCH 132/132] MultiSelect: added Changelog for the feature. Removed IMGUI_HAS_MULTI_SELECT. --- docs/CHANGELOG.txt | 51 ++++++++++++++++++++++++++++++++++++++++++++++ imgui.h | 6 ++---- imgui_internal.h | 4 ---- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index fea2fe5c29e0..ab1f0d806f47 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -77,6 +77,57 @@ Other changes: Disabling this was previously possible for Selectable() via a direct flag but not for MenuItem(). (#1379, #1468, #2200, #4936, #5216, #7302, #7573) - This was mostly all previously in imgui_internal.h. +- Multi-Select: added multi-select API and demos. (#1861) + - This system implements standard multi-selection idioms (CTRL+mouse click, CTRL+keyboard moves, + SHIFT+mouse click, SHIFT+keyboard moves, etc.) with support for clipper (not submitting non-visible + items), box-selection with scrolling, and many other details. + - In the spirit of Dear ImGui design, your code owns both items and actual selection data. + This is designed to allow all kinds of selection storage you may use in your application + (e.g. set/map/hash, intrusive selection, interval trees, up to you). + - The supported widgets are Selectable(), Checkbox(). TreeNode() is also technically supported but... + using this correctly is more complicated (you need some sort of linear/random access to your tree, + which is suited to advanced trees setups already implementing filters and clipper. + We will work toward simplifying and demoing this later. + - A helper ImGuiSelectionBasicStorage is provided to facilitate getting started in a typical app. + - Documentation: + - Wiki page https://github.com/ocornut/imgui/wiki/Multi-Select for API overview. + - Demo code. + - Headers are well commented. + - Added BeginMultiSelect(), EndMultiSelect(), SetNextItemSelectionUserData(). + - Added IsItemToggledSelection() for use if you need latest selection update during currnet iteration. + - Added ImGuiMultiSelectIO and ImGuiSelectionRequest structures: + - BeginMultiSelect() and EndMultiSelect() return a ImGuiMultiSelectIO structure, which + is mostly an array of ImGuiSelectionRequest actions (clear, select all, set range, etc.) + - Other fields are helpful when using a clipper, or wanting to handle deletion nicely. + - Added ImGuiSelectionBasicStorage helper to store and maintain a selection (optional): + - This is similar to if you used e.g. a std::set to store a selection, with all the right + glue to honor ImGuiMultiSelectIO requests. Most applications can use that. + - Added ImGuiSelectionExternalStorage helper to maintain an externally stored selection (optional): + - Helpful to easily bind multi-selection to e.g. an array of checkboxes. + - Added ImGuiMultiSelectFlags options: + - ImGuiMultiSelectFlags_SingleSelect + - ImGuiMultiSelectFlags_NoSelectAll + - ImGuiMultiSelectFlags_NoRangeSelect + - ImGuiMultiSelectFlags_NoAutoSelect + - ImGuiMultiSelectFlags_NoAutoClear + - ImGuiMultiSelectFlags_NoAutoClearOnReselect (#7424) + - ImGuiMultiSelectFlags_BoxSelect1d + - ImGuiMultiSelectFlags_BoxSelect2d + - ImGuiMultiSelectFlags_BoxSelectNoScroll + - ImGuiMultiSelectFlags_ClearOnEscape + - ImGuiMultiSelectFlags_ClearOnClickVoid + - ImGuiMultiSelectFlags_ScopeWindow (default), ImGuiMultiSelectFlags_ScopeRect + - ImGuiMultiSelectFlags_SelectOnClick (default), ImGuiMultiSelectFlags_SelectOnClickRelease + - ImGuiMultiSelectFlags_NavWrapX + - Demo: Added "Examples->Assets Browser" demo. + - Demo: Added "Widgets->Selection State & Multi-Select" section, with: + - Multi-Select + - Multi-Select (with clipper) + - Multi-Select (with deletion) + - Multi-Select (dual list box) (#6648) + - Multi-Select (checkboxes) + - Multi-Select (multiple scopes) + - Multi-Select (advanced) - Clipper: added SeekCursorForItem() function. When using ImGuiListClipper::Begin(INT_MAX) you can can use the clipper without knowing the amount of items beforehand. (#1311) In this situation, call ImGuiListClipper::SeekCursorForItem(items_count) as the end of your iteration diff --git a/imgui.h b/imgui.h index 5fb60300e61d..5550f110651a 100644 --- a/imgui.h +++ b/imgui.h @@ -28,7 +28,7 @@ // Library Version // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') #define IMGUI_VERSION "1.91.0 WIP" -#define IMGUI_VERSION_NUM 19095 +#define IMGUI_VERSION_NUM 19096 #define IMGUI_HAS_TABLE /* @@ -2729,8 +2729,6 @@ struct ImColor // [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO, ImGuiSelectionBasicStorage) //----------------------------------------------------------------------------- -#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. - // Multi-selection system // Documentation at: https://github.com/ocornut/imgui/wiki/Multi-Select // - Refer to 'Demo->Widgets->Selection State & Multi-Select' for demos using this. @@ -2797,7 +2795,7 @@ struct ImGuiMultiSelectIO { //------------------------------------------// BeginMultiSelect / EndMultiSelect ImVector Requests; // ms:w, app:r / ms:w app:r // Requests to apply to your selection data. - ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (often the first selected item) must never be clipped: use clipper.IncludeItemByIndex() to ensure it is submitted. ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). diff --git a/imgui_internal.h b/imgui_internal.h index c03839a8fb1b..b44d7b258c52 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1741,8 +1741,6 @@ struct ImGuiBoxSelectState // We always assume that -1 is an invalid value (which works for indices and pointers) #define ImGuiSelectionUserData_Invalid ((ImGuiSelectionUserData)-1) -#ifdef IMGUI_HAS_MULTI_SELECT - // Temporary storage for multi-select struct IMGUI_API ImGuiMultiSelectTempData { @@ -1783,8 +1781,6 @@ struct IMGUI_API ImGuiMultiSelectState ImGuiMultiSelectState() { Window = NULL; ID = 0; LastFrameActive = LastSelectionSize = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; } }; -#endif // #ifdef IMGUI_HAS_MULTI_SELECT - //----------------------------------------------------------------------------- // [SECTION] Docking support //-----------------------------------------------------------------------------