diff --git a/CLAUDE.md b/CLAUDE.md index 5a15433..d1041eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ ### Paso 3: Leer documentación ``` REFERENCE.md # ⭐ MANUAL DE REFERENCIA COMPLETO -docs/BUG_ADVANCEDTABLE_KEYBOARD_2025-12-17.md # 🔴 BUG PENDIENTE - LEER PRIMERO +docs/BUG_ADVANCEDTABLE_KEYBOARD_2025-12-17.md # ✅ BUG RESUELTO docs/ADVANCED_TABLE_MERGE_PLAN.md # Plan merge Table → AdvancedTable docs/research/DVUI_AUDIT_2025-12-17.md # Auditoría DVUI docs/DEVELOPMENT_PLAN.md # Plan de desarrollo por fases @@ -813,35 +813,27 @@ const stdout = std.fs.File.stdout(); // NO std.io.getStdOut() | 2025-12-17 | v0.18.0 | Paridad Visual DVUI Fase 1: RenderMode dual, esquinas redondeadas, sombras | | 2025-12-17 | v0.19.0 | Paridad Visual DVUI Fase 2: HoverTransition, Focus Ring AA en 9 widgets | | 2025-12-17 | v0.20.0 | AdvancedTable: 8 fases completas (~2,700 LOC) - Schema, CRUD, Sorting, Lookup | -| 2025-12-17 | v0.21.0 | AdvancedTable: +990 LOC (multi-select, search, validation) - 🔴 BUG TECLADO | +| 2025-12-17 | v0.21.0 | AdvancedTable: +990 LOC (multi-select, search, validation) | +| 2025-12-17 | v0.21.1 | Fix: AdvancedTable teclado - result.selected_row/col en handleKeyboard | --- -## 🔴 BUG PENDIENTE: AdvancedTable Teclado +## ✅ BUG RESUELTO: AdvancedTable Teclado -> **Estado:** NO RESUELTO (2025-12-17) +> **Estado:** RESUELTO (2025-12-17 19:30) > **Documentación:** `docs/BUG_ADVANCEDTABLE_KEYBOARD_2025-12-17.md` -### Síntoma -Las flechas ↑↓←→ no mueven la selección en AdvancedTable (zsimifactu WHO panel). +### Causa raíz +`handleKeyboard` seteaba `result.selection_changed = true` pero NO seteaba `result.selected_row` / `result.selected_col`. zsimifactu sincroniza selección desde DataManager cada frame, y sin esos valores no actualizaba DataManager → reset al valor anterior. -### Lo que sabemos -- Click en filas funciona -- `has_focus=true` cuando widget tiene foco -- `navKeyPressed()` detecta teclas (probado con debug) -- Pero `handleKeyboard()` no las procesa - -### Pista clave -El widget **Table original** SÍ funciona con teclado. Comparar implementaciones. - -### Próximo paso -Añadir debug DENTRO de `handleKeyboard()` (no antes de `if (has_focus)`). +### Solución +Añadir `result.selected_row` y `result.selected_col` a todas las teclas de navegación (up, down, left, right, page_up, page_down, home, end). --- ## ESTADO ACTUAL -**✅ PROYECTO COMPLETADO - v0.21.0** (con bug pendiente en AdvancedTable) +**✅ PROYECTO COMPLETADO - v0.21.1** > **Para detalles técnicos completos, ver `REFERENCE.md`** (1370 líneas de documentación) diff --git a/docs/BUG_ADVANCEDTABLE_KEYBOARD_2025-12-17.md b/docs/BUG_ADVANCEDTABLE_KEYBOARD_2025-12-17.md index 7735cfd..0d29daa 100644 --- a/docs/BUG_ADVANCEDTABLE_KEYBOARD_2025-12-17.md +++ b/docs/BUG_ADVANCEDTABLE_KEYBOARD_2025-12-17.md @@ -1,7 +1,7 @@ # Bug: AdvancedTable Keyboard Navigation No Funciona > **Fecha:** 2025-12-17 -> **Estado:** 🔴 NO RESUELTO +> **Estado:** ✅ RESUELTO > **Severidad:** ALTA --- @@ -12,123 +12,105 @@ Las flechas de teclado no mueven la selección en AdvancedTable cuando se usa en --- -## Lo que sabemos +## Causa Raíz (ENCONTRADA) -### Funciona -- El widget se renderiza correctamente -- El click en filas funciona (cambia selección) -- El sorting visual funciona (click en headers) -- `has_focus=true` cuando se hace click en la tabla -- `keyboard_nav=true` en la configuración +El problema era una combinación de dos factores: -### No funciona -- Flechas ↑↓←→ no mueven la selección -- No hay mensajes de `selection_changed` después de presionar flechas +### 1. `handleKeyboard` no seteaba `result.selected_row` / `result.selected_col` ---- - -## Investigación realizada - -### 1. Cambio de keyPressed a navKeyPressed -**Hipótesis:** `keyPressed()` no incluye key repeats. -**Cambio:** Línea 566 de `advanced_table.zig` usa `navKeyPressed()`. -**Resultado:** No resuelve el problema. - -### 2. Debug print consumía el evento -**Descubrimiento:** Al añadir debug print que llamaba `navKeyPressed()` ANTES del bloque `if (has_focus)`, el evento se consumía y no llegaba a `handleKeyboard`. -**Evidencia:** Con debug print, aparecían mensajes `navKeyPressed=.down, has_focus=true, keyboard_nav=true` pero sin movimiento. -**Conclusión:** `navKeyPressed()` solo puede leerse UNA VEZ por frame. - -### 3. Sin debug print, nada funciona -**Observación:** Al quitar el debug print, ni siquiera hay mensajes en consola. -**Implicación:** `handleKeyboard` no se está ejecutando, o `navKeyPressed()` devuelve null. - ---- - -## Código actual relevante - -### advanced_table.zig líneas 165-180 +En el código original: ```zig -// Handle keyboard -if (has_focus) { - if (table_state.editing) { - handleEditingKeyboard(ctx, table_state, table_schema, &result); - drawEditingOverlay(ctx, bounds, table_state, table_schema, header_h, state_col_w, colors); - } else if (config.keyboard_nav) { - handleKeyboard(ctx, table_state, table_schema, visible_rows, &result); +.down => { + if (table_state.selected_row < row_count - 1) { + table_state.selectCell(...); + result.selection_changed = true; + // FALTABA: result.selected_row = new_row; + // FALTABA: result.selected_col = new_col; + } +}, +``` + +### 2. zsimifactu sincroniza la selección desde DataManager cada frame + +En `who_list_advanced.zig` líneas 199-205: +```zig +// Sincronizar selección con DataManager +if (dm.getSelectedWhoIndex()) |idx| { + const idx_i32: i32 = @intCast(idx); + if (self.table_state.selected_row != idx_i32) { + self.table_state.selected_row = idx_i32; // ← RESETEA cada frame } } ``` -### handleKeyboard líneas 570-590 +### Flujo del bug: + +1. Usuario presiona flecha abajo +2. `handleKeyboard` detecta la tecla y ejecuta `selectCell(new_row, new_col)` +3. `selected_row` cambia de 1 a 2 ✓ +4. `result.selection_changed = true` ✓ +5. **PERO** `result.selected_row` es `null` ✗ +6. zsimifactu verifica `if (result.selected_row) |row_idx|` → es null → no actualiza DataManager +7. Siguiente frame: zsimifactu sincroniza desde DataManager → `selected_row` vuelve a 1 + +--- + +## Solución + +Añadir `result.selected_row` y `result.selected_col` a todas las teclas de navegación en `handleKeyboard`: + ```zig -if (ctx.input.navKeyPressed()) |nav_key| { - switch (nav_key) { - .up => { - if (table_state.selected_row > 0) { - table_state.selectCell(...); - result.selection_changed = true; - } - }, - .down => { - if (table_state.selected_row < row_count - 1) { - table_state.selectCell(...); - result.selection_changed = true; - } - }, - // ... etc +.down => { + if (table_state.selected_row < @as(i32, @intCast(row_count)) - 1) { + const new_row: usize = @intCast(table_state.selected_row + 1); + const new_col: usize = @intCast(@max(0, table_state.selected_col)); + table_state.selectCell(new_row, new_col); + result.selection_changed = true; + result.selected_row = new_row; // ← AÑADIDO + result.selected_col = new_col; // ← AÑADIDO } -} +}, ``` ---- - -## Hipótesis pendientes de verificar - -1. **¿`has_focus` es realmente true?** - - El debug mostró true, pero quizás el focus se pierde entre frames - -2. **¿`navKeyPressed()` funciona en AdvancedTable?** - - Funciona en otros widgets (Table original, TextInput) - - Quizás hay algo específico del contexto de AdvancedTable - -3. **¿El evento de teclado llega al widget?** - - zsimifactu tiene su propio event handling en main.zig - - Quizás los eventos se consumen antes de llegar al widget - -4. **¿Hay diferencia entre Table y AdvancedTable?** - - Table original funciona con teclado - - Comparar línea por línea el handling de teclado +Teclas corregidas: +- `.up` +- `.down` +- `.left` +- `.right` +- `.page_up` +- `.page_down` +- `.home` (y Ctrl+Home) +- `.end` (y Ctrl+End) --- -## Próximos pasos sugeridos +## Verificación -1. **Comparar con Table original** - El widget Table SÍ funciona con teclado. Comparar cómo maneja eventos. +Debug añadido temporalmente mostró: +``` +[ADV-TABLE] DOWN: after selectCell, selected_row=2 ← SÍ cambia +[ADV-TABLE] navKey detected: down, selected_row=1 ← Pero vuelve a 1 en siguiente frame +``` -2. **Añadir debug DENTRO de handleKeyboard** - No antes del `if (has_focus)`, sino dentro de la función misma. - -3. **Verificar InputState.navKeyPressed()** - Ver si devuelve algo cuando AdvancedTable tiene focus. - -4. **Revisar el focus system** - Quizás AdvancedTable no está registrándose correctamente en el FocusSystem. +Después del fix, las flechas funcionan correctamente. --- -## Archivos relevantes +## Archivos modificados -- `/mnt/cello2/arno/re/recode/zig/zcatgui/src/widgets/advanced_table/advanced_table.zig` -- `/mnt/cello2/arno/re/recode/zig/zcatgui/src/widgets/table/table.zig` (referencia funcional) -- `/mnt/cello2/arno/re/recode/zig/zcatgui/src/core/input.zig` (navKeyPressed) -- `/mnt/cello2/arno/re/recode/zig/zsimifactu/src/panels/who_list_advanced.zig` -- `/mnt/cello2/arno/re/recode/zig/zsimifactu/src/main.zig` (event loop) +- `src/widgets/advanced_table/advanced_table.zig` - handleKeyboard corregido --- -## Commit actual +## Lecciones aprendidas -Los cambios de "merge Table → AdvancedTable" están commiteados (af1bb76) pero el bug de teclado **NO está resuelto**. +1. **El resultado de un widget debe ser completo** - Si `selection_changed = true`, también debe informar QUÉ cambió (`selected_row`, `selected_col`). + +2. **Cuidado con la sincronización bidireccional** - Cuando un widget y un data manager ambos pueden modificar el mismo estado, hay que asegurar que los cambios del widget se propaguen al data manager antes de que éste resetee el estado. + +3. **Debug incremental** - Añadir debug DENTRO de la función problemática (no antes) para ver el flujo real. --- *Documentado por: Claude Code (Opus 4.5)* -*Fecha: 2025-12-17 ~19:00* +*Fecha resolución: 2025-12-17 ~19:30* diff --git a/docs/PROPUESTA_WIDGETS_BROWSER.md b/docs/PROPUESTA_WIDGETS_BROWSER.md new file mode 100644 index 0000000..00bebd6 --- /dev/null +++ b/docs/PROPUESTA_WIDGETS_BROWSER.md @@ -0,0 +1,201 @@ +# Propuesta: Widgets Browser Especializados + +**Fecha:** 2025-12-17 +**Origen:** Conversación zsimifactu sobre extracción de patrones reutilizables +**Estado:** PROPUESTA - Pendiente de implementación + +--- + +## Resumen Ejecutivo + +Se propone crear widgets especializados para navegación de datos tabulares que encapsulen patrones comunes identificados en zsimifactu. Estos widgets combinarían tabla + controles de navegación + filtros en componentes cohesivos. + +--- + +## Widgets Propuestos + +### 1. TableBrowser + +Widget compuesto que integra: +- Tabla con scroll y selección +- Barra de estado con posición (ej: "15/520") +- Botones de navegación (|< < > >|) +- Soporte para filtros (opcional) + +``` +┌─────────────────────────────────────────────┐ +│ [Filtro: ___________] [Tipo: v] │ ← Zona filtros (opcional) +├─────────────────────────────────────────────┤ +│ Codigo │ Nombre │ Ciudad │ ← Header tabla +├────────┼─────────────────┼─────────────────┤ +│ C0001 │ Empresa ABC │ Valencia │ +│ C0002 │ Empresa XYZ │ Madrid │ +│ ... │ ... │ ... │ +├─────────────────────────────────────────────┤ +│ [|<] [<] 15/520 [>] [>|] │ ← Navegación integrada +└─────────────────────────────────────────────┘ +``` + +**Caso de uso:** Panel de lista en aplicaciones CRUD (WhoListPanel, DocListPanel, etc.) + +### 2. ConfigBrowser + +Widget especializado para visualizar/editar configuración: +- Lista de variables agrupadas por categoría +- Editor inline según tipo (checkbox, input, color picker) +- Búsqueda/filtro de variables +- Indicador de cambios pendientes + +``` +┌─────────────────────────────────────────────┐ +│ [Buscar: ___________] │ +├─────────────────────────────────────────────┤ +│ ▼ General │ +│ auto_guardar_cada [15 ] minutos │ +│ backup_automatico [✓] │ +├─────────────────────────────────────────────┤ +│ ▼ Apariencia │ +│ color_azul_empresa [■] RGB(40,80,120) │ +│ font_size [14 ] │ +├─────────────────────────────────────────────┤ +│ ▶ Comportamiento (click para expandir) │ +└─────────────────────────────────────────────┘ +``` + +**Caso de uso:** Panel de configuración de aplicación + +--- + +## Justificación Técnica + +### Patrón Repetido en zsimifactu + +En `WhoListPanel` y futuros paneles de lista se repite: +1. Tabla con datos +2. Callback para obtener celdas +3. Botones navegación (First/Prev/Next/Last) +4. Indicador posición +5. Manejo de selección → DataManager + +Este código se duplicaría en cada panel de lista (DocListPanel, ProdListPanel, etc.). + +### Beneficios de Encapsular + +| Aspecto | Sin widget | Con TableBrowser | +|---------|------------|------------------| +| Líneas por panel | ~200 | ~50 | +| Bugs de navegación | Duplicados | Corregidos una vez | +| Consistencia UX | Manual | Automática | +| Nuevos paneles | Copy-paste | Instanciar widget | + +--- + +## API Propuesta + +### TableBrowser + +```zig +const TableBrowser = struct { + table_state: widgets.table.TableState, + nav_state: NavState, + filter_state: ?FilterState, + + pub const Config = struct { + columns: []const Column, + show_navigation: bool = true, + show_filters: bool = false, + row_height: u16 = 18, + }; + + pub const Callbacks = struct { + getCellData: *const fn (row: usize, col: usize) []const u8, + getRowCount: *const fn () usize, + onSelectionChanged: ?*const fn (row: usize) void = null, + }; + + pub fn init(config: Config) TableBrowser; + pub fn draw(self: *Self, ctx: *Context, rect: Rect, callbacks: Callbacks) DrawResult; + pub fn handleEvent(self: *Self, event: Event) bool; + + // Navegación programática + pub fn goFirst(self: *Self) void; + pub fn goPrev(self: *Self) void; + pub fn goNext(self: *Self) void; + pub fn goLast(self: *Self) void; + pub fn getPosition(self: *Self) struct { current: usize, total: usize }; +}; +``` + +### ConfigBrowser + +```zig +const ConfigBrowser = struct { + pub const Config = struct { + variables: []const ConfigVariable, // De zcatconfig + show_search: bool = true, + group_by_category: bool = true, + }; + + pub fn init(config: Config) ConfigBrowser; + pub fn draw(self: *Self, ctx: *Context, rect: Rect, config_ptr: anytype) DrawResult; + pub fn handleEvent(self: *Self, event: Event) bool; + + // Estado + pub fn hasChanges(self: *Self) bool; + pub fn getChangedVariables(self: *Self) []const []const u8; +}; +``` + +--- + +## Integración con zcatconfig + +El `ConfigBrowser` se integraría naturalmente con la nueva librería `zcatconfig`: + +```zig +const zcatconfig = @import("zcatconfig"); +const ConfigBrowser = zcatgui.widgets.ConfigBrowser; + +// En el proyecto consumidor: +const variables = @import("config/variables.zig"); +const Config = @import("config/structures.zig").Config; + +var config = Config{}; +var browser = ConfigBrowser.init(.{ + .variables = &variables.config_variables, +}); + +// En draw: +const result = browser.draw(ctx, rect, &config); +if (result.value_changed) { + try zcatconfig.save(&variables.config_variables, Config, &config, allocator, "config.txt"); +} +``` + +--- + +## Priorización Sugerida + +| Widget | Prioridad | Razón | +|--------|-----------|-------| +| TableBrowser | Alta | Necesario para DocListPanel, ProdListPanel | +| ConfigBrowser | Media | Panel config es nice-to-have, no bloqueante | + +--- + +## Dependencias + +- **TableBrowser**: Ninguna externa, usa widgets existentes (table, button) +- **ConfigBrowser**: Requiere `zcatconfig` como dependencia opcional + +--- + +## Siguiente Paso + +1. Revisar esta propuesta +2. Decidir si proceder con implementación +3. Crear issue/tarea en zcatgui + +--- + +*Documento generado desde conversación zsimifactu 2025-12-17* diff --git a/src/widgets/advanced_table/advanced_table.zig b/src/widgets/advanced_table/advanced_table.zig index 5932bf7..0026b67 100644 --- a/src/widgets/advanced_table/advanced_table.zig +++ b/src/widgets/advanced_table/advanced_table.zig @@ -86,6 +86,12 @@ pub fn advancedTableRect( const colors = custom_colors orelse table_schema.colors orelse &default_colors; const config = table_schema.config; + // Ensure valid selection if table has data (like Table widget does) + if (table_state.getRowCount() > 0 and table_schema.columns.len > 0) { + if (table_state.selected_row < 0) table_state.selected_row = 0; + if (table_state.selected_col < 0) table_state.selected_col = 0; + } + // Generate unique ID for focus system const widget_id: u64 = @intFromPtr(table_state); @@ -393,9 +399,12 @@ fn drawRow( const is_selected_cell = is_selected_row and table_state.selected_col == @as(i32, @intCast(col_idx)); const cell_clicked = cell_rect.contains(mouse.x, mouse.y) and ctx.input.mousePressed(.left); - // Cell background for selected cell + // Cell indicator for selected cell (outline instead of solid fill) if (is_selected_cell) { - ctx.pushCommand(Command.rect(col_x, bounds.y, col.width, config.row_height, colors.selected_cell)); + // Subtle background tint + ctx.pushCommand(Command.rect(col_x, bounds.y, col.width, config.row_height, blendColor(row_bg, colors.selected_cell, 0.15))); + // Border outline + ctx.pushCommand(Command.rectOutline(col_x, bounds.y, col.width, config.row_height, colors.selected_cell)); } // Get cell value @@ -567,74 +576,96 @@ fn handleKeyboard( switch (nav_key) { .up => { if (table_state.selected_row > 0) { - table_state.selectCell( - @intCast(table_state.selected_row - 1), - @intCast(@max(0, table_state.selected_col)), - ); + const new_row: usize = @intCast(table_state.selected_row - 1); + const new_col: usize = @intCast(@max(0, table_state.selected_col)); + table_state.selectCell(new_row, new_col); result.selection_changed = true; + result.selected_row = new_row; + result.selected_col = new_col; } }, .down => { if (table_state.selected_row < @as(i32, @intCast(row_count)) - 1) { - table_state.selectCell( - @intCast(table_state.selected_row + 1), - @intCast(@max(0, table_state.selected_col)), - ); + const new_row: usize = @intCast(table_state.selected_row + 1); + const new_col: usize = @intCast(@max(0, table_state.selected_col)); + table_state.selectCell(new_row, new_col); result.selection_changed = true; + result.selected_row = new_row; + result.selected_col = new_col; } }, .left => { if (table_state.selected_col > 0) { - table_state.selectCell( - @intCast(@max(0, table_state.selected_row)), - @intCast(table_state.selected_col - 1), - ); + const new_row: usize = @intCast(@max(0, table_state.selected_row)); + const new_col: usize = @intCast(table_state.selected_col - 1); + table_state.selectCell(new_row, new_col); result.selection_changed = true; + result.selected_row = new_row; + result.selected_col = new_col; } }, .right => { if (table_state.selected_col < @as(i32, @intCast(col_count)) - 1) { - table_state.selectCell( - @intCast(@max(0, table_state.selected_row)), - @intCast(table_state.selected_col + 1), - ); + const new_row: usize = @intCast(@max(0, table_state.selected_row)); + const new_col: usize = @intCast(table_state.selected_col + 1); + table_state.selectCell(new_row, new_col); result.selection_changed = true; + result.selected_row = new_row; + result.selected_col = new_col; } }, .page_up => { - const new_row = @max(0, table_state.selected_row - @as(i32, @intCast(visible_rows))); - table_state.selectCell(@intCast(new_row), @intCast(@max(0, table_state.selected_col))); + const new_row: usize = @intCast(@max(0, table_state.selected_row - @as(i32, @intCast(visible_rows)))); + const new_col: usize = @intCast(@max(0, table_state.selected_col)); + table_state.selectCell(new_row, new_col); result.selection_changed = true; + result.selected_row = new_row; + result.selected_col = new_col; }, .page_down => { - const new_row = @min( + const new_row: usize = @intCast(@min( @as(i32, @intCast(row_count)) - 1, table_state.selected_row + @as(i32, @intCast(visible_rows)), - ); - table_state.selectCell(@intCast(new_row), @intCast(@max(0, table_state.selected_col))); + )); + const new_col: usize = @intCast(@max(0, table_state.selected_col)); + table_state.selectCell(new_row, new_col); result.selection_changed = true; + result.selected_row = new_row; + result.selected_col = new_col; }, .home => { + var new_row: usize = undefined; + var new_col: usize = undefined; if (ctx.input.modifiers.ctrl) { // Ctrl+Home: first cell - table_state.selectCell(0, 0); + new_row = 0; + new_col = 0; } else { // Home: first column - table_state.selectCell(@intCast(@max(0, table_state.selected_row)), 0); + new_row = @intCast(@max(0, table_state.selected_row)); + new_col = 0; } + table_state.selectCell(new_row, new_col); result.selection_changed = true; + result.selected_row = new_row; + result.selected_col = new_col; }, .end => { + var new_row: usize = undefined; + var new_col: usize = undefined; if (ctx.input.modifiers.ctrl) { // Ctrl+End: last cell - const last_row = if (row_count > 0) row_count - 1 else 0; - const last_col = col_count - 1; - table_state.selectCell(last_row, last_col); + new_row = if (row_count > 0) row_count - 1 else 0; + new_col = col_count - 1; } else { // End: last column - table_state.selectCell(@intCast(@max(0, table_state.selected_row)), col_count - 1); + new_row = @intCast(@max(0, table_state.selected_row)); + new_col = col_count - 1; } + table_state.selectCell(new_row, new_col); result.selection_changed = true; + result.selected_row = new_row; + result.selected_col = new_col; }, .tab => { if (config.handle_tab) {