feat(table-ux): selectable rows + tables dropdown + filtros por columna
Tres cambios pequenos relacionados con la UX de las tablas: 1. fix views_table_window: la fila usaba TextUnformatted en col 0 que no registra hover/double-click sobre toda la fila. Reemplazado por ImGui::Selectable con SpanAllColumns + AllowDoubleClick — ahora el doble-click sobre fila no promovida promueve, sobre promovida abre Inspector. El popup right-click tambien funciona ahora. 2. Toolbar 'Tables (N)' dropdown que lista las Table windows abiertas con checkbox. Desmarcar = colapsar (cerrar ventana + expanded=false). Tambien tiene 'Collapse all' al final. 3. views_table (issue 0004) — filtros por columna: - Right-click sobre header de columna abre popup con InputText. - Apply / Clear / Enter aceptan y guardan en table_col_filters. - Chips arriba de la tabla con cada filtro activo + X para quitar. - Boton 'Clear all'. - build_visible aplica los filtros con substring case-insensitive.
This commit is contained in:
@@ -301,6 +301,38 @@ void views_toolbar(AppState& app) {
|
||||
app.import_error.clear();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
// Dropdown "Tables ▾" — toggle visibilidad de las ventanas Table
|
||||
// expandidas. Desmarcar = colapsar (cerrar ventana + expanded=false).
|
||||
{
|
||||
char btn[64];
|
||||
std::snprintf(btn, sizeof(btn), TI_TABLE " Tables (%zu)",
|
||||
app.table_windows.size());
|
||||
if (button(btn, ButtonVariant::Subtle)) {
|
||||
ImGui::OpenPopup("##tables_menu");
|
||||
}
|
||||
if (ImGui::BeginPopup("##tables_menu")) {
|
||||
if (app.table_windows.empty()) {
|
||||
ImGui::TextDisabled("(no expanded tables)");
|
||||
} else {
|
||||
for (auto& kv : app.table_windows) {
|
||||
bool checked = kv.second.open;
|
||||
char lbl[160];
|
||||
std::snprintf(lbl, sizeof(lbl), "%s (%lld rows)",
|
||||
kv.second.meta.name.c_str(),
|
||||
(long long)kv.second.total_rows);
|
||||
if (ImGui::MenuItem(lbl, nullptr, checked)) {
|
||||
kv.second.open = !checked;
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(TI_X " Collapse all")) {
|
||||
for (auto& kv : app.table_windows) kv.second.open = false;
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
// Add node — input + auto-deteccion de tipo. Enter o boton "Add" lo
|
||||
// confirman; main.cpp inserta en operations.db y dispara reload.
|
||||
@@ -1464,6 +1496,89 @@ bool ci_contains(const std::string& hay, const char* needle) {
|
||||
return h.find(n) != std::string::npos;
|
||||
}
|
||||
|
||||
// Mapeo column_user_id -> nombre legible y string-getter sobre TableRow.
|
||||
struct TableColMeta {
|
||||
int user_id;
|
||||
const char* name;
|
||||
};
|
||||
const TableColMeta k_table_cols[] = {
|
||||
{0, "id"}, {1, "name"}, {2, "type"}, {3, "status"},
|
||||
{4, "updated_at"}, {5, "neighbors"},
|
||||
};
|
||||
constexpr int k_table_col_n = (int)(sizeof(k_table_cols) / sizeof(k_table_cols[0]));
|
||||
|
||||
const std::string& table_row_field(const AppState::TableRow& r, int user_id) {
|
||||
static const std::string empty_str;
|
||||
static thread_local std::string scratch;
|
||||
switch (user_id) {
|
||||
case 0: return r.id;
|
||||
case 1: return r.name;
|
||||
case 2: return r.type_ref;
|
||||
case 3: return r.status;
|
||||
case 4: return r.updated_at;
|
||||
case 5: scratch = std::to_string(r.neighbors); return scratch;
|
||||
}
|
||||
return empty_str;
|
||||
}
|
||||
|
||||
const char* table_col_name_by_id(int user_id) {
|
||||
for (int i = 0; i < k_table_col_n; ++i)
|
||||
if (k_table_cols[i].user_id == user_id) return k_table_cols[i].name;
|
||||
return "?";
|
||||
}
|
||||
|
||||
// Render header row con popup right-click por columna para anadir filtro.
|
||||
void render_table_headers_with_filters(AppState& app) {
|
||||
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
|
||||
for (int i = 0; i < k_table_col_n; ++i) {
|
||||
ImGui::TableSetColumnIndex(i);
|
||||
const char* name = ImGui::TableGetColumnName(i);
|
||||
ImGui::PushID(i);
|
||||
ImGui::TableHeader(name);
|
||||
if (ImGui::BeginPopupContextItem("##colfilt",
|
||||
ImGuiPopupFlags_MouseButtonRight)) {
|
||||
int user_id = k_table_cols[i].user_id;
|
||||
ImGui::TextDisabled("Filter %s", k_table_cols[i].name);
|
||||
ImGui::Separator();
|
||||
// Si reabrimos el popup para esta columna, sembrar el buffer.
|
||||
if (app.table_filter_pending_col != user_id) {
|
||||
app.table_filter_pending_col = user_id;
|
||||
auto it = app.table_col_filters.find(user_id);
|
||||
std::snprintf(app.table_filter_input, sizeof(app.table_filter_input),
|
||||
"%s", it == app.table_col_filters.end() ? "" : it->second.c_str());
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
ImGui::SetNextItemWidth(220);
|
||||
ImGuiInputTextFlags fflags = ImGuiInputTextFlags_EnterReturnsTrue;
|
||||
bool commit = ImGui::InputTextWithHint("##filt_in", "substring (case-insensitive)",
|
||||
app.table_filter_input,
|
||||
sizeof(app.table_filter_input), fflags);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Apply") || commit) {
|
||||
if (app.table_filter_input[0]) {
|
||||
app.table_col_filters[user_id] = app.table_filter_input;
|
||||
} else {
|
||||
app.table_col_filters.erase(user_id);
|
||||
}
|
||||
app.table_filter_pending_col = -1;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Clear")) {
|
||||
app.table_col_filters.erase(user_id);
|
||||
app.table_filter_input[0] = 0;
|
||||
app.table_filter_pending_col = -1;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
} else if (app.table_filter_pending_col == k_table_cols[i].user_id) {
|
||||
// popup se cerro sin aplicar — limpiar pending.
|
||||
app.table_filter_pending_col = -1;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void render_one_table(AppState& app, std::vector<int>& visible_indices) {
|
||||
ImGuiTableFlags flags =
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
@@ -1480,7 +1595,7 @@ void render_one_table(AppState& app, std::vector<int>& visible_indices) {
|
||||
ImGui::TableSetupColumn("updated_at", ImGuiTableColumnFlags_None, 0, 4);
|
||||
ImGui::TableSetupColumn("neighbors", ImGuiTableColumnFlags_WidthFixed,
|
||||
80.0f, 5);
|
||||
ImGui::TableHeadersRow();
|
||||
render_table_headers_with_filters(app);
|
||||
|
||||
ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs();
|
||||
if (specs && specs->SpecsDirty) {
|
||||
@@ -1551,6 +1666,29 @@ void views_table(AppState& app) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%zu rows", app.table_rows.size());
|
||||
|
||||
// Chips de filtros activos por columna (right-click sobre header lo anade).
|
||||
if (!app.table_col_filters.empty()) {
|
||||
ImGui::TextDisabled("Filters:");
|
||||
ImGui::SameLine();
|
||||
int del_col = -1;
|
||||
for (auto& kv : app.table_col_filters) {
|
||||
ImGui::SameLine();
|
||||
char chip[160];
|
||||
std::snprintf(chip, sizeof(chip), TI_FILTER " %s: %s " TI_X "##chip_%d",
|
||||
table_col_name_by_id(kv.first), kv.second.c_str(), kv.first);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.30f, 0.50f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.40f, 0.65f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.35f, 0.50f, 0.75f, 1.0f));
|
||||
if (ImGui::SmallButton(chip)) del_col = kv.first;
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
if (del_col >= 0) app.table_col_filters.erase(del_col);
|
||||
ImGui::SameLine();
|
||||
if (fn_ui::button("Clear all", fn_ui::ButtonVariant::Subtle)) {
|
||||
app.table_col_filters.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (app.table_rows.empty()) {
|
||||
ImGui::TextDisabled("(no entities loaded)");
|
||||
ImGui::End();
|
||||
@@ -1577,6 +1715,13 @@ void views_table(AppState& app) {
|
||||
if (app.table_search_buf[0]
|
||||
&& !ci_contains(r.name, app.table_search_buf)
|
||||
&& !ci_contains(r.id, app.table_search_buf)) continue;
|
||||
// Filtros por columna (AND de todos).
|
||||
bool reject = false;
|
||||
for (auto& kv : app.table_col_filters) {
|
||||
const std::string& field = table_row_field(r, kv.first);
|
||||
if (!ci_contains(field, kv.second.c_str())) { reject = true; break; }
|
||||
}
|
||||
if (reject) continue;
|
||||
v.push_back((int)i);
|
||||
}
|
||||
return v;
|
||||
@@ -1722,7 +1867,12 @@ void views_table_window(AppState& app) {
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
bool is_promoted = !row.promoted_entity_id.empty();
|
||||
ImGui::TextUnformatted(row.id.c_str());
|
||||
|
||||
// Selectable spanning para que el doble-click y el right-click
|
||||
// funcionen sobre toda la fila, no solo el texto del id.
|
||||
ImGuiSelectableFlags sf = ImGuiSelectableFlags_SpanAllColumns
|
||||
| ImGuiSelectableFlags_AllowDoubleClick;
|
||||
ImGui::Selectable(row.id.c_str(), false, sf);
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
||||
if (is_promoted) {
|
||||
app.want_focus_entity = true;
|
||||
@@ -1733,7 +1883,7 @@ void views_table_window(AppState& app) {
|
||||
app.promote_row_id = row.id;
|
||||
}
|
||||
}
|
||||
if (ImGui::BeginPopupContextItem("##trowctx")) {
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (is_promoted) {
|
||||
if (ImGui::MenuItem(TI_FOCUS " Focus in Inspector")) {
|
||||
app.want_focus_entity = true;
|
||||
|
||||
Reference in New Issue
Block a user