diff --git a/imgui.h b/imgui.h index bfd4a8f1..ae1cba66 100644 --- a/imgui.h +++ b/imgui.h @@ -2342,31 +2342,36 @@ struct ImGuiListClipper }; // Abstract: -// - This system implements standard multi-selection idioms (CTRL+Click/Arrow, SHIFT+Click/Arrow, etc) in a way that allow items to be -// fully clipped (= not submitted at all) when not visible. Clipping is typically provided by ImGuiListClipper. +// - This system helps you implements standard multi-selection idioms (CTRL+Click/Arrow, SHIFT+Click/Arrow, etc) in a way that allow +// selectable items to be fully clipped (= not submitted at all) when not visible. Clipping is typically provided by ImGuiListClipper. // Handling all of this in a single pass imgui is a little tricky, and this is why we provide those functionalities. -// Note however that if you don't need SHIFT+Click/Arrow range-select, you can handle a simpler form of multi-selection yourself, -// by reacting to click/presses on Selectable() items and checking keyboard modifiers. -// The complexity of this system here is mostly caused by the handling of range-select while optionally allowing to clip elements. +// Note however that if you don't need SHIFT+Click/Arrow range-select + clipping, you can handle a simpler form of multi-selection +// yourself, by reacting to click/presses on Selectable() items and checking keyboard modifiers. +// The unusual complexity of this system is mostly caused by supporting SHIFT+Click/Arrow range-select with clipped elements. +// - TreeNode() and Selectable() are supported. // - The work involved to deal with multi-selection differs whether you want to only submit visible items (and clip others) or submit all items // regardless of their visibility. Clipping items is more efficient and will allow you to deal with large lists (1k~100k items) with near zero // performance penalty, but requires a little more work on the code. If you only have a few hundreds elements in your possible selection set, -// you may as well not bother with clipping, as the cost should be negligible (as least on imgui side). +// you may as well not bother with clipping, as the cost should be negligible (as least on Dear ImGui side). // If you are not sure, always start without clipping and you can work your way to the more optimized version afterwards. -// - The void* Src/Dst value represent a selectable object. They are the values you pass to SetNextItemSelectionData(). -// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points. But the code never assume that sortable integers are used. -// - In the spirit of imgui design, your code own the selection data. So this is designed to handle all kind of selection data: instructive (store a bool inside each object), -// external array (store an array aside from your objects), set (store only selected items in a hash/map/set), using intervals (store indices in an interval tree), etc. +// - The void* RangeSrc/RangeDst value represent a selectable object. They are the values you pass to SetNextItemSelectionData(). +// Storing an integer index is the easiest thing to do, as SetRange requests will give you two end points and you will need to interpolate +// between them to honor range selection. But the code never assume that sortable integers are used (you may store pointers to your object, +// and then from the pointer have your own way of iterating from RangeSrc to RangeDst). +// - In the spirit of Dear ImGui design, your code own the selection data. So this is designed to handle all kind of selection data: +// e.g. instructive selection (store a bool inside each object), external array (store an array aside from your objects), +// hash/map/set (store only selected items in a hash/map/set), or other structures (store indices in an interval tree), etc. // Usage flow: -// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc and its selection status. As a default value for the initial frame or when, -// resetting your selection state: you may use the value for your first item or a "null" value that matches the type stored in your void*. -// 2) Honor Clear/SelectAll requests by updating your selection data. [Only required if you are using a clipper in step 4] -// 3) Set RangeSrcPassedBy=true if the RangeSrc item is part of the items clipped before the first submitted/visible item. [Only required if you are using a clipper in step 4] +// 1) Call BeginMultiSelect() with the last saved value of ->RangeSrc and its selection state. +// It is because you need to pass its selection state (and you own selection) that we don't store this value in Dear ImGui. +// (For the initial frame or when resetting your selection state: you may use the value for your first item or a "null" value that matches the type stored in your void*). +// 2) Honor Clear/SelectAll requests by updating your selection data. [Only required if you are using a clipper in step 4] +// 3) Set RangeSrcPassedBy=true if the RangeSrc item is part of the items clipped before the first submitted/visible item. [Only required if you are using a clipper in step 4] // This is because for range-selection we need to know if we are currently "inside" or "outside" the range. -// If you are using integer indices everywhere, this is easy to compute: if (clipper.DisplayStart > (int)data->RangeSrc) { data->RangeSrcPassedBy = true; } +// If you are using integer indices everywhere, this is easy to compute: if (clipper.DisplayStart > (int)data->RangeSrc) { data->RangeSrcPassedBy = true; } // 4) Submit your items with SetNextItemSelectionData() + Selectable()/TreeNode() calls. -// Call IsItemSelectionToggled() to query with the selection state has been toggled, in which you need the info immediately (before EndMultiSelect()) for your display. -// When cannot reliably return a "IsItemSelected()" value because we need to consider clipped (unprocessed) item, this is why we return a toggle event instead. +// Call IsItemToggledSelection() to query if the selection state has been toggled, if you need the info immediately for your display (before EndMultiSelect()). +// When cannot return a "IsItemSelected()" value because we need to consider clipped/unprocessed items, this is why we return a "Toggle" event instead. // 5) Call EndMultiSelect(). Save the value of ->RangeSrc for the next frame (you may convert the value in a format that is safe for persistance) // 6) Honor Clear/SelectAll/SetRange requests by updating your selection data. Always process them in this order (as you will receive Clear+SetRange request simultaneously) // If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable() on a per-item basis. @@ -2376,7 +2381,7 @@ struct ImGuiMultiSelectData bool RequestClear; // Begin, End // Request user to clear selection bool RequestSelectAll; // Begin, End // Request user to select all bool RequestSetRange; // End // Request user to set or clear selection in the [RangeSrc..RangeDst] range - bool RangeSrcPassedBy; // After Begin // Need to be set by user is RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. + bool RangeSrcPassedBy; // In loop // (If clipping) Need to be set by user if RangeSrc was part of the clipped set before submitting the visible items. Ignore if not clipping. bool RangeValue; // End // End: parameter from RequestSetRange request. True = Select Range, False = Unselect range. void* RangeSrc; // Begin, End // End: parameter from RequestSetRange request + you need to save this value so you can pass it again next frame. / Begin: this is the value you passed to BeginMultiSelect() void* RangeDst; // End // End: parameter from RequestSetRange request. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 8345da38..14a172cf 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -566,12 +566,15 @@ void ImGui::ShowDemoWindow(bool* p_open) // are generally appropriate. Even a large array of bool might work for you... // - If you need to handle extremely large selections, it might be advantageous to support a "negative" mode in // your storage, so "Select All" becomes "Negative=1, Clear" and then sparse unselect can add to the storage. -struct ExampleSelectionData +// About RefItem: +// - The MultiSelect API requires you to store information about the reference/pivot item (generally the last clicked item). +struct ExampleSelection { ImGuiStorage Storage; - int SelectionSize; // Number of selected items (== number of 1 in the Storage) + int SelectionSize; // Number of selected items (== number of 1 in the Storage, maintained by this class) + int RangeRef; // Reference/pivot item (generally last clicked item) - ExampleSelectionData() { Clear(); } + ExampleSelection() { RangeRef = 0; Clear(); } void Clear() { Storage.Clear(); SelectionSize = 0; } bool GetSelected(int n) const { return Storage.GetInt((ImGuiID)n, 0) != 0; } void SetSelected(int n, bool v) { int* p_int = Storage.GetIntRef((ImGuiID)n, 0); if (*p_int == (int)v) return; if (v) SelectionSize++; else SelectionSize--; *p_int = (bool)v; } @@ -1252,8 +1255,7 @@ static void ShowDemoWindowWidgets() 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. - static int selection_ref = 0; // Selection pivot (last clicked item, we need to preserve this to handle range-select) - static ExampleSelectionData selection; + static ExampleSelection selection; const char* random_names[] = { "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", @@ -1281,7 +1283,7 @@ static void ShowDemoWindowWidgets() if (widget_type == WidgetType_TreeNode) ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection_ref, selection.GetSelected(selection_ref)); + ImGuiMultiSelectData* multi_select_data = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None, (void*)(intptr_t)selection.RangeRef, selection.GetSelected(selection.RangeRef)); if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } @@ -1295,7 +1297,7 @@ static void ShowDemoWindowWidgets() clipper.Begin(ITEMS_COUNT); while (clipper.Step()) { - if (clipper.DisplayStart > (int)selection_ref) + if (clipper.DisplayStart > selection.RangeRef) multi_select_data->RangeSrcPassedBy = true; for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) { @@ -1305,7 +1307,7 @@ static void ShowDemoWindowWidgets() sprintf(label, "Object %05d (category: %s)", n, category); bool item_is_selected = selection.GetSelected(n); - // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part + // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part // of the selection scope doesn't erroneously alter our selection (FIXME-TESTS: Add a test for that!). ImU32 dummy_col = (ImU32)ImGui::GetID(label); ImGui::ColorButton("##", ImColor(dummy_col), ImGuiColorEditFlags_NoTooltip, color_button_sz); @@ -1319,12 +1321,15 @@ static void ShowDemoWindowWidgets() } else if (widget_type == WidgetType_TreeNode) { - ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_OpenOnDoubleClick; + ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth; + tree_node_flags |= ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; if (item_is_selected) tree_node_flags |= ImGuiTreeNodeFlags_Selected; - ImGui::TreeNodeEx(label, tree_node_flags); + bool open = ImGui::TreeNodeEx(label, tree_node_flags); if (ImGui::IsItemToggledSelection()) selection.SetSelected(n, !item_is_selected); + if (open) + ImGui::TreePop(); } if (use_columns) @@ -1332,7 +1337,7 @@ static void ShowDemoWindowWidgets() ImGui::NextColumn(); ImGui::SetNextItemWidth(-FLT_MIN); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); - ImGui::InputText("###NoLabel", (char*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); + ImGui::InputText("###NoLabel", (char*)(void*)category, strlen(category), ImGuiInputTextFlags_ReadOnly); ImGui::PopStyleVar(); ImGui::NextColumn(); } @@ -1346,7 +1351,7 @@ static void ShowDemoWindowWidgets() // Apply multi-select requests multi_select_data = ImGui::EndMultiSelect(); - selection_ref = (int)(intptr_t)multi_select_data->RangeSrc; + selection.RangeRef = (int)(intptr_t)multi_select_data->RangeSrc; if (multi_select_data->RequestClear) { selection.Clear(); } if (multi_select_data->RequestSelectAll) { selection.SelectAll(ITEMS_COUNT); } if (multi_select_data->RequestSetRange) { selection.SetRange((int)(intptr_t)multi_select_data->RangeSrc, (int)(intptr_t)multi_select_data->RangeDst, multi_select_data->RangeValue ? 1 : 0); } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index c80cfbe5..ccb9fcec 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -5932,7 +5932,7 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l flags |= ImGuiTreeNodeFlags_OpenOnArrow; // To handle drag and drop of multiple items we need to avoid clearing selection on click. - // Enabling this test makes actions using CTRL+SHIFT delay their effect on the mouse release which is annoying, but it allows drag and drop of multiple items. + // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items. // FIXME-MULTISELECT: Consider opt-in for drag and drop behavior in ImGuiMultiSelectFlags? if (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) button_flags |= ImGuiButtonFlags_PressedOnClick;