From 6feff6ff0514db74b387843768cb023e39d148fc Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 22 Sep 2023 14:23:40 +0200 Subject: [PATCH] MultiSelect: (Breaking) io contains a ImVector list. --- imgui.h | 53 +++++++++++++++++++++++++++----------------- imgui_demo.cpp | 33 +++++++++++++++------------- imgui_internal.h | 4 +++- imgui_widgets.cpp | 56 +++++++++++++++++++++++++++++++---------------- 4 files changed, 91 insertions(+), 55 deletions(-) diff --git a/imgui.h b/imgui.h index 96b29d560..2ce1f1983 100644 --- a/imgui.h +++ b/imgui.h @@ -44,7 +44,7 @@ Index of this file: // [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, ImGuiMultiSelectIO) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO) // [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) @@ -2720,7 +2720,7 @@ struct ImColor }; //----------------------------------------------------------------------------- -// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO) +// [SECTION] Multi-Select API flags & structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO) //----------------------------------------------------------------------------- #define IMGUI_HAS_MULTI_SELECT // Multi-Select/Range-Select WIP branch // <-- This is currently _not_ in the top of imgui.h to prevent merge conflicts. @@ -2775,36 +2775,49 @@ enum ImGuiMultiSelectFlags_ // - If you need to wrap this API for another language/framework, feel free to expose this as 'int' if simpler. // Usage flow: // BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (2) [If using clipper] Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 6. +// - (2) [If using clipper] Honor request list (Clear/SelectAll/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.IncludeIndex(). If already using indices in ImGuiSelectionUserData, it is as simple as clipper.IncludeIndex((int)ms_io->RangeSrcItem); // LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. // END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. -// - (6) Honor Clear/SelectAll/SetRange requests by updating your selection data. Same code as Step 2. +// - (6) Honor request list (Clear/SelectAll/SetRange requests) by updating your selection data. Same code as Step 2. // If you submit all items (no clipper), Step 2 and 3 and will be handled by Selectable()/TreeNode on a per-item basis. // However it is perfectly fine to honor all steps even if you don't use a clipper. // Advanced: // - Deletion: If you need to handle items deletion a little more work if needed for post-deletion focus and scrolling to be correct. // refer to 'Demo->Widgets->Selection State' for demos supporting deletion. + +enum ImGuiSelectionRequestType +{ + ImGuiSelectionRequestType_None = 0, + ImGuiSelectionRequestType_Clear, // Request app to clear selection. + ImGuiSelectionRequestType_SelectAll, // Request app to select all. + ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items based on 'bool RangeSelected'. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. +}; + +// List of requests stored in ImGuiMultiSelectIO +// - Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. +// - Some fields are only necessary if your list is dynamic and allows deletion (handling deletion and getting "post-deletion" state right is shown in the demo) +// - Below: who reads/writes each fields? 'r'=read, 'w'=write, 'ms'=multi-select code, 'app'=application/user code, 'BEGIN'=BeginMultiSelect() and after, 'END'=EndMultiSelect() and after. +struct ImGuiSelectionRequest +{ + ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r + bool RangeSelected; // / ms:w, app:r // Parameter for SetRange request (true = select range, false = unselect range) + ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) + ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) + + ImGuiSelectionRequest(ImGuiSelectionRequestType type = ImGuiSelectionRequestType_None) { Type = type; RangeSelected = false; RangeFirstItem = RangeLastItem = (ImGuiSelectionUserData)-1; } +}; + struct ImGuiMultiSelectIO { - // - Always process requests in this order: Clear, SelectAll, SetRange. Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. - // - Some fields are only necessary if your list is dynamic and allows deletion (getting "post-deletion" state right is shown in the demo) - // - Below: who reads/writes each fields? 'r'=read, 'w'=write, 'ms'=multi-select code, 'app'=application/user code, 'BEGIN'=BeginMultiSelect() and after, 'END'=EndMultiSelect() and after. - // REQUESTS --------------------------------// BEGIN / END - bool RequestClear; // ms:w, app:r / ms:w, app:r // 1. Request app/user to clear selection. - bool RequestSelectAll; // ms:w, app:r / ms:w, app:r // 2. Request app/user to select all. - bool RequestSetRange; // / ms:w, app:r // 3. Request app/user to select/unselect [RangeFirstItem..RangeLastItem] items based on 'bool RangeSelected'. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. - // STATE/ARGUMENTS -------------------------// BEGIN / END - ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! - ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom) - ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // End: parameter for RequestSetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top) - bool RangeSelected; // / ms:w, app:r // End: parameter for RequestSetRange request. true = Select Range, false = Unselect Range. - bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). - bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). - ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + ImVector Requests; // ms:w, app:r / ms:w app:r // Requests + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (generally the first selected item when multi-selecting, which is used as a reference point) must never be clipped! + ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). + bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). ImGuiMultiSelectIO() { Clear(); } - void Clear() { memset(this, 0, sizeof(*this)); NavIdItem = RangeSrcItem = RangeFirstItem = RangeLastItem = (ImGuiSelectionUserData)-1; } + void Clear() { Requests.resize(0); RangeSrcItem = NavIdItem = (ImGuiSelectionUserData)-1; NavIdSelected = RangeSrcReset = false; } }; //----------------------------------------------------------------------------- diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 59b7162ac..770eb3c9d 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2838,26 +2838,29 @@ struct ExampleSelection // WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ExampleSelectionAdapter' INDIRECTION LOGIC. // Notice that with the simplest adapter (using indices everywhere), all functions return their parameters. // The most simple implementation (using indices everywhere) would look like: - // if (ms_io->RequestClear) { Clear(); } - // if (ms_io->RequestSelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } - // if (ms_io->RequestSetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } + // for (ImGuiSelectionRequest& req : ms_io->Requests) + // { + // if (req.Type == ImGuiSelectionRequestType_Clear) { Clear(); } + // if (req.Type == ImGuiSelectionRequestType_SelectAll) { Clear(); for (int n = 0; n < items_count; n++) { AddItem(n); } } + // if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { UpdateItem(n, ms_io->RangeSelected); } } + // } void ApplyRequests(ImGuiMultiSelectIO* ms_io, ExampleSelectionAdapter* adapter, int items_count) { IM_ASSERT(adapter->IndexToStorage != NULL); - - if (ms_io->RequestClear || ms_io->RequestSelectAll) - Clear(); - - if (ms_io->RequestSelectAll) + for (ImGuiSelectionRequest& req : ms_io->Requests) { - Storage.Data.reserve(items_count); - for (int idx = 0; idx < items_count; idx++) - AddItem(adapter->IndexToStorage(adapter, idx)); + if (req.Type == ImGuiSelectionRequestType_Clear || req.Type == ImGuiSelectionRequestType_SelectAll) + Clear(); + if (req.Type == ImGuiSelectionRequestType_SelectAll) + { + Storage.Data.reserve(items_count); + for (int idx = 0; idx < items_count; idx++) + AddItem(adapter->IndexToStorage(adapter, idx)); + } + if (req.Type == ImGuiSelectionRequestType_SetRange) + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) + UpdateItem(adapter->IndexToStorage(adapter, idx), req.RangeSelected); } - - if (ms_io->RequestSetRange) - for (int idx = (int)ms_io->RangeFirstItem; idx <= (int)ms_io->RangeLastItem; idx++) - UpdateItem(adapter->IndexToStorage(adapter, idx), ms_io->RangeSelected); } // Find which item should be Focused after deletion. diff --git a/imgui_internal.h b/imgui_internal.h index bdfaac28c..9df3b01e8 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1722,6 +1722,8 @@ struct IMGUI_API ImGuiMultiSelectTempData ImGuiKeyChord KeyMods; ImGuiMultiSelectIO BeginIO; // Requests are set and returned by BeginMultiSelect(), written to by user during the loop. ImGuiMultiSelectIO EndIO; // Requests are set during the loop and returned by EndMultiSelect(). + bool LoopRequestClear; + bool LoopRequestSelectAll; bool IsFocused; // Set if currently focusing the selection scope (any item of the selection). May be used if you have custom shortcut associated to selection. bool IsSetRange; // Set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. bool NavIdPassedBy; @@ -1730,7 +1732,7 @@ struct IMGUI_API ImGuiMultiSelectTempData //ImRect Rect; // Extent of selection scope between BeginMultiSelect() / EndMultiSelect(), used by ImGuiMultiSelectFlags_ClearOnClickRectVoid. ImGuiMultiSelectTempData() { Clear(); } - void Clear() { memset(this, 0, sizeof(*this)); BeginIO.Clear(); EndIO.Clear(); } + void Clear() { Storage = NULL; FocusScopeId = 0; Flags = 0; KeyMods = 0; BeginIO.Clear(); EndIO.Clear(); LoopRequestClear = LoopRequestSelectAll = IsFocused = IsSetRange = NavIdPassedBy = RangeSrcPassedBy = RangeDstPassedBy = false; } }; // Persistent storage for multi-select (as long as selection is alive) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index b2ae35cb0..17162e393 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7114,9 +7114,12 @@ void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) { ImGuiContext& g = *GImGui; - if (io->RequestClear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestClear\n", function); - if (io->RequestSelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSelectAll\n", function); - if (io->RequestSetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: RequestSetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, io->RangeFirstItem, io->RangeLastItem, io->RangeFirstItem, io->RangeLastItem, io->RangeSelected); + for (const ImGuiSelectionRequest& req : io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_Clear) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: Clear\n", function); + if (req.Type == ImGuiSelectionRequestType_SelectAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SelectAll\n", function); + if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.RangeSelected); + } } // Return ImGuiMultiSelectIO structure. Lifetime: valid until corresponding call to EndMultiSelect(). @@ -7151,6 +7154,9 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) ms->BeginIO.NavIdItem = ms->EndIO.NavIdItem = storage->NavIdItem; ms->BeginIO.NavIdSelected = ms->EndIO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; + bool request_clear = false; + bool request_select_all = false; + // Clear when using Navigation to move within the scope // (we compare FocusScopeId so it possible to use multiple selections inside a same window) if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) @@ -7160,13 +7166,13 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) if (ms->IsSetRange) IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear? if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) - ms->BeginIO.RequestClear = true; + 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) - ms->BeginIO.RequestClear = true; + request_clear = true; } if (ms->IsFocused) @@ -7176,14 +7182,19 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) // Otherwise may be done by caller but it means Shortcut() needs to be exposed. if (flags & ImGuiMultiSelectFlags_ClearOnEscape) if (Shortcut(ImGuiKey_Escape)) - ms->BeginIO.RequestClear = true; + request_clear = true; // Shortcut: Select all (CTRL+A) if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) - ms->BeginIO.RequestSelectAll = true; + request_select_all = true; } + if (request_clear || request_select_all) + ms->BeginIO.Requests.push_back(ImGuiSelectionRequest(request_select_all ? ImGuiSelectionRequestType_SelectAll : ImGuiSelectionRequestType_Clear)); + ms->LoopRequestClear = request_clear; + ms->LoopRequestSelectAll = request_select_all; + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) DebugLogMultiSelectRequests("BeginMultiSelect", &ms->BeginIO); @@ -7220,8 +7231,8 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() if (IsWindowHovered() && g.HoveredId == 0) if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) { - ms->EndIO.RequestClear = true; - ms->EndIO.RequestSelectAll = ms->EndIO.RequestSetRange = false; + ms->EndIO.Requests.resize(0); + ms->EndIO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); } // Unwind @@ -7273,9 +7284,9 @@ void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags // 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() - if (ms->BeginIO.RequestClear) + if (ms->LoopRequestClear) selected = false; - else if (ms->BeginIO.RequestSelectAll) + else if (ms->LoopRequestSelectAll) selected = 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) @@ -7396,22 +7407,28 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) //---------------------------------------------------------------------------------------- const ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + bool request_clear = false; if (is_singleselect) - ms->EndIO.RequestClear = true; + request_clear = true; else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) - ms->EndIO.RequestClear = true; + request_clear = true; else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) - ms->EndIO.RequestClear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. + request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. + if (request_clear) + { + ms->EndIO.Requests.resize(0); + ms->EndIO.Requests.push_back(ImGuiSelectionRequest(ImGuiSelectionRequestType_Clear)); + } int range_direction; - ms->EndIO.RequestSetRange = true; + ImGuiSelectionRequest req(ImGuiSelectionRequestType_SetRange); if (is_shift && !is_singleselect) { // Shift+Arrow always select // Ctrl+Shift+Arrow copy source selection state (alrady stored by BeginMultiSelect() in RangeSelected) //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); ms->EndIO.RangeSrcItem = (storage->RangeSrcItem != ImGuiSelectionUserData_Invalid) ? storage->RangeSrcItem : item_data; - ms->EndIO.RangeSelected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + req.RangeSelected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; range_direction = ms->RangeSrcPassedBy ? +1 : -1; } else @@ -7419,12 +7436,13 @@ void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) // Ctrl inverts selection, otherwise always select selected = is_ctrl ? !selected : true; ms->EndIO.RangeSrcItem = storage->RangeSrcItem = item_data; - ms->EndIO.RangeSelected = selected; + req.RangeSelected = selected; range_direction = +1; } ImGuiSelectionUserData range_dst_item = item_data; - ms->EndIO.RangeFirstItem = (range_direction > 0) ? ms->EndIO.RangeSrcItem : range_dst_item; - ms->EndIO.RangeLastItem = (range_direction > 0) ? range_dst_item : ms->EndIO.RangeSrcItem; + req.RangeFirstItem = (range_direction > 0) ? ms->EndIO.RangeSrcItem : range_dst_item; + req.RangeLastItem = (range_direction > 0) ? range_dst_item : ms->EndIO.RangeSrcItem; + ms->EndIO.Requests.push_back(req); } // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect)