From 554db6bc0f3b8d8ebdcd596d95eaaa364f51e575 Mon Sep 17 00:00:00 2001 From: omar Date: Fri, 1 Feb 2019 12:22:57 +0100 Subject: [PATCH] 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 733a5dfe1..b1e990353 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 26965fd86..80e6ca7e1 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 dd253454f..8e7312602 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 5d8f2332c..63b80400c 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 a7ab721c8..a07908cd9 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