feat(table_core): DRY planTabNavigation for Excel-style Tab commit

- Add planTabNavigation() in table_core.zig: central function for Tab navigation with auto-commit
- Uses row_id comparison (not indices) to detect row changes - robust for virtual tables
- Returns TabAction enum: move, move_with_commit, exit, exit_with_commit
- Integrates in virtual_advanced_table.zig with RowIdGetter wrapper
- Removes obsolete tab_out commit logic
- Fix: Tab at end of ghost row now commits before wrap

🤖 Generated with Claude Code
This commit is contained in:
reugenio 2025-12-28 01:50:04 +01:00
parent 51705f8fc7
commit 2a92c7530c
2 changed files with 205 additions and 27 deletions

View file

@ -1589,6 +1589,113 @@ pub fn calculatePrevCell(
return .{ .row = current_row, .col = current_col, .result = .tab_out }; 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) // Ordenación (compartida)
// ============================================================================= // =============================================================================

View file

@ -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 // 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) { if (list_state.selection_changed and list_state.row_edit_buffer.has_changes) {
@ -1139,34 +1231,13 @@ fn handleKeyboard(
} }
// ========================================================================= // =========================================================================
// Tab navigation (con commit de cambios pendientes) // Tab sin edición activa (pasar focus al siguiente widget)
// ========================================================================= // =========================================================================
if (events.tab_out) { // NOTA: Tab DURANTE edición se procesa en planTabNavigation más arriba
// Solo si CellEditor no procesó Tab (evita doble procesamiento) if (events.tab_out and !list_state.isEditing()) {
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_out = true;
result.tab_shift = events.tab_shift; result.tab_shift = events.tab_shift;
} }
}
} }
// ============================================================================= // =============================================================================