From c9d958f1c04afae02d92528c1e9e2ef675c804fb Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 1 May 2026 02:16:14 +0200 Subject: [PATCH] feat(table-ux): selectable rows + tables dropdown + filtros por columna MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- views.cpp | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- views.h | 6 +++ 2 files changed, 159 insertions(+), 3 deletions(-) diff --git a/views.cpp b/views.cpp index 265efb7..71de634 100644 --- a/views.cpp +++ b/views.cpp @@ -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& visible_indices) { ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | @@ -1480,7 +1595,7 @@ void render_one_table(AppState& app, std::vector& 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; diff --git a/views.h b/views.h index 5ad581a..41c2663 100644 --- a/views.h +++ b/views.h @@ -204,6 +204,12 @@ struct AppState { char table_search_buf[96] = {}; bool table_show_all = false; int table_active_tab = 0; + // Filtros por columna: column_user_id (0..5) -> substring filter. + // Visible UX: right-click sobre header de columna abre popup con input; + // chips con filtros activos por encima de la tabla. + std::unordered_map table_col_filters; + char table_filter_input[96] = {}; // buffer del popup activo + int table_filter_pending_col = -1; // col_user_id en edicion // ---- Type Editor (issue 0007) ------------------------------------------ // Draft del editor de tipos. Se inicializa con una copia de parsed_types