refactor: Remove injection logic, prepare for Modo Inserción Cronológico

- Remove injected_row_idx, injected_committed from VirtualAdvancedTableState
- Remove is_injected, injection_index from RowEditBuffer and RowCommitInfo
- Remove startInjectedEdit() method
- Simplify PagedDataSource (no injection offset)
- Simplify drawRowsWithDataSource (no injection handling)
- Add is_insertion_mode, insertion_session_active to State
- Add enterInsertionMode(), exitInsertionMode(), isInInsertionMode() methods
- Ctrl+N now emits insert_row_requested for Panel to handle

Part of Modo Inserción Cronológico implementation.
This commit is contained in:
reugenio 2025-12-28 22:01:17 +01:00
parent 3a6398e90d
commit c9bdf56a80
5 changed files with 41 additions and 253 deletions

View file

@ -807,8 +807,8 @@ fn handleKeyboard(
0; 0;
if (table_state.insertRow(insert_idx)) |new_idx| { if (table_state.insertRow(insert_idx)) |new_idx| {
table_state.selectCell(new_idx, 0); table_state.selectCell(new_idx, 0);
// Inicializar buffer para fila inyectada (Excel-style) // Inicializar buffer para nueva fila (Excel-style)
table_state.row_edit_buffer.startInjectedEdit(new_idx); table_state.row_edit_buffer.startEdit(table_core.NEW_ROW_ID, new_idx, true);
result.row_inserted = true; result.row_inserted = true;
result.selection_changed = true; result.selection_changed = true;
} else |_| {} } else |_| {}

View file

@ -678,10 +678,12 @@ pub fn drawRowsWithDataSource(
} }
// Obtener texto de la celda // Obtener texto de la celda
// PRIORIDAD 1: Valor pendiente en RowEditBuffer (lo que el usuario tecleó) // PRIORIDAD 1: Fila con cambios pendientes leer del buffer
// PRIORIDAD 2: Valor del DataSource (BD o memoria) // PRIORIDAD 2: Leer del DataSource (BD o memoria)
var cell_text: []const u8 = ""; var cell_text: []const u8 = "";
const row_id = datasource.getRowId(row_idx); const row_id = datasource.getRowId(row_idx);
// Intentar leer del buffer si tiene cambios pendientes
if (config.edit_buffer) |eb| { if (config.edit_buffer) |eb| {
if (eb.row_id == row_id) { if (eb.row_id == row_id) {
if (eb.getPendingValue(col_idx)) |pending| { if (eb.getPendingValue(col_idx)) |pending| {
@ -689,6 +691,8 @@ pub fn drawRowsWithDataSource(
} }
} }
} }
// Ir al datasource si no tenemos texto del buffer
if (cell_text.len == 0) { if (cell_text.len == 0) {
cell_text = datasource.getCellValueInto(row_idx, col_idx, cell_buffer); cell_text = datasource.getCellValueInto(row_idx, col_idx, cell_buffer);
} }
@ -1309,12 +1313,6 @@ pub const RowEditBuffer = struct {
/// Hay cambios pendientes /// Hay cambios pendientes
has_changes: bool = false, has_changes: bool = false,
/// True si es una fila inyectada (Ctrl+N entre líneas)
is_injected: bool = false,
/// Índice donde se insertó la fila inyectada (null si no es inyección)
injection_index: ?usize = null,
/// Buffers de valores por columna (almacenamiento fijo) /// Buffers de valores por columna (almacenamiento fijo)
value_buffers: [MAX_PENDING_COLUMNS][MAX_CELL_VALUE_LEN]u8 = undefined, value_buffers: [MAX_PENDING_COLUMNS][MAX_CELL_VALUE_LEN]u8 = undefined,
@ -1333,24 +1331,6 @@ pub const RowEditBuffer = struct {
self.row_index = row_index; self.row_index = row_index;
self.is_new_row = is_new; self.is_new_row = is_new;
self.has_changes = false; self.has_changes = false;
self.is_injected = false;
self.injection_index = null;
self.change_count = 0;
for (0..MAX_PENDING_COLUMNS) |i| {
self.changed_cols[i] = false;
self.value_lens[i] = 0;
}
}
/// Inicializa buffer para una fila inyectada (Ctrl+N entre líneas)
/// insertion_idx es el índice visual donde aparece la fila nueva
pub fn startInjectedEdit(self: *RowEditBuffer, insertion_idx: usize) void {
self.row_id = NEW_ROW_ID;
self.row_index = insertion_idx;
self.is_new_row = true;
self.has_changes = false;
self.is_injected = true;
self.injection_index = insertion_idx;
self.change_count = 0; self.change_count = 0;
for (0..MAX_PENDING_COLUMNS) |i| { for (0..MAX_PENDING_COLUMNS) |i| {
self.changed_cols[i] = false; self.changed_cols[i] = false;
@ -1389,8 +1369,6 @@ pub const RowEditBuffer = struct {
self.row_index = 0; self.row_index = 0;
self.is_new_row = false; self.is_new_row = false;
self.has_changes = false; self.has_changes = false;
self.is_injected = false;
self.injection_index = null;
self.change_count = 0; self.change_count = 0;
for (0..MAX_PENDING_COLUMNS) |i| { for (0..MAX_PENDING_COLUMNS) |i| {
self.changed_cols[i] = false; self.changed_cols[i] = false;
@ -1413,12 +1391,6 @@ pub const RowCommitInfo = struct {
/// Número de cambios /// Número de cambios
change_count: usize, change_count: usize,
/// True si era una fila inyectada (Ctrl+N entre líneas)
is_injected: bool = false,
/// Índice visual donde fue inyectada (válido si is_injected = true)
injection_index: ?usize = null,
}; };
/// Construye la info de commit desde un RowEditBuffer /// Construye la info de commit desde un RowEditBuffer
@ -1445,8 +1417,6 @@ pub fn buildCommitInfo(
.is_insert = buffer.is_new_row, .is_insert = buffer.is_new_row,
.changes = changes_out[0..count], .changes = changes_out[0..count],
.change_count = count, .change_count = count,
.is_injected = buffer.is_injected,
.injection_index = buffer.injection_index,
}; };
} }

View file

@ -51,51 +51,16 @@ pub const PagedDataSource = struct {
// Implementación de TableDataSource // Implementación de TableDataSource
// ========================================================================= // =========================================================================
/// Retorna el número total de filas (filtered count + inyección) /// Retorna el número total de filas (filtered count + inyección visual)
/// Retorna el número de filas disponibles
pub fn getRowCount(self: *Self) usize { pub fn getRowCount(self: *Self) usize {
// Usar conteo filtrado si está disponible
const count_info = self.state.getDisplayCount(); const count_info = self.state.getDisplayCount();
var count = count_info.value; return count_info.value;
// Si hay una fila inyectada, sumar 1 al conteo visual
if (self.state.injected_row_idx != null) {
count += 1;
}
return count;
} }
/// Escribe el valor de una celda en el buffer proporcionado. /// Escribe el valor de una celda en el buffer proporcionado.
/// El row es índice global, se convierte a índice de ventana.
/// Maneja filas inyectadas (Ctrl+N entre líneas).
/// Retorna slice del buffer con el contenido.
pub fn getCellValueInto(self: *Self, row: usize, col: usize, buf: []u8) []const u8 { pub fn getCellValueInto(self: *Self, row: usize, col: usize, buf: []u8) []const u8 {
// Validar columna
if (col >= self.columns.len) return ""; if (col >= self.columns.len) return "";
// =====================================================================
// Manejo de fila inyectada
// =====================================================================
if (self.state.injected_row_idx) |inj_idx| {
if (row == inj_idx) {
// Esta es la fila inyectada - leer del edit buffer
if (self.state.row_edit_buffer.getPendingValue(col)) |pending| {
std.debug.print("[PDS-DEBUG] injected row={} col={} pending=\"{s}\"\n", .{ row, col, pending });
const copy_len = @min(pending.len, buf.len);
@memcpy(buf[0..copy_len], pending[0..copy_len]);
return buf[0..copy_len];
}
// Sin valor pendiente - retornar vacío (celda nueva)
std.debug.print("[PDS-DEBUG] injected row={} col={} NO pending\n", .{ row, col });
return "";
} else if (row > inj_idx) {
// Fila después de la inyección - ajustar índice (-1)
return self.getCellValueFromProvider(row - 1, col, buf);
}
// row < inj_idx: continuar normal
}
// Flujo normal (sin inyección o row < inj_idx)
return self.getCellValueFromProvider(row, col, buf); return self.getCellValueFromProvider(row, col, buf);
} }
@ -122,21 +87,7 @@ pub const PagedDataSource = struct {
} }
/// Retorna el ID único de una fila. /// Retorna el ID único de una fila.
/// Si está en ventana, usa window data. Si no, consulta al provider.
/// Maneja filas inyectadas (Ctrl+N entre líneas).
pub fn getRowId(self: *Self, row: usize) i64 { pub fn getRowId(self: *Self, row: usize) i64 {
// Manejo de fila inyectada
if (self.state.injected_row_idx) |inj_idx| {
if (row == inj_idx) {
// Esta es la fila inyectada
return self.state.injected_row_id;
} else if (row > inj_idx) {
// Fila después de la inyección - ajustar índice
return self.getRowIdFromProvider(row - 1);
}
}
// Flujo normal
return self.getRowIdFromProvider(row); return self.getRowIdFromProvider(row);
} }

View file

@ -164,17 +164,15 @@ pub const VirtualAdvancedTableState = struct {
row_edit_buffer: table_core.RowEditBuffer = .{}, row_edit_buffer: table_core.RowEditBuffer = .{},
// ========================================================================= // =========================================================================
// Estado de inyección local (Ctrl+N entre líneas) // Modo Inserción Cronológico (reemplaza inyección local)
// ========================================================================= // =========================================================================
/// Índice visual donde está la fila inyectada (null = sin inyección) /// True si estamos en modo inserción (Ctrl+N activo)
injected_row_idx: ?usize = null, is_insertion_mode: bool = false,
/// ID de la fila inyectada tras commit (NEW_ROW_ID si aún no guardada) /// IDs de filas insertadas en esta sesión (para ORDER BY cronológico)
injected_row_id: i64 = table_core.NEW_ROW_ID, /// El Panel gestiona esta lista via el DataProvider
insertion_session_active: bool = false,
/// True si la fila inyectada ya fue guardada en BD (tiene ID real)
injected_committed: bool = false,
const Self = @This(); const Self = @This();
@ -730,43 +728,24 @@ pub const VirtualAdvancedTableState = struct {
} }
// ========================================================================= // =========================================================================
// Métodos de inyección local (Ctrl+N entre líneas) // Métodos de Modo Inserción Cronológico
// ========================================================================= // =========================================================================
/// Verifica si hay una fila inyectada activa /// Entra en modo inserción
pub fn hasInjection(self: *const Self) bool { pub fn enterInsertionMode(self: *Self) void {
return self.injected_row_idx != null; self.is_insertion_mode = true;
self.insertion_session_active = true;
} }
/// Inicia una inyección en el índice especificado /// Sale del modo inserción
pub fn startInjection(self: *Self, visual_idx: usize) void { pub fn exitInsertionMode(self: *Self) void {
self.injected_row_idx = visual_idx; self.is_insertion_mode = false;
self.injected_row_id = table_core.NEW_ROW_ID; self.insertion_session_active = false;
self.injected_committed = false;
self.row_edit_buffer.startInjectedEdit(visual_idx);
} }
/// Marca la fila inyectada como guardada en BD /// Verifica si estamos en modo inserción
pub fn markInjectionCommitted(self: *Self, real_id: i64) void { pub fn isInInsertionMode(self: *const Self) bool {
self.injected_row_id = real_id; return self.is_insertion_mode;
self.injected_committed = true;
}
/// Limpia el estado de inyección (después de reload)
pub fn clearInjection(self: *Self) void {
self.injected_row_idx = null;
self.injected_row_id = table_core.NEW_ROW_ID;
self.injected_committed = false;
}
/// Verifica si la fila inyectada está visible en la ventana actual
pub fn isInjectionVisible(self: *const Self, visible_rows: usize) bool {
if (self.injected_row_idx) |idx| {
const scroll_start = self.nav.scroll_row;
const scroll_end = scroll_start + visible_rows;
return idx >= scroll_start and idx < scroll_end;
}
return false;
} }
}; };

View file

@ -130,19 +130,6 @@ pub const VirtualAdvancedTableResult = struct {
/// Shift estaba presionado con Tab (para tab_out inverso) /// Shift estaba presionado con Tab (para tab_out inverso)
tab_shift: bool = false, tab_shift: bool = false,
// =========================================================================
// Inyección local (Ctrl+N entre líneas)
// =========================================================================
/// Una fila inyectada fue guardada en BD (el panel debe hacer INSERT)
injection_committed: bool = false,
/// Índice visual donde se inyectó la fila
injection_row_idx: ?usize = null,
/// La tabla necesita recargarse desde BD (fila inyectada salió del viewport)
needs_reload: bool = false,
// ========================================================================= // =========================================================================
// Compatibilidad (DEPRECADO - usar row_committed) // Compatibilidad (DEPRECADO - usar row_committed)
// ========================================================================= // =========================================================================
@ -197,11 +184,6 @@ pub fn virtualAdvancedTableRect(
) VirtualAdvancedTableResult { ) VirtualAdvancedTableResult {
var result = VirtualAdvancedTableResult{}; var result = VirtualAdvancedTableResult{};
// Debug: verificar si hay inyección al inicio del frame
if (list_state.injected_row_idx != null) {
std.debug.print("[VT-FRAME] Frame con inyección activa: idx={}\n", .{list_state.injected_row_idx.?});
}
if (bounds.isEmpty() or config.columns.len == 0) return result; if (bounds.isEmpty() or config.columns.len == 0) return result;
// Reset frame flags // Reset frame flags
@ -348,12 +330,11 @@ pub fn virtualAdvancedTableRect(
const edited_cell = list_state.getEditingCell().?; const edited_cell = list_state.getEditingCell().?;
const new_value = list_state.getEditText(); const new_value = list_state.getEditText();
std.debug.print("[VT-DEBUG] editor committed: cell=({},{}) value=\"{s}\" hasChanged={} is_injected={}\n", .{ std.debug.print("[VT-DEBUG] editor committed: cell=({},{}) value=\"{s}\" hasChanged={}\n", .{
edited_cell.row, edited_cell.row,
edited_cell.col, edited_cell.col,
new_value, new_value,
list_state.hasValueChanged(), list_state.hasValueChanged(),
list_state.row_edit_buffer.is_injected,
}); });
// Añadir cambio al buffer de fila (NO commit inmediato) // Añadir cambio al buffer de fila (NO commit inmediato)
@ -449,53 +430,28 @@ pub fn virtualAdvancedTableRect(
const is_tab = result.navigate_direction == .next_cell or result.navigate_direction == .prev_cell; const is_tab = result.navigate_direction == .next_cell or result.navigate_direction == .prev_cell;
if (is_tab) { if (is_tab) {
// Wrapper para DataProvider que implementa getRowId(usize) -> i64 // Wrapper para DataProvider que implementa getRowId(usize) -> i64
// Tiene en cuenta filas inyectadas (Ctrl+N) y ghost row
const RowIdGetter = struct { const RowIdGetter = struct {
prov: DataProvider, prov: DataProvider,
total: usize, total: usize,
injected_idx: ?usize,
pub fn getRowId(self: @This(), row: usize) i64 { pub fn getRowId(self: @This(), row: usize) i64 {
// Fila inyectada siempre retorna NEW_ROW_ID
if (self.injected_idx) |inj_idx| {
if (row == inj_idx) {
std.debug.print("[ROW-ID-GETTER] row={} == inj_idx={} -> NEW_ROW_ID\n", .{ row, inj_idx });
return table_core.NEW_ROW_ID;
}
// Filas después de inyección: ajustar índice hacia provider
if (row > inj_idx) {
const adjusted_row = row - 1;
if (adjusted_row >= self.total) return table_core.NEW_ROW_ID;
const id = self.prov.getRowId(adjusted_row) orelse table_core.NEW_ROW_ID;
std.debug.print("[ROW-ID-GETTER] row={} > inj_idx={}, adjusted={} -> id={}\n", .{ row, inj_idx, adjusted_row, id });
return id;
}
}
// Ghost row está al final // Ghost row está al final
if (row >= self.total) { if (row >= self.total) {
std.debug.print("[ROW-ID-GETTER] row={} >= total={} -> NEW_ROW_ID (ghost)\n", .{ row, self.total });
return table_core.NEW_ROW_ID; return table_core.NEW_ROW_ID;
} }
const id = self.prov.getRowId(row) orelse table_core.NEW_ROW_ID; return self.prov.getRowId(row) orelse table_core.NEW_ROW_ID;
std.debug.print("[ROW-ID-GETTER] row={} normal -> id={}\n", .{ row, id });
return id;
} }
}; };
const getter = RowIdGetter{ const getter = RowIdGetter{
.prov = provider, .prov = provider,
.total = total_rows, .total = total_rows,
.injected_idx = list_state.injected_row_idx,
}; };
const current_row = list_state.getSelectedRow() orelse 0; const current_row = list_state.getSelectedRow() orelse 0;
const forward = result.navigate_direction == .next_cell; const forward = result.navigate_direction == .next_cell;
const num_cols = config.columns.len; const num_cols = config.columns.len;
// VirtualAdvancedTable siempre tiene ghost row disponible // +1 para ghost row
// Si hay inyección, sumar 1 extra al conteo visual const num_rows = total_rows + 1;
var num_rows = total_rows + 1; // +1 para ghost row
if (list_state.injected_row_idx != null) {
num_rows += 1; // +1 para fila inyectada
}
const plan = table_core.planTabNavigation( const plan = table_core.planTabNavigation(
&list_state.row_edit_buffer, &list_state.row_edit_buffer,
@ -539,12 +495,6 @@ pub fn virtualAdvancedTableRect(
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;
result.row_changed = true; result.row_changed = true;
// Propagar info de inyección si aplica
if (info.is_injected) {
result.injection_committed = true;
result.injection_row_idx = info.injection_index;
}
} }
} else if (plan.action == .move_with_commit and plan.new_row == current_row) { } else if (plan.action == .move_with_commit and plan.new_row == current_row) {
std.debug.print("[VT-TAB] Commit SUPRIMIDO: misma fila {}, manteniendo buffer\n", .{current_row}); std.debug.print("[VT-TAB] Commit SUPRIMIDO: misma fila {}, manteniendo buffer\n", .{current_row});
@ -568,12 +518,6 @@ pub fn virtualAdvancedTableRect(
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;
result.row_changed = true; result.row_changed = true;
// Propagar info de inyección si aplica
if (info.is_injected) {
result.injection_committed = true;
result.injection_row_idx = info.injection_index;
}
} }
} }
@ -593,10 +537,6 @@ pub fn virtualAdvancedTableRect(
const new_row_idx = list_state.getSelectedRow() orelse 0; const new_row_idx = list_state.getSelectedRow() orelse 0;
const is_ghost = table_core.isGhostRow(new_row_id); const is_ghost = table_core.isGhostRow(new_row_id);
// Detectar si estamos abandonando una fila inyectada
const was_injected = list_state.row_edit_buffer.is_injected;
const injection_idx = list_state.row_edit_buffer.injection_index;
// checkRowChangeAndCommit compara row_ids y hace commit si son diferentes // checkRowChangeAndCommit compara row_ids y hace commit si son diferentes
if (table_core.checkRowChangeAndCommit( if (table_core.checkRowChangeAndCommit(
&list_state.row_edit_buffer, &list_state.row_edit_buffer,
@ -609,32 +549,10 @@ pub fn virtualAdvancedTableRect(
result.row_commit_id = commit_info.row_id; result.row_commit_id = commit_info.row_id;
result.row_commit_is_insert = commit_info.is_insert; result.row_commit_is_insert = commit_info.is_insert;
result.row_changes_count = commit_info.change_count; result.row_changes_count = commit_info.change_count;
// Si era una fila inyectada, señalar para que el panel haga INSERT
if (was_injected) {
result.injection_committed = true;
result.injection_row_idx = injection_idx;
// Mantener injected_row_idx en state para renderizado
// (se limpiará cuando el scroll la saque del viewport)
}
// Compatibilidad
result.row_changed = true; result.row_changed = true;
} }
} }
// =========================================================================
// Detectar si la fila inyectada salió del viewport (requiere reload)
// =========================================================================
if (list_state.hasInjection()) {
// Solo hacer reload si la fila ya fue guardada (injected_committed)
// y ya no es visible en el viewport actual
if (list_state.injected_committed and !list_state.isInjectionVisible(visible_rows)) {
result.needs_reload = true;
list_state.clearInjection();
}
}
// ========================================================================= // =========================================================================
// Tips Proactivos (FASE I): Rotar tips cada ~10 segundos // Tips Proactivos (FASE I): Rotar tips cada ~10 segundos
// ========================================================================= // =========================================================================
@ -1018,18 +936,14 @@ fn drawRows(
const table_ds = pds_ptr.toDataSource(); const table_ds = pds_ptr.toDataSource();
// Convertir selected_id a selected_row (índice global) // Convertir selected_id a selected_row (índice global)
// Si hay una fila inyectada activa, esa es la seleccionada const selected_row: i32 = if (list_state.findSelectedInWindow()) |window_idx|
const selected_row: i32 = if (list_state.injected_row_idx) |inj_idx|
@intCast(inj_idx)
else if (list_state.findSelectedInWindow()) |window_idx|
@intCast(list_state.windowToGlobalIndex(window_idx)) @intCast(list_state.windowToGlobalIndex(window_idx))
else else
-1; -1;
// Calcular rango de filas a dibujar // Calcular rango de filas a dibujar
const first_row = list_state.nav.scroll_row; const first_row = list_state.nav.scroll_row;
// Si hay inyección, hay una fila más en la ventana visual const window_rows = list_state.current_window.len;
const window_rows = list_state.current_window.len + @as(usize, if (list_state.hasInjection()) 1 else 0);
const last_row = @min( const last_row = @min(
list_state.nav.scroll_row + visible_rows, list_state.nav.scroll_row + visible_rows,
list_state.window_start + window_rows, list_state.window_start + window_rows,
@ -1302,40 +1216,14 @@ fn handleKeyboard(
} }
// ========================================================================= // =========================================================================
// Ctrl+N: Inyección local (insertar fila debajo de la actual) // Ctrl+N: Modo Inserción Cronológico (INSERT real en BD)
// ========================================================================= // =========================================================================
if (events.insert_row) { if (events.insert_row) {
std.debug.print("[VT-CTRL-N] insert_row event, hasInjection={}\n", .{list_state.hasInjection()}); std.debug.print("[VT-CTRL-N] insert_row event -> emitiendo insert_row_requested\n", .{});
// Solo permitir si no hay ya una inyección activa // Emitir evento para que el Panel haga INSERT real en BD
if (!list_state.hasInjection()) { result.insert_row_requested = true;
// Calcular índice de inyección (debajo de la fila actual) // Entrar en modo inserción
const current_row: usize = if (list_state.findSelectedInWindow()) |window_idx| list_state.enterInsertionMode();
list_state.windowToGlobalIndex(window_idx)
else
0;
const injection_idx = current_row + 1;
std.debug.print("[VT-CTRL-N] current_row={} injection_idx={}\n", .{ current_row, injection_idx });
// Iniciar inyección
list_state.startInjection(injection_idx);
std.debug.print("[VT-CTRL-N] startInjection OK\n", .{});
// Mover selección a la fila inyectada
list_state.selected_id = null; // La fila inyectada no tiene ID aún
// La selección visual se maneja por injected_row_idx
// Señalar que hay una nueva inyección
result.injection_row_idx = injection_idx;
result.selection_changed = true;
// Iniciar edición automática en primera columna
std.debug.print("[VT-CTRL-N] about to startEditing\n", .{});
list_state.cell_edit.startEditing(injection_idx, 0, "", null);
list_state.nav.active_col = 0;
std.debug.print("[VT-CTRL-N] DONE\n", .{});
}
// Nota: insert_row_requested ya NO se emite - la inyección es interna
} }
// ========================================================================= // =========================================================================