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 };
|
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)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue