Table/grid range select
Version/Branch of Dear ImGui:
Version 1.90.1, Branch: docking
Back-ends:
imgui_impl_win32.cpp + imgui_impl_opengl3.cpp
Compiler, OS:
Windows 11 + GCC 12.2.0
Full config/build information:
No response
Details:
I have question what is best method to implement range select in table.
I have to implement table with scalar input. In this table I need to be able to select an area and change the values in all selected cells. Also I have to know which cells have been changed.
At this moment i pass to my function vectors that hold cell state.
Problems:
- Current selection method need to hold each cell flag, and cell background is set in next loop iteration
- Selection is not limited to only one table (video)
- After i.e. change window size keyboard focus is lost
Maybe someone have some ideas how to improve my implementation?
Screenshots/Video:
https://github.com/ocornut/imgui/assets/34631947/21a44096-01c2-4284-942c-0020d9a3ee94
Minimal, Complete and Verifiable Example code:
static ImVec2 sel_start, sel_end;
static bool selActive;
static bool SelectionRect(ImVec2& start_pos, ImVec2& end_pos, ImGuiMouseButton mouse_button)
{
//IM_ASSERT(start_pos != NULL);
//IM_ASSERT(end_pos != NULL);
bool selecting = false;
if (ImGui::IsMouseClicked(mouse_button))
start_pos = ImGui::GetMousePos();
if (ImGui::IsMouseDown(mouse_button)) {
selecting = true;
end_pos = ImGui::GetMousePos();
ImDrawList* draw_list = ImGui::GetForegroundDrawList(); //ImGui::GetWindowDrawList();
draw_list->AddRect(start_pos, end_pos, ImGui::GetColorU32(IM_COL32(0, 130, 216, 255))); // Border
draw_list->AddRectFilled(start_pos, end_pos, ImGui::GetColorU32(IM_COL32(0, 130, 216, 50))); // Background
}
//return ImGui::IsMouseReleased(mouse_button);
return selecting;
}
static bool isSelected(ImVec2& sel_min, ImVec2& sel_max, ImVec2& item_min, ImVec2& item_max ) {
ImVec2 tmp_min = sel_min;
ImVec2 tmp_max = sel_max;
if(tmp_min.x > tmp_max.x) {
float x_min = tmp_min.x;
float x_max = tmp_max.x;
tmp_min.x = x_max;
tmp_max.x = x_min;
}
if(tmp_min.y > tmp_max.y) {
float y_min = tmp_min.y;
float y_max = tmp_max.y;
tmp_min.y = y_max;
tmp_max.y = y_min;
}
if( tmp_min.x < item_max.x
&& tmp_min.y < item_max.y
&& tmp_max.x > item_min.x
&& tmp_max.y > item_min.y) {
return true;
}
else {
}
return false;
}
void * getter(void * base, int index, int stride) {
return base+(index*stride);
}
bool GUI::wdgMapNew(const char* label,
int sizeX,
int sizeY,
float* valX,
float* valY,
float* valV,
bool* selected,
bool* edited,
int lenX,
int lenY,
int stride,
const char* formatX,
const char* formatY,
const char* formatV) {
bool isEdited = false;
bool firstCellFlag = false;
bool setSelectionFlag = false;
float toSet = 0.0f;
ImGuiTableFlags flags =
ImGuiTableFlags_Borders | ImGuiTableFlags_NoPadInnerX | ImGuiTableFlags_NoPadOuterX;
//ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY |
//ImGuiTableFlags_NoHostExtendX | ImGuiTableFlags_NoHostExtendY;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0, 0));
if ( ImGui::BeginTable(label, sizeX+1, flags) ) {
//bool selActive = SelectionRect(sel_start, sel_end);
ImGui::PushID(label);
ImGui::TableNextColumn(); // first empty, could be i.e. units? bar/rpm
for(int x=0; x<sizeX; x++) {
if(x >= lenX) {
break;
}
ImGui::TableNextColumn();
ImGui::PushID(x);
ImGui::SetNextItemWidth(-FLT_MIN);
ImGui::InputScalar("##AxisX", ImGuiDataType_Float, getter(valX, x, stride), NULL, NULL, formatX);
ImGui::PopID();
}
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGui::GetStyleColorVec4(ImGuiCol_ChildBg));
for(int y=0; y<sizeY; y++) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
if(y < lenY) {
ImGui::PushID(y);
ImGui::SetNextItemWidth(-FLT_MIN);
ImGui::InputScalar("##AxisY", ImGuiDataType_Float, getter(valY, y, stride), NULL, NULL, formatY);
ImGui::PopID();
}
for(int x=0; x<sizeX; x++) {
int index = y*lenX + x;
if(index >= lenX*lenY) {
break;
}
ImGui::TableNextColumn();
ImGui::PushID(index);
float prev = *(float*)getter(valV, index, stride);
bool * pIsSelected = (bool*)getter(selected, index, stride);
if(*pIsSelected) {
ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(0, 130, 216, 50));
if(!firstCellFlag) {
firstCellFlag = true;
if(selActive) {
ImGui::SetKeyboardFocusHere();
}
}
if(setSelectionFlag) {
*(float*)getter(valV, index, stride) = toSet;
}
}
ImGui::SetNextItemWidth(-FLT_MIN);
if (ImGui::InputScalar("##Data", ImGuiDataType_Float, getter(valV, index, stride), NULL, NULL, formatV, ImGuiInputTextFlags_EnterReturnsTrue) ) {
// on enter edit all other values
*(bool*)getter(edited, index, stride) = true;
}
else {
*(bool*)getter(edited, index, stride) = false;
}
// catch event when there is no change in value, but i want to set other cells
bool enterPressed = ImGui::IsKeyPressed(ImGuiKey_Enter) | ImGui::IsKeyPressed(ImGuiKey_KeypadEnter);
if( enterPressed && ImGui::IsItemDeactivated() ) {
setSelectionFlag = true;
toSet = *(float*)getter(valV, index, stride);
isEdited = true;
}
// check if item changed
if(isEdited) {
if(prev != *(float*)getter(valV, index, stride)) {
*(bool*)getter(edited, index, stride) = true;
}
else {
*(bool*)getter(edited, index, stride) = false;
}
}
if(*pIsSelected) {
ImGui::PopStyleColor();
}
ImVec2 cell_start = ImGui::GetItemRectMin();
ImVec2 cell_end = ImGui::GetItemRectMax();
if(selActive) {
*pIsSelected = isSelected(sel_start, sel_end, cell_start, cell_end);
}
ImGui::PopID();
}
}
ImGui::PopStyleColor();
ImGui::PopID();
ImGui::EndTable();
bool isActive = ImGui::IsItemActive();
bool isHovered = ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly);
bool isFocused = ImGui::IsItemFocused();
if(ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly)) {
selActive = SelectionRect(sel_start, sel_end);
}
else {
// selActive = false;
}
ImGui::Text("active: %d, hovered: %d, focused: %d, selActive: %d", isActive, isHovered, isFocused, selActive);
}
ImGui::PopStyleVar(2);
return isEdited;
}
typedef struct {
float val;
bool isSelected;
bool isEdited;
}TABLE_CELL;
class TABLE {
public:
TABLE() {};
TABLE(unsigned int sizeX, unsigned int sizeY) {
resize(sizeX, sizeY);
}
~TABLE() {};
int cellSize(void) { return sizeof(TABLE_CELL); }
void resize(unsigned int sizeX, unsigned int sizeY) {
X.resize(sizeX);
Y.resize(sizeY);
V.resize(sizeX*sizeY);
}
public:
std::vector<TABLE_CELL> X;
std::vector<TABLE_CELL> Y;
std::vector<TABLE_CELL> V;
};
static bool wdgTable(const char* label, TABLE &t) {
return GUI::wdgMapNew(label,
t.X.size(),
t.Y.size(),
&t.X[0].val,
&t.Y[0].val,
&t.V[0].val,
&t.V[0].isSelected,
&t.V[0].isEdited,
t.X.size(),
t.Y.size(),
t.cellSize(),
"%.0f", "%.0f");
}
Have you tried the range_select branch to see if it meets your needs?
I know Omar is looking for feedback on it, and in the near future it should be the canonical way to handle what you're doing.
It can be merged into docking with some minor conflict resolution.
I need couple days to try it :)
This feature looks very promising. But actually i don't know if there is posibility to select input widgets i.e. InputScalar?. Also i have some strange behavior during rectangle selection (video)
https://github.com/ocornut/imgui/assets/34631947/4b36f793-99bd-4b6c-88f3-d2f1a0914185
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ClearOnClickVoid | ImGuiMultiSelectFlags_BoxSelect2d);
static ImGuiSelectionBasicStorage selection;
selection.ApplyRequests(ms_io, 16*16);
ImGui::Text("Selection: %d/%d", selection.Size, 16*16);
if ( ImGui::BeginTable("MAP NEW", 16, flags) ) {
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGui::GetStyleColorVec4(ImGuiCol_ChildBg));
for(int y=0; y<16; y++) {
ImGui::TableNextRow();
for(int x=0; x<16; x++) {
ImGui::TableNextColumn();
ImGui::PushID(y+x*16);
ImGui::SetNextItemWidth(50.0);
bool IsSelected = selection.Contains((ImGuiID)(y+x*16));
ImGui::SetNextItemSelectionUserData(y+x*16);
if(IsSelected) {
ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(0, 130, 216, 50));
}
char label[64];
sprintf(label, "%d", y+x*16);
ImGui::Selectable(label, IsSelected);
//ImGui::Text(label);
//ImGui::Checkbox(label, (bool*)&data[y][x]);
/*if (ImGui::InputScalar("##Data", ImGuiDataType_Float, &data[y][x], NULL, NULL, "%.2f", ImGuiInputTextFlags_EnterReturnsTrue) ) {
dataToSet = data[y][x];
}*/
if(IsSelected) {
ImGui::PopStyleColor();
}
ImGui::PopID();
}
}
ImGui::PopStyleColor();
ImGui::EndTable();
}
ms_io = ImGui::EndMultiSelect();
selection.ApplyRequests(ms_io, 16*16);
Thank you for your thoughtful and careful repro, I will investigate it.
An InputScalar() is not selectable but I'm not sure what it would mean to select it.
Would the underlying intent to e.g. select many fields and type in all of them together ?
Note how you are using "y + x * 16" everywhere, meaning your selectables are not submitted in the same sequential order as their value, and by default ImGuiSelectionBasicStorage assume that value passed to ImGui::SetNextItemSelectionUserData() are interpolable indices.
[A] If you instead use:
//int idx = y + x * 16; // Broken
int idx = x + y * 16; // OK
(and change all values to use idx)
Note the value order now goes left-to-right, top-to-bottom:
Then it works, but note that shift+down/up assume a type of selection that's not necessarily what you want here (I think we may need a flag to make shift+select use 2d coordinates rather than sequential?). That's the case for all three alternatives.
[B] Alternatively, you change change the idx->stored selection id mapping:
selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx)
{
int x = idx % 16;
int y = idx / 16;
return (ImGuiID)(y + x * 16);
};
int idx = x + y * 16; // Submission index
int id = y + x * 16; // ID (== selection.AdapterIndexToStorageId(idx))
//IM_ASSERT(selection.AdapterIndexToStorageId(&selection, idx) == id);
[...]
ImGui::PushID(idx); // <-- here it doesn't matter which you use as long as it is unique
[...]
ImGui::SetNextItemSelectionUserData(idx); // <-- here you pass index
[...]
bool IsSelected = selection.Contains((ImGuiID)(id)); // <-- Stored selection ID, == selection.AdapterIndexToStorageId(idx)
[C] A third alternative would to submit items in the same order as the id you want to use. aka fill entire column first, but it may be harder to perform clipping there.
( I also found an issue when using box-select in a window that is not a child window. The current logic prevents focusing, steals hovers and nav id. I pushed a mitigation (a304677) to allow clicking on title bar at least, and will need to revisit some of the logic for box-select. )
This is really useful feedback as I found two things to improve already. Thanks!
(I also found an issue when using box-select in a window that is not a child window. The current logic prevents focusing, steals hovers and nav id. I pushed a mitigation to allow clicking on title bar at least, and will need to revisit some of the logic for box-select.)
Pushed a better fix d60299d for both ScopeWindow and ScopeRect cases.
Thank you for your thoughtful and careful repro, I will investigate it.
An
InputScalar()is not selectable but I'm not sure what it would mean to select it. Would the underlying intent to e.g. select many fields and type in all of them together ?
Yes exactly. I need to select cells and be able to change them to same value, increase all or even extrapolate (smooth)
Yes exactly. I need to select cells and be able to change them to same value, increase all or even extrapolate (smooth)
I think in this case it makes sense to display and focus a single InputText() widget, and when edited apply the value to all of selection.
Aka you don't need (and you cannot have) multiple active InputText(), but one can represent the selection.
Is this possible to draw this InputText Over Selectable?
Yes, you need to pass ImGuiSelectableFlags_AllowOverlap to the `Selectable().
See Demo->Layout->Overlap Mode or Demo->Widgets->Selectables->Rendering more items on the same line
I am going to try working on a simple demo to demonstrate a grid with text editable items that allows multi-write like this.
I wrote a draft of it but it doesn't allow to multi-edit as currently multi-select system has a bias toward unselecting others when e.g. pressing enter on an item, so may need an improvement of multi-select.
void DemoSelectableEditableGrid()
{
ImGui::Begin("Selection #7424");
{
const int COUNT_X = 10;
const int COUNT_Y = 16;
const int COUNT = COUNT_X * COUNT_Y;
static ImGuiSelectionBasicStorage selection;
static float data[COUNT];
static int editing_n = -1;
static int focus_n_next = -1;
// FIXME: don't clear selection when clicking selected item
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect2d);
selection.ApplyRequests(ms_io, COUNT);
ImGui::Text("Selection: %d/%d", selection.Size, COUNT);
const int focus_n_curr = focus_n_next;
focus_n_next = -1;
if (ImGui::BeginTable("Array", COUNT_X, ImGuiTableFlags_Borders))
{
for (int n = 0; n < COUNT; n++)
{
ImGui::TableNextColumn(); // Next cell w/ auto-wrap
ImGui::PushID(n);
const bool is_selected = selection.Contains((ImGuiID)n);
ImGui::SetNextItemSelectionUserData(n);
ImVec2 p = ImGui::GetCursorScreenPos();
if (focus_n_curr == n)
ImGui::SetKeyboardFocusHere(0);
if (editing_n != n)
{
char label[64];
sprintf(label, "%g###sel", data[n]);
ImGui::Selectable(label, is_selected);
if (ImGui::IsItemClicked() && ImGui::IsMouseDoubleClicked(0))
editing_n = focus_n_next = n;
if (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Enter))
editing_n = focus_n_next = n;
}
if (editing_n == n)
{
// May be easier if we had a public-facing version of TempInputXXX functions
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ImGui::GetColorU32(ImGuiCol_Header));
ImGui::SetCursorScreenPos(p);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
ImGui::SetNextItemWidth(-FLT_MIN);
// FIXME: May want to use InputText() and convert empty string to 0.0f (vs InputFloat preserve value)
ImGui::InputFloat("###edit", &data[n], 0.0f, 0.0f, "%g");
if (ImGui::IsItemDeactivated())
{
editing_n = -1;
if (ImGui::IsItemFocused() && !ImGui::IsMouseClicked(0))
focus_n_next = n;
}
ImGui::PopStyleVar();
}
ImGui::PopID();
}
ImGui::EndTable();
}
ms_io = ImGui::EndMultiSelect();
selection.ApplyRequests(ms_io, COUNT);
}
ImGui::End();
}
Honestly this is the kind of thing where there are lots of subtleties which are not trivial to get right/perfect with dear imgui, so it'll require more work. It'll be an interesting demo.
Hello
Thank @ocornut for Your reply. I think it is almost done.... One bad thing is that i have changed a little ImGui source code. This should be done in more sophisticated way.
https://github.com/ocornut/imgui/assets/34631947/036b4db9-f20b-4114-a134-2d5aa4457f29
void DemoSelectableEditableGrid()
{
ImGui::Begin("Selection #7424");
{
const int COUNT_X = 10;
const int COUNT_Y = 16;
const int COUNT = COUNT_X * COUNT_Y;
static ImGuiSelectionBasicStorage selection;
static float data[COUNT];
static int editing_n = -1;
static int focus_n_next = -1;
static bool set_flag = false;
static bool change_flag = false;
float change = 0.0f;
// FIXME: don't clear selection when clicking selected item
ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(
ImGuiMultiSelectFlags_ClearOnEscape
| ImGuiMultiSelectFlags_BoxSelect2d
| ImGuiMultiSelectFlags_ClearOnClickVoid
);
selection.ApplyRequests(ms_io, COUNT);
ImGui::Text("Selection: %d/%d", selection.Size, COUNT);
const int focus_n_curr = focus_n_next;
focus_n_next = -1;
if (ImGui::BeginTable("Array", COUNT_X, ImGuiTableFlags_Borders))
{
if(ImGui::IsKeyPressed(ImGuiKey_KeypadAdd)) {
change_flag = true;
change = 1.0f;
}
else if(ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract)) {
change_flag = true;
change = -1.0f;
}
for (int n = 0; n < COUNT; n++)
{
ImGui::TableNextColumn(); // Next cell w/ auto-wrap
ImGui::PushID(n);
const bool is_selected = selection.Contains((ImGuiID)n);
ImGui::SetNextItemSelectionUserData(n);
ImVec2 p = ImGui::GetCursorScreenPos();
if (focus_n_curr == n)
ImGui::SetKeyboardFocusHere(0);
char label[64];
sprintf(label, "%g###sel", data[n]);
ImGui::SetNextItemAllowOverlap();
ImGui::Selectable(label, is_selected);
if (ImGui::IsItemClicked() && ImGui::IsMouseDoubleClicked(0)) {
editing_n = focus_n_next = n;
}
if (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Enter)) {
editing_n = focus_n_next = n;
}
if (editing_n == n)
{
// May be easier if we had a public-facing version of TempInputXXX functions
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ImGui::GetColorU32(ImGuiCol_Header));
ImGui::SetCursorScreenPos(p);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
ImGui::SetNextItemWidth(-FLT_MIN);
// FIXME: May want to use InputText() and convert empty string to 0.0f (vs InputFloat preserve value)
ImGui::SetKeyboardFocusHere(0);
ImGui::InputFloat("###edit", &data[n], 0.0f, 0.0f, "%g");
if (ImGui::IsItemDeactivated())
{
set_flag = true;
change = data[n];
editing_n = -1;
if (ImGui::IsItemFocused() && !ImGui::IsMouseClicked(0))
focus_n_next = n;
}
ImGui::PopStyleVar();
}
ImGui::PopID();
}
ImGui::EndTable();
}
ms_io = ImGui::EndMultiSelect();
selection.ApplyRequests(ms_io, COUNT);
if(set_flag) {
set_flag = false;
for(int n = 0; n < COUNT; n++) {
if(selection.Contains((ImGuiID)n) ) {
data[n] = change;
}
}
}
if(change_flag) {
change_flag = false;
for(int n = 0; n < COUNT; n++) {
if(selection.Contains((ImGuiID)n) ) {
data[n] += change;
}
}
}
}
ImGui::End();
}
In Source code i just commented out clear request when focus is lost. ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) in imgui_widgets.cpp
else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
{
// Also clear on leaving scope (may be optional?)
if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) {
//request_clear = true;
}
}
In Source code i just commented out clear request when focus is lost. ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) in imgui_widgets.cpp
This is not specifically when focus is lost but when LEAVING the current scope. Can you clarify why you want/need to disable it? (then I can see if it's worth adding an option for it)
In Source code i just commented out clear request when focus is lost. ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags) in imgui_widgets.cpp
This is not specifically when focus is lost but when LEAVING the current scope. Can you clarify why you want/need to disable it? (then I can see if it's worth adding an option for it)
It was the easiest way to keep selection after enter pressed and switch focus to InputFloat
It was the easiest way to keep selection after enter pressed and switch focus to InputFloat
OK so that's a workaround but I will find a way to design a solution for it. Thanks for clarifying!
else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
{
// Also clear on leaving scope (may be optional?)
if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) {
//request_clear = true;
}
}
It's interesting because the logic in my code triggers the NavJustMovedToFocusScopeId block, while the logic in yours triggers the NavJustMovedFromFocusScopeId block. Will need further research before I can design a proper flag.
@tasiek30 I think the right fix is currently to do this:
Everytime you call SetKeyboardFocusHere(), add this line:
ImGui::SetKeyboardFocusHere(0);
ImGui::GetCurrentContext()->NavMoveFlags |= ImGuiNavMoveFlags_NoSelect;
Can you try it?
I am going to refactor and expose new functions such as ActivateItem() which will have those flags exposed in some way.
Note that API for BeginMultiSelect() changed since your example, you have to pass selection.Size and COUNT to BeginMultiSelect().
FYI
- multi-select has now been merged into master.
- ~~i have added a
ImGuiMultiSelectFlags_NoAutoClearOnReselectflag which essentially has the effect you wanted (so you don't need to use theImGuiNavMoveFlags_NoSelecthack)~~ apparently it is still needed
I am keeping this open because I would like to finish this "2d spreadsheet demo", however simple. I think one missing this for this is a flag to make RangeSelect a "2D" operation based on item geometry, so Shift+Down would select a single item in the next row instead of selecting in a 1D manner like e.g. explorer does. For some reason this is not trivial to implement so I'll do that later.
Hi. Thank You for support! Currently a have a lot of other work so i will check everything later.
Currently my table looks like below