diff --git a/src/widgets/table_core.zig b/src/widgets/table_core.zig index fc63fea..b1ec025 100644 --- a/src/widgets/table_core.zig +++ b/src/widgets/table_core.zig @@ -1589,6 +1589,113 @@ pub fn calculatePrevCell( return .{ .row = current_row, .col = current_col, .result = .tab_out }; } +/// Acción a ejecutar después de navegación Tab +pub const TabAction = enum { + /// Navegar a nueva celda, sin commit + move, + /// Navegar a nueva celda, con commit de fila anterior + move_with_commit, + /// Salir del widget, sin commit + exit, + /// Salir del widget, con commit de fila actual + exit_with_commit, +}; + +/// Plan completo de navegación Tab (resultado de planTabNavigation) +pub const TabNavigationPlan = struct { + action: TabAction, + new_row: usize, + new_col: usize, + commit_info: ?RowCommitInfo, +}; + +/// Planifica navegación Tab con commit automático al cambiar de fila. +/// +/// Esta es la función central DRY para navegación Excel-style. +/// El widget solo pasa parámetros y recibe el plan completo. +/// +/// Parámetros: +/// - buffer: RowEditBuffer con cambios pendientes +/// - current_row/col: posición actual +/// - num_cols/rows: dimensiones de la tabla +/// - forward: true=Tab, false=Shift+Tab +/// - wrap: si hacer wrap al llegar al final +/// - row_id_getter: cualquier tipo con fn getRowId(usize) i64 +/// - changes_out: buffer para almacenar cambios del commit +/// +/// El widget ejecuta el plan: +/// - .move: actualizar posición +/// - .move_with_commit: guardar commit_info en BD, luego actualizar posición +/// - .exit: establecer tab_out=true +/// - .exit_with_commit: guardar commit_info, luego tab_out=true +pub fn planTabNavigation( + buffer: *RowEditBuffer, + current_row: usize, + current_col: usize, + num_cols: usize, + num_rows: usize, + forward: bool, + wrap: bool, + row_id_getter: anytype, + changes_out: []PendingCellChange, +) TabNavigationPlan { + // 1. Calcular nueva posición + const pos = if (forward) + calculateNextCell(current_row, current_col, num_cols, num_rows, wrap) + else + calculatePrevCell(current_row, current_col, num_cols, num_rows, wrap); + + // 2. Si es tab_out, verificar si hay commit pendiente + if (pos.result == .tab_out) { + if (buffer.has_changes) { + const info = buildCommitInfo(buffer, changes_out); + buffer.clear(); + return .{ + .action = .exit_with_commit, + .new_row = pos.row, + .new_col = pos.col, + .commit_info = info, + }; + } + return .{ + .action = .exit, + .new_row = pos.row, + .new_col = pos.col, + .commit_info = null, + }; + } + + // 3. Navegación dentro del widget - verificar si cambió de fila + const current_row_id = buffer.row_id; + const new_row_id = row_id_getter.getRowId(pos.row); + + if (current_row_id != new_row_id and buffer.has_changes) { + // Cambió de fila con cambios pendientes → commit + const info = buildCommitInfo(buffer, changes_out); + // Iniciar buffer para nueva fila + buffer.startEdit(new_row_id, pos.row, isGhostRow(new_row_id)); + return .{ + .action = .move_with_commit, + .new_row = pos.row, + .new_col = pos.col, + .commit_info = info, + }; + } + + // Sin cambio de fila o sin cambios pendientes + if (current_row_id != new_row_id) { + // Cambió de fila pero sin cambios → solo actualizar buffer + buffer.startEdit(new_row_id, pos.row, isGhostRow(new_row_id)); + } + + return .{ + .action = .move, + .new_row = pos.row, + .new_col = pos.col, + .commit_info = null, + }; +} + // ============================================================================= // Ordenación (compartida) // ============================================================================= diff --git a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig index 1e261db..3f79b4f 100644 --- a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig +++ b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig @@ -416,7 +416,99 @@ pub fn virtualAdvancedTableRect( } // ========================================================================= - // Commit de fila al cambiar de selección + // Navegación Tab con commit automático (DRY: lógica en table_core) + // ========================================================================= + if (result.navigate_direction != .none) { + const is_tab = result.navigate_direction == .next_cell or result.navigate_direction == .prev_cell; + if (is_tab) { + // Wrapper para DataProvider que implementa getRowId(usize) -> i64 + const RowIdGetter = struct { + prov: DataProvider, + total: usize, + + pub fn getRowId(self: @This(), row: usize) i64 { + // Ghost row está al final (índice = total) + if (row >= self.total) return table_core.NEW_ROW_ID; + return self.prov.getRowId(row) orelse table_core.NEW_ROW_ID; + } + }; + + const getter = RowIdGetter{ .prov = provider, .total = total_rows }; + const current_row = list_state.getSelectedRow() orelse 0; + const forward = result.navigate_direction == .next_cell; + const num_cols = config.columns.len; + // VirtualAdvancedTable siempre tiene ghost row disponible + const num_rows = total_rows + 1; + + const plan = table_core.planTabNavigation( + &list_state.row_edit_buffer, + current_row, + list_state.nav.active_col, + num_cols, + num_rows, + forward, + true, // wrap habilitado + getter, + &result.row_changes, + ); + + // Ejecutar el plan + switch (plan.action) { + .move, .move_with_commit => { + // Actualizar columna + list_state.nav.active_col = plan.new_col; + + // Si cambió de fila, navegar + if (plan.new_row != current_row) { + if (plan.new_row == 0) { + list_state.goToStart(); + } else if (plan.new_row < current_row) { + list_state.moveUp(); + } else { + list_state.moveDown(visible_rows); + } + } + + // Si hay commit, establecer flags + if (plan.action == .move_with_commit) { + if (plan.commit_info) |info| { + result.row_committed = true; + result.row_commit_id = info.row_id; + result.row_commit_is_insert = info.is_insert; + result.row_changes_count = info.change_count; + result.row_changed = true; + } + } + + // Indicar al panel que debe auto-editar la nueva celda + result.edited_cell = .{ .row = plan.new_row, .col = plan.new_col }; + + // Marcar que navegación fue procesada internamente + result.navigate_direction = .none; + }, + .exit, .exit_with_commit => { + result.tab_out = true; + result.tab_shift = !forward; + + if (plan.action == .exit_with_commit) { + if (plan.commit_info) |info| { + result.row_committed = true; + result.row_commit_id = info.row_id; + result.row_commit_is_insert = info.is_insert; + result.row_changes_count = info.change_count; + result.row_changed = true; + } + } + + // Marcar que navegación fue procesada internamente + result.navigate_direction = .none; + }, + } + } + } + + // ========================================================================= + // Commit de fila al cambiar de selección (mouse/flechas - backup) // ========================================================================= // Si la selección cambió y hay cambios pendientes en otra fila, hacer commit if (list_state.selection_changed and list_state.row_edit_buffer.has_changes) { @@ -1139,33 +1231,12 @@ fn handleKeyboard( } // ========================================================================= - // Tab navigation (con commit de cambios pendientes) + // Tab sin edición activa (pasar focus al siguiente widget) // ========================================================================= - if (events.tab_out) { - // Solo si CellEditor no procesó Tab (evita doble procesamiento) - if (result.navigate_direction == .none) { - // IMPORTANTE: Commit cambios pendientes antes de salir de la tabla - if (list_state.row_edit_buffer.has_changes) { - const current_row_id = list_state.row_edit_buffer.row_id; - const is_ghost = table_core.isGhostRow(current_row_id); - - // Forzar commit de la fila actual (pasamos NEW_ROW_ID para forzar diferencia) - if (table_core.checkRowChangeAndCommit( - &list_state.row_edit_buffer, - table_core.NEW_ROW_ID - 1, // ID diferente para forzar commit - 0, - false, - &result.row_changes, - )) |commit_info| { - result.row_committed = true; - result.row_commit_id = current_row_id; - result.row_commit_is_insert = is_ghost; - result.row_changes_count = commit_info.change_count; - } - } - result.tab_out = true; - result.tab_shift = events.tab_shift; - } + // NOTA: Tab DURANTE edición se procesa en planTabNavigation más arriba + if (events.tab_out and !list_state.isEditing()) { + result.tab_out = true; + result.tab_shift = events.tab_shift; } }