5 Multi Select
omar edited this page 2023-12-21 16:03:14 +01:00

Multi-Select

  • This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) and supports a clipper being used. Handling this manually and correctly is tricky, this is why we provide the functionality. If you don't need SHIFT+Mouse/Keyboard range-select + clipping, you could technically implement a simple form of multi-selection yourself, by reacting to click/presses on Selectable() items.
  • TreeNode() and Selectable() are supported but custom widgets may use it as well.
  • In the spirit of Dear ImGui design, your code owns actual selection data. This is designed to allow all kinds of selection storage you may use in your application e.g. external selection (set/map/hash), intrusive selection (bool inside your objects) etc.
  • 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). See "Usage flow" section below for details. If you are not sure, always start without clipping! You can work your way to the optimized version afterwards.

Features

  • Design allows all item data and selection data to be fully owned by user. Agnostic to storage type.
  • Support CTRL+Click
  • Support Shift+Click
  • Support mouse box-selection (with scrolling).
  • Compatible with keyboard navigation, incl CTRL+Arrow, SHIFT+Arrows but also naturally works with PageUp/PageDown, Home/End etc.
  • Compatible with ImGuiListClipper.
  • Compatible with drag and drop idioms.
  • ImGuiSelectionBasicStorage helper used by demos and for quick-start/convenience. Advanced users may bypass it.
  • Demos in Widgets->Selection State & Multi-Select and Examples->Assets Browser.

Principal APIs

// Main API
ImGuiMultiSelectIO*   BeginMultiSelect(ImGuiMultiSelectFlags flags);
ImGuiMultiSelectIO*   EndMultiSelect();
void                  SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data);
// Main IO structure returned by BeginMultiSelect()/EndMultiSelect().
// This mainly contains a list of selection requests.
// - Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen.
// - Some fields are only useful if your list is dynamic and allows deletion (getting post-deletion focus/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.
struct ImGuiMultiSelectIO
{
    //------------------------------------------// BeginMultiSelect / EndMultiSelect
    ImVector<ImGuiSelectionRequest> Requests;   //  ms:w, app:r     /  ms:w  app:r   // Requests to apply to your selection data.
    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).
};

// Selection request item
struct ImGuiSelectionRequest
{
    //------------------------------------------// BeginMultiSelect / EndMultiSelect
    ImGuiSelectionRequestType   Type;           //  ms:w, app:r     /  ms:w, app:r   // Request type. You'll most often receive 1 Clear + 1 SetRange with a single-item range.
    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)
};

// Selection request type
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.
};

TL;DR;

  • Identify submitted items with SetNextItemSelectionUserData(), most likely using an index into your current data-set.
  • Store and maintain actual selection data using persistent object identifiers.
  • Usage Flow:
    • (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result.
    • (2) 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.IncludeItemByIndex().
      • If storing indices in ImGuiSelectionUserData, a simple clipper.IncludeItemByIndex(ms_io->RangeSrcItem) call will work.
    • (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls.
    • (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result.
    • (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 are optional and will be handled by each item themselves. It is fine to always honor those steps.

About ImGuiSelectionUserData

  • For each item is it submitted by your call to SetNextItemSelectionUserData().
  • This can store an application-defined identifier (e.g. index or pointer).
  • In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO.
  • Most applications will store an object INDEX, hence the chosen name and type. Storing an integer index is the easiest thing to do, as SetRange requests will give you two end-points and you will need to iterate/interpolate between them to update your selection.
  • However it is perfectly possible to store a POINTER or another IDENTIFIER inside this value! Our system never assume that you identify items by indices, it never attempts to interpolate between two values.
  • As most users will want to store an index, for convenience and to reduce confusion we use ImS64 instead of void*, being syntactically easier to downcast. Feel free to reinterpret_cast and store a pointer inside.
  • If you need to wrap this API for another language/framework, feel free to expose this as 'int' if simpler.

With the ImGuiSelectionBasicStorage helper

ImGuiSelectionBasicStorage is an optional helper to store multi-selection state + apply multi-selection requests.

  • Used by our demos and provided as a convenience to easily implement basic multi-selection.
  • USING THIS IS NOT MANDATORY. This is only a helper and not a required API. Advanced users are likely to implement their own.

Minimum pseudo-code example using this helper:

static vector<MyItem> items;                  // Your items
static ImGuiSelectionBasicStorage selection;  // Your selection
selection.AdapterData = (void*)&items;        // Setup adapter so selection.ApplyRequests() function can convert indexes to identifiers.
selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((vector<MyItem>*)self->AdapterData))[idx].ID; };

ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_None);
selection.ApplyRequests(ms_io, items.Size);
for (int idx = 0; idx < items.Size; idx++)
{
    bool item_is_selected = selection.Contains(items[idx].ID);
    ImGui::SetNextItemSelectionUserData(idx);
    ImGui::Selectable(label, item_is_selected);
}
ms_io = ImGui::EndMultiSelect();
selection.ApplyRequests(ms_io, items.Size);

To store a multi-selection, in your real application you could:

  • A) Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement.
  • B) Use your own external storage: e.g. std::set, std::vector, interval trees, etc.
  • C) Use intrusively stored selection (e.g. 'bool IsSelected' inside objects). Not recommended because you can't have multiple views over same objects. Also some features requires to provide selection size, which with this strategy requires additional work.

Our BeginMultiSelect() api/system doesn't make assumption about:

  • how you want to identify items in multi-selection API? (Indices or Custom Ids or Pointers? Indices are better: easy to iterate/interpolate)
  • how you want to store persistent selection data? (Indices or Custom Ids or Pointers? Custom Ids is better: as selection can persist)

In ImGuiSelectionBasicStorage we:

  • always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO)
  • use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index.
  • in some cases we use Index as custom identifier (default implementation returns Index cast as Identifier): only valid for a never changing item list.
  • in some cases we read an ID from some custom item data structure (better, and closer to what you would do in your codebase)

Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. When your application settles on a choice, you may want to get rid of this indirection layer and do your own thing.