feat: Paridad Excel-style AdvancedTable ↔ VirtualAdvancedTable

Propuesta Gemini: Unificación funcional entre tablas.

1. Modo inserción centralizado en NavigationState (table_core)
   - is_insertion_mode, last_inserted_id
   - enterInsertionMode(), exitInsertionMode(), isInInsertionMode()

2. Auto-edit en Ctrl+N (AdvancedTable)
   - Tras insertar fila, inicia edición inmediatamente en col 0

3. Auto-insert en Tab (Modo Pro)
   - En modo inserción, Tab al final de fila inserta nueva fila
   - Permite meter 50+ líneas sin soltar teclado

4. Fix ghost row clonada
   - Comparar row_id AND row_index en rendering
   - Evita que fila insertada y ghost row compartan buffer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
R.Eugenio 2025-12-29 14:55:30 +01:00
parent bb2d6a7be1
commit b2a4081493
5 changed files with 82 additions and 28 deletions

View file

@ -284,15 +284,43 @@ pub fn handleKeyboard(
} }
}, },
.exit, .exit_with_commit => { .exit, .exit_with_commit => {
result.tab_out = true; // MODO PRO: Si estamos en modo inserción y Tab hacia adelante,
result.tab_shift = events.tab_shift; // auto-insertar nueva fila en lugar de salir del widget
if (table_state.nav.isInInsertionMode() and forward and config.allow_row_operations) {
// Commit la fila actual si hay cambios
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;
}
}
// Auto-insertar nueva fila
const insert_idx = current_row + 1;
if (table_state.insertRow(insert_idx)) |new_idx| {
table_state.selectCell(new_idx, 0);
table_state.row_edit_buffer.startEdit(table_core.NEW_ROW_ID, new_idx, true);
table_state.cell_edit.startEditing(new_idx, 0, "", null);
result.row_inserted = true;
result.selection_changed = true;
} else |_| {
// Si falla la inserción, salir normalmente
result.tab_out = true;
result.tab_shift = events.tab_shift;
}
} else {
// Comportamiento normal: salir del widget
result.tab_out = true;
result.tab_shift = events.tab_shift;
if (plan.action == .exit_with_commit) { if (plan.action == .exit_with_commit) {
if (plan.commit_info) |info| { if (plan.commit_info) |info| {
result.row_committed = true; result.row_committed = true;
result.row_commit_id = info.row_id; result.row_commit_id = info.row_id;
result.row_commit_is_insert = info.is_insert; result.row_commit_is_insert = info.is_insert;
result.row_changes_count = info.change_count; result.row_changes_count = info.change_count;
}
} }
} }
}, },
@ -329,7 +357,7 @@ pub fn handleKeyboard(
// Operaciones CRUD (Ctrl+N, Ctrl+Delete, Ctrl+B desde el Core) // Operaciones CRUD (Ctrl+N, Ctrl+Delete, Ctrl+B desde el Core)
// ========================================================================= // =========================================================================
if (config.allow_row_operations) { if (config.allow_row_operations) {
// Ctrl+N: Insert row BELOW current row (inyección local) // Ctrl+N: Insert row BELOW current row + auto-edit (Excel Pro)
if (events.insert_row) { if (events.insert_row) {
const insert_idx: usize = if (table_state.selected_row >= 0) const insert_idx: usize = if (table_state.selected_row >= 0)
@as(usize, @intCast(table_state.selected_row)) + 1 // +1 = debajo @as(usize, @intCast(table_state.selected_row)) + 1 // +1 = debajo
@ -339,6 +367,10 @@ pub fn handleKeyboard(
table_state.selectCell(new_idx, 0); table_state.selectCell(new_idx, 0);
// Inicializar buffer para nueva fila (Excel-style) // Inicializar buffer para nueva fila (Excel-style)
table_state.row_edit_buffer.startEdit(table_core.NEW_ROW_ID, new_idx, true); table_state.row_edit_buffer.startEdit(table_core.NEW_ROW_ID, new_idx, true);
// AUTO-EDIT: Iniciar edición inmediatamente en columna 0
table_state.cell_edit.startEditing(new_idx, 0, "", null);
// Entrar en modo inserción
table_state.nav.enterInsertionMode(table_core.NEW_ROW_ID);
result.row_inserted = true; result.row_inserted = true;
result.selection_changed = true; result.selection_changed = true;
} else |_| {} } else |_| {}
@ -481,6 +513,10 @@ pub fn handleEditingKeyboard(
// Escape canceló la edición // Escape canceló la edición
if (kb_result.cancelled) { if (kb_result.cancelled) {
table_state.stopEditing(); table_state.stopEditing();
// Salir del modo inserción si estaba activo
if (table_state.nav.isInInsertionMode()) {
table_state.nav.exitInsertionMode();
}
result.edit_ended = true; result.edit_ended = true;
return; return;
} }

View file

@ -311,8 +311,10 @@ pub fn drawRowsWithDataSource(
const row_id = datasource_arg.getRowId(row_idx); const row_id = datasource_arg.getRowId(row_idx);
// Intentar leer del buffer si tiene cambios pendientes // Intentar leer del buffer si tiene cambios pendientes
// FIX: Comparar AMBOS row_id Y row_index para evitar ghost row clonada
// (cuando dos filas tienen el mismo ID -1, como fila insertada y ghost row)
if (config.edit_buffer) |eb| { if (config.edit_buffer) |eb| {
if (eb.row_id == row_id) { if (eb.row_id == row_id and eb.row_index == row_idx) {
if (eb.getPendingValue(col_idx)) |pending| { if (eb.getPendingValue(col_idx)) |pending| {
cell_text = pending; cell_text = pending;
} }

View file

@ -173,8 +173,31 @@ pub const NavigationState = struct {
/// Double-click state /// Double-click state
double_click: DoubleClickState = .{}, double_click: DoubleClickState = .{},
/// Modo inserción: sesión de creación de registros (Ctrl+N activo)
is_insertion_mode: bool = false,
/// ID del último registro insertado en esta sesión
last_inserted_id: i64 = types.NEW_ROW_ID,
const Self = @This(); const Self = @This();
/// Entra en modo inserción (Ctrl+N)
pub fn enterInsertionMode(self: *Self, inserted_id: i64) void {
self.is_insertion_mode = true;
self.last_inserted_id = inserted_id;
}
/// Sale del modo inserción
pub fn exitInsertionMode(self: *Self) void {
self.is_insertion_mode = false;
self.last_inserted_id = types.NEW_ROW_ID;
}
/// Verifica si está en modo inserción
pub fn isInInsertionMode(self: *const Self) bool {
return self.is_insertion_mode;
}
/// Navega a siguiente celda (Tab) /// Navega a siguiente celda (Tab)
/// Retorna nueva posición y si navegó o salió del widget /// Retorna nueva posición y si navegó o salió del widget
pub fn tabToNextCell(self: *Self, current_row: usize, num_cols: usize, num_rows: usize, wrap: bool) struct { row: usize, col: usize, result: TabNavigateResult } { pub fn tabToNextCell(self: *Self, current_row: usize, num_cols: usize, num_rows: usize, wrap: bool) struct { row: usize, col: usize, result: TabNavigateResult } {

View file

@ -129,7 +129,8 @@ pub fn handleKeyboard(
// Ctrl+N // Ctrl+N
if (events.insert_row) { if (events.insert_row) {
result.insert_row_requested = true; result.insert_row_requested = true;
list_state.enterInsertionMode(); // Entrar en modo inserción con ID pendiente (panel lo actualizará tras INSERT)
list_state.enterInsertionMode(table_core.NEW_ROW_ID);
} }
// Ctrl+Delete/B // Ctrl+Delete/B

View file

@ -164,16 +164,10 @@ pub const VirtualAdvancedTableState = struct {
row_edit_buffer: table_core.RowEditBuffer = .{}, row_edit_buffer: table_core.RowEditBuffer = .{},
// ========================================================================= // =========================================================================
// Modo Inserción Cronológico (reemplaza inyección local) // Modo Inserción: Ahora centralizado en nav (NavigationState)
// Usar: nav.enterInsertionMode(), nav.exitInsertionMode(), nav.isInInsertionMode()
// ========================================================================= // =========================================================================
/// True si estamos en modo inserción (Ctrl+N activo)
is_insertion_mode: bool = false,
/// IDs de filas insertadas en esta sesión (para ORDER BY cronológico)
/// El Panel gestiona esta lista via el DataProvider
insertion_session_active: bool = false,
const Self = @This(); const Self = @This();
// ========================================================================= // =========================================================================
@ -731,21 +725,19 @@ pub const VirtualAdvancedTableState = struct {
// Métodos de Modo Inserción Cronológico // Métodos de Modo Inserción Cronológico
// ========================================================================= // =========================================================================
/// Entra en modo inserción /// Entra en modo inserción (delega a nav)
pub fn enterInsertionMode(self: *Self) void { pub fn enterInsertionMode(self: *Self, inserted_id: i64) void {
self.is_insertion_mode = true; self.nav.enterInsertionMode(inserted_id);
self.insertion_session_active = true;
} }
/// Sale del modo inserción /// Sale del modo inserción (delega a nav)
pub fn exitInsertionMode(self: *Self) void { pub fn exitInsertionMode(self: *Self) void {
self.is_insertion_mode = false; self.nav.exitInsertionMode();
self.insertion_session_active = false;
} }
/// Verifica si estamos en modo inserción /// Verifica si estamos en modo inserción (delega a nav)
pub fn isInInsertionMode(self: *const Self) bool { pub fn isInInsertionMode(self: *const Self) bool {
return self.is_insertion_mode; return self.nav.isInInsertionMode();
} }
}; };