From 0d535afb410dba7197d7aae02965fbb7d98ba1f5 Mon Sep 17 00:00:00 2001 From: omar Date: Fri, 1 Feb 2019 12:22:57 +0100 Subject: [PATCH] RangeSelect/MultiSelect: WIP range-select (ref 1861) [rebased] Internals: Rename ImGuiSelectableFlags_PressedOnXXX to ImGuiSelectableFlags_SelectOnXXX, ImGuiButtonFlags_NoHoveredOnNav to ImGuiButtonFlags_NoHoveredOnFocus. --- imgui.cpp | 4 + imgui.h | 73 +++++++++++ imgui_demo.cpp | 76 ++++++++++- imgui_internal.h | 31 ++++- imgui_widgets.cpp | 323 +++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 485 insertions(+), 22 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 0949f430..65b3536d 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1063,6 +1063,7 @@ ImGuiStyle::ImGuiStyle() TabMinWidthForCloseButton = 0.0f; // Minimum width for close button to appears on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. 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,0); // 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. DisplayWindowPadding = ImVec2(19,19); // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. DisplaySafeAreaPadding = ImVec2(3,3); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. @@ -1101,6 +1102,7 @@ void ImGuiStyle::ScaleAllSizes(float scale_factor) LogSliderDeadzone = ImFloor(LogSliderDeadzone * scale_factor); TabRounding = ImFloor(TabRounding * scale_factor); TabMinWidthForCloseButton = (TabMinWidthForCloseButton != FLT_MAX) ? ImFloor(TabMinWidthForCloseButton * scale_factor) : FLT_MAX; + SelectableSpacing = ImFloor(SelectableSpacing * scale_factor); DisplayWindowPadding = ImFloor(DisplayWindowPadding * scale_factor); DisplaySafeAreaPadding = ImFloor(DisplaySafeAreaPadding * scale_factor); MouseCursorScale = ImFloor(MouseCursorScale * scale_factor); @@ -2836,6 +2838,7 @@ static const ImGuiStyleVarInfo GStyleVarInfo[] = { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SelectableSpacing) }, // ImGuiStyleVar_SelectableSpacing { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign }; @@ -4554,6 +4557,7 @@ void ImGui::Shutdown(ImGuiContext* context) g.ClipboardHandlerData.clear(); g.MenusIdSubmittedThisFrame.clear(); g.InputTextState.ClearFreeMemory(); + g.MultiSelectScopeWindow = NULL; g.SettingsWindows.clear(); g.SettingsHandlers.clear(); diff --git a/imgui.h b/imgui.h index f92c70dc..0f82149f 100644 --- a/imgui.h +++ b/imgui.h @@ -150,6 +150,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.SetPlatformImeDataFn() function. @@ -191,6 +192,7 @@ typedef int ImGuiHoveredFlags; // -> enum ImGuiHoveredFlags_ // Flags: f typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: for InputText(), InputTextMultiline() typedef int ImGuiKeyModFlags; // -> enum ImGuiKeyModFlags_ // Flags: for io.KeyMods (Ctrl/Shift/Alt/Super) 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() @@ -612,6 +614,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 SetNextItemMultiSelectData(void* item_data); + // Widgets: List Boxes // - This is essentially a thin wrapper to using BeginChild/EndChild with some stylistic changes. // - The BeginListBox()/EndListBox() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() or any items. @@ -835,6 +845,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 requires 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 requires 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? @@ -1615,6 +1626,7 @@ enum ImGuiStyleVar_ ImGuiStyleVar_GrabRounding, // float GrabRounding ImGuiStyleVar_TabRounding, // float TabRounding ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_SelectableSpacing, // ImVec2 SelectableSpacing ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign ImGuiStyleVar_COUNT }; @@ -1720,6 +1732,17 @@ enum ImGuiMouseCursor_ ImGuiMouseCursor_COUNT }; +// 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 +}; + // Enumeration for ImGui::SetWindow***(), SetNextWindow***(), SetNextItem***() functions // Represent a condition. // Important: Treat as a regular enum! Do NOT combine multiple values using binary operators! All the functions above treat 0 as a shortcut to ImGuiCond_Always. @@ -1867,6 +1890,7 @@ struct ImGuiStyle float TabMinWidthForCloseButton; // Minimum width for close button to appears on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. 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. ImVec2 DisplayWindowPadding; // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. ImVec2 DisplaySafeAreaPadding; // If you cannot see the edges of your screen (e.g. on a TV) increase the safe area padding. Apply to popups/tooltips as well regular windows. NB: Prefer configuring your TV sets correctly! @@ -2317,6 +2341,55 @@ struct ImGuiListClipper #endif }; +// 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; + } +}; + // Helpers macros to generate 32-bit encoded colors #ifdef IMGUI_USE_BGRA_PACKED_COLOR #define IM_COL32_R_SHIFT 16 diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 5f9742d1..f087f706 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -1198,7 +1198,7 @@ static void ShowDemoWindowWidgets() ImGui::TreePop(); } IMGUI_DEMO_MARKER("Widgets/Selectables/Multiple Selection"); - if (ImGui::TreeNode("Selection State: Multiple Selection")) + if (ImGui::TreeNode("Selection State: Multiple Selection (Simplified)")) { HelpMarker("Hold CTRL and click to select multiple items."); static bool selection[5] = { false, false, false, false, false }; @@ -1215,6 +1215,68 @@ static void ShowDemoWindowWidgets() } ImGui::TreePop(); } + if (ImGui::TreeNode("Selection State: 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(ImGuiStorage::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::SetNextItemMultiSelectData((void*)(intptr_t)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_DEMO_MARKER("Widgets/Selectables/Rendering more text into the same line"); if (ImGui::TreeNode("Rendering more text into the same line")) { @@ -1272,6 +1334,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++) { @@ -1290,8 +1361,10 @@ static void ShowDemoWindowWidgets() ImGui::PopID(); } + ImGui::PopStyleVar(2); if (winning_state) ImGui::PopStyleVar(); + ImGui::TreePop(); } IMGUI_DEMO_MARKER("Widgets/Selectables/Alignment"); @@ -6147,6 +6220,7 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SliderFloat2("CellPadding", (float*)&style.CellPadding, 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 8cf412ba..1a22d270 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -122,6 +122,7 @@ struct ImGuiGroupData; // Stacked storage data for BeginGroup()/End struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box struct ImGuiLastItemData; // Status storage for last submitted items 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 @@ -1093,6 +1094,8 @@ struct ImGuiNextItemData ImGuiID FocusScopeId; // Set by SetNextItemMultiSelectData() (!= 0 signify value has been set, so it's an alternate version of HasSelectionData, we don't use Flags for this because they are cleared too early. This is mostly used for debugging) ImGuiCond OpenCond; bool OpenVal; // Set by SetNextItemOpen() + bool MultiSelectDataIsSet; + void* MultiSelectData; ImGuiNextItemData() { memset(this, 0, sizeof(*this)); } inline void ClearFlags() { Flags = ImGuiNextItemDataFlags_None; } // Also cleared manually by ItemAdd()! @@ -1400,8 +1403,20 @@ struct ImGuiOldColumns // [SECTION] Multi-select support //----------------------------------------------------------------------------- +#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 //----------------------------------------------------------------------------- @@ -1699,6 +1714,12 @@ struct ImGuiContext float NavWindowingHighlightAlpha; bool NavWindowingToggleLayer; + // 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) ImGuiMouseCursor MouseCursor; @@ -1894,6 +1915,10 @@ struct ImGuiContext NavWindowingTimer = NavWindowingHighlightAlpha = 0.0f; NavWindowingToggleLayer = false; + MultiSelectScopeId = 0; + MultiSelectScopeWindow = NULL; + MultiSelectFlags = 0; + DimBgRatio = 0.0f; MouseCursor = ImGuiMouseCursor_Arrow; @@ -2686,6 +2711,10 @@ namespace ImGui IMGUI_API void ClearDragDrop(); IMGUI_API bool IsDragDropPayloadBeingAccepted(); + // New Multi-Selection/Range-Selection API (FIXME-WIP) + 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(). diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 407ae51c..cf6614e2 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -18,6 +18,7 @@ Index of this file: // [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. // [SECTION] Widgets: TreeNode, CollapsingHeader, etc. // [SECTION] Widgets: Selectable +// [SECTION] Widgets: Multi-Selection System // [SECTION] Widgets: ListBox // [SECTION] Widgets: PlotLines, PlotHistogram // [SECTION] Widgets: Value helpers @@ -5922,6 +5923,26 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l 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; @@ -5929,7 +5950,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l { 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 @@ -5961,16 +5982,27 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen; } } + + // 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, window->DC.NavFocusScopeIdCurrent, interact_bb); + } + if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) SetItemAllowOverlap(); - // In this branch, TreeNodeBehavior() cannot toggle the selection so this will never trigger. - if (selected != was_selected) //-V547 + if (selected != was_selected) g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; // Render const ImU32 text_col = GetColorU32(ImGuiCol_Text); ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_TypeThin; + if (is_multi_select) + nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle if (display_frame) { // Framed type @@ -6170,8 +6202,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_FLOOR(spacing_x * 0.50f); const float spacing_U = IM_FLOOR(spacing_y * 0.50f); bb.Min.x -= spacing_L; @@ -6220,20 +6252,43 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; } if (flags & ImGuiSelectableFlags_AllowItemOverlap) { button_flags |= ImGuiButtonFlags_AllowItemOverlap; } + // 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 == window->DC.NavFocusScopeIdCurrent) - 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 == window->DC.NavFocusScopeIdCurrent) + 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))) @@ -6250,8 +6305,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (flags & ImGuiSelectableFlags_AllowItemOverlap) SetItemAllowOverlap(); - // 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 @@ -6259,10 +6313,18 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl hovered = true; 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); } - RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding); + ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_TypeThin | 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 && window->DC.CurrentColumns) PopColumnsBackground(); @@ -6279,7 +6341,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) @@ -6292,6 +6354,227 @@ bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags return false; } +//------------------------------------------------------------------------- +// [SECTION] Widgets: Multi-Selection System +//------------------------------------------------------------------------- +// - 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* state = &g.MultiSelectState; + g.MultiSelectScopeId = window->IDStack.back(); + g.MultiSelectScopeWindow = window; + g.MultiSelectFlags = flags; + state->Clear(); + + if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0) + { + state->In.RangeSrc = state->Out.RangeSrc = range_ref; + state->In.RangeValue = state->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) + state->InRequestSetRangeNav = true; + if (!g.IO.KeyCtrl && !g.IO.KeyShift) + state->In.RequestClear = true; + } + + // Select All helper shortcut + if (!(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) + if (IsWindowFocused() && g.IO.KeyCtrl && IsKeyPressed(GetKeyIndex(ImGuiKey_A))) + state->In.RequestSelectAll = true; + +#ifdef IMGUI_DEBUG_MULTISELECT + if (state->In.RequestClear) printf("[%05d] BeginMultiSelect: RequestClear\n", g.FrameCount); + if (state->In.RequestSelectAll) printf("[%05d] BeginMultiSelect: RequestSelectAll\n", g.FrameCount); +#endif + + return &state->In; +} + +ImGuiMultiSelectData* ImGui::EndMultiSelect() +{ + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiMultiSelectState* state = &g.MultiSelectState; + IM_ASSERT(g.MultiSelectScopeId != 0); + if (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect) + state->Out.RangeValue = true; + g.MultiSelectScopeId = 0; + g.MultiSelectScopeWindow = NULL; + g.MultiSelectFlags = 0; + +#ifdef IMGUI_DEBUG_MULTISELECT + if (state->Out.RequestClear) printf("[%05d] EndMultiSelect: RequestClear\n", g.FrameCount); + if (state->Out.RequestSelectAll) printf("[%05d] EndMultiSelect: RequestSelectAll\n", g.FrameCount); + if (state->Out.RequestSetRange) printf("[%05d] EndMultiSelect: RequestSetRange %p..%p = %d\n", g.FrameCount, state->Out.RangeSrc, state->Out.RangeDst, state->Out.RangeValue); +#endif + + return &state->Out; +} + +void ImGui::SetNextItemMultiSelectData(void* item_data) +{ + ImGuiContext& g = *GImGui; + g.NextItemData.MultiSelectData = item_data; + g.NextItemData.MultiSelectDataIsSet = true; +} + +void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) +{ + ImGuiContext& g = *GImGui; + ImGuiMultiSelectState* state = &g.MultiSelectState; + + IM_ASSERT(g.NextItemData.MultiSelectDataIsSet && "Forgot to call SetNextItemMultiSelectData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); + void* item_data = g.NextItemData.MultiSelectData; + + // 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 (state->In.RequestClear) + selected = false; + else if (state->In.RequestSelectAll) + selected = true; + + const bool is_range_src = (state->In.RangeSrc == item_data); + if (is_range_src) + state->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 (state->InRequestSetRangeNav) + { + IM_ASSERT(id != 0); + IM_ASSERT(g.IO.KeyShift); + const bool is_range_dst = !state->InRangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. + if (is_range_dst) + state->InRangeDstPassedBy = true; + if (is_range_src || is_range_dst || state->In.RangeSrcPassedBy != state->InRangeDstPassedBy) + selected = state->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* state = &g.MultiSelectState; + + void* item_data = g.NextItemData.MultiSelectData; + g.NextItemData.MultiSelectDataIsSet = false; + + 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) ? ImGuiInputSource_Nav : ImGuiInputSource_Mouse; + if (is_shift && is_multiselect) + { + state->Out.RequestSetRange = true; + state->Out.RangeDst = item_data; + if (!is_ctrl) + state->Out.RangeValue = true; + state->Out.RangeDirection = state->In.RangeSrcPassedBy ? +1 : -1; + } + else + { + selected = (!is_ctrl || (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect)) ? true : !selected; + state->Out.RangeSrc = state->Out.RangeDst = item_data; + state->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) + state->Out.RequestClear = true; + if (is_multiselect && !is_shift && !preserve_existing_selection && state->Out.RequestClear) + { + // For toggle selection unless there is a Clear request, we can handle it completely locally without sending a RangeSet request. + IM_ASSERT(state->Out.RangeSrc == state->Out.RangeDst); // Setup by block above + state->Out.RequestSetRange = true; + state->Out.RangeValue = selected; + state->Out.RangeDirection = +1; + } + if (!is_multiselect) + { + // Clear selection, set single item range + IM_ASSERT(state->Out.RangeSrc == item_data && state->Out.RangeDst == item_data); // Setup by block above + state->Out.RequestClear = true; + state->Out.RequestSetRange = true; + } + } + else if (input_source == ImGuiInputSource_Nav) + { + if (!is_multiselect) + state->Out.RequestClear = true; + else if (is_shift && !is_ctrl && is_multiselect) + state->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 (state->Out.RangeSrc == item_data && is_ctrl && is_shift && is_multiselect && !(g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect)) + state->Out.RangeValue = selected; + + *p_selected = selected; + *p_pressed = pressed; +} + //------------------------------------------------------------------------- // [SECTION] Widgets: ListBox //-------------------------------------------------------------------------