data_table: Phase 2 — Button + events + tooltip + RightClick + TQL persist column_specs (issue 0081-O)
- CellRenderer::Button=5: renders SmallButton per cell; emits TableEvent::ButtonClick on click - TableEventKind enum (ButtonClick/RowDoubleClick/RowRightClick/CellEdit) + TableEvent struct - render() extended overload: adds events_out parameter (nullptr = back-compat, no events) - RowDoubleClick and RowRightClick detection in raw table loop (stage 0) - RowRightClick also detected in aggregated stage table (stage 1+) - Tooltip per cell: tooltip_on_hover + tooltip fields on ColumnSpec; "auto" = show cell value - State::aux_column_specs: TQL-persisted column specs sidecar per table - tql_emit: serializes aux_column_specs[0] as column_specs block (badge/progress/duration/icon/button/tooltip) - tql_apply: parses column_specs block back into state.aux_column_specs[0] - render() merges aux_column_specs into TableInput when caller passes empty column_specs - test_column_specs: 5->8 tests (Button struct, tooltip fields, both render() signatures link) - tql_emit_test: 3 new tests (column_specs badge/button/tooltip emit) — 52 passed - tql_apply_test: 3 new tests (column_specs badge/button/tooltip roundtrip) — 106 passed - Back-compat: existing apps (graph_explorer, registry_dashboard) unchanged - Version bump: data_table v1.1.0 -> v1.2.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -182,10 +182,14 @@ static const char* icon_name_to_glyph(const std::string& name) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_cell_custom: render a cell using the declarative ColumnSpec.
|
||||
// Called only when spec.renderer != CellRenderer::Text.
|
||||
// Issue 0081-N, v1.1.0.
|
||||
// Issue 0081-N, v1.1.0. Phase 2 (v1.2.0): Button renderer + tooltip.
|
||||
//
|
||||
// events_out: if non-null and renderer==Button, ButtonClick is pushed on click.
|
||||
// row_idx / col_idx: logical indices in the TableInput (for event payload).
|
||||
// ---------------------------------------------------------------------------
|
||||
static void draw_cell_custom(const ColumnSpec& spec, const char* value,
|
||||
int /*row_idx*/, int /*col_idx*/) {
|
||||
int row_idx, int col_idx,
|
||||
std::vector<TableEvent>* events_out) {
|
||||
if (!value) value = "";
|
||||
|
||||
switch (spec.renderer) {
|
||||
@@ -284,11 +288,57 @@ static void draw_cell_custom(const ColumnSpec& spec, const char* value,
|
||||
break;
|
||||
}
|
||||
|
||||
case CellRenderer::Button: {
|
||||
// Skip empty cell values — app decides when to show a button.
|
||||
if (value[0] == '\0') break;
|
||||
const char* label = spec.button_label.empty() ? value : spec.button_label.c_str();
|
||||
bool has_color = !spec.button_color_hex.empty();
|
||||
if (has_color) {
|
||||
ImVec4 btn_col = hex_to_imcolor(spec.button_color_hex);
|
||||
if (btn_col.x >= 0.f) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, btn_col);
|
||||
ImVec4 hov = ImVec4(
|
||||
std::min(btn_col.x + 0.12f, 1.f),
|
||||
std::min(btn_col.y + 0.12f, 1.f),
|
||||
std::min(btn_col.z + 0.12f, 1.f),
|
||||
btn_col.w);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hov);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, hov);
|
||||
} else {
|
||||
has_color = false;
|
||||
}
|
||||
}
|
||||
// Unique button ID: combines label + row + col to avoid ImGui ID
|
||||
// collisions when the same label appears in multiple rows.
|
||||
char btn_id[128];
|
||||
std::snprintf(btn_id, sizeof(btn_id), "%s##btn_%d_%d",
|
||||
label, row_idx, col_idx);
|
||||
if (ImGui::SmallButton(btn_id) && events_out) {
|
||||
TableEvent ev;
|
||||
ev.kind = TableEventKind::ButtonClick;
|
||||
ev.row = row_idx;
|
||||
ev.col = col_idx;
|
||||
ev.column_id = spec.id;
|
||||
ev.action_id = spec.button_action;
|
||||
ev.value = value;
|
||||
events_out->push_back(std::move(ev));
|
||||
}
|
||||
if (has_color) ImGui::PopStyleColor(3);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// CellRenderer::Text or unknown — plain text.
|
||||
ImGui::TextUnformatted(value);
|
||||
break;
|
||||
}
|
||||
|
||||
// Tooltip: show on hover if tooltip_on_hover is set.
|
||||
// "auto" shows the raw cell value (useful for truncated text columns).
|
||||
if (spec.tooltip_on_hover && ImGui::IsItemHovered()) {
|
||||
const char* tip = (spec.tooltip == "auto") ? value : spec.tooltip.c_str();
|
||||
if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
|
||||
}
|
||||
}
|
||||
|
||||
// compare: cell-level comparison supporting all Op variants.
|
||||
@@ -1254,11 +1304,12 @@ bool draw_extra_panel(State& st, VizPanel& p, int idx, const StageOutput& so,
|
||||
ImGui::TableSetColumnIndex(c);
|
||||
const char* s = so.cells[(size_t)r * so.cols + c];
|
||||
// Issue 0081-N: declarative renderer for extra panel mini-table.
|
||||
// events_out not propagated to mini-table (secondary render).
|
||||
bool custom_ep = false;
|
||||
if (col_specs && c < (int)col_specs->size()) {
|
||||
const ColumnSpec& cs = (*col_specs)[(size_t)c];
|
||||
if (cs.renderer != CellRenderer::Text) {
|
||||
draw_cell_custom(cs, s, r, c);
|
||||
draw_cell_custom(cs, s, r, c, nullptr);
|
||||
custom_ep = true;
|
||||
}
|
||||
}
|
||||
@@ -2458,14 +2509,30 @@ void drill_into(State& st, int from_stage,
|
||||
void render(const char* id,
|
||||
const std::vector<TableInput>& tables,
|
||||
State& st,
|
||||
std::vector<TableEvent>* events_out,
|
||||
bool show_chrome)
|
||||
{
|
||||
if (tables.empty()) return;
|
||||
int main_idx = resolve_main_idx(tables, st.main_source);
|
||||
if (main_idx < 0) return;
|
||||
|
||||
// Construir headers ptrs desde main table.
|
||||
const TableInput& main_t = tables[(size_t)main_idx];
|
||||
// Merge aux_column_specs from State into TableInput when the caller passed
|
||||
// empty column_specs. Caller-provided specs always take precedence.
|
||||
// We keep a local copy to avoid mutating the caller's const tables.
|
||||
static thread_local TableInput main_t_merged;
|
||||
{
|
||||
const TableInput& src = tables[(size_t)main_idx];
|
||||
if (src.column_specs.empty() &&
|
||||
main_idx < (int)st.aux_column_specs.size() &&
|
||||
!st.aux_column_specs[(size_t)main_idx].empty())
|
||||
{
|
||||
main_t_merged = src;
|
||||
main_t_merged.column_specs = st.aux_column_specs[(size_t)main_idx];
|
||||
} else {
|
||||
main_t_merged = src;
|
||||
}
|
||||
}
|
||||
const TableInput& main_t = main_t_merged;
|
||||
static thread_local std::vector<const char*> main_hdr_ptrs;
|
||||
main_hdr_ptrs.clear();
|
||||
main_hdr_ptrs.reserve(main_t.cols);
|
||||
@@ -3105,20 +3172,29 @@ void render(const char* id,
|
||||
ri >= sel_rmin && ri <= sel_rmax &&
|
||||
oc >= sel_cmin && oc <= sel_cmax);
|
||||
ImGui::PushID(r * eff_cols + c);
|
||||
// Issue 0081-N: use declarative renderer when column_specs set.
|
||||
// Issue 0081-N/O: use declarative renderer when column_specs set.
|
||||
{
|
||||
bool custom_rendered = false;
|
||||
const ColumnSpec* cell_cs = nullptr;
|
||||
if (!main_t.column_specs.empty() &&
|
||||
c < (int)main_t.column_specs.size()) {
|
||||
const ColumnSpec& cs = main_t.column_specs[(size_t)c];
|
||||
if (cs.renderer != CellRenderer::Text) {
|
||||
draw_cell_custom(cs, cell, ri, c);
|
||||
cell_cs = &main_t.column_specs[(size_t)c];
|
||||
if (cell_cs->renderer != CellRenderer::Text) {
|
||||
draw_cell_custom(*cell_cs, cell, r, c, events_out);
|
||||
custom_rendered = true;
|
||||
}
|
||||
}
|
||||
if (!custom_rendered) {
|
||||
ImGui::Selectable(cell ? cell : "", in_sel,
|
||||
ImGuiSelectableFlags_AllowDoubleClick);
|
||||
// Tooltip for Text cells (Phase 2).
|
||||
if (cell_cs && cell_cs->tooltip_on_hover &&
|
||||
ImGui::IsItemHovered()) {
|
||||
const char* tip = (cell_cs->tooltip == "auto")
|
||||
? (cell ? cell : "")
|
||||
: cell_cs->tooltip.c_str();
|
||||
if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
|
||||
}
|
||||
}
|
||||
}
|
||||
// AllowWhenBlockedByActiveItem: durante drag,
|
||||
@@ -3135,10 +3211,36 @@ void render(const char* id,
|
||||
} else if (U.sel_dragging) {
|
||||
U.sel_end_row = ri; U.sel_end_col = oc;
|
||||
}
|
||||
// RowDoubleClick event (Phase 2, v1.2.0).
|
||||
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)
|
||||
&& events_out) {
|
||||
TableEvent ev;
|
||||
ev.kind = TableEventKind::RowDoubleClick;
|
||||
ev.row = r;
|
||||
ev.col = c;
|
||||
ev.value = cell ? cell : "";
|
||||
if (!main_t.column_specs.empty() &&
|
||||
c < (int)main_t.column_specs.size())
|
||||
ev.column_id = main_t.column_specs[(size_t)c].id;
|
||||
events_out->push_back(std::move(ev));
|
||||
}
|
||||
// RowRightClick event: emit event only, no popup drawn here.
|
||||
// Caller inspects events_out and opens its own context menu.
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||
U.pending_col = c;
|
||||
U.pending_value = cell ? cell : "";
|
||||
U.open_cell_popup = true;
|
||||
if (events_out) {
|
||||
TableEvent ev;
|
||||
ev.kind = TableEventKind::RowRightClick;
|
||||
ev.row = r;
|
||||
ev.col = c;
|
||||
ev.value = cell ? cell : "";
|
||||
if (!main_t.column_specs.empty() &&
|
||||
c < (int)main_t.column_specs.size())
|
||||
ev.column_id = main_t.column_specs[(size_t)c].id;
|
||||
events_out->push_back(std::move(ev));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
@@ -3658,19 +3760,28 @@ void render(const char* id,
|
||||
ImGui::TableSetColumnIndex(c);
|
||||
const char* cell = cur_cells[r * cur_cols_n + c];
|
||||
ImGui::PushID(r * cur_cols_n + c);
|
||||
// Issue 0081-N: declarative renderer for aggregated stage tables.
|
||||
// Issue 0081-N/O: declarative renderer for aggregated stage tables.
|
||||
{
|
||||
bool custom_rendered = false;
|
||||
const ColumnSpec* cell_cs2 = nullptr;
|
||||
if (!main_t.column_specs.empty() &&
|
||||
c < (int)main_t.column_specs.size()) {
|
||||
const ColumnSpec& cs = main_t.column_specs[(size_t)c];
|
||||
if (cs.renderer != CellRenderer::Text) {
|
||||
draw_cell_custom(cs, cell, r, c);
|
||||
cell_cs2 = &main_t.column_specs[(size_t)c];
|
||||
if (cell_cs2->renderer != CellRenderer::Text) {
|
||||
draw_cell_custom(*cell_cs2, cell, r, c, events_out);
|
||||
custom_rendered = true;
|
||||
}
|
||||
}
|
||||
if (!custom_rendered) {
|
||||
ImGui::Selectable(cell ? cell : "");
|
||||
// Tooltip for Text cells (Phase 2).
|
||||
if (cell_cs2 && cell_cs2->tooltip_on_hover &&
|
||||
ImGui::IsItemHovered()) {
|
||||
const char* tip = (cell_cs2->tooltip == "auto")
|
||||
? (cell ? cell : "")
|
||||
: cell_cs2->tooltip.c_str();
|
||||
if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||
@@ -3678,6 +3789,18 @@ void render(const char* id,
|
||||
U.pending_value = cell ? cell : "";
|
||||
U.inspect_row = r;
|
||||
ImGui::OpenPopup("##drill_popup");
|
||||
// RowRightClick event (Phase 2, v1.2.0).
|
||||
if (events_out) {
|
||||
TableEvent ev;
|
||||
ev.kind = TableEventKind::RowRightClick;
|
||||
ev.row = r;
|
||||
ev.col = c;
|
||||
ev.value = cell ? cell : "";
|
||||
if (!main_t.column_specs.empty() &&
|
||||
c < (int)main_t.column_specs.size())
|
||||
ev.column_id = main_t.column_specs[(size_t)c].id;
|
||||
events_out->push_back(std::move(ev));
|
||||
}
|
||||
}
|
||||
if (ImGui::BeginPopup("##drill_popup")) {
|
||||
if (c < n_brk) {
|
||||
|
||||
Reference in New Issue
Block a user