diff --git a/apps/dvxbasic/compiler/lexer.c b/apps/dvxbasic/compiler/lexer.c index 53ad4c3..e83b75b 100644 --- a/apps/dvxbasic/compiler/lexer.c +++ b/apps/dvxbasic/compiler/lexer.c @@ -23,6 +23,7 @@ typedef struct { static const KeywordEntryT sKeywords[] = { { "AND", TOK_AND }, + { "APP", TOK_APP }, { "APPEND", TOK_APPEND }, { "AS", TOK_AS }, { "BASE", TOK_BASE }, @@ -64,6 +65,8 @@ static const KeywordEntryT sKeywords[] = { { "HIDE", TOK_HIDE }, { "IF", TOK_IF }, { "IMP", TOK_IMP }, + { "INIREAD", TOK_INIREAD }, + { "INIWRITE", TOK_INIWRITE }, { "INPUT", TOK_INPUT }, { "INTEGER", TOK_INTEGER }, { "IS", TOK_IS }, diff --git a/apps/dvxbasic/compiler/lexer.h b/apps/dvxbasic/compiler/lexer.h index cfaf081..0acf3fd 100644 --- a/apps/dvxbasic/compiler/lexer.h +++ b/apps/dvxbasic/compiler/lexer.h @@ -57,6 +57,7 @@ typedef enum { // Keywords TOK_AND, + TOK_APP, TOK_AS, TOK_BASE, TOK_BOOLEAN, @@ -133,6 +134,8 @@ typedef enum { TOK_SHOW, TOK_SINGLE, TOK_SLEEP, + TOK_INIREAD, + TOK_INIWRITE, TOK_SQLCLOSE, TOK_SQLEOF, TOK_SQLERROR, diff --git a/apps/dvxbasic/compiler/opcodes.h b/apps/dvxbasic/compiler/opcodes.h index a430d55..4e9ffa3 100644 --- a/apps/dvxbasic/compiler/opcodes.h +++ b/apps/dvxbasic/compiler/opcodes.h @@ -333,6 +333,15 @@ #define OP_SQL_FREE_RESULT 0xDB // pop rs #define OP_SQL_AFFECTED 0xDC // pop db, push int +// App object +#define OP_APP_PATH 0xDD // push App.Path string +#define OP_APP_CONFIG 0xDE // push App.Config string +#define OP_APP_DATA 0xDF // push App.Data string + +// INI file operations +#define OP_INI_READ 0xE0 // pop default, pop key, pop section, pop file, push string +#define OP_INI_WRITE 0xE1 // pop value, pop key, pop section, pop file + // ============================================================ // Halt // ============================================================ diff --git a/apps/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c index 1524701..143ac63 100644 --- a/apps/dvxbasic/compiler/parser.c +++ b/apps/dvxbasic/compiler/parser.c @@ -1115,6 +1115,27 @@ static void parsePrimary(BasParserT *p) { BasTokenTypeE tt = p->lex.token.type; + // App.Path / App.Config / App.Data + if (tt == TOK_APP) { + advance(p); + expect(p, TOK_DOT); + + if (checkIdent(p, "Path")) { + advance(p); + basEmit8(&p->cg, OP_APP_PATH); + } else if (checkIdent(p, "Config")) { + advance(p); + basEmit8(&p->cg, OP_APP_CONFIG); + } else if (checkIdent(p, "Data")) { + advance(p); + basEmit8(&p->cg, OP_APP_DATA); + } else { + parserError(p, "Expected 'Path', 'Config', or 'Data' after 'App.'"); + } + + return; + } + // Integer literal if (tt == TOK_INT_LIT) { int32_t val = p->lex.token.intVal; @@ -1242,6 +1263,22 @@ static void parsePrimary(BasParserT *p) { return; } + // IniRead$(file, section, key, default) + if (tt == TOK_INIREAD) { + advance(p); + expect(p, TOK_LPAREN); + parseExpression(p); // file + expect(p, TOK_COMMA); + parseExpression(p); // section + expect(p, TOK_COMMA); + parseExpression(p); // key + expect(p, TOK_COMMA); + parseExpression(p); // default + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_INI_READ); + return; + } + if (tt == TOK_SQLERROR) { advance(p); expect(p, TOK_LPAREN); @@ -4823,6 +4860,19 @@ static void parseStatement(BasParserT *p) { basEmit8(&p->cg, OP_SQL_CLOSE); break; + case TOK_INIWRITE: + // IniWrite file, section, key, value + advance(p); + parseExpression(p); // file + expect(p, TOK_COMMA); + parseExpression(p); // section + expect(p, TOK_COMMA); + parseExpression(p); // key + expect(p, TOK_COMMA); + parseExpression(p); // value + basEmit8(&p->cg, OP_INI_WRITE); + break; + case TOK_SQLEXEC: // SQLExec db, sql (statement form, discard result) advance(p); diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index 953b682..43431f0 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -10,10 +10,11 @@ #include "dvxDialog.h" #include "dvxWm.h" #include "widgetBox.h" +#include "widgetDataCtrl.h" +#include "widgetDbGrid.h" #include "thirdparty/stb_ds_wrap.h" #include -#include #include #include #include @@ -73,6 +74,7 @@ static void onFormClose(WindowT *win); static void onFormDeactivate(WindowT *win); static void onFormResize(WindowT *win, int32_t newW, int32_t newH); static void onWidgetBlur(WidgetT *w); +static bool onWidgetValidate(WidgetT *w); static void onWidgetChange(WidgetT *w); static void onWidgetClick(WidgetT *w); static void onWidgetDblClick(WidgetT *w); @@ -89,6 +91,7 @@ static void rebuildListBoxItems(BasControlT *ctrl); static const char *resolveTypeName(const char *typeName); static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value); static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value); +static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl); static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl); static BasValueT zeroValue(void); @@ -290,6 +293,7 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const widget->onChange = onWidgetChange; widget->onFocus = onWidgetFocus; widget->onBlur = onWidgetBlur; + widget->onValidate = onWidgetValidate; return ctrl; } @@ -876,6 +880,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen widget->onChange = onWidgetChange; widget->onFocus = onWidgetFocus; widget->onBlur = onWidgetBlur; + widget->onValidate = onWidgetValidate; widget->onKeyPress = onWidgetKeyPress; widget->onKeyDown = onWidgetKeyDown; widget->onKeyUp = onWidgetKeyUp; @@ -1156,17 +1161,35 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen if (form) { basFormRtFireEvent(rt, form, form->name, "Load"); - // Auto-refresh Data controls and update bound controls - typedef void (*RefreshFnT)(WidgetT *); - RefreshFnT refreshFn = (RefreshFnT)dlsym(NULL, "_dataCtrlRefresh"); + // Auto-refresh Data controls that have no MasterSource (masters/standalone). + // Detail controls are refreshed automatically by the master-detail cascade + // when the master's Reposition event fires. + for (int32_t i = 0; i < form->controlCount; i++) { + BasControlT *dc = &form->controls[i]; - if (refreshFn) { - for (int32_t i = 0; i < form->controlCount; i++) { - if (strcasecmp(form->controls[i].typeName, "Data") == 0 && form->controls[i].widget) { - refreshFn(form->controls[i].widget); - updateBoundControls(form, &form->controls[i]); + if (strcasecmp(dc->typeName, "Data") != 0 || !dc->widget) { + continue; + } + + // Skip details — they'll be refreshed by the cascade + const char *ms = NULL; + + if (dc->iface) { + for (int32_t p = 0; p < dc->iface->propCount; p++) { + if (strcasecmp(dc->iface->props[p].name, "MasterSource") == 0 && dc->iface->props[p].getFn) { + ms = ((const char *(*)(const WidgetT *))dc->iface->props[p].getFn)(dc->widget); + break; + } } } + + if (ms && ms[0]) { + continue; + } + + wgtDataCtrlRefresh(dc->widget); + updateBoundControls(form, dc); + refreshDetailControls(form, dc); } } @@ -1833,6 +1856,56 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa // Widget event callbacks // ============================================================ +static bool onWidgetValidate(WidgetT *w) { + BasControlT *ctrl = (BasControlT *)w->userData; + + if (!ctrl || !ctrl->form || !ctrl->form->vm) { + return true; + } + + BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx; + + if (!rt || !rt->module) { + return true; + } + + // Look for Data1_Validate(Cancel As Integer) handler + char handlerName[MAX_EVENT_NAME_LEN]; + snprintf(handlerName, sizeof(handlerName), "%s_Validate", ctrl->name); + const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName); + + if (!proc || proc->isFunction) { + return true; + } + + // Pass Cancel = 0 (False). If the handler sets it to non-zero, cancel. + BasValueT cancelArg = basValLong(0); + + BasFormT *prevForm = rt->currentForm; + BasValueT *prevVars = rt->vm->currentFormVars; + int32_t prevVarCount = rt->vm->currentFormVarCount; + + rt->currentForm = ctrl->form; + basVmSetCurrentForm(rt->vm, ctrl->form); + basVmSetCurrentFormVars(rt->vm, ctrl->form->formVars, ctrl->form->formVarCount); + + BasValueT outCancel = basValLong(0); + + if (proc->paramCount == 1) { + basVmCallSubWithArgsOut(rt->vm, proc->codeAddr, &cancelArg, 1, &outCancel, 1); + } else { + basVmCallSub(rt->vm, proc->codeAddr); + } + + rt->currentForm = prevForm; + basVmSetCurrentForm(rt->vm, prevForm); + basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount); + + // Non-zero Cancel means abort the write + return !basValIsTruthy(outCancel); +} + + static void onWidgetBlur(WidgetT *w) { BasControlT *ctrl = (BasControlT *)w->userData; @@ -1845,27 +1918,111 @@ static void onWidgetBlur(WidgetT *w) { if (rt) { fireCtrlEvent(rt, ctrl, "LostFocus", NULL, 0); } + + // Write-back: if this control is data-bound, update the Data control's + // cache and persist to the database + if (ctrl->dataSource[0] && ctrl->dataField[0] && ctrl->widget && ctrl->form) { + for (int32_t i = 0; i < ctrl->form->controlCount; i++) { + BasControlT *dc = &ctrl->form->controls[i]; + + if (strcasecmp(dc->typeName, "Data") == 0 && + strcasecmp(dc->name, ctrl->dataSource) == 0 && + dc->widget) { + const char *text = wgtGetText(ctrl->widget); + + if (text) { + wgtDataCtrlSetField(dc->widget, ctrl->dataField, text); + wgtDataCtrlUpdate(dc->widget); + } + + break; + } + } + } } -// updateBoundControls -- sync bound controls from Data control's current record -static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) { - // Resolve the getField function from the Data control's API - typedef const char *(*GetFieldFnT)(WidgetT *, const char *); - GetFieldFnT getField = (GetFieldFnT)dlsym(NULL, "_dataCtrlGetField"); - - if (!getField || !dataCtrl->widget) { +// refreshDetailControls -- cascade master-detail: refresh detail Data controls +static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) { + if (!masterCtrl->widget) { return; } for (int32_t i = 0; i < form->controlCount; i++) { BasControlT *ctrl = &form->controls[i]; - if (ctrl->dataSource[0] && ctrl->dataField[0] && - strcasecmp(ctrl->dataSource, dataCtrl->name) == 0) { - const char *val = getField(dataCtrl->widget, ctrl->dataField); + if (strcasecmp(ctrl->typeName, "Data") != 0 || !ctrl->widget || !ctrl->iface) { + continue; + } - if (val && ctrl->widget) { + // Read MasterSource, MasterField, DetailField from the widget's interface + const char *ms = NULL; + const char *mf = NULL; + + for (int32_t p = 0; p < ctrl->iface->propCount; p++) { + const WgtPropDescT *pd = &ctrl->iface->props[p]; + + if (!pd->getFn) { + continue; + } + + if (strcasecmp(pd->name, "MasterSource") == 0) { + ms = ((const char *(*)(const WidgetT *))pd->getFn)(ctrl->widget); + } else if (strcasecmp(pd->name, "MasterField") == 0) { + mf = ((const char *(*)(const WidgetT *))pd->getFn)(ctrl->widget); + } + } + + if (!ms || !ms[0] || strcasecmp(ms, masterCtrl->name) != 0) { + continue; + } + + // Get the master's current value for MasterField + const char *val = ""; + + if (mf && mf[0]) { + val = wgtDataCtrlGetField(masterCtrl->widget, mf); + } + + // Set the filter value and refresh the detail + wgtDataCtrlSetMasterValue(ctrl->widget, val); + wgtDataCtrlRefresh(ctrl->widget); + + // Update bound controls for this detail + updateBoundControls(form, ctrl); + } +} + + +// updateBoundControls -- sync bound controls from Data control's current record +static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) { + if (!dataCtrl->widget) { + return; + } + + for (int32_t i = 0; i < form->controlCount; i++) { + BasControlT *ctrl = &form->controls[i]; + + if (!ctrl->dataSource[0] || strcasecmp(ctrl->dataSource, dataCtrl->name) != 0) { + continue; + } + + if (!ctrl->widget) { + continue; + } + + // DBGrid: bind to the Data control widget and refresh + if (strcasecmp(ctrl->typeName, "DBGrid") == 0) { + wgtDbGridSetDataWidget(ctrl->widget, dataCtrl->widget); + wgtDbGridRefresh(ctrl->widget); + continue; + } + + // Text-based controls: set text from current record field + if (ctrl->dataField[0]) { + const char *val = wgtDataCtrlGetField(dataCtrl->widget, ctrl->dataField); + + if (val) { snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", val); wgtSetText(ctrl->widget, ctrl->textBuf); } @@ -1884,10 +2041,11 @@ static void onWidgetChange(WidgetT *w) { BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx; if (rt) { - // Data controls fire "Reposition" and update bound controls first + // Data controls fire "Reposition", update bound controls, and cascade to details if (strcasecmp(ctrl->typeName, "Data") == 0) { updateBoundControls(ctrl->form, ctrl); fireCtrlEvent(rt, ctrl, "Reposition", NULL, 0); + refreshDetailControls(ctrl->form, ctrl); return; } diff --git a/apps/dvxbasic/ide/ideDesigner.c b/apps/dvxbasic/ide/ideDesigner.c index 4591ac0..e8593e4 100644 --- a/apps/dvxbasic/ide/ideDesigner.c +++ b/apps/dvxbasic/ide/ideDesigner.c @@ -1038,7 +1038,7 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i for (int32_t j = 0; j < iface->propCount; j++) { const WgtPropDescT *p = &iface->props[j]; - if (!p->getFn) { + if (!p->getFn || !p->setFn) { continue; } diff --git a/apps/dvxbasic/ide/ideDesigner.h b/apps/dvxbasic/ide/ideDesigner.h index 4f61bc2..75574b1 100644 --- a/apps/dvxbasic/ide/ideDesigner.h +++ b/apps/dvxbasic/ide/ideDesigner.h @@ -137,6 +137,7 @@ typedef struct { int32_t dragStartX; // mouse X at resize start WindowT *formWin; AppContextT *ctx; + const char *projectDir; // project directory (for resolving relative paths) } DsgnStateT; // ============================================================ diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index f296b65..641a7a3 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -42,6 +42,7 @@ #include "stb_ds_wrap.h" +#include #include #include #include @@ -173,6 +174,8 @@ static void onObjDropdownChange(WidgetT *w); static void printCallback(void *ctx, const char *text, bool newline); static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize); static bool doEventsCallback(void *ctx); +static void *resolveExternCallback(void *ctx, const char *libName, const char *funcName); +static BasValueT callExternCallback(void *ctx, void *funcPtr, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType); static void runCached(void); static void runModule(BasModuleT *mod); static void onEditorChange(WidgetT *w); @@ -690,6 +693,7 @@ static void buildWindow(void) { // Initialize designer (form window created on demand) dsgnInit(&sDesigner, sAc); + sDesigner.projectDir = sProject.projectDir; showOutputWindow(); showImmediateWindow(); @@ -1249,6 +1253,17 @@ static void runModule(BasModuleT *mod) { BasVmT *vm = basVmCreate(); basVmLoadModule(vm, mod); + // Set App.Path/Config/Data. In the IDE, config and data live under + // the project directory so everything stays together during development. + // Standalone apps use the DVX root-level CONFIG/ and DATA/ directories + // since the app directory (on CD-ROM) is read-only. + snprintf(vm->appPath, sizeof(vm->appPath), "%s", sProject.projectDir); + snprintf(vm->appConfig, sizeof(vm->appConfig), "%s/CONFIG", sProject.projectDir); + snprintf(vm->appData, sizeof(vm->appData), "%s/DATA", sProject.projectDir); + + platformMkdirRecursive(vm->appConfig); + platformMkdirRecursive(vm->appData); + // Set up implicit main frame vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount; vm->callDepth = 1; @@ -1278,6 +1293,13 @@ static void runModule(BasModuleT *mod) { sqlCb.sqlAffectedRows = dvxSqlAffectedRows; basVmSetSqlCallbacks(vm, &sqlCb); + // Set extern library callbacks (DECLARE LIBRARY support) + BasExternCallbacksT extCb; + extCb.resolveExtern = resolveExternCallback; + extCb.callExtern = callExternCallback; + extCb.ctx = NULL; + basVmSetExternCallbacks(vm, &extCb); + // Create form runtime (bridges UI opcodes to DVX widgets) BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod); @@ -3900,6 +3922,162 @@ static void printCallback(void *ctx, const char *text, bool newline) { } } +// ============================================================ +// Extern library callbacks (DECLARE LIBRARY support) +// ============================================================ + +// Resolve a native function by library and symbol name. +// Library name is ignored (all DXE exports are globally visible via +// RTLD_GLOBAL), but could be used to dlopen a specific library in +// the future. The underscore prefix is added automatically to match +// DJGPP's cdecl name mangling. +static void *resolveExternCallback(void *ctx, const char *libName, const char *funcName) { + (void)ctx; + (void)libName; + + // DJGPP adds underscore prefix to C symbols + char mangledName[256]; + snprintf(mangledName, sizeof(mangledName), "_%s", funcName); + + return dlsym(NULL, mangledName); +} + + +// Marshal a call from BASIC to a native C function. +// Converts BasValueT arguments to C types, calls the function using +// inline assembly (cdecl: push args right-to-left, caller cleans up), +// and converts the return value back to BasValueT. +// +// Supported types: Integer (int16_t passed as int32_t), Long (int32_t), +// Single/Double (double), String (const char *), Boolean (int32_t). +static BasValueT callExternCallback(void *ctx, void *funcPtr, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType) { + (void)ctx; + (void)funcName; + + // Convert BASIC values to native C values and collect pointers + // to temporary C strings so we can free them after the call. + uint32_t nativeArgs[32]; + char *tempStrings[16]; + int32_t tempStringCount = 0; + int32_t nativeCount = 0; + + for (int32_t i = 0; i < argc && i < 16; i++) { + switch (args[i].type) { + case BAS_TYPE_STRING: { + BasStringT *s = basValFormatString(args[i]); + char *cstr = strdup(s->data); + basStringUnref(s); + nativeArgs[nativeCount++] = (uint32_t)(uintptr_t)cstr; + tempStrings[tempStringCount++] = cstr; + break; + } + + case BAS_TYPE_DOUBLE: + case BAS_TYPE_SINGLE: { + // Doubles are passed as 8 bytes (two 32-bit pushes on i386) + union { double d; uint32_t u[2]; } conv; + conv.d = args[i].dblVal; + nativeArgs[nativeCount++] = conv.u[0]; + nativeArgs[nativeCount++] = conv.u[1]; + break; + } + + default: { + // Integer, Long, Boolean — all fit in 32 bits + nativeArgs[nativeCount++] = (uint32_t)(int32_t)basValToNumber(args[i]); + break; + } + } + } + + // Call the function using cdecl convention. + // Push args right-to-left, call, read return value from eax (or FPU for double). + BasValueT result; + memset(&result, 0, sizeof(result)); + + if (retType == BAS_TYPE_DOUBLE || retType == BAS_TYPE_SINGLE) { + // Return value in st(0) + double dblResult = 0.0; + + // Push args right-to-left and call + __asm__ __volatile__( + "movl %%esp, %%ebx\n\t" // save stack pointer + ::: "ebx" + ); + + for (int32_t i = nativeCount - 1; i >= 0; i--) { + uint32_t val = nativeArgs[i]; + __asm__ __volatile__("pushl %0" :: "r"(val)); + } + + __asm__ __volatile__( + "call *%1\n\t" + "fstpl %0\n\t" + "movl %%ebx, %%esp\n\t" // restore stack pointer + : "=m"(dblResult) + : "r"(funcPtr) + : "eax", "ecx", "edx", "memory" + ); + + result = basValDouble(dblResult); + } else if (retType == BAS_TYPE_STRING) { + // Return value is const char * in eax + uint32_t rawResult = 0; + + __asm__ __volatile__( + "movl %%esp, %%ebx\n\t" + ::: "ebx" + ); + + for (int32_t i = nativeCount - 1; i >= 0; i--) { + uint32_t val = nativeArgs[i]; + __asm__ __volatile__("pushl %0" :: "r"(val)); + } + + __asm__ __volatile__( + "call *%1\n\t" + "movl %%ebx, %%esp\n\t" + : "=a"(rawResult) + : "r"(funcPtr) + : "ecx", "edx", "memory" + ); + + const char *str = (const char *)(uintptr_t)rawResult; + result = basValStringFromC(str ? str : ""); + } else { + // Integer/Long/Boolean — return in eax + uint32_t rawResult = 0; + + __asm__ __volatile__( + "movl %%esp, %%ebx\n\t" + ::: "ebx" + ); + + for (int32_t i = nativeCount - 1; i >= 0; i--) { + uint32_t val = nativeArgs[i]; + __asm__ __volatile__("pushl %0" :: "r"(val)); + } + + __asm__ __volatile__( + "call *%1\n\t" + "movl %%ebx, %%esp\n\t" + : "=a"(rawResult) + : "r"(funcPtr) + : "ecx", "edx", "memory" + ); + + result = basValLong((int32_t)rawResult); + } + + // Free temporary strings + for (int32_t i = 0; i < tempStringCount; i++) { + free(tempStrings[i]); + } + + return result; +} + + // ============================================================ // onFormWinMouse // ============================================================ diff --git a/apps/dvxbasic/ide/ideProperties.c b/apps/dvxbasic/ide/ideProperties.c index 5c10ccd..1359150 100644 --- a/apps/dvxbasic/ide/ideProperties.c +++ b/apps/dvxbasic/ide/ideProperties.c @@ -94,6 +94,26 @@ static void onPrpClose(WindowT *win) { } +// ============================================================ +// resolveDbPath -- resolve a DatabaseName against the project directory +// ============================================================ + +static void resolveDbPath(const char *dbName, char *out, int32_t outSize) { + // If it's already an absolute path (starts with drive letter or /), use as-is + if ((dbName[0] && dbName[1] == ':') || dbName[0] == '/' || dbName[0] == '\\') { + snprintf(out, outSize, "%s", dbName); + return; + } + + // Resolve relative to project directory + if (sDs && sDs->projectDir && sDs->projectDir[0]) { + snprintf(out, outSize, "%s/%s", sDs->projectDir, dbName); + } else { + snprintf(out, outSize, "%s", dbName); + } +} + + // ============================================================ // getDataFieldNames -- query column names from a Data control's database // ============================================================ @@ -154,7 +174,10 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam return 0; } - int32_t db = sqlOpen(dbName); + char fullPath[DVX_MAX_PATH]; + resolveDbPath(dbName, fullPath, sizeof(fullPath)); + + int32_t db = sqlOpen(fullPath); if (db <= 0) { return 0; @@ -220,7 +243,10 @@ static int32_t getTableNames(const char *dbName, char names[][DSGN_MAX_NAME], in return 0; } - int32_t db = sqlOpen(dbName); + char fullPath[DVX_MAX_PATH]; + resolveDbPath(dbName, fullPath, sizeof(fullPath)); + + int32_t db = sqlOpen(fullPath); if (db <= 0) { return 0; @@ -273,6 +299,8 @@ static uint8_t getPropType(const char *propName, const char *typeName) { // Read-only properties if (strcasecmp(propName, "Type") == 0) { return PROP_TYPE_READONLY; } if (strcasecmp(propName, "Index") == 0) { return PROP_TYPE_READONLY; } + if (strcasecmp(propName, "BOF") == 0) { return PROP_TYPE_READONLY; } + if (strcasecmp(propName, "EOF") == 0) { return PROP_TYPE_READONLY; } // Known built-in types if (strcasecmp(propName, "Name") == 0) { return PROP_TYPE_STRING; } @@ -289,9 +317,13 @@ static uint8_t getPropType(const char *propName, const char *typeName) { if (strcasecmp(propName, "Centered") == 0) { return PROP_TYPE_BOOL; } if (strcasecmp(propName, "Visible") == 0) { return PROP_TYPE_BOOL; } if (strcasecmp(propName, "Enabled") == 0) { return PROP_TYPE_BOOL; } - if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; } - if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; } + if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; } + if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; } if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; } + if (strcasecmp(propName, "KeyColumn") == 0) { return PROP_TYPE_DATAFIELD; } + if (strcasecmp(propName, "MasterSource") == 0) { return PROP_TYPE_DATASOURCE; } + if (strcasecmp(propName, "MasterField") == 0) { return PROP_TYPE_DATAFIELD; } + if (strcasecmp(propName, "DetailField") == 0) { return PROP_TYPE_DATAFIELD; } // Look up in the widget's interface descriptor if (typeName && typeName[0]) { @@ -522,17 +554,23 @@ static void onPropDblClick(WidgetT *w) { snprintf(newValue, sizeof(newValue), "%s", dataNames[chosenIdx]); } } else if (propType == PROP_TYPE_DATAFIELD) { - // Show dropdown of column names from the Data control's database + // Show dropdown of column names from the Data control's database. + // If the selected control IS a Data control (e.g. editing KeyColumn), + // use its own name. Otherwise look up its DataSource property. int32_t selCount = (int32_t)arrlen(sDs->form->controls); const char *dataSrc = ""; if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) { DsgnControlT *selCtrl = &sDs->form->controls[sDs->selectedIdx]; - for (int32_t i = 0; i < selCtrl->propCount; i++) { - if (strcasecmp(selCtrl->props[i].name, "DataSource") == 0) { - dataSrc = selCtrl->props[i].value; - break; + if (strcasecmp(selCtrl->typeName, "Data") == 0) { + dataSrc = selCtrl->name; + } else { + for (int32_t i = 0; i < selCtrl->propCount; i++) { + if (strcasecmp(selCtrl->props[i].name, "DataSource") == 0) { + dataSrc = selCtrl->props[i].value; + break; + } } } } @@ -550,28 +588,33 @@ static void onPropDblClick(WidgetT *w) { return; } } else { - const char *fieldPtrs[MAX_DATAFIELD_COLS]; + const char *fieldPtrs[MAX_DATAFIELD_COLS + 1]; + fieldPtrs[0] = "(none)"; for (int32_t i = 0; i < fieldCount; i++) { - fieldPtrs[i] = fieldNames[i]; + fieldPtrs[i + 1] = fieldNames[i]; } - int32_t defIdx = -1; + int32_t defIdx = 0; for (int32_t i = 0; i < fieldCount; i++) { if (strcasecmp(fieldNames[i], curValue) == 0) { - defIdx = i; + defIdx = i + 1; break; } } int32_t chosenIdx = 0; - if (!dvxChoiceDialog(sPrpCtx, "DataField", "Select column:", fieldPtrs, fieldCount, defIdx, &chosenIdx)) { + if (!dvxChoiceDialog(sPrpCtx, propName, "Select column:", fieldPtrs, fieldCount + 1, defIdx, &chosenIdx)) { return; } - snprintf(newValue, sizeof(newValue), "%s", fieldNames[chosenIdx]); + if (chosenIdx == 0) { + snprintf(newValue, sizeof(newValue), ""); + } else { + snprintf(newValue, sizeof(newValue), "%s", fieldNames[chosenIdx - 1]); + } } } else if (propType == PROP_TYPE_RECORDSRC) { // Show dropdown of table names from the Data control's database @@ -603,28 +646,33 @@ static void onPropDblClick(WidgetT *w) { return; } } else { - const char *tablePtrs[MAX_TABLES]; + const char *tablePtrs[MAX_TABLES + 1]; + tablePtrs[0] = "(none)"; for (int32_t i = 0; i < tableCount; i++) { - tablePtrs[i] = tableNames[i]; + tablePtrs[i + 1] = tableNames[i]; } - int32_t defIdx = -1; + int32_t defIdx = 0; for (int32_t i = 0; i < tableCount; i++) { if (strcasecmp(tableNames[i], curValue) == 0) { - defIdx = i; + defIdx = i + 1; break; } } int32_t chosenIdx = 0; - if (!dvxChoiceDialog(sPrpCtx, "RecordSource", "Select table:", tablePtrs, tableCount, defIdx, &chosenIdx)) { + if (!dvxChoiceDialog(sPrpCtx, "RecordSource", "Select table:", tablePtrs, tableCount + 1, defIdx, &chosenIdx)) { return; } - snprintf(newValue, sizeof(newValue), "%s", tableNames[chosenIdx]); + if (chosenIdx == 0) { + snprintf(newValue, sizeof(newValue), ""); + } else { + snprintf(newValue, sizeof(newValue), "%s", tableNames[chosenIdx - 1]); + } } } else if (propType == PROP_TYPE_INT) { // Spinner dialog for integers @@ -670,6 +718,22 @@ static void onPropDblClick(WidgetT *w) { } } + // If this is a Data control, update DataSource and MasterSource + // references on all other controls that pointed to the old name + if (strcasecmp(ctrl->typeName, "Data") == 0) { + for (int32_t i = 0; i < count; i++) { + DsgnControlT *c = &sDs->form->controls[i]; + + for (int32_t j = 0; j < c->propCount; j++) { + if ((strcasecmp(c->props[j].name, "DataSource") == 0 || + strcasecmp(c->props[j].name, "MasterSource") == 0) && + strcasecmp(c->props[j].value, oldName) == 0) { + snprintf(c->props[j].value, DSGN_MAX_TEXT, "%s", newValue); + } + } + } + } + ideRenameInCode(oldName, newValue); prpRebuildTree(sDs); } else if (strcasecmp(propName, "MinWidth") == 0) { @@ -1263,6 +1327,11 @@ void prpRefresh(DsgnStateT *ds) { for (int32_t i = 0; i < iface->propCount; i++) { const WgtPropDescT *p = &iface->props[i]; + // Skip read-only runtime properties (no setter) + if (!p->setFn) { + continue; + } + // Skip if already shown as a custom prop bool already = false; diff --git a/apps/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c index a58a138..1081f6d 100644 --- a/apps/dvxbasic/runtime/vm.c +++ b/apps/dvxbasic/runtime/vm.c @@ -3272,6 +3272,207 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_APP_PATH: { + push(vm, basValStringFromC(vm->appPath)); + break; + } + + case OP_APP_CONFIG: { + push(vm, basValStringFromC(vm->appConfig)); + break; + } + + case OP_APP_DATA: { + push(vm, basValStringFromC(vm->appData)); + break; + } + + case OP_INI_READ: { + // Stack: file, section, key, default -> result string + BasValueT defVal, keyVal, secVal, fileVal; + if (!pop(vm, &defVal) || !pop(vm, &keyVal) || !pop(vm, &secVal) || !pop(vm, &fileVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + BasStringT *fileStr = basValFormatString(fileVal); + BasStringT *secStr = basValFormatString(secVal); + BasStringT *keyStr = basValFormatString(keyVal); + BasStringT *defStr = basValFormatString(defVal); + const char *result = defStr->data; + + FILE *fp = fopen(fileStr->data, "r"); + if (fp) { + char line[512]; + bool inSection = false; + while (fgets(line, sizeof(line), fp)) { + // Strip trailing whitespace + char *end = line + strlen(line) - 1; + while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ')) { + *end-- = '\0'; + } + char *p = line; + while (*p == ' ' || *p == '\t') { p++; } + if (*p == '\0' || *p == ';' || *p == '#') { continue; } + if (*p == '[') { + char *rb = strchr(p, ']'); + if (rb) { + *rb = '\0'; + inSection = (strcasecmp(p + 1, secStr->data) == 0); + } + continue; + } + if (inSection) { + char *eq = strchr(p, '='); + if (eq) { + *eq = '\0'; + char *k = p; + char *ke = eq - 1; + while (ke >= k && (*ke == ' ' || *ke == '\t')) { *ke-- = '\0'; } + if (strcasecmp(k, keyStr->data) == 0) { + char *v = eq + 1; + while (*v == ' ' || *v == '\t') { v++; } + result = v; + break; + } + } + } + } + fclose(fp); + } + + push(vm, basValStringFromC(result)); + basStringUnref(fileStr); + basStringUnref(secStr); + basStringUnref(keyStr); + basStringUnref(defStr); + basValRelease(&fileVal); + basValRelease(&secVal); + basValRelease(&keyVal); + basValRelease(&defVal); + break; + } + + case OP_INI_WRITE: { + // Stack: file, section, key, value + BasValueT valVal, keyVal, secVal, fileVal; + if (!pop(vm, &valVal) || !pop(vm, &keyVal) || !pop(vm, &secVal) || !pop(vm, &fileVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + BasStringT *fileStr = basValFormatString(fileVal); + BasStringT *secStr = basValFormatString(secVal); + BasStringT *keyStr = basValFormatString(keyVal); + BasStringT *valStr = basValFormatString(valVal); + + // Read existing file into memory + char *content = NULL; + int32_t contentLen = 0; + FILE *fp = fopen(fileStr->data, "r"); + if (fp) { + fseek(fp, 0, SEEK_END); + contentLen = (int32_t)ftell(fp); + fseek(fp, 0, SEEK_SET); + content = (char *)malloc(contentLen + 1); + if (content) { + contentLen = (int32_t)fread(content, 1, contentLen, fp); + content[contentLen] = '\0'; + } + fclose(fp); + } + + // Write updated file + fp = fopen(fileStr->data, "w"); + if (fp) { + bool keyWritten = false; + bool inSection = false; + bool sectionFound = false; + + if (content) { + char *line = content; + while (line && *line) { + char *eol = strchr(line, '\n'); + char lineBuf[512]; + int32_t ll; + if (eol) { + ll = (int32_t)(eol - line); + if (ll > 0 && line[ll - 1] == '\r') { ll--; } + if (ll >= (int32_t)sizeof(lineBuf)) { ll = (int32_t)sizeof(lineBuf) - 1; } + memcpy(lineBuf, line, ll); + lineBuf[ll] = '\0'; + line = eol + 1; + } else { + snprintf(lineBuf, sizeof(lineBuf), "%s", line); + ll = (int32_t)strlen(lineBuf); + line = NULL; + } + + char *p = lineBuf; + while (*p == ' ' || *p == '\t') { p++; } + + if (*p == '[') { + // Write pending key before leaving section + if (inSection && !keyWritten) { + fprintf(fp, "%s = %s\n", keyStr->data, valStr->data); + keyWritten = true; + } + char *rb = strchr(p, ']'); + if (rb) { + char secName[256]; + int32_t sn = (int32_t)(rb - p - 1); + if (sn >= (int32_t)sizeof(secName)) { sn = (int32_t)sizeof(secName) - 1; } + memcpy(secName, p + 1, sn); + secName[sn] = '\0'; + inSection = (strcasecmp(secName, secStr->data) == 0); + if (inSection) { sectionFound = true; } + } + fprintf(fp, "%s\n", lineBuf); + continue; + } + + if (inSection && !keyWritten) { + char *eq = strchr(p, '='); + if (eq) { + char k[256]; + int32_t kl = (int32_t)(eq - p); + while (kl > 0 && (p[kl - 1] == ' ' || p[kl - 1] == '\t')) { kl--; } + if (kl >= (int32_t)sizeof(k)) { kl = (int32_t)sizeof(k) - 1; } + memcpy(k, p, kl); + k[kl] = '\0'; + if (strcasecmp(k, keyStr->data) == 0) { + fprintf(fp, "%s = %s\n", keyStr->data, valStr->data); + keyWritten = true; + continue; + } + } + } + + fprintf(fp, "%s\n", lineBuf); + } + } + + // Key not found in existing section — append + if (!keyWritten) { + if (!sectionFound) { + fprintf(fp, "[%s]\n", secStr->data); + } else if (inSection) { + // Still in the right section at EOF — just append + } + fprintf(fp, "%s = %s\n", keyStr->data, valStr->data); + } + + fclose(fp); + } + + free(content); + basStringUnref(fileStr); + basStringUnref(secStr); + basStringUnref(keyStr); + basStringUnref(valStr); + basValRelease(&fileVal); + basValRelease(&secVal); + basValRelease(&keyVal); + basValRelease(&valVal); + break; + } + default: runtimeError(vm, 51, "Bad opcode"); return BAS_VM_BAD_OPCODE; diff --git a/apps/dvxbasic/runtime/vm.h b/apps/dvxbasic/runtime/vm.h index d9991bb..e044360 100644 --- a/apps/dvxbasic/runtime/vm.h +++ b/apps/dvxbasic/runtime/vm.h @@ -15,6 +15,7 @@ #define DVXBASIC_VM_H #include "values.h" +#include "dvxTypes.h" #include #include @@ -347,6 +348,11 @@ typedef struct { void *currentForm; BasValueT *currentFormVars; // points to current form's variable storage int32_t currentFormVarCount; // number of form variables + + // App object paths (set by host) + char appPath[DVX_MAX_PATH]; // App.Path -- project/app directory + char appConfig[DVX_MAX_PATH]; // App.Config -- writable config directory + char appData[DVX_MAX_PATH]; // App.Data -- writable data directory } BasVmT; // ============================================================ diff --git a/core/dvxWidget.h b/core/dvxWidget.h index d309a5f..b78781d 100644 --- a/core/dvxWidget.h +++ b/core/dvxWidget.h @@ -270,6 +270,7 @@ typedef struct WidgetT { void (*onMouseUp)(struct WidgetT *w, int32_t button, int32_t x, int32_t y); void (*onMouseMove)(struct WidgetT *w, int32_t button, int32_t x, int32_t y); void (*onScroll)(struct WidgetT *w, int32_t delta); + bool (*onValidate)(struct WidgetT *w); // return false to cancel write } WidgetT; diff --git a/sql/dvxSql.c b/sql/dvxSql.c index 2af4674..08e9ed9 100644 --- a/sql/dvxSql.c +++ b/sql/dvxSql.c @@ -376,3 +376,36 @@ void dvxSqlFreeResult(int32_t rs) { sqlite3_finalize(cur->stmt); memset(cur, 0, sizeof(CursorEntryT)); } + + +// ============================================================ +// dvxSqlEscape +// ============================================================ + +int32_t dvxSqlEscape(const char *src, char *dst, int32_t dstSize) { + if (!src || !dst || dstSize <= 0) { + return -1; + } + + int32_t di = 0; + + for (int32_t si = 0; src[si]; si++) { + if (src[si] == '\'') { + if (di + 2 >= dstSize) { + return -1; + } + + dst[di++] = '\''; + dst[di++] = '\''; + } else { + if (di + 1 >= dstSize) { + return -1; + } + + dst[di++] = src[si]; + } + } + + dst[di] = '\0'; + return di; +} diff --git a/sql/dvxSql.h b/sql/dvxSql.h index ee18d8e..c03c4ef 100644 --- a/sql/dvxSql.h +++ b/sql/dvxSql.h @@ -67,4 +67,14 @@ double dvxSqlFieldDbl(int32_t rs, int32_t col); // Close a result set cursor and free its resources. void dvxSqlFreeResult(int32_t rs); +// ============================================================ +// Utility +// ============================================================ + +// Escape a string for safe use in SQL string literals. Doubles single +// quotes so that "O'Brien" becomes "O''Brien". Writes the result to +// dst (up to dstSize-1 chars + null terminator). Returns the length +// of the escaped string, or -1 if the buffer was too small. +int32_t dvxSqlEscape(const char *src, char *dst, int32_t dstSize); + #endif // DVX_SQL_H diff --git a/tools/mkwgticon.c b/tools/mkwgticon.c index 8eaae60..d482ce7 100644 --- a/tools/mkwgticon.c +++ b/tools/mkwgticon.c @@ -548,6 +548,24 @@ static void drawDatactrl(void) { } +static void drawDbgrid(void) { + clear(192, 192, 192); + // Grid outline + box(1, 1, 22, 22, 128, 128, 128); + // Header row (darker) + rect(2, 2, 20, 5, 160, 160, 200); + hline(2, 7, 20, 128, 128, 128); + // Column dividers + vline(8, 2, 20, 128, 128, 128); + vline(15, 2, 20, 128, 128, 128); + // Data rows + rect(2, 8, 20, 14, 255, 255, 255); + hline(2, 14, 20, 200, 200, 200); + // Selected row highlight + rect(2, 15, 20, 7, 0, 0, 128); +} + + int main(int argc, char **argv) { if (argc != 3) { fprintf(stderr, "Usage: mkwgticon \n"); @@ -589,6 +607,7 @@ int main(int argc, char **argv) { {"terminal", drawTerminal}, {"wrapbox", drawWrapbox}, {"datactrl", drawDatactrl}, + {"dbgrid", drawDbgrid}, {NULL, NULL} }; diff --git a/widgets/Makefile b/widgets/Makefile index f9d3b55..09be7f0 100644 --- a/widgets/Makefile +++ b/widgets/Makefile @@ -40,6 +40,7 @@ WIDGETS = \ textinpt:textInput:widgetTextInput:textinpt \ timer:timer:widgetTimer:timer \ datactrl:dataCtrl:widgetDataCtrl:datactr \ + dbgrid:dbGrid:widgetDbGrid:dbgrid \ toolbar:toolbar:widgetToolbar:toolbar \ treeview:treeView:widgetTreeView:treeview \ wrapbox:wrapBox:widgetWrapBox:wrapbox diff --git a/widgets/dataCtrl/widgetDataCtrl.c b/widgets/dataCtrl/widgetDataCtrl.c index 62328ca..ebc051f 100644 --- a/widgets/dataCtrl/widgetDataCtrl.c +++ b/widgets/dataCtrl/widgetDataCtrl.c @@ -7,14 +7,13 @@ // Reposition events when the cursor moves so bound controls can // update. // -// The control resolves dvxSql* functions via dlsym at first use, -// keeping the widget DXE independent of the SQL library DXE at -// link time (both are loaded by the same loader, so dlsym works). +// Depends on the dvxsql library DXE (via datactrl.dep) so all +// dvxSql* functions are resolved at load time. #include "dvxWidgetPlugin.h" +#include "../../sql/dvxSql.h" #include "thirdparty/stb_ds_wrap.h" -#include #include #include #include @@ -47,6 +46,13 @@ typedef struct { char databaseName[DATA_MAX_FIELD]; char recordSource[DATA_MAX_FIELD]; char caption[DATA_MAX_FIELD]; + char keyColumn[DATA_MAX_FIELD]; + + // Master-detail linking + char masterSource[DATA_MAX_FIELD]; // name of master Data control + char masterField[DATA_MAX_FIELD]; // column in master to read + char detailField[DATA_MAX_FIELD]; // column in this table to filter + char masterValue[DATA_MAX_FIELD]; // current filter value (set by runtime) // Cached result set DataRowT *rows; // stb_ds dynamic array @@ -57,44 +63,14 @@ typedef struct { bool bof; bool eof; - - // SQL function pointers (resolved via dlsym) - int32_t (*sqlOpen)(const char *); - void (*sqlClose)(int32_t); - int32_t (*sqlQuery)(int32_t, const char *); - bool (*sqlNext)(int32_t); - bool (*sqlEof)(int32_t); - int32_t (*sqlFieldCount)(int32_t); - const char *(*sqlFieldName)(int32_t, int32_t); - const char *(*sqlFieldText)(int32_t, int32_t); - void (*sqlFreeResult)(int32_t); - - bool sqlResolved; + bool dirty; // current row has unsaved changes + bool isNewRow; // current row was created by AddNew } DataCtrlDataT; static int32_t sTypeId = -1; - -// ============================================================ -// resolveSql -- lazily resolve dvxSql* function pointers -// ============================================================ - -static void resolveSql(DataCtrlDataT *d) { - if (d->sqlResolved) { - return; - } - - d->sqlOpen = (int32_t (*)(const char *))dlsym(NULL, "_dvxSqlOpen"); - d->sqlClose = (void (*)(int32_t))dlsym(NULL, "_dvxSqlClose"); - d->sqlQuery = (int32_t (*)(int32_t, const char *))dlsym(NULL, "_dvxSqlQuery"); - d->sqlNext = (bool (*)(int32_t))dlsym(NULL, "_dvxSqlNext"); - d->sqlEof = (bool (*)(int32_t))dlsym(NULL, "_dvxSqlEof"); - d->sqlFieldCount = (int32_t (*)(int32_t))dlsym(NULL, "_dvxSqlFieldCount"); - d->sqlFieldName = (const char *(*)(int32_t, int32_t))dlsym(NULL, "_dvxSqlFieldName"); - d->sqlFieldText = (const char *(*)(int32_t, int32_t))dlsym(NULL, "_dvxSqlFieldText"); - d->sqlFreeResult = (void (*)(int32_t))dlsym(NULL, "_dvxSqlFreeResult"); - d->sqlResolved = true; -} +// Forward declarations for functions referenced before definition +void dataCtrlUpdate(WidgetT *w); // ============================================================ @@ -133,22 +109,22 @@ static void freeCache(DataCtrlDataT *d) { void dataCtrlRefresh(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; - resolveSql(d); + freeCache(d); - if (!d->sqlOpen || !d->databaseName[0] || !d->recordSource[0]) { + if (!d->databaseName[0] || !d->recordSource[0]) { return; } - int32_t db = d->sqlOpen(d->databaseName); + int32_t db = dvxSqlOpen(d->databaseName); if (db <= 0) { return; } - // If recordSource doesn't start with SELECT, wrap it as "SELECT * FROM table" - char query[512]; + // Build query from RecordSource, with optional master-detail WHERE clause + char query[1024]; if (strncasecmp(d->recordSource, "SELECT ", 7) == 0) { snprintf(query, sizeof(query), "%s", d->recordSource); @@ -156,29 +132,38 @@ void dataCtrlRefresh(WidgetT *w) { snprintf(query, sizeof(query), "SELECT * FROM %s", d->recordSource); } - int32_t rs = d->sqlQuery(db, query); + // Append WHERE filter for master-detail linking + if (d->detailField[0] && d->masterValue[0]) { + char escaped[DATA_MAX_FIELD * 2]; + dvxSqlEscape(d->masterValue, escaped, sizeof(escaped)); + + int32_t len = (int32_t)strlen(query); + snprintf(query + len, sizeof(query) - len, " WHERE %s='%s'", d->detailField, escaped); + } + + int32_t rs = dvxSqlQuery(db, query); if (rs <= 0) { - d->sqlClose(db); + dvxSqlClose(db); return; } // Cache column names - d->colCount = d->sqlFieldCount(rs); + d->colCount = dvxSqlFieldCount(rs); for (int32_t i = 0; i < d->colCount; i++) { - const char *name = d->sqlFieldName(rs, i); + const char *name = dvxSqlFieldName(rs, i); arrput(d->colNames, strdup(name ? name : "")); } // Cache all rows - while (d->sqlNext(rs)) { + while (dvxSqlNext(rs)) { DataRowT row; row.fieldCount = d->colCount; row.fields = (char **)malloc(d->colCount * sizeof(char *)); for (int32_t i = 0; i < d->colCount; i++) { - const char *text = d->sqlFieldText(rs, i); + const char *text = dvxSqlFieldText(rs, i); row.fields[i] = strdup(text ? text : ""); } @@ -186,8 +171,8 @@ void dataCtrlRefresh(WidgetT *w) { } d->rowCount = (int32_t)arrlen(d->rows); - d->sqlFreeResult(rs); - d->sqlClose(db); + dvxSqlFreeResult(rs); + dvxSqlClose(db); if (d->rowCount > 0) { d->currentRow = 0; @@ -217,6 +202,15 @@ static void fireReposition(WidgetT *w) { } +static void autoSave(WidgetT *w) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (d->dirty) { + dataCtrlUpdate(w); + } +} + + static void dataCtrlMoveFirst(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; @@ -224,6 +218,7 @@ static void dataCtrlMoveFirst(WidgetT *w) { return; } + autoSave(w); d->currentRow = 0; d->bof = false; d->eof = false; @@ -239,6 +234,7 @@ static void dataCtrlMovePrev(WidgetT *w) { return; } + autoSave(w); d->currentRow--; d->bof = (d->currentRow == 0); d->eof = false; @@ -254,6 +250,7 @@ static void dataCtrlMoveNext(WidgetT *w) { return; } + autoSave(w); d->currentRow++; d->bof = false; d->eof = (d->currentRow >= d->rowCount - 1); @@ -268,6 +265,7 @@ static void dataCtrlMoveLast(WidgetT *w) { return; } + autoSave(w); d->currentRow = d->rowCount - 1; d->bof = false; d->eof = false; @@ -301,6 +299,274 @@ const char *dataCtrlGetField(WidgetT *w, const char *colName) { } +// ============================================================ +// dataCtrlSetField -- update a field value in the current cached row +// ============================================================ + +void dataCtrlSetField(WidgetT *w, const char *colName, const char *value) { + if (!w || !w->data || !colName || !value) { + return; + } + + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (d->currentRow < 0 || d->currentRow >= d->rowCount) { + return; + } + + for (int32_t i = 0; i < d->colCount; i++) { + if (strcasecmp(d->colNames[i], colName) == 0) { + free(d->rows[d->currentRow].fields[i]); + d->rows[d->currentRow].fields[i] = strdup(value); + d->dirty = true; + return; + } + } +} + + +// ============================================================ +// findKeyCol -- locate the key column index +// ============================================================ + +static int32_t findKeyCol(const DataCtrlDataT *d) { + if (d->keyColumn[0]) { + for (int32_t i = 0; i < d->colCount; i++) { + if (strcasecmp(d->colNames[i], d->keyColumn) == 0) { + return i; + } + } + } + + return 0; +} + + +// ============================================================ +// canWriteBack -- check if write operations are possible +// ============================================================ + +static bool canWriteBack(const DataCtrlDataT *d) { + if (!d->databaseName[0] || !d->recordSource[0]) { + return false; + } + + // Write-back only for simple table names, not SELECT queries + if (strncasecmp(d->recordSource, "SELECT ", 7) == 0) { + return false; + } + + return true; +} + + +// ============================================================ +// dataCtrlUpdate -- save the current row to the database +// ============================================================ +// +// If the current row was created by AddNew, executes an INSERT. +// Otherwise executes an UPDATE using the KeyColumn to identify the row. +// Clears the dirty and isNewRow flags on success. + +void dataCtrlUpdate(WidgetT *w) { + if (!w || !w->data) { + return; + } + + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (!d->dirty || !canWriteBack(d)) { + return; + } + + if (d->currentRow < 0 || d->currentRow >= d->rowCount || d->colCount == 0) { + return; + } + + // Fire Validate event — return false cancels the write + if (w->onValidate && !w->onValidate(w)) { + return; + } + + int32_t db = dvxSqlOpen(d->databaseName); + + if (db <= 0) { + return; + } + + char sql[2048]; + int32_t pos = 0; + + if (d->isNewRow) { + // INSERT INTO table (col1, col2, ...) VALUES ('val1', 'val2', ...) + pos = snprintf(sql, sizeof(sql), "INSERT INTO %s (", d->recordSource); + + for (int32_t i = 0; i < d->colCount; i++) { + if (i > 0) { + pos += snprintf(sql + pos, sizeof(sql) - pos, ", "); + } + + pos += snprintf(sql + pos, sizeof(sql) - pos, "%s", d->colNames[i]); + } + + pos += snprintf(sql + pos, sizeof(sql) - pos, ") VALUES ("); + + for (int32_t i = 0; i < d->colCount; i++) { + if (i > 0) { + pos += snprintf(sql + pos, sizeof(sql) - pos, ", "); + } + + char escaped[DATA_MAX_FIELD * 2]; + dvxSqlEscape(d->rows[d->currentRow].fields[i], escaped, sizeof(escaped)); + pos += snprintf(sql + pos, sizeof(sql) - pos, "'%s'", escaped); + } + + pos += snprintf(sql + pos, sizeof(sql) - pos, ")"); + } else { + // UPDATE table SET col1='val1', ... WHERE keyCol=keyVal + int32_t keyCol = findKeyCol(d); + const char *keyVal = d->rows[d->currentRow].fields[keyCol]; + + pos = snprintf(sql, sizeof(sql), "UPDATE %s SET ", d->recordSource); + + bool first = true; + + for (int32_t i = 0; i < d->colCount; i++) { + if (i == keyCol) { + continue; + } + + if (!first) { + pos += snprintf(sql + pos, sizeof(sql) - pos, ", "); + } + + char escaped[DATA_MAX_FIELD * 2]; + dvxSqlEscape(d->rows[d->currentRow].fields[i], escaped, sizeof(escaped)); + pos += snprintf(sql + pos, sizeof(sql) - pos, "%s='%s'", d->colNames[i], escaped); + first = false; + } + + pos += snprintf(sql + pos, sizeof(sql) - pos, " WHERE %s=%s", d->colNames[keyCol], keyVal); + } + + dvxSqlExec(db, sql); + dvxSqlClose(db); + d->dirty = false; + d->isNewRow = false; +} + + +// ============================================================ +// dataCtrlUpdateRow -- legacy wrapper, calls dataCtrlUpdate +// ============================================================ + +void dataCtrlUpdateRow(WidgetT *w) { + dataCtrlUpdate(w); +} + + +// ============================================================ +// dataCtrlAddNew -- append a blank row and move cursor to it +// ============================================================ + +void dataCtrlAddNew(WidgetT *w) { + if (!w || !w->data) { + return; + } + + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + // Auto-save current row if dirty + if (d->dirty) { + dataCtrlUpdate(w); + } + + if (d->colCount == 0) { + return; + } + + // Append a blank row + DataRowT row; + row.fieldCount = d->colCount; + row.fields = (char **)malloc(d->colCount * sizeof(char *)); + + for (int32_t i = 0; i < d->colCount; i++) { + row.fields[i] = strdup(""); + } + + arrput(d->rows, row); + d->rowCount = (int32_t)arrlen(d->rows); + d->currentRow = d->rowCount - 1; + d->bof = false; + d->eof = false; + d->dirty = true; + d->isNewRow = true; + + fireReposition(w); +} + + +// ============================================================ +// dataCtrlDelete -- delete the current row from DB and cache +// ============================================================ + +void dataCtrlDelete(WidgetT *w) { + if (!w || !w->data) { + return; + } + + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (d->currentRow < 0 || d->currentRow >= d->rowCount || d->colCount == 0) { + return; + } + + // Delete from database (unless it's an unsaved new row) + if (!d->isNewRow && canWriteBack(d)) { + int32_t db = dvxSqlOpen(d->databaseName); + + if (db > 0) { + int32_t keyCol = findKeyCol(d); + const char *keyVal = d->rows[d->currentRow].fields[keyCol]; + + char sql[512]; + snprintf(sql, sizeof(sql), "DELETE FROM %s WHERE %s=%s", + d->recordSource, d->colNames[keyCol], keyVal); + dvxSqlExec(db, sql); + dvxSqlClose(db); + } + } + + // Free the cached row + for (int32_t j = 0; j < d->rows[d->currentRow].fieldCount; j++) { + free(d->rows[d->currentRow].fields[j]); + } + + free(d->rows[d->currentRow].fields); + arrdel(d->rows, d->currentRow); + d->rowCount = (int32_t)arrlen(d->rows); + + // Adjust cursor + if (d->rowCount == 0) { + d->currentRow = -1; + d->bof = true; + d->eof = true; + } else { + if (d->currentRow >= d->rowCount) { + d->currentRow = d->rowCount - 1; + } + + d->bof = (d->currentRow == 0); + d->eof = (d->currentRow >= d->rowCount - 1); + } + + d->dirty = false; + d->isNewRow = false; + + fireReposition(w); +} + + // ============================================================ // Paint // ============================================================ @@ -444,6 +710,131 @@ static void dataCtrlSetRecordSource(WidgetT *w, const char *val) { snprintf(d->recordSource, sizeof(d->recordSource), "%s", val ? val : ""); } +static const char *dataCtrlGetKeyColumn(const WidgetT *w) { + return ((DataCtrlDataT *)w->data)->keyColumn; +} + +static void dataCtrlSetKeyColumn(WidgetT *w, const char *val) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + snprintf(d->keyColumn, sizeof(d->keyColumn), "%s", val ? val : ""); +} + +static const char *dataCtrlGetMasterSource(const WidgetT *w) { + return ((DataCtrlDataT *)w->data)->masterSource; +} + +static void dataCtrlSetMasterSource(WidgetT *w, const char *val) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + snprintf(d->masterSource, sizeof(d->masterSource), "%s", val ? val : ""); +} + +static const char *dataCtrlGetMasterField(const WidgetT *w) { + return ((DataCtrlDataT *)w->data)->masterField; +} + +static void dataCtrlSetMasterField(WidgetT *w, const char *val) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + snprintf(d->masterField, sizeof(d->masterField), "%s", val ? val : ""); +} + +// ============================================================ +// Row-level accessors for DBGrid and other consumers +// ============================================================ + +int32_t dataCtrlGetRowCount(WidgetT *w) { + if (!w || !w->data) { + return 0; + } + + return ((DataCtrlDataT *)w->data)->rowCount; +} + + +int32_t dataCtrlGetColCount(WidgetT *w) { + if (!w || !w->data) { + return 0; + } + + return ((DataCtrlDataT *)w->data)->colCount; +} + + +const char *dataCtrlGetColName(WidgetT *w, int32_t col) { + if (!w || !w->data) { + return ""; + } + + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (col < 0 || col >= d->colCount) { + return ""; + } + + return d->colNames[col]; +} + + +const char *dataCtrlGetCellText(WidgetT *w, int32_t row, int32_t col) { + if (!w || !w->data) { + return ""; + } + + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (row < 0 || row >= d->rowCount || col < 0 || col >= d->colCount) { + return ""; + } + + return d->rows[row].fields[col]; +} + + +void dataCtrlSetCurrentRow(WidgetT *w, int32_t row) { + if (!w || !w->data) { + return; + } + + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (row < 0 || row >= d->rowCount) { + return; + } + + // Auto-save before moving + if (d->dirty) { + dataCtrlUpdate(w); + } + + d->currentRow = row; + d->bof = (row == 0); + d->eof = (row >= d->rowCount - 1); + + wgtInvalidatePaint(w); + + if (w->onChange) { + w->onChange(w); + } +} + + +void dataCtrlSetMasterValue(WidgetT *w, const char *val) { + if (!w || !w->data) { + return; + } + + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + snprintf(d->masterValue, sizeof(d->masterValue), "%s", val ? val : ""); +} + +static const char *dataCtrlGetDetailField(const WidgetT *w) { + return ((DataCtrlDataT *)w->data)->detailField; +} + +static void dataCtrlSetDetailField(WidgetT *w, const char *val) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + snprintf(d->detailField, sizeof(d->detailField), "%s", val ? val : ""); +} + static const char *dataCtrlGetCaption(const WidgetT *w) { return ((DataCtrlDataT *)w->data)->caption; } @@ -525,6 +916,17 @@ static const struct { void (*moveNext)(WidgetT *w); void (*moveLast)(WidgetT *w); const char *(*getField)(WidgetT *w, const char *colName); + void (*setField)(WidgetT *w, const char *colName, const char *value); + void (*updateRow)(WidgetT *w); + void (*update)(WidgetT *w); + void (*addNew)(WidgetT *w); + void (*delete)(WidgetT *w); + void (*setMasterValue)(WidgetT *w, const char *val); + int32_t (*getRowCount)(WidgetT *w); + int32_t (*getColCount)(WidgetT *w); + const char *(*getColName)(WidgetT *w, int32_t col); + const char *(*getCellText)(WidgetT *w, int32_t row, int32_t col); + void (*setCurrentRow)(WidgetT *w, int32_t row); } sApi = { .create = dataCtrlCreate, .refresh = dataCtrlRefresh, @@ -533,36 +935,55 @@ static const struct { .moveNext = dataCtrlMoveNext, .moveLast = dataCtrlMoveLast, .getField = dataCtrlGetField, + .setField = dataCtrlSetField, + .updateRow = dataCtrlUpdateRow, + .update = dataCtrlUpdate, + .addNew = dataCtrlAddNew, + .delete = dataCtrlDelete, + .setMasterValue = dataCtrlSetMasterValue, + .getRowCount = dataCtrlGetRowCount, + .getColCount = dataCtrlGetColCount, + .getColName = dataCtrlGetColName, + .getCellText = dataCtrlGetCellText, + .setCurrentRow = dataCtrlSetCurrentRow, }; static const WgtPropDescT sProps[] = { - { "DatabaseName", WGT_IFACE_STRING, (void *)dataCtrlGetDatabaseName, (void *)dataCtrlSetDatabaseName, NULL }, - { "RecordSource", WGT_IFACE_STRING, (void *)dataCtrlGetRecordSource, (void *)dataCtrlSetRecordSource, NULL }, - { "Caption", WGT_IFACE_STRING, (void *)dataCtrlGetCaption, (void *)dataCtrlSetCaption, NULL }, - { "BOF", WGT_IFACE_BOOL, (void *)dataCtrlGetBof, NULL, NULL }, - { "EOF", WGT_IFACE_BOOL, (void *)dataCtrlGetEof, NULL, NULL }, + { "DatabaseName", WGT_IFACE_STRING, (void *)dataCtrlGetDatabaseName, (void *)dataCtrlSetDatabaseName, NULL }, + { "RecordSource", WGT_IFACE_STRING, (void *)dataCtrlGetRecordSource, (void *)dataCtrlSetRecordSource, NULL }, + { "KeyColumn", WGT_IFACE_STRING, (void *)dataCtrlGetKeyColumn, (void *)dataCtrlSetKeyColumn, NULL }, + { "MasterSource", WGT_IFACE_STRING, (void *)dataCtrlGetMasterSource, (void *)dataCtrlSetMasterSource, NULL }, + { "MasterField", WGT_IFACE_STRING, (void *)dataCtrlGetMasterField, (void *)dataCtrlSetMasterField, NULL }, + { "DetailField", WGT_IFACE_STRING, (void *)dataCtrlGetDetailField, (void *)dataCtrlSetDetailField, NULL }, + { "Caption", WGT_IFACE_STRING, (void *)dataCtrlGetCaption, (void *)dataCtrlSetCaption, NULL }, + { "BOF", WGT_IFACE_BOOL, (void *)dataCtrlGetBof, NULL, NULL }, + { "EOF", WGT_IFACE_BOOL, (void *)dataCtrlGetEof, NULL, NULL }, }; static const WgtMethodDescT sMethods[] = { - { "Refresh", WGT_SIG_VOID, (void *)dataCtrlRefresh }, + { "AddNew", WGT_SIG_VOID, (void *)dataCtrlAddNew }, + { "Delete", WGT_SIG_VOID, (void *)dataCtrlDelete }, { "MoveFirst", WGT_SIG_VOID, (void *)dataCtrlMoveFirst }, - { "MovePrevious", WGT_SIG_VOID, (void *)dataCtrlMovePrev }, - { "MoveNext", WGT_SIG_VOID, (void *)dataCtrlMoveNext }, { "MoveLast", WGT_SIG_VOID, (void *)dataCtrlMoveLast }, + { "MoveNext", WGT_SIG_VOID, (void *)dataCtrlMoveNext }, + { "MovePrevious", WGT_SIG_VOID, (void *)dataCtrlMovePrev }, + { "Refresh", WGT_SIG_VOID, (void *)dataCtrlRefresh }, + { "Update", WGT_SIG_VOID, (void *)dataCtrlUpdate }, }; static const WgtEventDescT sEvents[] = { { "Reposition" }, + { "Validate" }, }; static const WgtIfaceT sIface = { .basName = "Data", .props = sProps, - .propCount = 5, + .propCount = 9, .methods = sMethods, - .methodCount = 5, + .methodCount = 8, .events = sEvents, - .eventCount = 1, + .eventCount = 2, .createSig = WGT_CREATE_PARENT, .isContainer = false, .defaultEvent = "Reposition", diff --git a/widgets/dbGrid/dbgrid.bmp b/widgets/dbGrid/dbgrid.bmp new file mode 100644 index 0000000..6ac6b56 --- /dev/null +++ b/widgets/dbGrid/dbgrid.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a081308e89f4ddd799665a15ab6559fb041ea62f66278a6cab6453f5ad69e9d +size 1782 diff --git a/widgets/dbGrid/dbgrid.res b/widgets/dbGrid/dbgrid.res new file mode 100644 index 0000000..31f80f1 --- /dev/null +++ b/widgets/dbGrid/dbgrid.res @@ -0,0 +1,5 @@ +icon24 icon dbgrid.bmp +name text "DBGrid" +author text "Scott Duensing" +description text "Database grid for tabular records" +version text "1.0" diff --git a/widgets/dbGrid/widgetDbGrid.c b/widgets/dbGrid/widgetDbGrid.c new file mode 100644 index 0000000..12b5c3b --- /dev/null +++ b/widgets/dbGrid/widgetDbGrid.c @@ -0,0 +1,1221 @@ +#define DVX_WIDGET_IMPL +// widgetDbGrid.c -- Database grid widget for tabular record display +// +// A read-only grid that displays all records from a Data control in a +// scrollable, sortable table. Columns auto-populate from the Data +// control's column names and can be hidden, resized, and reordered +// by clicking headers. Selecting a row syncs the Data control's +// cursor position. +// +// The grid reads directly from the Data control's cached rows via +// the row-level accessor API (getRowCount, getCellText, etc.) so +// there is no separate copy of the data. The grid is refreshed by +// calling dbGridRefresh() which re-reads the Data control's state. +// +// Depends on the datactrl widget DXE for the DataCtrlApiT. + +#include "dvxWidgetPlugin.h" +#include "../widgetDataCtrl.h" + +#include +#include +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define DBGRID_BORDER 2 +#define DBGRID_MAX_COLS 32 +#define DBGRID_MIN_COL_W 20 +#define DBGRID_COL_PAD 6 +#define DBGRID_PAD 3 +#define DBGRID_SORT_W 10 +#define DBGRID_MIN_ROWS 3 +#define DBGRID_MAX_NAME 64 +#define DBGRID_RESIZE_ZONE 3 + +// ============================================================ +// Sort direction (matches ListView for consistency) +// ============================================================ + +#define SORT_NONE 0 +#define SORT_ASC 1 +#define SORT_DESC 2 + +// ============================================================ +// Column definition +// ============================================================ + +typedef struct { + char fieldName[DBGRID_MAX_NAME]; + char header[DBGRID_MAX_NAME]; + int32_t width; // tagged size (0 = auto) + int32_t align; // 0=left, 1=center, 2=right + bool visible; + int32_t dataCol; // index into Data control's columns (-1 = unresolved) +} DbGridColT; + +// ============================================================ +// Per-instance data +// ============================================================ + +typedef struct { + WidgetT *dataWidget; // the Data control (set by form runtime) + + // Column configuration + DbGridColT columns[DBGRID_MAX_COLS]; + int32_t colCount; + + // Display state + int32_t selectedRow; // data-row index (-1 = none) + int32_t scrollPos; // vertical scroll (row units) + int32_t scrollPosH; // horizontal scroll (pixel units) + int32_t resolvedW[DBGRID_MAX_COLS]; + int32_t totalColW; // 0 = needs recalc + bool gridLines; + + // Sort state + int32_t sortCol; // visible-column index (-1 = none) + int32_t sortDir; // SORT_NONE/ASC/DESC + int32_t *sortIndex; // display-row -> data-row mapping (NULL = natural) + int32_t sortCount; // length of sortIndex + + // Column resize drag state + int32_t resizeCol; + int32_t resizeStartX; + int32_t resizeOrigW; + bool resizeDragging; + + // Scrollbar drag state + int32_t sbDragOrient; // 0=vert, 1=horiz, -1=col resize + int32_t sbDragOff; +} DbGridDataT; + +static int32_t sTypeId = -1; + + +// ============================================================ +// Prototypes +// ============================================================ + +static int32_t colBorderHit(WidgetT *w, int32_t vx, int32_t vy); +static void dbGridBuildSortIndex(WidgetT *w); +static void dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font); +static void dbGridDestroy(WidgetT *w); +static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my); +static void dbGridOnDragEnd(WidgetT *w); +static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); +static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod); +static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); +static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static int32_t getDataRowCount(const DbGridDataT *d); +static void resolveColumns(WidgetT *w, const BitmapFontT *font); + + +// ============================================================ +// Helpers +// ============================================================ + +static int32_t getDataRowCount(const DbGridDataT *d) { + if (!d->dataWidget) { + return 0; + } + + return wgtDataCtrlGetRowCount(d->dataWidget); +} + + +// Map a data-row to display-row (linear search in sortIndex) +static int32_t dataRowToDisplay(const DbGridDataT *d, int32_t dataRow) { + if (!d->sortIndex) { + return dataRow; + } + + for (int32_t i = 0; i < d->sortCount; i++) { + if (d->sortIndex[i] == dataRow) { + return i; + } + } + + return dataRow; +} + + +// Map a display-row to data-row +static int32_t displayToDataRow(const DbGridDataT *d, int32_t dispRow) { + if (!d->sortIndex || dispRow < 0 || dispRow >= d->sortCount) { + return dispRow; + } + + return d->sortIndex[dispRow]; +} + + +// Get cell text for a display-row and visible-column index +static const char *getCellText(const DbGridDataT *d, int32_t dispRow, int32_t visCol) { + if (!d->dataWidget || visCol < 0 || visCol >= d->colCount || !d->columns[visCol].visible) { + return ""; + } + + int32_t dataRow = displayToDataRow(d, dispRow); + int32_t dataCol = d->columns[visCol].dataCol; + + if (dataCol < 0) { + return ""; + } + + return wgtDataCtrlGetCellText(d->dataWidget, dataRow, dataCol); +} + + +// ============================================================ +// resolveColumns -- compute pixel widths for each column +// ============================================================ + +static void resolveColumns(WidgetT *w, const BitmapFontT *font) { + DbGridDataT *d = (DbGridDataT *)w->data; + + if (d->totalColW > 0) { + return; + } + + int32_t rowCount = getDataRowCount(d); + + d->totalColW = 0; + + for (int32_t c = 0; c < d->colCount; c++) { + if (!d->columns[c].visible) { + d->resolvedW[c] = 0; + continue; + } + + if (d->columns[c].width > 0) { + d->resolvedW[c] = wgtResolveSize(d->columns[c].width, 0, font->charWidth); + } else { + // Auto-size: find widest content + int32_t maxW = textWidth(font, d->columns[c].header) + DBGRID_COL_PAD; + + for (int32_t r = 0; r < rowCount; r++) { + int32_t dataCol = d->columns[c].dataCol; + + if (dataCol >= 0) { + const char *text = wgtDataCtrlGetCellText(d->dataWidget, r, dataCol); + int32_t tw = textWidth(font, text) + DBGRID_COL_PAD; + + if (tw > maxW) { + maxW = tw; + } + } + } + + // Add space for sort indicator on header + maxW += DBGRID_SORT_W; + d->resolvedW[c] = maxW; + } + + if (d->resolvedW[c] < DBGRID_MIN_COL_W) { + d->resolvedW[c] = DBGRID_MIN_COL_W; + } + + d->totalColW += d->resolvedW[c]; + } +} + + +// ============================================================ +// dbGridBuildSortIndex -- sort rows by the current sort column +// ============================================================ + +static void dbGridBuildSortIndex(WidgetT *w) { + DbGridDataT *d = (DbGridDataT *)w->data; + + free(d->sortIndex); + d->sortIndex = NULL; + d->sortCount = 0; + + if (d->sortDir == SORT_NONE || d->sortCol < 0 || !d->dataWidget) { + return; + } + + int32_t rowCount = getDataRowCount(d); + + if (rowCount <= 0) { + return; + } + + int32_t dataCol = d->columns[d->sortCol].dataCol; + + if (dataCol < 0) { + return; + } + + d->sortIndex = (int32_t *)malloc(rowCount * sizeof(int32_t)); + d->sortCount = rowCount; + + for (int32_t i = 0; i < rowCount; i++) { + d->sortIndex[i] = i; + } + + // Insertion sort (stable, good for small N) + for (int32_t i = 1; i < rowCount; i++) { + int32_t key = d->sortIndex[i]; + const char *keyStr = wgtDataCtrlGetCellText(d->dataWidget, key, dataCol); + int32_t j = i - 1; + + while (j >= 0) { + int32_t cmpRow = d->sortIndex[j]; + const char *cmpStr = wgtDataCtrlGetCellText(d->dataWidget, cmpRow, dataCol); + int32_t cmp = strcasecmp(keyStr, cmpStr); + + if (d->sortDir == SORT_DESC) { + cmp = -cmp; + } + + if (cmp >= 0) { + break; + } + + d->sortIndex[j + 1] = d->sortIndex[j]; + j--; + } + + d->sortIndex[j + 1] = key; + } +} + + +// ============================================================ +// Scrollbar geometry helpers (matches ListView pattern) +// ============================================================ + +static void computeLayout(WidgetT *w, const BitmapFontT *font, int32_t *outInnerW, int32_t *outInnerH, int32_t *outHeaderH, int32_t *outVisRows, bool *outNeedV, bool *outNeedH) { + DbGridDataT *d = (DbGridDataT *)w->data; + + int32_t headerH = font->charHeight + 4; + int32_t innerW = w->w - DBGRID_BORDER * 2; + int32_t innerH = w->h - DBGRID_BORDER * 2 - headerH; + int32_t rowH = font->charHeight + 2; + int32_t rowCount = getDataRowCount(d); + int32_t visRows = innerH / rowH; + + resolveColumns(w, font); + + bool needV = (rowCount > visRows); + + if (needV) { + innerW -= WGT_SB_W; + } + + bool needH = (d->totalColW > innerW); + + if (needH) { + innerH -= WGT_SB_W; + visRows = innerH / rowH; + + if (!needV && rowCount > visRows) { + needV = true; + innerW -= WGT_SB_W; + } + } + + if (visRows < 0) { + visRows = 0; + } + + *outInnerW = innerW; + *outInnerH = innerH; + *outHeaderH = headerH; + *outVisRows = visRows; + *outNeedV = needV; + *outNeedH = needH; +} + + +// ============================================================ +// colBorderHit -- check if mouse X is on a column border +// ============================================================ + +static int32_t colBorderHit(WidgetT *w, int32_t vx, int32_t vy) { + DbGridDataT *d = (DbGridDataT *)w->data; + (void)vy; + + int32_t x = w->x + DBGRID_BORDER - d->scrollPosH; + + for (int32_t c = 0; c < d->colCount; c++) { + if (!d->columns[c].visible) { + continue; + } + + x += d->resolvedW[c]; + + if (vx >= x - DBGRID_RESIZE_ZONE && vx <= x + DBGRID_RESIZE_ZONE) { + return c; + } + } + + return -1; +} + + +// ============================================================ +// dbGridCalcMinSize +// ============================================================ + +static void dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font) { + w->calcMinW = font->charWidth * 20 + DBGRID_BORDER * 2; + w->calcMinH = (font->charHeight + 2) * (DBGRID_MIN_ROWS + 1) + DBGRID_BORDER * 2; +} + + +// ============================================================ +// dbGridDestroy +// ============================================================ + +static void dbGridDestroy(WidgetT *w) { + DbGridDataT *d = (DbGridDataT *)w->data; + + if (d) { + free(d->sortIndex); + free(d); + } +} + + +// ============================================================ +// dbGridGetCursorShape +// ============================================================ + +static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my) { + AppContextT *ctx = wgtGetContext(w); + + if (!ctx) { + return 0; + } + + int32_t headerH = ctx->font.charHeight + 4; + int32_t headerTop = w->y + DBGRID_BORDER; + + if (my >= headerTop && my < headerTop + headerH) { + resolveColumns(w, &ctx->font); + + if (colBorderHit(w, mx, my) >= 0) { + return CURSOR_RESIZE_H; + } + } + + return 0; +} + + +// ============================================================ +// dbGridOnKey +// ============================================================ + +static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + DbGridDataT *d = (DbGridDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + + if (!ctx) { + return; + } + + int32_t rowCount = getDataRowCount(d); + + if (rowCount <= 0) { + return; + } + + int32_t innerW, innerH, headerH, visRows; + bool needV, needH; + computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH); + + int32_t dispRow = dataRowToDisplay(d, d->selectedRow); + int32_t newDisp = dispRow; + + // Up arrow + if (key == (0x48 | 0x100)) { + newDisp = dispRow > 0 ? dispRow - 1 : 0; + } + + // Down arrow + if (key == (0x50 | 0x100)) { + newDisp = dispRow < rowCount - 1 ? dispRow + 1 : rowCount - 1; + } + + // Page Up + if (key == (0x49 | 0x100)) { + newDisp = dispRow - visRows; + + if (newDisp < 0) { + newDisp = 0; + } + } + + // Page Down + if (key == (0x51 | 0x100)) { + newDisp = dispRow + visRows; + + if (newDisp >= rowCount) { + newDisp = rowCount - 1; + } + } + + // Home + if (key == (0x47 | 0x100)) { + newDisp = 0; + } + + // End + if (key == (0x4F | 0x100)) { + newDisp = rowCount - 1; + } + + // Enter = activate (fire DblClick) + if (key == '\r' || key == '\n') { + if (w->onDblClick) { + w->onDblClick(w); + } + + return; + } + + if (newDisp != dispRow) { + int32_t newDataRow = displayToDataRow(d, newDisp); + d->selectedRow = newDataRow; + + // Auto-scroll + if (newDisp < d->scrollPos) { + d->scrollPos = newDisp; + } else if (newDisp >= d->scrollPos + visRows) { + d->scrollPos = newDisp - visRows + 1; + } + + // Sync Data control cursor + if (d->dataWidget) { + wgtDataCtrlSetCurrentRow(d->dataWidget, newDataRow); + } + + if (w->onClick) { + w->onClick(w); + } + + wgtInvalidatePaint(w); + } +} + + +// ============================================================ +// dbGridOnMouse +// ============================================================ + +static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + DbGridDataT *d = (DbGridDataT *)w->data; + AppContextT *ctx = (AppContextT *)root->userData; + + if (!ctx) { + return; + } + + sFocusedWidget = w; + + int32_t innerW, innerH, headerH, visRows; + bool needV, needH; + computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH); + + int32_t rowH = ctx->font.charHeight + 2; + int32_t headerTop = w->y + DBGRID_BORDER; + int32_t dataTop = headerTop + headerH; + int32_t rowCount = getDataRowCount(d); + + // Vertical scrollbar click + if (needV && vx >= w->x + w->w - DBGRID_BORDER - WGT_SB_W) { + int32_t sbY = dataTop; + int32_t sbH = innerH; + int32_t maxScroll = rowCount - visRows; + + if (maxScroll < 0) { + maxScroll = 0; + } + + int32_t relY = vy - sbY; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(sbH, WGT_SB_W, maxScroll, d->scrollPos, &thumbPos, &thumbSize); + + if (relY < WGT_SB_W) { + // Up arrow + if (d->scrollPos > 0) { + d->scrollPos--; + } + } else if (relY >= sbH - WGT_SB_W) { + // Down arrow + if (d->scrollPos < maxScroll) { + d->scrollPos++; + } + } else if (relY >= thumbPos && relY < thumbPos + thumbSize) { + // Thumb drag + d->sbDragOrient = 0; + d->sbDragOff = relY - thumbPos; + } else if (relY < thumbPos) { + d->scrollPos -= visRows; + + if (d->scrollPos < 0) { + d->scrollPos = 0; + } + } else { + d->scrollPos += visRows; + + if (d->scrollPos > maxScroll) { + d->scrollPos = maxScroll; + } + } + + wgtInvalidatePaint(w); + return; + } + + // Horizontal scrollbar click + if (needH && vy >= w->y + w->h - DBGRID_BORDER - WGT_SB_W) { + int32_t sbX = w->x + DBGRID_BORDER; + int32_t sbW = innerW; + int32_t maxScroll = d->totalColW - innerW; + + if (maxScroll < 0) { + maxScroll = 0; + } + + int32_t relX = vx - sbX; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(sbW, WGT_SB_W, maxScroll, d->scrollPosH, &thumbPos, &thumbSize); + + if (relX < WGT_SB_W) { + d->scrollPosH -= 20; + + if (d->scrollPosH < 0) { + d->scrollPosH = 0; + } + } else if (relX >= sbW - WGT_SB_W) { + d->scrollPosH += 20; + + if (d->scrollPosH > maxScroll) { + d->scrollPosH = maxScroll; + } + } else if (relX >= thumbPos && relX < thumbPos + thumbSize) { + d->sbDragOrient = 1; + d->sbDragOff = relX - thumbPos; + } + + wgtInvalidatePaint(w); + return; + } + + // Column header area + if (vy >= headerTop && vy < dataTop) { + resolveColumns(w, &ctx->font); + + // Check column border for resize + int32_t borderCol = colBorderHit(w, vx, vy); + + if (borderCol >= 0) { + d->resizeCol = borderCol; + d->resizeStartX = vx; + d->resizeOrigW = d->resolvedW[borderCol]; + d->resizeDragging = false; + d->sbDragOrient = -1; + return; + } + + // Header click = sort + int32_t x = w->x + DBGRID_BORDER - d->scrollPosH; + + for (int32_t c = 0; c < d->colCount; c++) { + if (!d->columns[c].visible) { + continue; + } + + if (vx >= x && vx < x + d->resolvedW[c]) { + if (d->sortCol == c) { + d->sortDir = (d->sortDir == SORT_ASC) ? SORT_DESC : SORT_ASC; + } else { + d->sortCol = c; + d->sortDir = SORT_ASC; + } + + dbGridBuildSortIndex(w); + wgtInvalidatePaint(w); + return; + } + + x += d->resolvedW[c]; + } + + return; + } + + // Data row click + if (vy >= dataTop && vy < dataTop + innerH) { + int32_t relY = vy - dataTop; + int32_t dispRow = d->scrollPos + relY / rowH; + + if (dispRow >= 0 && dispRow < rowCount) { + int32_t dataRow = displayToDataRow(d, dispRow); + d->selectedRow = dataRow; + + // Sync Data control cursor + if (d->dataWidget) { + wgtDataCtrlSetCurrentRow(d->dataWidget, dataRow); + } + + if (w->onClick) { + w->onClick(w); + } + + wgtInvalidatePaint(w); + } + } +} + + +// ============================================================ +// dbGridOnDragUpdate +// ============================================================ + +static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { + DbGridDataT *d = (DbGridDataT *)w->data; + (void)root; + (void)y; + + // Column resize drag + if (d->sbDragOrient == -1) { + d->resizeDragging = true; + int32_t delta = x - d->resizeStartX; + int32_t newW = d->resizeOrigW + delta; + + if (newW < DBGRID_MIN_COL_W) { + newW = DBGRID_MIN_COL_W; + } + + d->totalColW += (newW - d->resolvedW[d->resizeCol]); + d->resolvedW[d->resizeCol] = newW; + wgtInvalidatePaint(w); + return; + } + + // Vertical scrollbar thumb drag + if (d->sbDragOrient == 0) { + AppContextT *ctx = wgtGetContext(w); + + if (!ctx) { + return; + } + + int32_t innerW, innerH, headerH, visRows; + bool needV, needH; + computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH); + + int32_t rowCount = getDataRowCount(d); + int32_t maxScroll = rowCount - visRows; + + if (maxScroll < 1) { + return; + } + + int32_t sbY = w->y + DBGRID_BORDER + headerH; + int32_t sbH = innerH; + int32_t relY = y - sbY - d->sbDragOff; + int32_t track = sbH - WGT_SB_W * 2; + + if (track > 0) { + d->scrollPos = (relY - WGT_SB_W) * maxScroll / track; + + if (d->scrollPos < 0) { + d->scrollPos = 0; + } + + if (d->scrollPos > maxScroll) { + d->scrollPos = maxScroll; + } + } + + wgtInvalidatePaint(w); + return; + } + + // Horizontal scrollbar thumb drag + if (d->sbDragOrient == 1) { + AppContextT *ctx = wgtGetContext(w); + + if (!ctx) { + return; + } + + int32_t innerW, innerH, headerH, visRows; + bool needV, needH; + computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH); + + int32_t maxScroll = d->totalColW - innerW; + + if (maxScroll < 1) { + return; + } + + int32_t sbX = w->x + DBGRID_BORDER; + int32_t sbW = innerW; + int32_t relX = x - sbX - d->sbDragOff; + int32_t track = sbW - WGT_SB_W * 2; + + if (track > 0) { + d->scrollPosH = (relX - WGT_SB_W) * maxScroll / track; + + if (d->scrollPosH < 0) { + d->scrollPosH = 0; + } + + if (d->scrollPosH > maxScroll) { + d->scrollPosH = maxScroll; + } + } + + wgtInvalidatePaint(w); + } +} + + +// ============================================================ +// dbGridOnDragEnd +// ============================================================ + +static void dbGridOnDragEnd(WidgetT *w) { + DbGridDataT *d = (DbGridDataT *)w->data; + d->sbDragOrient = -2; + d->resizeDragging = false; +} + + +// ============================================================ +// dbGridPaint +// ============================================================ + +static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + DbGridDataT *d = (DbGridDataT *)w->data; + + int32_t innerW, innerH, headerH, visRows; + bool needV, needH; + computeLayout(w, font, &innerW, &innerH, &headerH, &visRows, &needV, &needH); + + int32_t rowH = font->charHeight + 2; + int32_t rowCount = getDataRowCount(d); + + // Outer sunken border + BevelStyleT bevel = { colors->windowShadow, colors->windowHighlight, colors->contentBg, DBGRID_BORDER }; + drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel); + + int32_t contentX = w->x + DBGRID_BORDER; + int32_t contentY = w->y + DBGRID_BORDER; + + // Save clip rect + int32_t oldClipX = disp->clipX; + int32_t oldClipY = disp->clipY; + int32_t oldClipW = disp->clipW; + int32_t oldClipH = disp->clipH; + + // ---- Column Headers ---- + setClipRect(disp, contentX, contentY, innerW, headerH); + + int32_t hx = contentX - d->scrollPosH; + + for (int32_t c = 0; c < d->colCount; c++) { + if (!d->columns[c].visible) { + continue; + } + + int32_t cw = d->resolvedW[c]; + + if (hx + cw > contentX - 20 && hx < contentX + innerW + 20) { + BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1); + drawBevel(disp, ops, hx, contentY, cw, headerH, &hdrBevel); + + // Header text (centered) + const char *hdr = d->columns[c].header; + int32_t tw = textWidth(font, hdr); + int32_t tx = hx + (cw - tw) / 2; + int32_t ty = contentY + (headerH - font->charHeight) / 2; + drawText(disp, ops, font, tx, ty, hdr, colors->contentFg, colors->buttonFace, false); + + // Sort indicator + if (c == d->sortCol && d->sortDir != SORT_NONE) { + int32_t sx = hx + cw - DBGRID_SORT_W; + int32_t sy = contentY + headerH / 2; + + if (d->sortDir == SORT_ASC) { + for (int32_t i = 0; i < 3; i++) { + drawHLine(disp, ops, sx + 3 - i, sy - 1 + i, 1 + i * 2, colors->contentFg); + } + } else { + for (int32_t i = 0; i < 3; i++) { + drawHLine(disp, ops, sx + 3 - i, sy + 1 - i, 1 + i * 2, colors->contentFg); + } + } + } + } + + hx += cw; + } + + // Fill remaining header space + if (hx < contentX + innerW) { + BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1); + drawBevel(disp, ops, hx, contentY, contentX + innerW - hx, headerH, &hdrBevel); + } + + // ---- Data Rows ---- + int32_t dataY = contentY + headerH; + setClipRect(disp, contentX, dataY, innerW, innerH); + + // Background fill + rectFill(disp, ops, contentX, dataY, innerW, innerH, colors->contentBg); + + for (int32_t i = 0; i < visRows && d->scrollPos + i < rowCount; i++) { + int32_t dispRow = d->scrollPos + i; + int32_t dataRow = displayToDataRow(d, dispRow); + int32_t ry = dataY + i * rowH; + + // Alternating row background + if (i % 2 == 1) { + uint32_t altBg = colors->contentBg - 0x080808; + rectFill(disp, ops, contentX, ry, innerW, rowH, altBg); + } + + // Selection highlight + bool selected = (dataRow == d->selectedRow); + + if (selected) { + rectFill(disp, ops, contentX, ry, innerW, rowH, colors->menuHighlightBg); + } + + uint32_t fg = selected ? colors->menuHighlightFg : colors->contentFg; + uint32_t bg = selected ? colors->menuHighlightBg : (i % 2 == 1 ? colors->contentBg - 0x080808 : colors->contentBg); + + // Draw cells + int32_t cx = contentX - d->scrollPosH; + + for (int32_t c = 0; c < d->colCount; c++) { + if (!d->columns[c].visible) { + continue; + } + + int32_t cw = d->resolvedW[c]; + + if (cx + cw > contentX && cx < contentX + innerW) { + const char *text = getCellText(d, dispRow, c); + int32_t tw = textWidth(font, text); + int32_t tx; + + if (d->columns[c].align == 1) { + tx = cx + (cw - tw) / 2; + } else if (d->columns[c].align == 2) { + tx = cx + cw - tw - DBGRID_PAD; + } else { + tx = cx + DBGRID_PAD; + } + + int32_t ty = ry + (rowH - font->charHeight) / 2; + drawText(disp, ops, font, tx, ty, text, fg, bg, false); + } + + // Grid line + if (d->gridLines && cx + cw > contentX && cx + cw <= contentX + innerW) { + drawVLine(disp, ops, cx + cw - 1, ry, rowH, colors->windowShadow); + } + + cx += cw; + } + + // Horizontal grid line + if (d->gridLines) { + drawHLine(disp, ops, contentX, ry + rowH - 1, innerW, colors->windowShadow); + } + } + + setClipRect(disp, oldClipX, oldClipY, oldClipW, oldClipH); + + // ---- Scrollbars ---- + if (needV) { + int32_t sbX = contentX + innerW; + int32_t sbY = dataY; + widgetDrawScrollbarV(disp, ops, colors, sbX, sbY, innerH, rowCount, visRows, d->scrollPos); + } + + if (needH) { + int32_t sbX = contentX; + int32_t sbY = dataY + innerH; + widgetDrawScrollbarH(disp, ops, colors, sbX, sbY, innerW, d->totalColW, innerW, d->scrollPosH); + + if (needV) { + rectFill(disp, ops, contentX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace); + } + } + + // Focus rect + if (w == sFocusedWidget) { + drawFocusRect(disp, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg); + } +} + + +// ============================================================ +// Public API +// ============================================================ + +static WidgetT *dbGridCreate(WidgetT *parent) { + if (!parent) { + return NULL; + } + + WidgetT *w = widgetAlloc(parent, sTypeId); + + if (w) { + DbGridDataT *d = (DbGridDataT *)calloc(1, sizeof(DbGridDataT)); + + if (d) { + d->selectedRow = -1; + d->sortCol = -1; + d->sbDragOrient = -2; + d->gridLines = true; + } + + w->data = d; + } + + return w; +} + + +// Set the Data control widget that this grid reads from. +// Also auto-populates columns from the Data control's column names. +void dbGridSetDataWidget(WidgetT *w, WidgetT *dataWidget) { + if (!w || !w->data) { + return; + } + + DbGridDataT *d = (DbGridDataT *)w->data; + d->dataWidget = dataWidget; + d->totalColW = 0; + + // Auto-populate columns + if (dataWidget) { + int32_t colCount = wgtDataCtrlGetColCount(dataWidget); + + if (colCount > DBGRID_MAX_COLS) { + colCount = DBGRID_MAX_COLS; + } + + d->colCount = colCount; + + for (int32_t i = 0; i < colCount; i++) { + const char *name = wgtDataCtrlGetColName(dataWidget, i); + snprintf(d->columns[i].fieldName, DBGRID_MAX_NAME, "%s", name); + snprintf(d->columns[i].header, DBGRID_MAX_NAME, "%s", name); + d->columns[i].width = 0; + d->columns[i].align = 0; + d->columns[i].visible = true; + d->columns[i].dataCol = i; + } + } + + // Reset sort + free(d->sortIndex); + d->sortIndex = NULL; + d->sortCount = 0; + d->sortCol = -1; + d->sortDir = SORT_NONE; + + wgtInvalidate(w); +} + + +// Refresh the grid display (e.g. after Data control refresh) +void dbGridRefresh(WidgetT *w) { + if (!w || !w->data) { + return; + } + + DbGridDataT *d = (DbGridDataT *)w->data; + d->totalColW = 0; + + // Rebuild sort index if sorting is active + if (d->sortDir != SORT_NONE) { + dbGridBuildSortIndex(w); + } + + // Clamp scroll and selection + int32_t rowCount = getDataRowCount(d); + + if (d->scrollPos >= rowCount) { + d->scrollPos = rowCount > 0 ? rowCount - 1 : 0; + } + + if (d->selectedRow >= rowCount) { + d->selectedRow = rowCount > 0 ? rowCount - 1 : -1; + } + + wgtInvalidate(w); +} + + +// Hide or show a column by field name +void dbGridSetColumnVisible(WidgetT *w, const char *fieldName, bool visible) { + if (!w || !w->data || !fieldName) { + return; + } + + DbGridDataT *d = (DbGridDataT *)w->data; + + for (int32_t i = 0; i < d->colCount; i++) { + if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) { + d->columns[i].visible = visible; + d->totalColW = 0; + wgtInvalidate(w); + return; + } + } +} + + +// Set a column's header text +void dbGridSetColumnHeader(WidgetT *w, const char *fieldName, const char *header) { + if (!w || !w->data || !fieldName || !header) { + return; + } + + DbGridDataT *d = (DbGridDataT *)w->data; + + for (int32_t i = 0; i < d->colCount; i++) { + if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) { + snprintf(d->columns[i].header, DBGRID_MAX_NAME, "%s", header); + d->totalColW = 0; + wgtInvalidate(w); + return; + } + } +} + + +// Set a column's width (tagged size, 0 = auto) +void dbGridSetColumnWidth(WidgetT *w, const char *fieldName, int32_t width) { + if (!w || !w->data || !fieldName) { + return; + } + + DbGridDataT *d = (DbGridDataT *)w->data; + + for (int32_t i = 0; i < d->colCount; i++) { + if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) { + d->columns[i].width = width; + d->totalColW = 0; + wgtInvalidate(w); + return; + } + } +} + + +// Get the selected data-row index (-1 = none) +int32_t dbGridGetSelectedRow(const WidgetT *w) { + if (!w || !w->data) { + return -1; + } + + return ((DbGridDataT *)w->data)->selectedRow; +} + + +// ============================================================ +// Property getters/setters +// ============================================================ + +static bool dbGridGetGridLines(const WidgetT *w) { + return ((DbGridDataT *)w->data)->gridLines; +} + +static void dbGridSetGridLines(WidgetT *w, bool val) { + ((DbGridDataT *)w->data)->gridLines = val; + wgtInvalidatePaint(w); +} + + +// ============================================================ +// DXE registration +// ============================================================ + +static const WidgetClassT sClass = { + .version = WGT_CLASS_VERSION, + .flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE, + .handlers = { + [WGT_METHOD_PAINT] = (void *)dbGridPaint, + [WGT_METHOD_CALC_MIN_SIZE] = (void *)dbGridCalcMinSize, + [WGT_METHOD_ON_MOUSE] = (void *)dbGridOnMouse, + [WGT_METHOD_ON_KEY] = (void *)dbGridOnKey, + [WGT_METHOD_DESTROY] = (void *)dbGridDestroy, + [WGT_METHOD_GET_CURSOR_SHAPE] = (void *)dbGridGetCursorShape, + [WGT_METHOD_ON_DRAG_UPDATE] = (void *)dbGridOnDragUpdate, + [WGT_METHOD_ON_DRAG_END] = (void *)dbGridOnDragEnd, + } +}; + + +static const struct { + WidgetT *(*create)(WidgetT *parent); + void (*setDataWidget)(WidgetT *w, WidgetT *dataWidget); + void (*refresh)(WidgetT *w); + void (*setColumnVisible)(WidgetT *w, const char *fieldName, bool visible); + void (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header); + void (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width); + int32_t (*getSelectedRow)(const WidgetT *w); +} sApi = { + .create = dbGridCreate, + .setDataWidget = dbGridSetDataWidget, + .refresh = dbGridRefresh, + .setColumnVisible = dbGridSetColumnVisible, + .setColumnHeader = dbGridSetColumnHeader, + .setColumnWidth = dbGridSetColumnWidth, + .getSelectedRow = dbGridGetSelectedRow, +}; + +static const WgtPropDescT sProps[] = { + { "GridLines", WGT_IFACE_BOOL, (void *)dbGridGetGridLines, (void *)dbGridSetGridLines, NULL }, +}; + +static const WgtMethodDescT sMethods[] = { + { "Refresh", WGT_SIG_VOID, (void *)dbGridRefresh }, +}; + +static const WgtEventDescT sEvents[] = { + { "Click" }, + { "DblClick" }, +}; + +static const WgtIfaceT sIface = { + .basName = "DBGrid", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 1, + .events = sEvents, + .eventCount = 2, + .createSig = WGT_CREATE_PARENT, + .isContainer = false, + .defaultEvent = "DblClick", + .namePrefix = "DBGrid", +}; + + +void wgtRegister(void) { + sTypeId = wgtRegisterClass(&sClass); + wgtRegisterApi("dbgrid", &sApi); + wgtRegisterIface("dbgrid", &sIface); +} diff --git a/widgets/image/widgetImage.c b/widgets/image/widgetImage.c index 131251a..1958279 100644 --- a/widgets/image/widgetImage.c +++ b/widgets/image/widgetImage.c @@ -112,6 +112,9 @@ void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const Bit } +// Forward declaration +static void wgtImageLoadFile(WidgetT *w, const char *path); + // ============================================================ // DXE registration // ============================================================ @@ -124,6 +127,7 @@ static const WidgetClassT sClassImage = { [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetImageCalcMinSize, [WGT_METHOD_ON_MOUSE] = (void *)widgetImageOnMouse, [WGT_METHOD_DESTROY] = (void *)widgetImageDestroy, + [WGT_METHOD_SET_TEXT] = (void *)wgtImageLoadFile, } }; diff --git a/widgets/widgetDataCtrl.h b/widgets/widgetDataCtrl.h index 697da23..156d242 100644 --- a/widgets/widgetDataCtrl.h +++ b/widgets/widgetDataCtrl.h @@ -13,18 +13,40 @@ typedef struct { void (*moveNext)(WidgetT *w); void (*moveLast)(WidgetT *w); const char *(*getField)(WidgetT *w, const char *colName); + void (*setField)(WidgetT *w, const char *colName, const char *value); + void (*updateRow)(WidgetT *w); + void (*update)(WidgetT *w); + void (*addNew)(WidgetT *w); + void (*delete)(WidgetT *w); + void (*setMasterValue)(WidgetT *w, const char *val); + int32_t (*getRowCount)(WidgetT *w); + int32_t (*getColCount)(WidgetT *w); + const char *(*getColName)(WidgetT *w, int32_t col); + const char *(*getCellText)(WidgetT *w, int32_t row, int32_t col); + void (*setCurrentRow)(WidgetT *w, int32_t row); } DataCtrlApiT; static inline const DataCtrlApiT *dvxDataCtrlApi(void) { return (const DataCtrlApiT *)wgtGetApi("data"); } -#define wgtDataCtrl(parent) dvxDataCtrlApi()->create(parent) -#define wgtDataCtrlRefresh(w) dvxDataCtrlApi()->refresh(w) -#define wgtDataCtrlMoveFirst(w) dvxDataCtrlApi()->moveFirst(w) -#define wgtDataCtrlMovePrev(w) dvxDataCtrlApi()->movePrev(w) -#define wgtDataCtrlMoveNext(w) dvxDataCtrlApi()->moveNext(w) -#define wgtDataCtrlMoveLast(w) dvxDataCtrlApi()->moveLast(w) -#define wgtDataCtrlGetField(w, col) dvxDataCtrlApi()->getField(w, col) +#define wgtDataCtrl(parent) dvxDataCtrlApi()->create(parent) +#define wgtDataCtrlRefresh(w) dvxDataCtrlApi()->refresh(w) +#define wgtDataCtrlMoveFirst(w) dvxDataCtrlApi()->moveFirst(w) +#define wgtDataCtrlMovePrev(w) dvxDataCtrlApi()->movePrev(w) +#define wgtDataCtrlMoveNext(w) dvxDataCtrlApi()->moveNext(w) +#define wgtDataCtrlMoveLast(w) dvxDataCtrlApi()->moveLast(w) +#define wgtDataCtrlGetField(w, col) dvxDataCtrlApi()->getField(w, col) +#define wgtDataCtrlSetField(w, col, v) dvxDataCtrlApi()->setField(w, col, v) +#define wgtDataCtrlUpdateRow(w) dvxDataCtrlApi()->updateRow(w) +#define wgtDataCtrlUpdate(w) dvxDataCtrlApi()->update(w) +#define wgtDataCtrlAddNew(w) dvxDataCtrlApi()->addNew(w) +#define wgtDataCtrlDelete(w) dvxDataCtrlApi()->delete(w) +#define wgtDataCtrlSetMasterValue(w, val) dvxDataCtrlApi()->setMasterValue(w, val) +#define wgtDataCtrlGetRowCount(w) dvxDataCtrlApi()->getRowCount(w) +#define wgtDataCtrlGetColCount(w) dvxDataCtrlApi()->getColCount(w) +#define wgtDataCtrlGetColName(w, col) dvxDataCtrlApi()->getColName(w, col) +#define wgtDataCtrlGetCellText(w, r, c) dvxDataCtrlApi()->getCellText(w, r, c) +#define wgtDataCtrlSetCurrentRow(w, row) dvxDataCtrlApi()->setCurrentRow(w, row) #endif // WIDGET_DATACTRL_H diff --git a/widgets/widgetDbGrid.h b/widgets/widgetDbGrid.h new file mode 100644 index 0000000..6680b33 --- /dev/null +++ b/widgets/widgetDbGrid.h @@ -0,0 +1,30 @@ +// widgetDbGrid.h -- Database grid widget API + +#ifndef WIDGET_DBGRID_H +#define WIDGET_DBGRID_H + +#include "dvxWidget.h" + +typedef struct { + WidgetT *(*create)(WidgetT *parent); + void (*setDataWidget)(WidgetT *w, WidgetT *dataWidget); + void (*refresh)(WidgetT *w); + void (*setColumnVisible)(WidgetT *w, const char *fieldName, bool visible); + void (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header); + void (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width); + int32_t (*getSelectedRow)(const WidgetT *w); +} DbGridApiT; + +static inline const DbGridApiT *dvxDbGridApi(void) { + return (const DbGridApiT *)wgtGetApi("dbgrid"); +} + +#define wgtDbGrid(parent) dvxDbGridApi()->create(parent) +#define wgtDbGridSetDataWidget(w, dw) dvxDbGridApi()->setDataWidget(w, dw) +#define wgtDbGridRefresh(w) dvxDbGridApi()->refresh(w) +#define wgtDbGridSetColumnVisible(w, field, vis) dvxDbGridApi()->setColumnVisible(w, field, vis) +#define wgtDbGridSetColumnHeader(w, field, hdr) dvxDbGridApi()->setColumnHeader(w, field, hdr) +#define wgtDbGridSetColumnWidth(w, field, width) dvxDbGridApi()->setColumnWidth(w, field, width) +#define wgtDbGridGetSelectedRow(w) dvxDbGridApi()->getSelectedRow(w) + +#endif // WIDGET_DBGRID_H