MultiSelect: added ImGuiMultiSelectFlags_NoAutoSelect, ImGuiMultiSelectFlags_NoAutoClear features + added Checkbox Demo

Refer to "widgets_multiselect_checkboxes" in imgui_test_suite.
This commit is contained in:
ocornut 2024-03-07 16:26:22 +01:00
parent a639346fba
commit e7a734f78d
4 changed files with 158 additions and 50 deletions

View File

@ -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;

24
imgui.h
View File

@ -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.
};

View File

@ -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);

View File

@ -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;