Skip to content

Commit cbe3381

Browse files
committed
0.10.0.47: Ctrl+Z to undo row delete, supporting multi-row deletion (Ctrl+Z must be pressed once for each undo operation for now
1 parent 53937cf commit cbe3381

3 files changed

Lines changed: 250 additions & 17 deletions

File tree

src/sqlite-ce-edit/globals.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ BOOL WINAPI GetSaveFileNameW(CE_OPENFILENAME*);
8787
** Version
8888
**============================================================================*/
8989

90-
#define SQLITECEDIT_VERSION L"0.10.0.35"
90+
#define SQLITECEDIT_VERSION L"0.10.0.47"
9191

9292
/*============================================================================
9393
** Menu IDs
@@ -322,6 +322,7 @@ void OnGridColumnClick(int col);
322322
void GridFindNext(void);
323323
LRESULT CALLBACK GridProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
324324
void OnGridDoubleClick(int row, int col);
325+
void ClearUndoStack(void);
325326
void OnSchemaDelete(void);
326327
int GetSelectedObjectType(void); /* Returns IMG_TABLE, IMG_VIEW, IMG_TRIGGER, or -1 */
327328
void GetSchemaStatus(wchar_t *buf, int bufLen);

src/sqlite-ce-edit/grid.c

Lines changed: 218 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ static WNDPROC g_pfnEditOverlayProc = NULL;
1919
static int g_editRow = -1; /* Display row being edited */
2020
static int g_editCol = -1; /* Display column being edited */
2121

22+
/* Undo state for deleted rows */
23+
#define UNDO_MAX_BYTES 65536 /* 64KB limit for undo cache */
24+
typedef struct {
25+
char **values; /* Column values (NULL-terminated strings) */
26+
int numCols;
27+
} UndoRow;
28+
static UndoRow *g_undoStack = NULL;
29+
static int g_undoCount = 0;
30+
static int g_undoCapacity = 0;
31+
static int g_undoBytes = 0; /* Current memory usage */
32+
2233
/* Forward declarations */
2334
static void StartCellEdit(int row, int col);
2435
static void CommitCellEdit(void);
@@ -28,6 +39,8 @@ static void InitInsertMode(void);
2839
static void ClearInsertMode(void);
2940
static void CommitInsert(void);
3041
static int NextEditableColumn(int col, int direction);
42+
static void PushUndo(char **values, int numCols);
43+
static void UndoDelete(void);
3144

3245
void CreateGridView(HWND hwndParent, int x, int y, int cx, int cy) {
3346
HIMAGELIST hIml;
@@ -122,6 +135,11 @@ LRESULT CALLBACK GridProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
122135
CopySelectedRow();
123136
return 0;
124137
}
138+
/* Ctrl+Z - Undo last delete (only in edit mode) */
139+
if (ctrl && wParam == 'Z' && g_editMode) {
140+
if (g_undoCount > 0) UndoDelete();
141+
return 0;
142+
}
125143
/* Ctrl+F - Find */
126144
if (ctrl && wParam == 'F') {
127145
DoFind();
@@ -790,6 +808,180 @@ static void CancelCellEdit(void) {
790808
SetFocus(g_hwndGrid);
791809
}
792810

811+
/*============================================================================
812+
** Undo Support for Row Deletion
813+
**============================================================================*/
814+
815+
static void FreeUndoRow(UndoRow *row) {
816+
int i;
817+
if (row->values) {
818+
for (i = 0; i < row->numCols; i++) {
819+
if (row->values[i]) LocalFree(row->values[i]);
820+
}
821+
LocalFree(row->values);
822+
}
823+
}
824+
825+
void ClearUndoStack(void) {
826+
int i;
827+
for (i = 0; i < g_undoCount; i++) {
828+
FreeUndoRow(&g_undoStack[i]);
829+
}
830+
if (g_undoStack) LocalFree(g_undoStack);
831+
g_undoStack = NULL;
832+
g_undoCount = 0;
833+
g_undoCapacity = 0;
834+
g_undoBytes = 0;
835+
}
836+
837+
/* Check available memory before caching */
838+
static int CanCacheUndo(int bytesNeeded) {
839+
/* Drop oldest entries if over limit */
840+
while (g_undoCount > 0 && g_undoBytes + bytesNeeded > UNDO_MAX_BYTES) {
841+
int oldBytes = g_undoStack[0].numCols * 32;
842+
int j;
843+
FreeUndoRow(&g_undoStack[0]);
844+
g_undoBytes -= oldBytes;
845+
if (g_undoBytes < 0) g_undoBytes = 0;
846+
g_undoCount--;
847+
for (j = 0; j < g_undoCount; j++)
848+
g_undoStack[j] = g_undoStack[j + 1];
849+
}
850+
return 1;
851+
}
852+
853+
static void PushUndo(char **values, int numCols) {
854+
int i, rowBytes = 0;
855+
UndoRow *row;
856+
857+
if (!values || numCols < 1) return;
858+
859+
/* Estimate memory needed */
860+
for (i = 0; i < numCols; i++) {
861+
if (values[i]) {
862+
int len = 0;
863+
while (values[i][len]) len++;
864+
rowBytes += len + 1;
865+
}
866+
}
867+
rowBytes += numCols * sizeof(char *);
868+
869+
if (!CanCacheUndo(rowBytes)) return;
870+
871+
/* Grow stack if needed */
872+
if (g_undoCount >= g_undoCapacity) {
873+
int newCap = g_undoCapacity ? g_undoCapacity * 2 : 8;
874+
UndoRow *newStack = (UndoRow *)LocalAlloc(LMEM_FIXED, newCap * sizeof(UndoRow));
875+
if (!newStack) return;
876+
if (g_undoStack) {
877+
for (i = 0; i < g_undoCount; i++) newStack[i] = g_undoStack[i];
878+
LocalFree(g_undoStack);
879+
}
880+
g_undoStack = newStack;
881+
g_undoCapacity = newCap;
882+
}
883+
884+
/* Copy row data */
885+
row = &g_undoStack[g_undoCount];
886+
row->numCols = numCols;
887+
row->values = (char **)LocalAlloc(LMEM_FIXED, numCols * sizeof(char *));
888+
if (!row->values) return;
889+
890+
for (i = 0; i < numCols; i++) {
891+
if (values[i]) {
892+
int len = 0, j;
893+
while (values[i][len]) len++;
894+
row->values[i] = (char *)LocalAlloc(LMEM_FIXED, len + 1);
895+
if (row->values[i]) {
896+
for (j = 0; j <= len; j++) row->values[i][j] = values[i][j];
897+
}
898+
} else {
899+
row->values[i] = NULL;
900+
}
901+
}
902+
903+
g_undoCount++;
904+
g_undoBytes += rowBytes;
905+
}
906+
907+
static void UndoDelete(void) {
908+
UndoRow *row;
909+
char sql[2048];
910+
char *p;
911+
const char *s;
912+
int i, rc;
913+
char *errmsg = NULL;
914+
char tableName[128];
915+
916+
if (g_undoCount < 1 || !g_editMode) return;
917+
918+
/* Save table name */
919+
s = g_editTableName;
920+
p = tableName;
921+
while (*s) *p++ = *s++;
922+
*p = '\0';
923+
924+
row = &g_undoStack[g_undoCount - 1];
925+
926+
/* Build INSERT - skip column 0 (rowid) */
927+
p = sql;
928+
s = "INSERT INTO \"";
929+
while (*s) *p++ = *s++;
930+
s = tableName;
931+
while (*s) *p++ = *s++;
932+
s = "\" (";
933+
while (*s) *p++ = *s++;
934+
935+
/* Column names from metadata (skip rowid) */
936+
for (i = 1; i < row->numCols && i < g_colMetaCount + 1; i++) {
937+
if (i > 1) *p++ = ',';
938+
*p++ = '"';
939+
s = g_colMeta[i - 1].name;
940+
while (*s) *p++ = *s++;
941+
*p++ = '"';
942+
}
943+
944+
s = ") VALUES (";
945+
while (*s) *p++ = *s++;
946+
947+
/* Values (skip rowid at index 0) */
948+
for (i = 1; i < row->numCols; i++) {
949+
if (i > 1) *p++ = ',';
950+
if (row->values[i]) {
951+
*p++ = '\'';
952+
s = row->values[i];
953+
while (*s) {
954+
if (*s == '\'') *p++ = '\''; /* Escape quotes */
955+
*p++ = *s++;
956+
}
957+
*p++ = '\'';
958+
} else {
959+
s = "NULL";
960+
while (*s) *p++ = *s++;
961+
}
962+
}
963+
*p++ = ')';
964+
*p++ = ';';
965+
*p = '\0';
966+
967+
rc = sqlite_exec(g_db, sql, NULL, NULL, &errmsg);
968+
if (rc != SQLITE_OK) {
969+
wchar_t wmsg[256];
970+
MultiByteToWideChar(CP_ACP, 0, errmsg ? errmsg : "Unknown error", -1, wmsg, 256);
971+
MessageBoxW(g_hwndMain, wmsg, L"Undo Failed", MB_OK | MB_ICONERROR);
972+
if (errmsg) sqlite_freemem(errmsg);
973+
return;
974+
}
975+
976+
/* Remove from undo stack */
977+
FreeUndoRow(row);
978+
g_undoCount--;
979+
980+
/* Refresh grid */
981+
OpenTableForEditing(tableName);
982+
SendMessageW(g_hwndStatus, SB_SETTEXTW, 1, (LPARAM)L"Row restored");
983+
}
984+
793985
static void DeleteSelectedRow(void) {
794986
int sel, count, dataRow, rowidIdx, deleted, i;
795987
char *rowid;
@@ -801,6 +993,7 @@ static void DeleteSelectedRow(void) {
801993
int rc;
802994
wchar_t msg[64];
803995
char **rowids;
996+
int *selRows;
804997

805998
/* Count selected rows (excluding placeholder) */
806999
count = 0;
@@ -810,6 +1003,16 @@ static void DeleteSelectedRow(void) {
8101003
}
8111004
if (count < 1) return;
8121005

1006+
/* Collect selection indices BEFORE dialog (dialog may clear selection) */
1007+
selRows = (int *)LocalAlloc(LMEM_FIXED, count * sizeof(int));
1008+
if (!selRows) return;
1009+
i = 0;
1010+
sel = -1;
1011+
while ((sel = ListView_GetNextItem(g_hwndGrid, sel, LVNI_SELECTED)) >= 0) {
1012+
if (sel < g_lastResultRows && i < count) selRows[i++] = sel;
1013+
}
1014+
count = i;
1015+
8131016
/* Save table name before it gets cleared */
8141017
s = g_editTableName;
8151018
p = tableName;
@@ -822,36 +1025,41 @@ static void DeleteSelectedRow(void) {
8221025
else
8231026
wsprintfW(msg, L"Delete %d rows?", count);
8241027
if (MessageBoxW(g_hwndMain, msg, L"Confirm Delete",
825-
MB_YESNO | MB_ICONQUESTION) != IDYES)
1028+
MB_YESNO | MB_ICONQUESTION) != IDYES) {
1029+
LocalFree(selRows);
8261030
return;
1031+
}
8271032

828-
/* Collect rowids first (data will be freed on re-query) */
1033+
/* Collect rowids and save row data for undo */
8291034
rowids = (char **)LocalAlloc(LMEM_FIXED, count * sizeof(char *));
830-
if (!rowids) return;
1035+
if (!rowids) { LocalFree(selRows); return; }
8311036

832-
i = 0;
833-
sel = -1;
834-
while ((sel = ListView_GetNextItem(g_hwndGrid, sel, LVNI_SELECTED)) >= 0) {
1037+
for (i = 0; i < count; i++) {
8351038
int j, len;
836-
if (sel >= g_lastResultRows) continue;
1039+
sel = selRows[i];
8371040
dataRow = g_sortIndex ? g_sortIndex[sel] : sel;
8381041
rowidIdx = (dataRow + 1) * g_lastResultCols;
8391042
rowid = g_lastResult[rowidIdx];
840-
if (rowid && i < count) {
1043+
if (rowid) {
1044+
/* Save row data for undo (entire row including rowid) */
1045+
PushUndo(&g_lastResult[rowidIdx], g_lastResultCols);
1046+
8411047
len = 0;
8421048
while (rowid[len]) len++;
8431049
rowids[i] = (char *)LocalAlloc(LMEM_FIXED, len + 1);
8441050
if (rowids[i]) {
8451051
for (j = 0; j <= len; j++) rowids[i][j] = rowid[j];
846-
i++;
8471052
}
1053+
} else {
1054+
rowids[i] = NULL;
8481055
}
8491056
}
850-
count = i; /* Actual count collected */
1057+
LocalFree(selRows);
8511058

8521059
/* Delete by rowid */
8531060
deleted = 0;
8541061
for (i = 0; i < count; i++) {
1062+
if (!rowids[i]) continue;
8551063
p = sql;
8561064
s = "DELETE FROM \"";
8571065
while (*s) *p++ = *s++;
@@ -891,10 +1099,6 @@ static void DeleteSelectedRow(void) {
8911099
}
8921100
}
8931101

894-
/*============================================================================
895-
** Insert Mode - editing the placeholder row
896-
**============================================================================*/
897-
8981102
static void ClearInsertMode(void) {
8991103
int i;
9001104
if (g_pendingValues) {

src/sqlite-ce-edit/schema.c

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,9 @@ void ClearEditMode(void) {
436436
g_editMode = 0;
437437
g_editTableName[0] = '\0';
438438

439+
/* Clear undo stack */
440+
ClearUndoStack();
441+
439442
/* Free insert mode state */
440443
if (g_pendingValues) {
441444
for (i = 0; i < g_colMetaCount; i++) {
@@ -593,11 +596,36 @@ void OpenTableForEditing(const char *tablename) {
593596
char **results = NULL;
594597
int nRows = 0, nCols = 0;
595598
int i, total;
599+
int sameTable = 0;
596600

597601
if (!g_db || !tablename) return;
598602

599-
/* Clear any previous edit state */
600-
ClearEditMode();
603+
/* Check if re-opening same table (preserve undo stack) */
604+
if (g_editMode && g_editTableName[0]) {
605+
const char *a = tablename;
606+
const char *b = g_editTableName;
607+
sameTable = 1;
608+
while (*a && *b) {
609+
if (*a++ != *b++) { sameTable = 0; break; }
610+
}
611+
if (*a || *b) sameTable = 0;
612+
}
613+
614+
/* Clear previous edit state (but preserve undo if same table) */
615+
if (!sameTable) {
616+
ClearEditMode();
617+
} else {
618+
/* Just clear insert mode, keep undo stack */
619+
if (g_pendingValues) {
620+
for (i = 0; i < g_colMetaCount; i++) {
621+
if (g_pendingValues[i]) LocalFree(g_pendingValues[i]);
622+
}
623+
LocalFree(g_pendingValues);
624+
g_pendingValues = NULL;
625+
}
626+
g_insertMode = 0;
627+
FreeColumnMetadata();
628+
}
601629

602630
/* Load column metadata first - needed for empty tables */
603631
LoadColumnMetadata(tablename);

0 commit comments

Comments
 (0)