From ceeedc5d34ac567836cabc499c398a457f775989 Mon Sep 17 00:00:00 2001 From: omar Date: Fri, 18 Jan 2019 14:18:50 +0100 Subject: [PATCH] RangeSelect/MultiSelect: WIP range-select (#1861) --- imgui.cpp | 21 ++- imgui.h | 73 ++++++++++ imgui_demo.cpp | 72 +++++++++- imgui_internal.h | 43 +++++- imgui_widgets.cpp | 350 ++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 524 insertions(+), 35 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 2e26ddcb1ba1..d7fe1783aa08 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -939,6 +939,7 @@ CODE // Debug options #define IMGUI_DEBUG_NAV_SCORING 0 #define IMGUI_DEBUG_NAV_RECTS 0 +#define IMGUI_DEBUG_MULTISELECT 0 // Visual Studio warnings #ifdef _MSC_VER @@ -1095,6 +1096,7 @@ ImGuiStyle::ImGuiStyle() FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) + SelectableSpacing = ImVec2(0,0); // Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing). TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns @@ -1130,6 +1132,7 @@ void ImGuiStyle::ScaleAllSizes(float scale_factor) TabRounding = ImFloor(TabRounding * scale_factor); ItemSpacing = ImFloor(ItemSpacing * scale_factor); ItemInnerSpacing = ImFloor(ItemInnerSpacing * scale_factor); + SelectableSpacing = ImFloor(SelectableSpacing * scale_factor); TouchExtraPadding = ImFloor(TouchExtraPadding * scale_factor); IndentSpacing = ImFloor(IndentSpacing * scale_factor); ColumnsMinSpacing = ImFloor(ColumnsMinSpacing * scale_factor); @@ -2635,6 +2638,7 @@ void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window) if (g.ActiveIdIsJustActivated) { g.ActiveIdTimer = 0.0f; + g.ActiveIdPressed = false; g.ActiveIdHasBeenEdited = false; if (id != 0) { @@ -3600,6 +3604,7 @@ void ImGui::Shutdown(ImGuiContext* context) g.InputTextState.TextW.clear(); g.InputTextState.InitialText.clear(); g.InputTextState.TempBuffer.clear(); + g.MultiSelectScopeWindow = NULL; for (int i = 0; i < g.SettingsWindows.Size; i++) IM_DELETE(g.SettingsWindows[i].Name); @@ -4235,6 +4240,12 @@ bool ImGui::IsItemClicked(int mouse_button) return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_None); } +bool ImGui::IsItemToggledSelection() +{ + ImGuiContext& g = *GImGui; + return (g.CurrentWindow->DC.LastItemStatusFlags & ImGuiItemStatusFlags_ToggledSelection) ? true : false; +} + bool ImGui::IsAnyItemHovered() { ImGuiContext& g = *GImGui; @@ -5777,6 +5788,7 @@ static const ImGuiStyleVarInfo GStyleVarInfo[] = { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing + { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SelectableSpacing) }, // ImGuiStyleVar_SelectableSpacing { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding @@ -7295,6 +7307,7 @@ static void ImGui::NavProcessItem(ImGuiWindow* window, const ImRect& nav_bb, con if (new_best) { result->ID = id; + result->SelectScopeId = g.MultiSelectScopeId; result->Window = window; result->RectRel = nav_bb_rel; } @@ -7306,6 +7319,7 @@ static void ImGui::NavProcessItem(ImGuiWindow* window, const ImRect& nav_bb, con { result = &g.NavMoveResultLocalVisibleSet; result->ID = id; + result->SelectScopeId = g.MultiSelectScopeId; result->Window = window; result->RectRel = nav_bb_rel; } @@ -7846,8 +7860,13 @@ static void ImGui::NavUpdateMoveResult() ClearActiveID(); g.NavWindow = result->Window; + if (g.NavId != result->ID) + { + // Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags_AllowCurrentNavId) + g.NavJustMovedToId = result->ID; + g.NavJustMovedToSelectScopeId = result->SelectScopeId; + } SetNavIDWithRectRel(result->ID, g.NavLayer, result->RectRel); - g.NavJustMovedToId = result->ID; g.NavMoveFromClampedRefRect = false; } diff --git a/imgui.h b/imgui.h index dd8594412c70..2adb076b0ff3 100644 --- a/imgui.h +++ b/imgui.h @@ -106,6 +106,7 @@ struct ImGuiContext; // Dear ImGui context (opaque structure, unl struct ImGuiIO; // Main configuration and I/O between your application and ImGui struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use) 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, used by IMGUI_ONCE_UPON_A_FRAME macro struct ImGuiPayload; // User data payload for drag and drop operations struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) @@ -141,6 +142,7 @@ typedef int ImGuiDragDropFlags; // -> enum ImGuiDragDropFlags_ // Flags: f typedef int ImGuiFocusedFlags; // -> enum ImGuiFocusedFlags_ // Flags: for IsWindowFocused() typedef int ImGuiHoveredFlags; // -> enum ImGuiHoveredFlags_ // Flags: for IsItemHovered(), IsWindowHovered() etc. typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: for InputText*() +typedef int ImGuiMultiSelectFlags; // -> enum ImGuiMultiSelectFlags_// Flags: for BeginMultiSelect() typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable() typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar() typedef int ImGuiTabItemFlags; // -> enum ImGuiTabItemFlags_ // Flags: for BeginTabItem() @@ -492,6 +494,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 // - FIXME: To be consistent with all the newer API, ListBoxHeader/ListBoxFooter should in reality be called BeginListBox/EndListBox. Will rename them. IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1); @@ -607,6 +617,7 @@ namespace ImGui IMGUI_API bool IsItemEdited(); // did the last item modify its underlying value this frame? or was pressed? This is generally the same as the "bool" return value of many widgets. 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 IsItemToggledSelection(); // was the last item selection toggled? (after Selectable(), TreeNode() etc. We only returns toggle _event_ in order to handle clipping correctly) IMGUI_API bool IsAnyItemHovered(); IMGUI_API bool IsAnyItemActive(); IMGUI_API bool IsAnyItemFocused(); @@ -1073,6 +1084,7 @@ enum ImGuiStyleVar_ ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing ImGuiStyleVar_IndentSpacing, // float IndentSpacing + ImGuiStyleVar_SelectableSpacing, // ImVec2 SelectableSpacing ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding ImGuiStyleVar_GrabMinSize, // float GrabMinSize @@ -1142,6 +1154,17 @@ enum ImGuiMouseCursor_ #endif }; +// 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 +}; + // Enumateration for ImGui::SetWindow***(), SetNextWindow***(), SetNextTreeNode***() 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. @@ -1244,6 +1267,7 @@ struct ImGuiStyle float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). + ImVec2 SelectableSpacing; // Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing). ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. @@ -1672,6 +1696,55 @@ struct ImGuiListClipper IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. }; +// 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-bits 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 ffdc1060e0e6..52724bf30021 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -832,7 +832,7 @@ static void ShowDemoWindowWidgets() } ImGui::TreePop(); } - if (ImGui::TreeNode("Selection State: Multiple Selection")) + if (ImGui::TreeNode("Selection State: Multiple Selection (Simplified)")) { ShowHelpMarker("Hold CTRL and click to select multiple items."); static bool selection[5] = { false, false, false, false, false }; @@ -849,6 +849,66 @@ 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::Pair((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; + ShowHelpMarker("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); + + ImGui::ListBoxHeader("##Basket", ImVec2(-1, 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(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::ListBoxFooter(); + 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(); + } if (ImGui::TreeNode("Rendering more text into the same line")) { // Using the Selectable() override that takes "bool* p_selected" parameter and toggle your booleans automatically. @@ -874,6 +934,14 @@ static void ShowDemoWindowWidgets() if (ImGui::TreeNode("Grid")) { static bool selected[4*4] = { true, false, false, false, false, true, false, false, false, false, true, false, false, false, false, true }; + static float spacing = 0.0f; + ImGui::PushItemWidth(100); + ImGui::SliderFloat("SelectableSpacing", &spacing, 0, 20, "%.0f"); + ImGui::SameLine(); ShowHelpMarker("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 i = 0; i < 4*4; i++) { ImGui::PushID(i); @@ -890,6 +958,7 @@ static void ShowDemoWindowWidgets() if ((i % 4) < 3) ImGui::SameLine(); ImGui::PopID(); } + ImGui::PopStyleVar(2); ImGui::TreePop(); } ImGui::TreePop(); @@ -2795,6 +2864,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(); ShowHelpMarker("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 141c05caa04f..f67f59d68314 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -69,6 +69,7 @@ struct ImGuiGroupData; // Stacked storage data for BeginGroup()/End struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box struct ImGuiItemHoveredDataBackup; // Backup and restore IsItemHovered() internal data struct ImGuiMenuColumns; // Simple column measurement, currently used for MenuItem() only +struct ImGuiMultiSelectState; // Multi-selection state struct ImGuiNavMoveResult; // Result of a directional navigation move query result struct ImGuiNextWindowData; // Storage for SetNexWindow** functions struct ImGuiPopupRef; // Storage for current popup stack @@ -310,7 +311,8 @@ enum ImGuiButtonFlags_ ImGuiButtonFlags_NoKeyModifiers = 1 << 10, // disable interaction if a key modifier is held ImGuiButtonFlags_NoHoldingActiveID = 1 << 11, // don't set ActiveId while holding the mouse (ImGuiButtonFlags_PressedOnClick only) ImGuiButtonFlags_PressedOnDragDropHold = 1 << 12, // press when held into while we are drag and dropping another item (used by e.g. tree nodes, collapsing headers) - ImGuiButtonFlags_NoNavFocus = 1 << 13 // don't override navigation focus when activated + ImGuiButtonFlags_NoNavFocus = 1 << 13, // don't override navigation focus when activated + ImGuiButtonFlags_NoHoveredOnNav = 1 << 14 // don't report as hovered when navigated on }; enum ImGuiSliderFlags_ @@ -358,7 +360,8 @@ enum ImGuiItemStatusFlags_ ImGuiItemStatusFlags_None = 0, ImGuiItemStatusFlags_HoveredRect = 1 << 0, ImGuiItemStatusFlags_HasDisplayRect = 1 << 1, - ImGuiItemStatusFlags_Edited = 1 << 2 // Value exposed by item was edited in the current frame (should match the bool return value of most widgets) + ImGuiItemStatusFlags_Edited = 1 << 2, // Value exposed by item was edited in the current frame (should match the bool return value of most widgets) + ImGuiItemStatusFlags_ToggledSelection = 1 << 3 // Set when Selectable(), TreeNode() reports toggling a selection. We don't report "Selected" because reporting the change allows us to handle clipping with less issues. #ifdef IMGUI_ENABLE_TEST_ENGINE , // [imgui-test only] @@ -417,7 +420,7 @@ enum ImGuiNavHighlightFlags_ ImGuiNavHighlightFlags_None = 0, ImGuiNavHighlightFlags_TypeDefault = 1 << 0, ImGuiNavHighlightFlags_TypeThin = 1 << 1, - ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, + ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, // Draw rectangular highlight if (g.NavId == id) _even_ when using the mouse. ImGuiNavHighlightFlags_NoRounding = 1 << 3 }; @@ -648,6 +651,17 @@ struct ImGuiColumnsSet } }; +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; } +}; + // Data shared between all ImDrawList instances struct IMGUI_API ImDrawListSharedData { @@ -676,6 +690,7 @@ struct ImDrawDataBuilder struct ImGuiNavMoveResult { ImGuiID ID; // Best candidate + ImGuiID SelectScopeId;// Best candidate window current selectable group ID ImGuiWindow* Window; // Best candidate window float DistBox; // Best candidate box distance to current NavId float DistCenter; // Best candidate center distance to current NavId @@ -683,7 +698,7 @@ struct ImGuiNavMoveResult ImRect RectRel; // Best candidate bounding box in window relative space ImGuiNavMoveResult() { Clear(); } - void Clear() { ID = 0; Window = NULL; DistBox = DistCenter = DistAxial = FLT_MAX; RectRel = ImRect(); } + void Clear() { ID = SelectScopeId = 0; Window = NULL; DistBox = DistCenter = DistAxial = FLT_MAX; RectRel = ImRect(); } }; // Storage for SetNexWindow** functions @@ -773,6 +788,7 @@ struct ImGuiContext float ActiveIdTimer; bool ActiveIdIsJustActivated; // Set at the time of activation for one frame bool ActiveIdAllowOverlap; // Active widget allows another widget to steal active id (generally for overlapping widgets, but not always) + bool ActiveIdPressed; // Track whether the active id led to a press (this is to allow changing between PressOnClick and PressOnRelease without pressing twice) bool ActiveIdHasBeenEdited; // Was the value associated to the widget Edited over the course of the Active state. bool ActiveIdPreviousFrameIsAlive; bool ActiveIdPreviousFrameHasBeenEdited; @@ -792,6 +808,8 @@ struct ImGuiContext ImVector OpenPopupStack; // Which popups are open (persistent) ImVector BeginPopupStack; // Which level of BeginPopup() we are in (reset every frame) ImGuiNextWindowData NextWindowData; // Storage for SetNextWindow** functions + void* NextItemMultiSelectData; + bool NextItemMultiSelectDataIsSet; bool NextTreeNodeOpenVal; // Storage for SetNextTreeNode** functions ImGuiCond NextTreeNodeOpenCond; @@ -804,6 +822,7 @@ struct ImGuiContext ImGuiID NavInputId; // ~~ IsNavInputPressed(ImGuiNavInput_Input) ? NavId : 0 ImGuiID NavJustTabbedId; // Just tabbed to this id. ImGuiID NavJustMovedToId; // Just navigated to this id (result of a successfully MoveRequest) + ImGuiID NavJustMovedToSelectScopeId; ImGuiID NavNextActivateId; // Set by ActivateItem(), queued until next frame ImGuiInputSource NavInputSource; // Keyboard or Gamepad mode? THIS WILL ONLY BE None or NavGamepad or NavKeyboard. ImRect NavScoringRectScreen; // Rectangle used for scoring, in screen space. Based of window->DC.NavRefRectRel[], modified for directional navigation scoring. @@ -879,6 +898,10 @@ struct ImGuiContext // Platform support ImVec2 PlatformImePos, PlatformImeLastPos; // Cursor position request & last passed to the OS Input Method Editor + ImGuiID MultiSelectScopeId; + ImGuiWindow* MultiSelectScopeWindow; + ImGuiMultiSelectFlags MultiSelectFlags; + ImGuiMultiSelectState MultiSelectState; // Settings bool SettingsLoaded; @@ -929,6 +952,7 @@ struct ImGuiContext ActiveIdTimer = 0.0f; ActiveIdIsJustActivated = false; ActiveIdAllowOverlap = false; + ActiveIdPressed = false; ActiveIdHasBeenEdited = false; ActiveIdPreviousFrameIsAlive = false; ActiveIdPreviousFrameHasBeenEdited = false; @@ -941,12 +965,14 @@ struct ImGuiContext LastActiveIdTimer = 0.0f; LastValidMousePos = ImVec2(0.0f, 0.0f); MovingWindow = NULL; + NextItemMultiSelectData = NULL; + NextItemMultiSelectDataIsSet = false; NextTreeNodeOpenVal = false; NextTreeNodeOpenCond = 0; NavWindow = NULL; NavId = NavActivateId = NavActivateDownId = NavActivatePressedId = NavInputId = 0; - NavJustTabbedId = NavJustMovedToId = NavNextActivateId = 0; + NavJustTabbedId = NavJustMovedToId = NavJustMovedToSelectScopeId = NavNextActivateId = 0; NavInputSource = ImGuiInputSource_None; NavScoringRectScreen = ImRect(); NavScoringCount = 0; @@ -993,6 +1019,9 @@ struct ImGuiContext ScrollbarClickDeltaToGrabCenter = ImVec2(0.0f, 0.0f); TooltipOverrideCount = 0; PlatformImePos = PlatformImeLastPos = ImVec2(FLT_MAX, FLT_MAX); + MultiSelectScopeId = 0; + MultiSelectScopeWindow = NULL; + MultiSelectFlags = 0; SettingsLoaded = false; SettingsDirtyTimer = 0.0f; @@ -1385,6 +1414,10 @@ namespace ImGui IMGUI_API void EndColumns(); // close columns IMGUI_API void PushColumnClipRect(int column_index = -1); + // 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); + // Tab Bars IMGUI_API bool BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& bb, ImGuiTabBarFlags flags); IMGUI_API ImGuiTabItem* TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index feeb3f9ec655..75aae7b657b5 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 @@ -485,7 +486,8 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool // Gamepad/Keyboard navigation // We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse. if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId)) - hovered = true; + if (!(flags & ImGuiButtonFlags_NoHoveredOnNav)) + hovered = true; if (g.NavActivateDownId == id) { @@ -507,6 +509,8 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool bool held = false; if (g.ActiveId == id) { + if (pressed) + g.ActiveIdPressed = true; if (g.ActiveIdSource == ImGuiInputSource_Mouse) { if (g.ActiveIdIsJustActivated) @@ -4809,23 +4813,46 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l } // Flags that affects opening behavior: - // - 0(default) ..................... single-click anywhere to open + // - 0 (default) .................... single-click anywhere to open // - OpenOnDoubleClick .............. double-click anywhere to open // - OpenOnArrow .................... single-click on arrow to open // - OpenOnDoubleClick|OpenOnArrow .. single-click on arrow or double-click anywhere to open - ImGuiButtonFlags button_flags = ImGuiButtonFlags_NoKeyModifiers | ((flags & ImGuiTreeNodeFlags_AllowItemOverlap) ? ImGuiButtonFlags_AllowItemOverlap : 0); - if (!is_leaf) - button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; - if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) - button_flags |= ImGuiButtonFlags_PressedOnDoubleClick | ((flags & ImGuiTreeNodeFlags_OpenOnArrow) ? ImGuiButtonFlags_PressedOnClickRelease : 0); + ImGuiButtonFlags button_flags = 0; + if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) button_flags |= ImGuiButtonFlags_AllowItemOverlap; + if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) button_flags |= ImGuiButtonFlags_PressedOnDoubleClick | ((flags & ImGuiTreeNodeFlags_OpenOnArrow) ? ImGuiButtonFlags_PressedOnClickRelease : 0); + if (!is_leaf) button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; + + // Multi-selection support (header) + const bool is_multi_select = (g.MultiSelectScopeWindow == window); + bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0; + const bool was_selected = selected; + if (is_multi_select) + { + flags |= ImGuiTreeNodeFlags_OpenOnArrow; + MultiSelectItemHeader(id, &selected); + button_flags |= ImGuiButtonFlags_NoHoveredOnNav; + + // 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.ActiveIdPressed)) + 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 hovered, held, pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); + bool toggled = false; if (!is_leaf) { - bool toggled = false; if (pressed) { - toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id); + toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id && !is_multi_select); if (flags & ImGuiTreeNodeFlags_OpenOnArrow) toggled |= IsMouseHoveringRect(interact_bb.Min, ImVec2(interact_bb.Min.x + text_offset_x, interact_bb.Max.y)) && (!g.NavDisableMouseHover); if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) @@ -4851,17 +4878,32 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l window->DC.StateStorage->SetInt(id, is_open); } } + + // 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); + } + if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) SetItemAllowOverlap(); + if (selected != was_selected) + window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_ToggledSelection; // Render const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); const ImVec2 text_pos = frame_bb.Min + ImVec2(text_offset_x, text_base_offset_y); + 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 RenderFrame(frame_bb.Min, frame_bb.Max, col, true, style.FrameRounding); - RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin); + RenderNavHighlight(frame_bb, id, nav_highlight_flags); RenderArrow(frame_bb.Min + ImVec2(padding.x, text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 1.0f); if (g.LogEnabled) { @@ -4880,11 +4922,9 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l else { // Unframed typed for tree nodes - if (hovered || (flags & ImGuiTreeNodeFlags_Selected)) - { + if (hovered || selected) RenderFrame(frame_bb.Min, frame_bb.Max, col, false); - RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin); - } + RenderNavHighlight(frame_bb, id, nav_highlight_flags); if (flags & ImGuiTreeNodeFlags_Bullet) RenderBullet(frame_bb.Min + ImVec2(text_offset_x * 0.5f, g.FontSize*0.50f + text_base_offset_y)); @@ -5040,15 +5080,15 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_DrawFillAvailWidth)) bb.Max.x += window_padding.x; - // Selectables are tightly packed together, we extend the box to cover spacing between selectable. - float spacing_L = (float)(int)(style.ItemSpacing.x * 0.5f); - float spacing_U = (float)(int)(style.ItemSpacing.y * 0.5f); - float spacing_R = style.ItemSpacing.x - spacing_L; - float spacing_D = style.ItemSpacing.y - spacing_U; + // Selectables are tightly packed together so we extend the box to cover spacing between selectable. + float spacing_x = ImMax(style.ItemSpacing.x - style.SelectableSpacing.x, 0.0f); + float spacing_y = ImMax(style.ItemSpacing.y - style.SelectableSpacing.y, 0.0f); + float spacing_L = (float)(int)(spacing_x * 0.50f); + float spacing_U = (float)(int)(spacing_y * 0.50f); bb.Min.x -= spacing_L; bb.Min.y -= spacing_U; - bb.Max.x += spacing_R; - bb.Max.y += spacing_D; + bb.Max.x += (spacing_x - spacing_L); + bb.Max.y += (spacing_y - spacing_U); if (!ItemAdd(bb, id)) { if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) @@ -5063,13 +5103,34 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (flags & ImGuiSelectableFlags_PressedOnRelease) button_flags |= ImGuiButtonFlags_PressedOnRelease; if (flags & ImGuiSelectableFlags_Disabled) button_flags |= ImGuiButtonFlags_Disabled; if (flags & ImGuiSelectableFlags_AllowDoubleClick) button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); if (flags & ImGuiSelectableFlags_Disabled) selected = false; + // 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_NoHoveredOnNav; + + // 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.ActiveIdPressed)) + button_flags |= ImGuiButtonFlags_PressedOnClick; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + } + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); + + // Multi-selection support (footer) + if (is_multi_select) + MultiSelectItemFooter(id, &selected, &pressed); + // Hovering selectable with mouse updates NavId accordingly so navigation can be resumed with gamepad/keyboard (this doesn't happen on most widgets) - if (pressed || hovered) + if (pressed || (hovered && !is_multi_select)) if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent) { g.NavDisableHighlight = true; @@ -5081,10 +5142,18 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl // 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); - 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 ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) { @@ -5099,7 +5168,11 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl // Automatically close popups if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(window->DC.ItemFlags & ImGuiItemFlags_SelectableDontClosePopup)) CloseCurrentPopup(); - return pressed; + + if (selected != was_selected) + window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + + return pressed || (was_selected != selected); } bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) @@ -5112,6 +5185,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.NavJustMovedToSelectScopeId == 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; + +#if 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; + +#if 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.NextItemMultiSelectData = item_data; + g.NextItemMultiSelectDataIsSet = true; +} + +void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected) +{ + ImGuiContext& g = *GImGui; + ImGuiMultiSelectState* state = &g.MultiSelectState; + + IM_ASSERT(g.NextItemMultiSelectDataIsSet && "Forgot to call SetNextItemMultiSelectData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); + void* item_data = g.NextItemMultiSelectData; + + // 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.NextItemMultiSelectData; + g.NextItemMultiSelectDataIsSet = 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(window->DC.LastItemId, 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 == window->DC.LastItemId) ? 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 //-------------------------------------------------------------------------