External library support. DBGrid widget. More data binding. App path properties.

This commit is contained in:
Scott Duensing 2026-04-05 00:04:11 -05:00
parent 827d73fbd1
commit eb5e4e567e
23 changed files with 2560 additions and 112 deletions

View file

@ -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 },

View file

@ -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,

View file

@ -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
// ============================================================

View file

@ -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);

View file

@ -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 <ctype.h>
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -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;
}

View file

@ -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;
}

View file

@ -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;
// ============================================================

View file

@ -42,6 +42,7 @@
#include "stb_ds_wrap.h"
#include <dlfcn.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
@ -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
// ============================================================

View file

@ -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;

View file

@ -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;

View file

@ -15,6 +15,7 @@
#define DVXBASIC_VM_H
#include "values.h"
#include "dvxTypes.h"
#include <stdint.h>
#include <stdbool.h>
@ -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;
// ============================================================

View file

@ -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;

View file

@ -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;
}

View file

@ -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

View file

@ -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 <output.bmp> <type>\n");
@ -589,6 +607,7 @@ int main(int argc, char **argv) {
{"terminal", drawTerminal},
{"wrapbox", drawWrapbox},
{"datactrl", drawDatactrl},
{"dbgrid", drawDbgrid},
{NULL, NULL}
};

View file

@ -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

View file

@ -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 <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -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",

BIN
widgets/dbGrid/dbgrid.bmp (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -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"

File diff suppressed because it is too large Load diff

View file

@ -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,
}
};

View file

@ -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

30
widgets/widgetDbGrid.h Normal file
View file

@ -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