MultiSelect: WIP range-select (#1861) (rebased six millions times)
diff --git a/imgui.cpp b/imgui.cpp
index 733a5df..b1e9903 100644
--- a/imgui.cpp
+++ b/imgui.cpp
@@ -1273,6 +1273,7 @@
TableAngledHeadersTextAlign = ImVec2(0.5f,0.0f);// Alignment of angled headers within the cell
ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right.
ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text.
+ SelectableSpacing = ImVec2(0.0f,0.0f);// Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing).
SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line.
SeparatorTextBorderSize = 3.0f; // Thickkness of border in SeparatorText()
SeparatorTextAlign = ImVec2(0.0f,0.5f);// Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center).
@@ -1321,6 +1322,7 @@
LogSliderDeadzone = ImTrunc(LogSliderDeadzone * scale_factor);
TabRounding = ImTrunc(TabRounding * scale_factor);
TabMinWidthForCloseButton = (TabMinWidthForCloseButton != FLT_MAX) ? ImTrunc(TabMinWidthForCloseButton * scale_factor) : FLT_MAX;
+ SelectableSpacing = ImTrunc(SelectableSpacing * scale_factor);
SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor);
DisplayWindowPadding = ImTrunc(DisplayWindowPadding * scale_factor);
DisplaySafeAreaPadding = ImTrunc(DisplaySafeAreaPadding * scale_factor);
@@ -3257,6 +3259,7 @@
{ ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersAngle)}, // ImGuiStyleVar_TableAngledHeadersAngle
{ ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign
{ ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign
+ { ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SelectableSpacing) }, // ImGuiStyleVar_SelectableSpacing
{ ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign
{ ImGuiDataType_Float, 1, (ImU32)offsetof(ImGuiStyle, SeparatorTextBorderSize)}, // ImGuiStyleVar_SeparatorTextBorderSize
{ ImGuiDataType_Float, 2, (ImU32)offsetof(ImGuiStyle, SeparatorTextAlign) }, // ImGuiStyleVar_SeparatorTextAlign
@@ -3822,6 +3825,7 @@
g.MenusIdSubmittedThisFrame.clear();
g.InputTextState.ClearFreeMemory();
g.InputTextDeactivatedState.ClearFreeMemory();
+ g.MultiSelectScopeWindow = NULL;
g.SettingsWindows.clear();
g.SettingsHandlers.clear();
diff --git a/imgui.h b/imgui.h
index 26965fd..80e6ca7 100644
--- a/imgui.h
+++ b/imgui.h
@@ -44,6 +44,7 @@
// [SECTION] ImGuiIO
// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload)
// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor)
+// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectData)
// [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData)
// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont)
// [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport)
@@ -174,6 +175,7 @@
struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use)
struct ImGuiKeyData; // Storage for ImGuiIO and IsKeyDown(), IsKeyPressed() etc functions.
struct ImGuiListClipper; // Helper to manually clip large list of items
+struct ImGuiMultiSelectData; // State for a BeginMultiSelect() block
struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame
struct ImGuiPayload; // User data payload for drag and drop operations
struct ImGuiPlatformImeData; // Platform IME data for io.PlatformSetImeDataFn() function.
@@ -227,6 +229,7 @@
typedef int ImGuiItemFlags; // -> enum ImGuiItemFlags_ // Flags: for PushItemFlag(), shared by all items
typedef int ImGuiKeyChord; // -> ImGuiKey | ImGuiMod_XXX // Flags: for IsKeyChordPressed(), Shortcut() etc. an ImGuiKey optionally OR-ed with one or more ImGuiMod_XXX values.
typedef int ImGuiPopupFlags; // -> enum ImGuiPopupFlags_ // Flags: for OpenPopup*(), BeginPopupContext*(), IsPopupOpen()
+typedef int ImGuiMultiSelectFlags; // -> enum ImGuiMultiSelectFlags_// Flags: for BeginMultiSelect()
typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable()
typedef int ImGuiSliderFlags; // -> enum ImGuiSliderFlags_ // Flags: for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc.
typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar()
@@ -262,6 +265,10 @@
typedef ImWchar16 ImWchar;
#endif
+// Multi-Selection item index or identifier when using SetNextItemSelectionUserData()/BeginMultiSelect()
+// (Most users are likely to use this store an item INDEX but this may be used to store a POINTER as well.)
+typedef ImS64 ImGuiSelectionUserData;
+
// Callback and functions types
typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData* data); // Callback function for ImGui::InputText()
typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); // Callback function for ImGui::SetNextWindowSizeConstraints()
@@ -661,6 +668,14 @@
IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height
IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper.
+ // Multi-selection system for Selectable() and TreeNode() functions.
+ // This enables standard multi-selection/range-selection idioms (CTRL+Click/Arrow, SHIFT+Click/Arrow, etc) in a way that allow items to be fully clipped (= not submitted at all) when not visible.
+ // Read comments near ImGuiMultiSelectData for details.
+ // When enabled, Selectable() and TreeNode() functions will return true when selection needs toggling.
+ IMGUI_API ImGuiMultiSelectData* BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected);
+ IMGUI_API ImGuiMultiSelectData* EndMultiSelect();
+ IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data);
+
// Widgets: List Boxes
// - This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
// - You can submit contents and manage your selection state however you want it, by creating e.g. Selectable() or any other items.
@@ -893,6 +908,7 @@
IMGUI_API bool IsItemDeactivated(); // was the last item just made inactive (item was previously active). Useful for Undo/Redo patterns with widgets that require continuous editing.
IMGUI_API bool IsItemDeactivatedAfterEdit(); // was the last item just made inactive and made a value change when it was active? (e.g. Slider/Drag moved). Useful for Undo/Redo patterns with widgets that require continuous editing. Note that you may get false positives (some widgets such as Combo()/ListBox()/Selectable() will return true even when clicking an already selected item).
IMGUI_API bool IsItemToggledOpen(); // was the last item open state toggled? set by TreeNode().
+ IMGUI_API bool IsItemToggledSelection(); // was the last item selection state toggled? (after Selectable(), TreeNode() etc. We only returns toggle _event_ in order to handle clipping correctly)
IMGUI_API bool IsAnyItemHovered(); // is any item hovered?
IMGUI_API bool IsAnyItemActive(); // is any item active?
IMGUI_API bool IsAnyItemFocused(); // is any item focused?
@@ -1683,6 +1699,7 @@
ImGuiStyleVar_TableAngledHeadersAngle, // float TableAngledHeadersAngle
ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2 TableAngledHeadersTextAlign
ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign
+ ImGuiStyleVar_SelectableSpacing, // ImVec2 SelectableSpacing
ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign
ImGuiStyleVar_SeparatorTextBorderSize, // float SeparatorTextBorderSize
ImGuiStyleVar_SeparatorTextAlign, // ImVec2 SeparatorTextAlign
@@ -2127,6 +2144,7 @@
ImVec2 TableAngledHeadersTextAlign;// Alignment of angled headers within the cell
ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right.
ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered).
+ ImVec2 SelectableSpacing; // Horizontal and vertical spacing between selectables (by default they are canceling out the effect of ItemSpacing).
ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line.
float SeparatorTextBorderSize; // Thickkness of border in SeparatorText()
ImVec2 SeparatorTextAlign; // Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center).
@@ -2703,6 +2721,70 @@
};
//-----------------------------------------------------------------------------
+// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectData)
+//-----------------------------------------------------------------------------
+
+// Flags for BeginMultiSelect().
+// This system is designed to allow mouse/keyboard multi-selection, including support for range-selection (SHIFT + click) which is difficult to re-implement manually.
+// If you disable multi-selection with ImGuiMultiSelectFlags_NoMultiSelect (which is provided for consistency and flexibility), the whole BeginMultiSelect() system
+// becomes largely overkill as you can handle single-selection in a simpler manner by just calling Selectable() and reacting on clicks yourself.
+enum ImGuiMultiSelectFlags_
+{
+ ImGuiMultiSelectFlags_NoMultiSelect = 1 << 0,
+ ImGuiMultiSelectFlags_NoUnselect = 1 << 1, // Disable unselecting items with CTRL+Click, CTRL+Space etc.
+ ImGuiMultiSelectFlags_NoSelectAll = 1 << 2, // Disable CTRL+A shortcut to set RequestSelectAll
+};
+
+// Abstract:
+// - This system implements standard multi-selection idioms (CTRL+Click/Arrow, SHIFT+Click/Arrow, etc) in a way that allow items to be
+// fully clipped (= not submitted at all) when not visible. Clipping is typically provided by ImGuiListClipper.
+// Handling all of this in a single pass imgui is a little tricky, and this is why we provide those functionalities.
+// Note however that if you don't need SHIFT+Click/Arrow range-select, you can handle a simpler form of multi-selection yourself,
+// by reacting to click/presses on Selectable() items and checking keyboard modifiers.
+// The complexity of this system here is mostly caused by the handling of range-select while optionally allowing to clip elements.
+// - The work involved to deal with multi-selection differs whether you want to only submit visible items (and clip others) or submit all items
+// regardless of their visibility. Clipping items is more efficient and will allow you to deal with large lists (1k~100k items) with near zero
+// performance penalty, but requires a little more work on the code. If you only have a few hundreds elements in your possible selection set,
+// you may as well not bother with clipping, as the cost should be negligible (as least on imgui side).
+// If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards.
+// - The void* Src/Dst value represent a selectable object. They are the values you pass to SetNextItemMultiSelectData().
+// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points. But the code never assume that sortable integers are used.
+// - In the spirit of imgui design, your code own the selection data. So this is designed to handle all kind of selection data: instructive (store a bool inside each object),
+// external array (store an array aside from your objects), set (store only selected items in a hash/map/set), using intervals (store indices in an interval tree), etc.
+// Usage flow:
+// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc and its selection status. As a default value for the initial frame or when,
+// resetting your selection state: you may use the value for your first item or a "null" value that matches the type stored in your void*.
+// 2) Honor Clear/SelectAll requests by updating your selection data. [Only required if you are using a clipper in step 4]
+// 3) Set RangeSrcPassedBy=true if the RangeSrc item is part of the items clipped before the first submitted/visible item. [Only required if you are using a clipper in step 4]
+// This is because for range-selection we need to know if we are currently "inside" or "outside" the range.
+// If you are using integer indices everywhere, this is easy to compute: if (clipper.DisplayStart > (int)data->RangeSrc) { data->RangeSrcPassedBy = true; }
+// 4) Submit your items with SetNextItemMultiSelectData() + Selectable()/TreeNode() calls.
+// Call IsItemSelectionToggled() to query with the selection state has been toggled, in which you need the info immediately (before EndMultiSelect()) for your display.
+// When cannot reliably return a "IsItemSelected()" value because we need to consider clipped (unprocessed) item, this is why we return a toggle event instead.
+// 5) Call EndMultiSelect(). Save the value of ->RangeSrc for the next frame (you may convert the value in a format that is safe for persistance)
+// 6) Honor Clear/SelectAll/SetRange requests by updating your selection data. Always process them in this order (as you will receive Clear+SetRange request simultaneously)
+// If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable() on a per-item basis.
+struct ImGuiMultiSelectData
+{
+ bool RequestClear; // Begin, End // Request user to clear selection
+ bool RequestSelectAll; // Begin, End // Request user to select all
+ bool RequestSetRange; // End // Request user to set or clear selection in the [RangeSrc..RangeDst] range
+ bool RangeSrcPassedBy; // After Begin // Need to be set by user is RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping.
+ bool RangeValue; // End // End: parameter from RequestSetRange request. True = Select Range, False = Unselect range.
+ void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect()
+ void* RangeDst; // End // End: parameter from RequestSetRange request.
+ int RangeDirection; // End // End: parameter from RequestSetRange request. +1 if RangeSrc came before RangeDst, -1 otherwise. Available as an indicator in case you cannot infer order from the void* values.
+
+ ImGuiMultiSelectData() { Clear(); }
+ void Clear()
+ {
+ RequestClear = RequestSelectAll = RequestSetRange = RangeSrcPassedBy = RangeValue = false;
+ RangeSrc = RangeDst = NULL;
+ RangeDirection = 0;
+ }
+};
+
+//-----------------------------------------------------------------------------
// [SECTION] Drawing API (ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData)
// Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList.
//-----------------------------------------------------------------------------
diff --git a/imgui_demo.cpp b/imgui_demo.cpp
index dd25345..8e73126 100644
--- a/imgui_demo.cpp
+++ b/imgui_demo.cpp
@@ -72,6 +72,7 @@
// [SECTION] Demo Window / ShowDemoWindow()
// - ShowDemoWindow()
// - sub section: ShowDemoWindowWidgets()
+// - sub section: ShowDemoWindowMultiSelect()
// - sub section: ShowDemoWindowLayout()
// - sub section: ShowDemoWindowPopups()
// - sub section: ShowDemoWindowTables()
@@ -214,6 +215,7 @@
// We split the contents of the big ShowDemoWindow() function into smaller functions
// (because the link time of very large functions grow non-linearly)
static void ShowDemoWindowWidgets();
+static void ShowDemoWindowMultiSelect();
static void ShowDemoWindowLayout();
static void ShowDemoWindowPopups();
static void ShowDemoWindowTables();
@@ -251,6 +253,7 @@
//-----------------------------------------------------------------------------
// - ShowDemoWindow()
// - ShowDemoWindowWidgets()
+// - ShowDemoWindowMultiSelect()
// - ShowDemoWindowLayout()
// - ShowDemoWindowPopups()
// - ShowDemoWindowTables()
@@ -1371,37 +1374,6 @@
ImGui::TreePop();
}
- IMGUI_DEMO_MARKER("Widgets/Selectables/Single Selection");
- if (ImGui::TreeNode("Selection State: Single Selection"))
- {
- static int selected = -1;
- for (int n = 0; n < 5; n++)
- {
- char buf[32];
- sprintf(buf, "Object %d", n);
- if (ImGui::Selectable(buf, selected == n))
- selected = n;
- }
- ImGui::TreePop();
- }
- IMGUI_DEMO_MARKER("Widgets/Selectables/Multiple Selection");
- if (ImGui::TreeNode("Selection State: Multiple Selection"))
- {
- HelpMarker("Hold CTRL and click to select multiple items.");
- static bool selection[5] = { false, false, false, false, false };
- for (int n = 0; n < 5; n++)
- {
- char buf[32];
- sprintf(buf, "Object %d", n);
- if (ImGui::Selectable(buf, selection[n]))
- {
- if (!ImGui::GetIO().KeyCtrl) // Clear selection when CTRL is not held
- memset(selection, 0, sizeof(selection));
- selection[n] ^= 1;
- }
- }
- ImGui::TreePop();
- }
IMGUI_DEMO_MARKER("Widgets/Selectables/Rendering more items on the same line");
if (ImGui::TreeNode("Rendering more items on the same line"))
{
@@ -1461,6 +1433,15 @@
if (winning_state)
ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f + 0.5f * cosf(time * 2.0f), 0.5f + 0.5f * sinf(time * 3.0f)));
+ static float spacing = 0.0f;
+ ImGui::PushItemWidth(100);
+ ImGui::SliderFloat("SelectableSpacing", &spacing, 0, 20, "%.0f");
+ ImGui::SameLine(); HelpMarker("Selectable cancel out the regular spacing between items by extending itself by ItemSpacing/2 in each direction.\nThis has two purposes:\n- Avoid the gap between items so the mouse is always hitting something.\n- Avoid the gap between items so range-selected item looks connected.\nBy changing SelectableSpacing we can enforce spacing between selectables.");
+ ImGui::PopItemWidth();
+ ImGui::Spacing();
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 8));
+ ImGui::PushStyleVar(ImGuiStyleVar_SelectableSpacing, ImVec2(spacing, spacing));
+
for (int y = 0; y < 4; y++)
for (int x = 0; x < 4; x++)
{
@@ -1479,8 +1460,10 @@
ImGui::PopID();
}
+ ImGui::PopStyleVar(2);
if (winning_state)
ImGui::PopStyleVar();
+
ImGui::TreePop();
}
IMGUI_DEMO_MARKER("Widgets/Selectables/Alignment");
@@ -1509,6 +1492,8 @@
ImGui::TreePop();
}
+ ShowDemoWindowMultiSelect();
+
// To wire InputText() with std::string or any other custom string type,
// see the "Text Input > Resize Callback" section of this demo, and the misc/cpp/imgui_stdlib.h file.
IMGUI_DEMO_MARKER("Widgets/Text Input");
@@ -2785,6 +2770,113 @@
}
}
+static void ShowDemoWindowMultiSelect()
+{
+ IMGUI_DEMO_MARKER("Widgets/Selection State");
+ if (ImGui::TreeNode("Selection State"))
+ {
+ HelpMarker("Selections can be built under Selectable(), TreeNode() or other widgets. Selection state is owned by application code/data.");
+
+ IMGUI_DEMO_MARKER("Widgets/Selection State/Single Selection");
+ if (ImGui::TreeNode("Single Selection"))
+ {
+ static int selected = -1;
+ for (int n = 0; n < 5; n++)
+ {
+ char buf[32];
+ sprintf(buf, "Object %d", n);
+ if (ImGui::Selectable(buf, selected == n))
+ selected = n;
+ }
+ ImGui::TreePop();
+ }
+
+ IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (Basic)");
+ if (ImGui::TreeNode("Multiple Selection (Basic)"))
+ {
+ HelpMarker("Hold CTRL and click to select multiple items.");
+ static bool selection[5] = { false, false, false, false, false };
+ for (int n = 0; n < 5; n++)
+ {
+ char buf[32];
+ sprintf(buf, "Object %d", n);
+ if (ImGui::Selectable(buf, selection[n]))
+ {
+ if (!ImGui::GetIO().KeyCtrl) // Clear selection when CTRL is not held
+ memset(selection, 0, sizeof(selection));
+ selection[n] ^= 1;
+ }
+ }
+ ImGui::TreePop();
+ }
+
+ IMGUI_DEMO_MARKER("Widgets/Selection State/Multiple Selection (Full)");
+ if (ImGui::TreeNode("Multiple Selection (Full)"))
+ {
+ // Demonstrate holding/updating multi-selection data and using the BeginMultiSelect/EndMultiSelect API to support range-selection and clipping.
+ // In this demo we use ImGuiStorage (simple key->value storage) to avoid external dependencies but it's probably not optimal.
+ // In your real code you could use e.g std::unordered_set<> or your own data structure for storing selection.
+ // If you don't mind being limited to one view over your objects, the simplest way is to use an intrusive selection (e.g. store bool inside object, as used in examples above).
+ // Otherwise external set/hash/map/interval trees (storing indices, etc.) may be appropriate.
+ struct MySelection
+ {
+ ImGuiStorage Storage;
+ void Clear() { Storage.Clear(); }
+ void SelectAll(int count) { Storage.Data.reserve(count); Storage.Data.resize(0); for (int n = 0; n < count; n++) Storage.Data.push_back(ImGuiStoragePair((ImGuiID)n, 1)); }
+ void SetRange(int a, int b, int sel) { if (b < a) { int tmp = b; b = a; a = tmp; } for (int n = a; n <= b; n++) Storage.SetInt((ImGuiID)n, sel); }
+ bool GetSelected(int id) const { return Storage.GetInt((ImGuiID)id) != 0; }
+ void SetSelected(int id, bool v) { SetRange(id, id, v ? 1 : 0); }
+ };
+
+ static int selection_ref = 0; // Selection pivot (last clicked item, we need to preserve this to handle range-select)
+ static MySelection selection;
+ const char* random_names[] =
+ {
+ "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper",
+ "Bitter Gourd", "Bok Choy", "Broccoli", "Brussels Sprouts", "Burdock Root", "Cabbage", "Calabash", "Capers", "Carrot", "Cassava",
+ "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Celtuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber"
+ };
+
+ int COUNT = 1000;
+ HelpMarker("Hold CTRL and click to select multiple items. Hold SHIFT to select a range.");
+ ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", (unsigned int*)&ImGui::GetIO().ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard);
+
+ if (ImGui::BeginListBox("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20)))
+ {
+ ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(0, (void*)(intptr_t)selection_ref, selection.GetSelected((int)selection_ref));
+ if (multi_select_data->RequestClear) { selection.Clear(); }
+ if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); }
+ ImGuiListClipper clipper;
+ clipper.Begin(COUNT);
+ while (clipper.Step())
+ {
+ if (clipper.DisplayStart > (int)selection_ref)
+ multi_select_data->RangeSrcPassedBy = true;
+ for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++)
+ {
+ ImGui::PushID(n);
+ char label[64];
+ sprintf(label, "Object %05d (category: %s)", n, random_names[n % IM_ARRAYSIZE(random_names)]);
+ bool item_is_selected = selection.GetSelected(n);
+ ImGui::SetNextItemSelectionUserData(n);
+ if (ImGui::Selectable(label, item_is_selected))
+ selection.SetSelected(n, !item_is_selected);
+ ImGui::PopID();
+ }
+ }
+ multi_select_data = ImGui::EndMultiSelect();
+ selection_ref = (int)(intptr_t)multi_select_data->RangeSrc;
+ ImGui::EndListBox();
+ if (multi_select_data->RequestClear) { selection.Clear(); }
+ if (multi_select_data->RequestSelectAll) { selection.SelectAll(COUNT); }
+ if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); }
+ }
+ ImGui::TreePop();
+ }
+ ImGui::TreePop();
+ }
+}
+
static void ShowDemoWindowLayout()
{
IMGUI_DEMO_MARKER("Layout");
@@ -6771,6 +6863,7 @@
ImGui::SliderFloat2("FramePadding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f");
ImGui::SliderFloat2("ItemSpacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f");
ImGui::SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f");
+ ImGui::SliderFloat2("SelectableSpacing", (float*)&style.SelectableSpacing, 0.0f, 20.0f, "%.0f"); ImGui::SameLine(); HelpMarker("SelectableSpacing must be < ItemSpacing.\nSelectables display their highlight after canceling out the effect of ItemSpacing, so they can be look tightly packed. This setting allows to enforce spacing between them.");
ImGui::SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, 0.0f, 10.0f, "%.0f");
ImGui::SliderFloat("IndentSpacing", &style.IndentSpacing, 0.0f, 30.0f, "%.0f");
ImGui::SliderFloat("ScrollbarSize", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f");
diff --git a/imgui_internal.h b/imgui_internal.h
index 5d8f233..63b8040 100644
--- a/imgui_internal.h
+++ b/imgui_internal.h
@@ -134,6 +134,7 @@
struct ImGuiLastItemData; // Status storage for last submitted items
struct ImGuiLocEntry; // A localization entry.
struct ImGuiMenuColumns; // Simple column measurement, currently used for MenuItem() only
+struct ImGuiMultiSelectState; // Multi-selection state
struct ImGuiNavItemData; // Result of a gamepad/keyboard directional navigation move query result
struct ImGuiMetricsConfig; // Storage for ShowMetricsWindow() and DebugNodeXXX() functions
struct ImGuiNextWindowData; // Storage for SetNextWindow** functions
@@ -854,6 +855,7 @@
// Controlled by widget code
ImGuiItemFlags_Inputable = 1 << 20, // false // [WIP] Auto-activate input mode when tab focused. Currently only used and supported by a few items before it becomes a generic feature.
ImGuiItemFlags_HasSelectionUserData = 1 << 21, // false // Set by SetNextItemSelectionUserData()
+ ImGuiItemFlags_IsMultiSelect = 1 << 22, // false // Set by SetNextItemSelectionUserData()
ImGuiItemFlags_Default_ = ImGuiItemFlags_AutoClosePopups, // Please don't change, use PushItemFlag() instead.
@@ -1201,10 +1203,6 @@
inline void ClearFlags() { Flags = ImGuiNextWindowDataFlags_None; }
};
-// Multi-Selection item index or identifier when using SetNextItemSelectionUserData()/BeginMultiSelect()
-// (Most users are likely to use this store an item INDEX but this may be used to store a POINTER as well.)
-typedef ImS64 ImGuiSelectionUserData;
-
enum ImGuiNextItemDataFlags_
{
ImGuiNextItemDataFlags_None = 0,
@@ -1711,8 +1709,20 @@
// We always assume that -1 is an invalid value (which works for indices and pointers)
#define ImGuiSelectionUserData_Invalid ((ImGuiSelectionUserData)-1)
+#define IMGUI_HAS_MULTI_SELECT 1
#ifdef IMGUI_HAS_MULTI_SELECT
-// <this is filled in 'range_select' branch>
+
+struct IMGUI_API ImGuiMultiSelectState
+{
+ ImGuiMultiSelectData In; // The In requests are set and returned by BeginMultiSelect()
+ ImGuiMultiSelectData Out; // The Out requests are finalized and returned by EndMultiSelect()
+ bool InRangeDstPassedBy; // (Internal) set by the the item that match NavJustMovedToId when InRequestRangeSetNav is set.
+ bool InRequestSetRangeNav; // (Internal) set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation.
+
+ ImGuiMultiSelectState() { Clear(); }
+ void Clear() { In.Clear(); Out.Clear(); InRangeDstPassedBy = InRequestSetRangeNav = false; }
+};
+
#endif // #ifdef IMGUI_HAS_MULTI_SELECT
//-----------------------------------------------------------------------------
@@ -2107,6 +2117,12 @@
ImVec2 NavWindowingAccumDeltaPos;
ImVec2 NavWindowingAccumDeltaSize;
+ // Range-Select/Multi-Select
+ ImGuiID MultiSelectScopeId;
+ ImGuiWindow* MultiSelectScopeWindow;
+ ImGuiMultiSelectFlags MultiSelectFlags;
+ ImGuiMultiSelectState MultiSelectState;
+
// Render
float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list)
@@ -2370,6 +2386,10 @@
NavWindowingToggleLayer = false;
NavWindowingToggleKey = ImGuiKey_None;
+ MultiSelectScopeId = 0;
+ MultiSelectScopeWindow = NULL;
+ MultiSelectFlags = 0;
+
DimBgRatio = 0.0f;
DragDropActive = DragDropWithinSource = DragDropWithinTarget = false;
@@ -3144,7 +3164,6 @@
IMGUI_API ImVec2 CalcItemSize(ImVec2 size, float default_w, float default_h);
IMGUI_API float CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x);
IMGUI_API void PushMultiItemsWidths(int components, float width_full);
- IMGUI_API bool IsItemToggledSelection(); // Was the last item selection toggled? (after Selectable(), TreeNode() etc. We only returns toggle _event_ in order to handle clipping correctly)
IMGUI_API ImVec2 GetContentRegionMaxAbs();
IMGUI_API void ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess);
@@ -3325,6 +3344,10 @@
IMGUI_API int TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx);
IMGUI_API int TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data);
+ // Multi-Select/Range-Select API
+ IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected);
+ IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed);
+
// Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API)
IMGUI_API void SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect);
IMGUI_API void BeginColumns(const char* str_id, int count, ImGuiOldColumnFlags flags = 0); // setup number of columns. use an identifier to distinguish multiple column sets. close with EndColumns().
@@ -3461,7 +3484,6 @@
IMGUI_API bool DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags);
IMGUI_API bool SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb);
IMGUI_API bool SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend = 0.0f, float hover_visibility_delay = 0.0f, ImU32 bg_col = 0);
- IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data);
// Widgets: Tree Nodes
IMGUI_API bool TreeNodeBehavior(ImGuiID id, ImGuiID storage_id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end = NULL);
diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp
index a7ab721..a07908c 100644
--- a/imgui_widgets.cpp
+++ b/imgui_widgets.cpp
@@ -6465,6 +6465,26 @@
bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
const bool was_selected = selected;
+ // Multi-selection support (header)
+ const bool is_multi_select = (g.MultiSelectScopeWindow == window);
+ if (is_multi_select)
+ {
+ flags |= ImGuiTreeNodeFlags_OpenOnArrow;
+ MultiSelectItemHeader(id, &selected);
+ button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
+
+ // To handle drag and drop of multiple items we need to avoid clearing selection on click.
+ // Enabling this test makes actions using CTRL+SHIFT delay their effect on the mouse release which is annoying, but it allows drag and drop of multiple items.
+ if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore))
+ button_flags |= ImGuiButtonFlags_PressedOnClick;
+ else
+ button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
+ }
+ else
+ {
+ button_flags |= ImGuiButtonFlags_NoKeyModifiers;
+ }
+
bool hovered, held;
bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags);
bool toggled = false;
@@ -6472,7 +6492,7 @@
{
if (pressed && g.DragDropHoldJustPressedId != id)
{
- if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id))
+ if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id && !is_multi_select))
toggled = true;
if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
toggled |= is_mouse_x_over_arrow && !g.NavDisableMouseHover; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
@@ -6507,13 +6527,23 @@
}
}
- // In this branch, TreeNodeBehavior() cannot toggle the selection so this will never trigger.
- if (selected != was_selected) //-V547
+ // Multi-selection support (footer)
+ if (is_multi_select)
+ {
+ bool pressed_copy = pressed && !toggled;
+ MultiSelectItemFooter(id, &selected, &pressed_copy);
+ if (pressed)
+ SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb);
+ }
+
+ if (selected != was_selected)
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
// Render
const ImU32 text_col = GetColorU32(ImGuiCol_Text);
ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact;
+ if (is_multi_select)
+ nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle
if (display_frame)
{
// Framed type
@@ -6727,8 +6757,8 @@
ImRect bb(min_x, pos.y, text_max.x, text_max.y);
if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
{
- const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
- const float spacing_y = style.ItemSpacing.y;
+ const float spacing_x = span_all_columns ? 0.0f : ImMax(style.ItemSpacing.x - style.SelectableSpacing.x, 0.0f);
+ const float spacing_y = ImMax(style.ItemSpacing.y - style.SelectableSpacing.y, 0.0f);
const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
bb.Min.x -= spacing_L;
@@ -6783,20 +6813,43 @@
if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
+ // Multi-selection support (header)
+ const bool is_multi_select = (g.MultiSelectScopeWindow == window);
const bool was_selected = selected;
+ if (is_multi_select)
+ {
+ MultiSelectItemHeader(id, &selected);
+ button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
+
+ // To handle drag and drop of multiple items we need to avoid clearing selection on click.
+ // Enabling this test makes actions using CTRL+SHIFT delay their effect on the mouse release which is annoying, but it allows drag and drop of multiple items.
+ if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore))
+ button_flags |= ImGuiButtonFlags_PressedOnClick;
+ else
+ button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
+ }
+
bool hovered, held;
bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
- // Auto-select when moved into
- // - This will be more fully fleshed in the range-select branch
- // - This is not exposed as it won't nicely work with some user side handling of shift/control
- // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
- // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
- // - (2) usage will fail with clipped items
- // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
- if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
- if (g.NavJustMovedToId == id)
- selected = pressed = true;
+ // Multi-selection support (footer)
+ if (is_multi_select)
+ {
+ MultiSelectItemFooter(id, &selected, &pressed);
+ }
+ else
+ {
+ // Auto-select when moved into
+ // - This will be more fully fleshed in the range-select branch
+ // - This is not exposed as it won't nicely work with some user side handling of shift/control
+ // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
+ // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
+ // - (2) usage will fail with clipped items
+ // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
+ if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
+ if (g.NavJustMovedToId == id)
+ selected = pressed = true;
+ }
// Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with gamepad/keyboard
if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
@@ -6810,18 +6863,27 @@
if (pressed)
MarkItemEdited(id);
- // In this branch, Selectable() cannot toggle the selection so this will never trigger.
- if (selected != was_selected) //-V547
+ if (selected != was_selected)
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
// Render
if (hovered || selected)
{
- const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
+ // FIXME-MULTISELECT, FIXME-STYLE: Color for 'selected' elements? ImGuiCol_HeaderSelected
+ ImU32 col;
+ if (selected && !hovered)
+ col = GetColorU32(ImLerp(GetStyleColorVec4(ImGuiCol_Header), GetStyleColorVec4(ImGuiCol_HeaderHovered), 0.5f));
+ else
+ col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
RenderFrame(bb.Min, bb.Max, col, false, 0.0f);
}
if (g.NavId == id)
- RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding);
+ {
+ ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding;
+ if (is_multi_select)
+ nav_highlight_flags |= ImGuiNavHighlightFlags_AlwaysDraw; // Always show the nav rectangle
+ RenderNavHighlight(bb, id, nav_highlight_flags);
+ }
if (span_all_columns)
{
@@ -6841,7 +6903,7 @@
EndDisabled();
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
- return pressed; //-V1020
+ return pressed || (was_selected != selected); //-V1020
}
bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
@@ -7049,16 +7111,227 @@
//-------------------------------------------------------------------------
// [SECTION] Widgets: Multi-Select support
//-------------------------------------------------------------------------
+// - BeginMultiSelect()
+// - EndMultiSelect()
+// - SetNextItemMultiSelectData()
+// - MultiSelectItemHeader() [Internal]
+// - MultiSelectItemFooter() [Internal]
+//-------------------------------------------------------------------------
+
+ImGuiMultiSelectData* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, void* range_ref, bool range_ref_is_selected)
+{
+ ImGuiContext& g = *ImGui::GetCurrentContext();
+ ImGuiWindow* window = g.CurrentWindow;
+
+ IM_ASSERT(g.MultiSelectScopeId == 0); // No recursion allowed yet (we could allow it if we deem it useful)
+ IM_ASSERT(g.MultiSelectFlags == 0);
+
+ ImGuiMultiSelectState* ms = &g.MultiSelectState;
+ g.MultiSelectScopeId = window->IDStack.back();
+ g.MultiSelectScopeWindow = window;
+ g.MultiSelectFlags = flags;
+ ms->Clear();
+
+ if ((flags & ImGuiMultiSelectFlags_NoMultiSelect) == 0)
+ {
+ ms->In.RangeSrc = ms->Out.RangeSrc = range_ref;
+ ms->In.RangeValue = ms->Out.RangeValue = range_ref_is_selected;
+ }
+
+ // Auto clear when using Navigation to move within the selection (we compare SelectScopeId so it possible to use multiple lists inside a same window)
+ if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.MultiSelectScopeId)
+ {
+ if (g.IO.KeyShift)
+ ms->InRequestSetRangeNav = true;
+ if (!g.IO.KeyCtrl && !g.IO.KeyShift)
+ ms->In.RequestClear = true;
+ }
+
+ // Select All helper shortcut
+ if (!(flags & ImGuiMultiSelectFlags_NoMultiSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
+ if (IsWindowFocused() && g.IO.KeyCtrl && IsKeyPressed(GetKeyIndex(ImGuiKey_A)))
+ ms->In.RequestSelectAll = true;
+
+#ifdef IMGUI_DEBUG_MULTISELECT
+ if (ms->In.RequestClear) printf("[%05d] BeginMultiSelect: RequestClear\n", g.FrameCount);
+ if (ms->In.RequestSelectAll) printf("[%05d] BeginMultiSelect: RequestSelectAll\n", g.FrameCount);
+#endif
+
+ return &ms->In;
+}
+
+ImGuiMultiSelectData* ImGui::EndMultiSelect()
+{
+ ImGuiContext& g = *ImGui::GetCurrentContext();
+ ImGuiMultiSelectState* ms = &g.MultiSelectState;
+ IM_ASSERT(g.MultiSelectScopeId != 0);
+ if (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect)
+ ms->Out.RangeValue = true;
+ g.MultiSelectScopeId = 0;
+ g.MultiSelectScopeWindow = NULL;
+ g.MultiSelectFlags = 0;
+
+#ifdef IMGUI_DEBUG_MULTISELECT
+ if (ms->Out.RequestClear) printf("[%05d] EndMultiSelect: RequestClear\n", g.FrameCount);
+ if (ms->Out.RequestSelectAll) printf("[%05d] EndMultiSelect: RequestSelectAll\n", g.FrameCount);
+ if (ms->Out.RequestSetRange) printf("[%05d] EndMultiSelect: RequestSetRange %p..%p = %d\n", g.FrameCount, ms->Out.RangeSrc, ms->Out.RangeDst, ms->Out.RangeValue);
+#endif
+
+ return &ms->Out;
+}
void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
{
// Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
// This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
ImGuiContext& g = *GImGui;
- g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
+ if (g.MultiSelectScopeId != 0)
+ g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
+ else
+ g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
g.NextItemData.SelectionUserData = selection_user_data;
}
+void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected)
+{
+ ImGuiContext& g = *GImGui;
+ ImGuiMultiSelectState* ms = &g.MultiSelectState;
+
+ IM_ASSERT((g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid) && "Forgot to call SetNextItemMultiSelectData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
+ void* item_data = (void*)g.NextItemData.SelectionUserData;
+
+ // Apply Clear/SelectAll requests requested by BeginMultiSelect().
+ // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper.
+ // If you are using a clipper (aka not submitting every element of the list) you need to process the Clear/SelectAll request after calling BeginMultiSelect()
+ bool selected = *p_selected;
+ if (ms->In.RequestClear)
+ selected = false;
+ else if (ms->In.RequestSelectAll)
+ selected = true;
+
+ const bool is_range_src = (ms->In.RangeSrc == item_data);
+ if (is_range_src)
+ ms->In.RangeSrcPassedBy = true;
+
+ // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection)
+ // For this to work, IF the user is clipping items, they need to set RangeSrcPassedBy = true to notify the system.
+ if (ms->InRequestSetRangeNav)
+ {
+ IM_ASSERT(id != 0);
+ IM_ASSERT(g.IO.KeyShift);
+ const bool is_range_dst = !ms->InRangeDstPassedBy && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
+ if (is_range_dst)
+ ms->InRangeDstPassedBy = true;
+ if (is_range_src || is_range_dst || ms->In.RangeSrcPassedBy != ms->InRangeDstPassedBy)
+ selected = ms->In.RangeValue;
+ else if (!g.IO.KeyCtrl)
+ selected = false;
+ }
+
+ *p_selected = selected;
+}
+
+void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
+{
+ ImGuiContext& g = *GImGui;
+ ImGuiWindow* window = g.CurrentWindow;
+ ImGuiMultiSelectState* ms = &g.MultiSelectState;
+
+ void* item_data = (void*)g.NextItemData.SelectionUserData;
+
+ bool selected = *p_selected;
+ bool pressed = *p_pressed;
+ bool is_ctrl = g.IO.KeyCtrl;
+ bool is_shift = g.IO.KeyShift;
+ const bool is_multiselect = (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoMultiSelect) == 0;
+
+ // Auto-select as you navigate a list
+ if (g.NavJustMovedToId == id)
+ {
+ if (!g.IO.KeyCtrl)
+ selected = pressed = true;
+ else if (g.IO.KeyCtrl && g.IO.KeyShift)
+ pressed = true;
+ }
+
+ // Right-click handling: this could be moved at the Selectable() level.
+ bool hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
+ if (hovered && IsMouseClicked(1))
+ {
+ SetFocusID(g.LastItemData.ID, window);
+ if (!pressed && !selected)
+ {
+ pressed = true;
+ is_ctrl = is_shift = false;
+ }
+ }
+
+ if (pressed)
+ {
+ //-------------------------------------------------------------------------------------------------------------------------------------------------
+ // ACTION | Begin | Item Old | Item New | End
+ //-------------------------------------------------------------------------------------------------------------------------------------------------
+ // Keys Navigated, Ctrl=0, Shift=0 | In.Clear | Clear -> Sel=0 | Src=item, Pressed -> Sel=1 |
+ // Keys Navigated, Ctrl=0, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange
+ // Keys Navigated, Ctrl=1, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=Src, Out.Clear, Out.SetRange=Src | Clear + SetRange
+ // Mouse Pressed, Ctrl=0, Shift=0 | n/a | n/a (Sel=1) | Src=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange
+ // Mouse Pressed, Ctrl=0, Shift=1 | n/a | n/a | Dst=item, Pressed -> Sel=1, Out.Clear, Out.SetRange=1 | Clear + SetRange
+ //-------------------------------------------------------------------------------------------------------------------------------------------------
+
+ ImGuiInputSource input_source = (g.NavJustMovedToId != 0 && g.NavWindow == window && g.NavJustMovedToId == g.LastItemData.ID) ? g.NavInputSource : ImGuiInputSource_Mouse;
+ if (is_shift && is_multiselect)
+ {
+ ms->Out.RequestSetRange = true;
+ ms->Out.RangeDst = item_data;
+ if (!is_ctrl)
+ ms->Out.RangeValue = true;
+ ms->Out.RangeDirection = ms->In.RangeSrcPassedBy ? +1 : -1;
+ }
+ else
+ {
+ selected = (!is_ctrl || (g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect)) ? true : !selected;
+ ms->Out.RangeSrc = ms->Out.RangeDst = item_data;
+ ms->Out.RangeValue = selected;
+ }
+
+ if (input_source == ImGuiInputSource_Mouse)
+ {
+ // Mouse click without CTRL clears the selection, unless the clicked item is already selected
+ bool preserve_existing_selection = g.DragDropActive;
+ if (is_multiselect && !is_ctrl && !preserve_existing_selection)
+ ms->Out.RequestClear = true;
+ if (is_multiselect && !is_shift && !preserve_existing_selection && ms->Out.RequestClear)
+ {
+ // For toggle selection unless there is a Clear request, we can handle it completely locally without sending a RangeSet request.
+ IM_ASSERT(ms->Out.RangeSrc == ms->Out.RangeDst); // Setup by block above
+ ms->Out.RequestSetRange = true;
+ ms->Out.RangeValue = selected;
+ ms->Out.RangeDirection = +1;
+ }
+ if (!is_multiselect)
+ {
+ // Clear selection, set single item range
+ IM_ASSERT(ms->Out.RangeSrc == item_data && ms->Out.RangeDst == item_data); // Setup by block above
+ ms->Out.RequestClear = true;
+ ms->Out.RequestSetRange = true;
+ }
+ }
+ else if (input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad)
+ {
+ if (!is_multiselect)
+ ms->Out.RequestClear = true;
+ else if (is_shift && !is_ctrl && is_multiselect)
+ ms->Out.RequestClear = true;
+ }
+ }
+
+ // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect)
+ if (ms->Out.RangeSrc == item_data && is_ctrl && is_shift && is_multiselect && !(g.MultiSelectFlags & ImGuiMultiSelectFlags_NoUnselect))
+ ms->Out.RangeValue = selected;
+
+ *p_selected = selected;
+ *p_pressed = pressed;
+}
//-------------------------------------------------------------------------
// [SECTION] Widgets: ListBox