Skip to content

Multi Select

omar edited this page Jul 30, 2024 · 14 revisions

The feature has been merged into master on July 18, 2024 (version 1.91.0 WIP)

Multi-Select

Index


Overview

  • 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.
  • Selectable(), Checkbox() are supported but custom widgets may use it as well.
  • TreeNode() is technically supported but... using this correctly is more complicated: you need some sort of linear/random access to your tree, which is suited to advanced trees setups also implementing filters and clipper. We will work toward simplifying and demoing it.
  • 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.
  • ImGuiSelectionExternalStorage helper used to easily wire multi-select to existing randomly accessible storage.

Demo Code

Always refer to demo code for usage. Demos are in Demo->Widgets->Selection State & Multi-Select and Demo->Examples->Assets Browser.


Using 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.

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.Size, items.Size);
selection.ApplyRequests(ms_io);
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);

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.

(In theory, for maximum abstraction, this class could contains AdapterIndexToUserData() and AdapterUserDataToIndex() functions as well, but because we mostly use indices in SetNextItemSelectionUserData(), we omit those indirection for clarity.)


Main API

💡 This is the low-level API. When using the ImGuiSelectionBasicStorage you may not need to care about details of ImGuiMultiSelectIO and ImGuiSelectionRequest.

// Flags for BeginMultiSelect()
enum ImGuiMultiSelectFlags_
{
    ImGuiMultiSelectFlags_None                  = 0,
    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+selection mouse/keyboard support (useful for unordered 2D selection). With BoxSelect is also ensure contiguous SetRange requests are not combined into one. This allows not handling interpolation in SetRange requests.
    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 selection when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes)
    ImGuiMultiSelectFlags_NoAutoClearOnReselect = 1 << 5,   // Disable clearing selection when clicking/selecting an already selected item
    ImGuiMultiSelectFlags_BoxSelect1d           = 1 << 6,   // Enable box-selection with same width and same x pos items (e.g. only full row Selectable()). 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 << 7,   // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This is slower: alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items.
    ImGuiMultiSelectFlags_BoxSelectNoScroll     = 1 << 8,   // Disable scrolling when box-selecting near edges of scope.
    ImGuiMultiSelectFlags_ClearOnEscape         = 1 << 9,   // Clear selection when pressing Escape while scope is focused.
    ImGuiMultiSelectFlags_ClearOnClickVoid      = 1 << 10,  // Clear selection when clicking on empty location within scope.
    ImGuiMultiSelectFlags_ScopeWindow           = 1 << 11,  // Scope for _BoxSelect and _ClearOnClickVoid is whole window (Default). Use if BeginMultiSelect() covers a whole window or used a single time in same window.
    ImGuiMultiSelectFlags_ScopeRect             = 1 << 12,  // Scope for _BoxSelect and _ClearOnClickVoid is rectangle encompassing BeginMultiSelect()/EndMultiSelect(). Use if BeginMultiSelect() is called multiple times in same window.
    ImGuiMultiSelectFlags_SelectOnClick         = 1 << 13,  // Apply selection on mouse down when clicking on unselected item. (Default)
    ImGuiMultiSelectFlags_SelectOnClickRelease  = 1 << 14,  // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection.
    //ImGuiMultiSelectFlags_RangeSelect2d       = 1 << 15,  // Shift+Selection uses 2d geometry instead of linear sequence, so possible to use Shift+up/down to select vertically in grid. Analogous to what BoxSelect does.
    ImGuiMultiSelectFlags_NavWrapX              = 1 << 16,  // [Temporary] Enable navigation wrapping on X axis. Provided as a convenience because we don't have a design for the general Nav API for this yet. When the more general feature be public we may obsolete this flag in favor of new one.
};
// Main API
ImGuiMultiSelectIO*   BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size = -1, int items_count = -1);
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).
    int                         ItemsCount;     //  ms:w, app:r     /        app:r   // 'int items_count' parameter to BeginMultiSelect() is copied here for convenience, allowing simpler calls to your ApplyRequests handler. Not used internally.
};

// 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                        Selected;       //                  /  ms:w, app:r   // Parameter for SetAll/SetRange request (true = select, false = unselect)
    ImS8                        RangeDirection; //                  /  ms:w  app:r   // Parameter for SetRange request: +1 when RangeFirstItem comes before RangeLastItem, -1 otherwise. Useful if you want to preserve selection order on a backward Shift+Click.
    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_SetAll,           // Request app to clear selection (if Selected==false) or select all items (if Selected==true)
    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.
};

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) [If using clipper] 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.
    • (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls.
    • (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result.
    • (6) Honor request list (SetAll/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.

Using ImGuiSelectionExternalStorage helper

Optional helper to apply multi-selection requests to existing randomly accessible storage. Convenient if you want to quickly wire multi-select API on e.g. items storing their own selection state, or an array of bools.


Using multi-select with trees

We do provide a demo, however, THIS IS CURRENTLY NOT SIMPLE.
Next version after 1.91, I aim to provide better idioms and examples for dealing with trees.

multiselect_trees