4c04162e23
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
245 lines
8.6 KiB
C++
245 lines
8.6 KiB
C++
#include "data_table.h"
|
|
#include "imgui.h"
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
|
|
namespace data_table {
|
|
|
|
namespace {
|
|
|
|
// Estado UI por-celda/por-header — sobrevive entre frames pero NO se persiste
|
|
// a disco. Si se promueve al registry hay que pasarlo al State del caller.
|
|
struct UiState {
|
|
int pending_col = -1;
|
|
std::string pending_value;
|
|
bool open_cell_popup = false;
|
|
|
|
int header_popup_col = -1;
|
|
std::unordered_map<int, std::string> filter_inputs; // col -> buffer
|
|
std::unordered_map<int, std::string> color_value_inputs; // col -> buffer
|
|
std::unordered_map<int, ImVec4> color_picker_vals; // col -> color
|
|
};
|
|
|
|
UiState& ui() { static UiState s; return s; }
|
|
|
|
void ensure_init(State& st, int cols) {
|
|
if ((int)st.col_visible.size() != cols) st.col_visible.assign(cols, true);
|
|
}
|
|
|
|
void draw_chips(State& st, const char* const* headers, int cols) {
|
|
if (st.filters.empty()) {
|
|
ImGui::TextDisabled("Sin filtros. Click en celda -> elige operador.");
|
|
return;
|
|
}
|
|
for (size_t i = 0; i < st.filters.size(); ) {
|
|
const auto& f = st.filters[i];
|
|
const char* hdr = (f.col >= 0 && f.col < cols) ? headers[f.col] : "?";
|
|
char buf[256];
|
|
std::snprintf(buf, sizeof(buf), "%s %s %s x##chip%zu",
|
|
hdr, op_label(f.op), f.value.c_str(), i);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(60, 100, 160, 220));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(80, 130, 200, 240));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(45, 80, 130, 240));
|
|
bool clicked = ImGui::SmallButton(buf);
|
|
ImGui::PopStyleColor(3);
|
|
if (clicked) { st.filters.erase(st.filters.begin() + i); continue; }
|
|
ImGui::SameLine();
|
|
++i;
|
|
}
|
|
ImGui::NewLine();
|
|
}
|
|
|
|
// Devuelve true y rellena out si el usuario eligio un operador.
|
|
bool draw_op_menu_items(Op& out) {
|
|
const Op ops[] = {Op::Eq, Op::Neq, Op::Gt, Op::Gte, Op::Lt, Op::Lte};
|
|
for (Op o : ops) {
|
|
if (ImGui::MenuItem(op_label(o))) { out = o; return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void draw_header_menu(State& st, int col, const char* const* headers, int col_count) {
|
|
auto& U = ui();
|
|
auto& fbuf = U.filter_inputs[col];
|
|
fbuf.resize(256, '\0');
|
|
|
|
if (ImGui::BeginMenu("Filter...")) {
|
|
ImGui::SetNextItemWidth(180);
|
|
ImGui::InputText("##filterval", fbuf.data(), fbuf.size());
|
|
std::string val(fbuf.c_str());
|
|
const Op ops[] = {Op::Eq, Op::Neq, Op::Gt, Op::Gte, Op::Lt, Op::Lte};
|
|
for (size_t i = 0; i < sizeof(ops)/sizeof(ops[0]); ++i) {
|
|
if (i > 0) ImGui::SameLine();
|
|
if (ImGui::SmallButton(op_label(ops[i]))) {
|
|
st.filters.push_back({col, ops[i], val});
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("Conditional color")) {
|
|
auto& vbuf = U.color_value_inputs[col];
|
|
vbuf.resize(256, '\0');
|
|
auto it = U.color_picker_vals.find(col);
|
|
if (it == U.color_picker_vals.end()) {
|
|
U.color_picker_vals[col] = ImVec4(0.85f, 0.40f, 0.30f, 0.60f);
|
|
}
|
|
ImVec4& cv = U.color_picker_vals[col];
|
|
ImGui::SetNextItemWidth(180);
|
|
ImGui::InputText("equals", vbuf.data(), vbuf.size());
|
|
ImGui::ColorEdit4("color", &cv.x, ImGuiColorEditFlags_NoInputs);
|
|
if (ImGui::Button("Apply")) {
|
|
ImU32 c = ImGui::ColorConvertFloat4ToU32(cv);
|
|
st.color_rules.push_back({col, std::string(vbuf.c_str()), (unsigned int)c});
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Clear col")) {
|
|
for (size_t i = 0; i < st.color_rules.size();) {
|
|
if (st.color_rules[i].col == col) st.color_rules.erase(st.color_rules.begin() + i);
|
|
else ++i;
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::MenuItem("Hide column")) {
|
|
st.col_visible[col] = false;
|
|
}
|
|
|
|
ImGui::Separator();
|
|
if (ImGui::BeginMenu("Columns")) {
|
|
for (int k = 0; k < col_count; ++k) {
|
|
bool v = st.col_visible[k];
|
|
if (ImGui::Checkbox(headers[k], &v)) st.col_visible[k] = v;
|
|
}
|
|
if (ImGui::MenuItem("Show all")) {
|
|
for (int k = 0; k < col_count; ++k) st.col_visible[k] = true;
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void render(const char* id,
|
|
const char* const* headers,
|
|
int col_count,
|
|
const char* const* cells,
|
|
int row_count,
|
|
State& st)
|
|
{
|
|
ensure_init(st, col_count);
|
|
auto& U = ui();
|
|
|
|
draw_chips(st, headers, col_count);
|
|
|
|
auto visible_rows = compute_visible_rows(cells, row_count, col_count, st);
|
|
int visible_cols = 0;
|
|
for (bool v : st.col_visible) if (v) ++visible_cols;
|
|
|
|
ImGui::Text("Filas: %d / %d Columnas: %d / %d",
|
|
(int)visible_rows.size(), row_count, visible_cols, col_count);
|
|
|
|
if (visible_cols == 0) {
|
|
ImGui::TextDisabled("(todas las columnas ocultas - click derecho en cabecera anterior)");
|
|
return;
|
|
}
|
|
|
|
ImGuiTableFlags flags =
|
|
ImGuiTableFlags_Borders |
|
|
ImGuiTableFlags_Sortable |
|
|
ImGuiTableFlags_SortMulti |
|
|
ImGuiTableFlags_RowBg |
|
|
ImGuiTableFlags_Resizable |
|
|
ImGuiTableFlags_ScrollY |
|
|
ImGuiTableFlags_Reorderable;
|
|
|
|
if (!ImGui::BeginTable(id, visible_cols, flags, ImVec2(0, 0))) return;
|
|
|
|
// Setup columns with UserID = column index del dataset original.
|
|
for (int c = 0; c < col_count; ++c) {
|
|
if (!st.col_visible[c]) continue;
|
|
ImGui::TableSetupColumn(headers[c], ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c);
|
|
}
|
|
ImGui::TableSetupScrollFreeze(0, 1);
|
|
|
|
// Custom header row para soportar right-click context menu por columna.
|
|
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
|
|
int draw_col = 0;
|
|
for (int c = 0; c < col_count; ++c) {
|
|
if (!st.col_visible[c]) continue;
|
|
ImGui::TableSetColumnIndex(draw_col++);
|
|
ImGui::PushID(c);
|
|
ImGui::TableHeader(headers[c]);
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
U.header_popup_col = c;
|
|
ImGui::OpenPopup("##hdr_menu");
|
|
}
|
|
if (ImGui::BeginPopup("##hdr_menu") && U.header_popup_col == c) {
|
|
draw_header_menu(st, c, headers, col_count);
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
// Aplicar sort specs de ImGui -> State.
|
|
if (ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs()) {
|
|
if (specs->SpecsDirty && specs->SpecsCount > 0) {
|
|
const ImGuiTableColumnSortSpecs& s = specs->Specs[0];
|
|
st.sort_col = (int)s.ColumnUserID;
|
|
st.sort_desc = (s.SortDirection == ImGuiSortDirection_Descending);
|
|
specs->SpecsDirty = false;
|
|
visible_rows = compute_visible_rows(cells, row_count, col_count, st);
|
|
} else if (specs->SpecsCount == 0 && st.sort_col >= 0) {
|
|
st.sort_col = -1;
|
|
visible_rows = compute_visible_rows(cells, row_count, col_count, st);
|
|
}
|
|
}
|
|
|
|
// Body.
|
|
for (int r : visible_rows) {
|
|
ImGui::TableNextRow();
|
|
int dc = 0;
|
|
for (int c = 0; c < col_count; ++c) {
|
|
if (!st.col_visible[c]) continue;
|
|
ImGui::TableSetColumnIndex(dc++);
|
|
const char* cell = cells[r * col_count + c];
|
|
for (const auto& cr : st.color_rules) {
|
|
if (cr.col == c && cell && cr.equals == cell) {
|
|
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, (ImU32)cr.color);
|
|
break;
|
|
}
|
|
}
|
|
ImGui::PushID(r * col_count + c);
|
|
if (ImGui::Selectable(cell ? cell : "", false, ImGuiSelectableFlags_AllowDoubleClick)) {
|
|
U.pending_col = c;
|
|
U.pending_value = cell ? cell : "";
|
|
U.open_cell_popup = true;
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
|
|
ImGui::EndTable();
|
|
|
|
if (U.open_cell_popup) { ImGui::OpenPopup("##cell_op"); U.open_cell_popup = false; }
|
|
if (ImGui::BeginPopup("##cell_op")) {
|
|
const char* hdr = (U.pending_col >= 0 && U.pending_col < col_count)
|
|
? headers[U.pending_col] : "?";
|
|
ImGui::TextDisabled("%s ?? \"%s\"", hdr, U.pending_value.c_str());
|
|
ImGui::Separator();
|
|
Op picked;
|
|
if (draw_op_menu_items(picked)) {
|
|
st.filters.push_back({U.pending_col, picked, U.pending_value});
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
} // namespace data_table
|