diff --git a/imgui.cpp b/imgui.cpp index 291863e6f..ee9462a9a 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -4075,7 +4075,7 @@ void ImGui::MarkItemEdited(ImGuiID id) // We accept a MarkItemEdited() on drag and drop targets (see https://github.com/ocornut/imgui/issues/1875#issuecomment-978243343) // We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714) - IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id); + IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive)); //IM_ASSERT(g.CurrentWindow->DC.LastItemId == id); g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited; diff --git a/imgui.h b/imgui.h index a5a3d9265..88445d5b7 100644 --- a/imgui.h +++ b/imgui.h @@ -2744,7 +2744,7 @@ struct ImColor // - Store and maintain actual selection data using persistent object identifiers. // - Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (2) [If using clipper] Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 6. +// - (2) Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 6. // - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeItemByIndex(). If storing indices in ImGuiSelectionUserData, a simple clipper.IncludeItemByIndex(ms_io->RangeSrcItem) call will work. // LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. @@ -2773,15 +2773,17 @@ enum ImGuiMultiSelectFlags_ ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to select all. ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+Click/Shift+Keyboard handling (useful for unordered 2D selection). - ImGuiMultiSelectFlags_BoxSelect = 1 << 3, // Enable box-selection (only supporting 1D list when using clipper, not 2D grids). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. - ImGuiMultiSelectFlags_BoxSelect2d = 1 << 4, // Enable box-selection with 2D layout/grid support. This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. - ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 5, // Disable scrolling when box-selecting near edges of scope. - ImGuiMultiSelectFlags_ClearOnEscape = 1 << 6, // Clear selection when pressing Escape while scope is focused. - ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 7, // Clear selection when clicking on empty location within scope. - ImGuiMultiSelectFlags_ScopeWindow = 1 << 8, // Use if BeginMultiSelect() covers a whole window (Default): Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). - ImGuiMultiSelectFlags_ScopeRect = 1 << 9, // Use if multiple BeginMultiSelect() are used in the same host window: Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. - ImGuiMultiSelectFlags_SelectOnClick = 1 << 10, // Apply selection on mouse down when clicking on unselected item. (Default) - ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 11, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes) + ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing other items when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes) + ImGuiMultiSelectFlags_BoxSelect = 1 << 5, // Enable box-selection (only supporting 1D list when using clipper, not 2D grids). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 6, // Enable box-selection with 2D layout/grid support. This alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 7, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 8, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 9, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 10, // Use if BeginMultiSelect() covers a whole window (Default): Scope for _ClearOnClickVoid and _BoxSelect is whole window (Default). + ImGuiMultiSelectFlags_ScopeRect = 1 << 11, // Use if multiple BeginMultiSelect() are used in the same host window: Scope for _ClearOnClickVoid and _BoxSelect is rectangle covering submitted items. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 12, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 13, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. }; // Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). @@ -2803,7 +2805,7 @@ struct ImGuiMultiSelectIO enum ImGuiSelectionRequestType { ImGuiSelectionRequestType_None = 0, - ImGuiSelectionRequestType_SetAll, // Request app to clear selection (if Selected==false) or select all items (if Selected==true) + ImGuiSelectionRequestType_SetAll, // Request app to clear selection (if Selected==false) or select all items (if Selected==true). We cannot set RangeFirstItem/RangeLastItem as its contents is entirely up to user (not necessarily an index) ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items (inclusive) based on value of Selected. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. }; diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 9d23d4661..f5cfa53e1 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3205,6 +3205,55 @@ static void ShowDemoWindowMultiSelect() ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (checkboxes)"); + if (ImGui::TreeNode("Multi-Select (checkboxes)")) + { + ImGui::Text("In a list of checkboxes (not selectable):"); + ImGui::BulletText("Using _NoAutoSelect + _NoAutoClear flags."); + ImGui::BulletText("Shift+Click to check multiple boxes."); + ImGui::BulletText("Shift+Keyboard to copy current value to other boxes."); + + static bool values[20] = {}; + static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_ClearOnEscape; + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); + + struct Funcs + { + static void ApplyMultiSelectRequestsToBoolArray(ImGuiMultiSelectIO* ms_io, bool items[], int items_count) + { + for (ImGuiSelectionRequest& req : ms_io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_SetAll) + for (int n = 0; n < items_count; n++) + items[n] = req.Selected; + else if (req.Type == ImGuiSelectionRequestType_SetRange) + for (int n = (int)req.RangeFirstItem; n <= (int)req.RangeLastItem; n++) + items[n] = req.Selected; + } + } + }; + + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY)) + { + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags); + Funcs::ApplyMultiSelectRequestsToBoolArray(ms_io, values, IM_ARRAYSIZE(values)); //// By specs, it could be optional to apply requests from BeginMultiSelect() if not using a clipper. + for (int n = 0; n < 20; n++) + { + char label[32]; + sprintf(label, "Item %d", n); + ImGui::SetNextItemSelectionUserData(n); + ImGui::Checkbox(label, &values[n]); + } + ms_io = ImGui::EndMultiSelect(); + Funcs::ApplyMultiSelectRequestsToBoolArray(ms_io, values, IM_ARRAYSIZE(values)); + } + ImGui::EndChild(); + + ImGui::TreePop(); + } + // Demonstrate individual selection scopes in same window IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (multiple scopes)"); if (ImGui::TreeNode("Multi-Select (multiple scopes)")) @@ -3290,6 +3339,8 @@ static void ShowDemoWindowMultiSelect() ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoRangeSelect", &flags, ImGuiMultiSelectFlags_NoRangeSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect", &flags, ImGuiMultiSelectFlags_BoxSelect); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index eef613071..2eefe8fea 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -1141,11 +1141,25 @@ bool ImGui::Checkbox(const char* label, bool* v) return false; } + // Range-Selection/Multi-selection support (header) + bool checked = *v; + const bool is_multi_select = (g.LastItemData.InFlags & ImGuiItemFlags_IsMultiSelect) != 0; + if (is_multi_select) + MultiSelectItemHeader(id, &checked, NULL); + bool hovered, held; bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); - if (pressed) + + // Range-Selection/Multi-selection support (footer) + if (is_multi_select) + MultiSelectItemFooter(id, &checked, &pressed); + else if (pressed) + checked = !checked; + + if (*v != checked) { - *v = !(*v); + *v = checked; + pressed = true; // return value MarkItemEdited(id); } @@ -7333,13 +7347,13 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ms->IsKeyboardSetRange = true; if (ms->IsKeyboardSetRange) IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear? - if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) + if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) request_clear = true; } else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId) { // Also clear on leaving scope (may be optional?) - if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) + if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) request_clear = true; } @@ -7517,11 +7531,15 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags const bool is_range_src = storage->RangeSrcItem == item_data; if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy) { + // Apply range-select value to visible items IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1); selected = (storage->RangeSelected != 0); } - else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0) + else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0) + { + // Clear other items selected = false; + } } *p_selected = selected; } @@ -7529,13 +7547,16 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags // Alter button behavior flags // 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 MouseUp which is annoying, but it allows drag and drop of multiple items. - ImGuiButtonFlags button_flags = *p_button_flags; - button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; - if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) - button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; - else - button_flags |= ImGuiButtonFlags_PressedOnClickRelease; - *p_button_flags = button_flags; + if (p_button_flags != NULL) + { + ImGuiButtonFlags button_flags = *p_button_flags; + button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; + if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) + button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + *p_button_flags = button_flags; + } } // In charge of: @@ -7570,11 +7591,10 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0; bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0; + bool apply_to_range_src = false; + if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) - { - storage->RangeSrcItem = item_data; - storage->RangeSelected = selected; // Will be updated at the end of this function anyway. - } + apply_to_range_src = true; if (ms->IsEndIO == false) { ms->IO.Requests.resize(0); @@ -7584,10 +7604,27 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Auto-select as you navigate a list if (g.NavJustMovedToId == id) { - if (is_ctrl && is_shift) - pressed = true; - else if (!is_ctrl) - selected = pressed = true; + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + { + if (is_ctrl && is_shift) + pressed = true; + else if (!is_ctrl) + selected = pressed = true; + } + else + { + // With NoAutoSelect, using Shift+keyboard performs a write/copy + if (is_shift) + pressed = true; + else if (!is_ctrl) + apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this + } + } + + if (apply_to_range_src) + { + storage->RangeSrcItem = item_data; + storage->RangeSelected = selected; // Will be updated at the end of this function anyway. } // Box-select toggle handling @@ -7608,9 +7645,9 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) ms->BoxSelectLastitem = item_data; } - // Right-click handling: this could be moved at the Selectable() level. - // FIXME-MULTISELECT: See https://github.com/ocornut/imgui/pull/5816 - if (hovered && IsMouseClicked(1)) + // Right-click handling. + // FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816 + if (hovered && IsMouseClicked(1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) { if (g.ActiveId != 0 && g.ActiveId != id) ClearActiveID(); @@ -7653,36 +7690,54 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst //---------------------------------------------------------------------------------------- - bool request_clear = false; - if (is_singleselect) - request_clear = true; - else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) - request_clear = true; - else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) - request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. - if (request_clear) + if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0) { - ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, (ImGuiSelectionUserData)-1, (ImGuiSelectionUserData)-1 }; - ms->IO.Requests.resize(0); - ms->IO.Requests.push_back(req); + bool request_clear = false; + if (is_singleselect) + request_clear = true; + else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) + request_clear = true; + else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) + request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. + if (request_clear) + { + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, false, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; + ms->IO.Requests.resize(0); + ms->IO.Requests.push_back(req); + } } int range_direction; bool range_selected; if (is_shift && !is_singleselect) { - // Shift+Arrow always select - // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected) //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) storage->RangeSrcItem = item_data; - range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + { + // Shift+Arrow always select + // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected) + range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + } + else + { + // Shift+Arrow copy source selection state + // Shift+Click always copy from target selection state + if (ms->IsKeyboardSetRange) + range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + else + range_selected = !selected; + } range_direction = ms->RangeSrcPassedBy ? +1 : -1; } else { // Ctrl inverts selection, otherwise always select - selected = is_ctrl ? !selected : true; + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + selected = is_ctrl ? !selected : true; + else + selected = !selected; storage->RangeSrcItem = item_data; range_selected = selected; range_direction = +1;