External library support. DBGrid widget. More data binding. App path properties.
This commit is contained in:
parent
827d73fbd1
commit
eb5e4e567e
23 changed files with 2560 additions and 112 deletions
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
33
sql/dvxSql.c
33
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;
|
||||
}
|
||||
|
|
|
|||
10
sql/dvxSql.h
10
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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
BIN
widgets/dbGrid/dbgrid.bmp
(Stored with Git LFS)
Normal file
Binary file not shown.
5
widgets/dbGrid/dbgrid.res
Normal file
5
widgets/dbGrid/dbgrid.res
Normal 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"
|
||||
1221
widgets/dbGrid/widgetDbGrid.c
Normal file
1221
widgets/dbGrid/widgetDbGrid.c
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
30
widgets/widgetDbGrid.h
Normal 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
|
||||
Loading…
Add table
Reference in a new issue