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:
parent
51705f8fc7
commit
2a92c7530c
2 changed files with 205 additions and 27 deletions
|
|
@ -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)
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -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,35 +1231,14 @@ 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;
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Handle: Mouse Click
|
||||
|
|
|
|||
Loading…
Reference in a new issue