From 827d73fbd19d243dcdd04e72d8c8672c40c9e855 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sat, 4 Apr 2026 20:00:25 -0500 Subject: [PATCH] Data binding! --- apps/dvxbasic/formrt/formrt.c | 90 ++++- apps/dvxbasic/formrt/formrt.h | 2 + apps/dvxbasic/ide/ideMain.c | 8 - apps/dvxbasic/ide/ideProperties.c | 314 +++++++++++++++- core/dvxDialog.c | 134 +++++++ core/dvxDialog.h | 11 + core/dvxWidget.h | 5 + sql/Makefile | 2 +- tools/mkwgticon.c | 23 ++ widgets/Makefile | 6 +- widgets/dataCtrl/datactr.bmp | 3 + widgets/dataCtrl/datactr.res | 5 + widgets/dataCtrl/widgetDataCtrl.c | 577 ++++++++++++++++++++++++++++++ widgets/spinner/widgetSpinner.c | 386 +++++++++++++++----- widgets/widgetDataCtrl.h | 30 ++ widgets/widgetSpinner.h | 12 + 16 files changed, 1506 insertions(+), 102 deletions(-) create mode 100644 widgets/dataCtrl/datactr.bmp create mode 100644 widgets/dataCtrl/datactr.res create mode 100644 widgets/dataCtrl/widgetDataCtrl.c create mode 100644 widgets/widgetDataCtrl.h diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index 7d89489..953b682 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -13,6 +13,7 @@ #include "thirdparty/stb_ds_wrap.h" #include +#include #include #include #include @@ -88,6 +89,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 updateBoundControls(BasFormT *form, BasControlT *dataCtrl); static BasValueT zeroValue(void); // ============================================================ @@ -523,6 +525,15 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { return basValStringFromC(text ? text : ""); } + // Data binding properties + if (strcasecmp(propName, "DataSource") == 0) { + return basValStringFromC(ctrl->dataSource); + } + + if (strcasecmp(propName, "DataField") == 0) { + return basValStringFromC(ctrl->dataField); + } + // "ListCount" for any widget with item storage if (strcasecmp(propName, "ListCount") == 0) { ListBoxItemsT *lb = getListBoxItems(ctrl); @@ -815,13 +826,18 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen } else if (form && nestDepth > 0) { // Create the content box on first control if not yet done if (!form->contentBox && form->root) { + // Use the root widget directly as the content box. + // wgtInitWindow already created a VBox root. If the + // form wants HBox, create one inside. Otherwise reuse + // root to avoid double-nesting (which doubles padding + // and can trigger unwanted scrollbars). if (form->frmHBox) { form->contentBox = wgtHBox(form->root); + form->contentBox->weight = 100; } else { - form->contentBox = wgtVBox(form->root); + form->contentBox = form->root; } - form->contentBox->weight = 100; parentStack[0] = form->contentBox; } WidgetT *parent = parentStack[nestDepth - 1]; @@ -1012,11 +1028,10 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen if (!form->contentBox && form->root) { if (form->frmHBox) { form->contentBox = wgtHBox(form->root); + form->contentBox->weight = 100; } else { - form->contentBox = wgtVBox(form->root); + form->contentBox = form->root; } - - form->contentBox->weight = 100; } // Build menu bar from accumulated menu items @@ -1140,6 +1155,19 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // Fire the Load event now that the form and controls are ready 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"); + + 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]); + } + } + } } return form; @@ -1185,6 +1213,21 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT return; } + // Data binding properties (stored on BasControlT, not on the widget) + if (strcasecmp(propName, "DataSource") == 0) { + BasStringT *s = basValFormatString(value); + snprintf(ctrl->dataSource, BAS_MAX_CTRL_NAME, "%s", s->data); + basStringUnref(s); + return; + } + + if (strcasecmp(propName, "DataField") == 0) { + BasStringT *s = basValFormatString(value); + snprintf(ctrl->dataField, BAS_MAX_CTRL_NAME, "%s", s->data); + basStringUnref(s); + return; + } + // Interface descriptor properties if (ctrl->iface) { if (setIfaceProp(ctrl->iface, ctrl->widget, propName, value)) { @@ -1707,8 +1750,8 @@ static void onFormClose(WindowT *win) { // ============================================================ static void onFormResize(WindowT *win, int32_t newW, int32_t newH) { - (void)newW; - (void)newH; + // Let the widget system re-evaluate scrollbars for the new size + widgetOnResize(win, newW, newH); if (!sFormRt) { return; @@ -1805,6 +1848,32 @@ static void onWidgetBlur(WidgetT *w) { } +// 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) { + 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 (val && ctrl->widget) { + snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", val); + wgtSetText(ctrl->widget, ctrl->textBuf); + } + } + } +} + + static void onWidgetChange(WidgetT *w) { BasControlT *ctrl = (BasControlT *)w->userData; @@ -1815,6 +1884,13 @@ static void onWidgetChange(WidgetT *w) { BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx; if (rt) { + // Data controls fire "Reposition" and update bound controls first + if (strcasecmp(ctrl->typeName, "Data") == 0) { + updateBoundControls(ctrl->form, ctrl); + fireCtrlEvent(rt, ctrl, "Reposition", NULL, 0); + return; + } + // Timer widgets fire "Timer" event, everything else fires "Change" const char *evtName = (strcasecmp(ctrl->typeName, "Timer") == 0) ? "Timer" : "Change"; fireCtrlEvent(rt, ctrl, evtName, NULL, 0); diff --git a/apps/dvxbasic/formrt/formrt.h b/apps/dvxbasic/formrt/formrt.h index 2f6c8ce..fbc4984 100644 --- a/apps/dvxbasic/formrt/formrt.h +++ b/apps/dvxbasic/formrt/formrt.h @@ -51,6 +51,8 @@ typedef struct BasControlT { BasFormT *form; // owning form const WgtIfaceT *iface; // interface descriptor (from .wgt) char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text + char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding + char dataField[BAS_MAX_CTRL_NAME]; // column name for binding } BasControlT; // ============================================================ diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 14ba81e..f296b65 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -1864,14 +1864,6 @@ static void openProject(void) { prjRebuildTree(&sProject); } - // Open the first .bas file in the editor - for (int32_t i = 0; i < sProject.fileCount; i++) { - if (!sProject.files[i].isForm) { - onPrjFileClick(i, false); - break; - } - } - char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); dvxSetTitle(sAc, sWin, title); diff --git a/apps/dvxbasic/ide/ideProperties.c b/apps/dvxbasic/ide/ideProperties.c index 6fba58f..5c10ccd 100644 --- a/apps/dvxbasic/ide/ideProperties.c +++ b/apps/dvxbasic/ide/ideProperties.c @@ -13,6 +13,7 @@ #include "widgetSplitter.h" #include "widgetTreeView.h" +#include #include #include #include @@ -76,6 +77,8 @@ static void addPropRow(const char *name, const char *value) { // Prototypes // ============================================================ +static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char names[][DSGN_MAX_NAME], int32_t maxNames); +static int32_t getTableNames(const char *dbName, char names[][DSGN_MAX_NAME], int32_t maxNames); static void onPrpClose(WindowT *win); static void onPropDblClick(WidgetT *w); static void onTreeItemClick(WidgetT *w); @@ -91,6 +94,161 @@ static void onPrpClose(WindowT *win) { } +// ============================================================ +// getDataFieldNames -- query column names from a Data control's database +// ============================================================ +// +// Finds the named Data control in the designer, reads its DatabaseName +// and RecordSource properties, opens the database via dvxSql* (resolved +// through dlsym), and returns up to maxNames column names. Returns the +// count of names found (0 if anything fails). + +static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char names[][DSGN_MAX_NAME], int32_t maxNames) { + if (!ds || !ds->form || !dataSourceName || !dataSourceName[0]) { + return 0; + } + + // Find the Data control in the designer + const char *dbName = NULL; + const char *recSrc = NULL; + int32_t ctrlCount = (int32_t)arrlen(ds->form->controls); + + for (int32_t i = 0; i < ctrlCount; i++) { + DsgnControlT *ctrl = &ds->form->controls[i]; + + if (strcasecmp(ctrl->typeName, "Data") != 0 || strcasecmp(ctrl->name, dataSourceName) != 0) { + continue; + } + + for (int32_t j = 0; j < ctrl->propCount; j++) { + if (strcasecmp(ctrl->props[j].name, "DatabaseName") == 0) { + dbName = ctrl->props[j].value; + } else if (strcasecmp(ctrl->props[j].name, "RecordSource") == 0) { + recSrc = ctrl->props[j].value; + } + } + + break; + } + + if (!dbName || !dbName[0] || !recSrc || !recSrc[0]) { + return 0; + } + + // Resolve SQL functions via dlsym + typedef int32_t (*SqlOpenFnT)(const char *); + typedef void (*SqlCloseFnT)(int32_t); + typedef int32_t (*SqlQueryFnT)(int32_t, const char *); + typedef int32_t (*SqlFieldCountFnT)(int32_t); + typedef const char *(*SqlFieldNameFnT)(int32_t, int32_t); + typedef void (*SqlFreeResultFnT)(int32_t); + + SqlOpenFnT sqlOpen = (SqlOpenFnT)dlsym(NULL, "_dvxSqlOpen"); + SqlCloseFnT sqlClose = (SqlCloseFnT)dlsym(NULL, "_dvxSqlClose"); + SqlQueryFnT sqlQuery = (SqlQueryFnT)dlsym(NULL, "_dvxSqlQuery"); + SqlFieldCountFnT sqlFieldCount = (SqlFieldCountFnT)dlsym(NULL, "_dvxSqlFieldCount"); + SqlFieldNameFnT sqlFieldName = (SqlFieldNameFnT)dlsym(NULL, "_dvxSqlFieldName"); + SqlFreeResultFnT sqlFreeResult = (SqlFreeResultFnT)dlsym(NULL, "_dvxSqlFreeResult"); + + if (!sqlOpen || !sqlClose || !sqlQuery || !sqlFieldCount || !sqlFieldName || !sqlFreeResult) { + return 0; + } + + int32_t db = sqlOpen(dbName); + + if (db <= 0) { + return 0; + } + + // Query with LIMIT 0 to get column names without fetching rows + char query[512]; + + if (strncasecmp(recSrc, "SELECT ", 7) == 0) { + snprintf(query, sizeof(query), "%s LIMIT 0", recSrc); + } else { + snprintf(query, sizeof(query), "SELECT * FROM %s LIMIT 0", recSrc); + } + + int32_t rs = sqlQuery(db, query); + + if (rs <= 0) { + sqlClose(db); + return 0; + } + + int32_t colCount = sqlFieldCount(rs); + int32_t count = 0; + + for (int32_t i = 0; i < colCount && count < maxNames; i++) { + const char *name = sqlFieldName(rs, i); + + if (name) { + snprintf(names[count++], DSGN_MAX_NAME, "%s", name); + } + } + + sqlFreeResult(rs); + sqlClose(db); + return count; +} + + +// ============================================================ +// getTableNames -- query table names from a SQLite database +// ============================================================ + +static int32_t getTableNames(const char *dbName, char names[][DSGN_MAX_NAME], int32_t maxNames) { + if (!dbName || !dbName[0]) { + return 0; + } + + typedef int32_t (*SqlOpenFnT)(const char *); + typedef void (*SqlCloseFnT)(int32_t); + typedef int32_t (*SqlQueryFnT)(int32_t, const char *); + typedef bool (*SqlNextFnT)(int32_t); + typedef const char *(*SqlFieldTextFnT)(int32_t, int32_t); + typedef void (*SqlFreeResultFnT)(int32_t); + + SqlOpenFnT sqlOpen = (SqlOpenFnT)dlsym(NULL, "_dvxSqlOpen"); + SqlCloseFnT sqlClose = (SqlCloseFnT)dlsym(NULL, "_dvxSqlClose"); + SqlQueryFnT sqlQuery = (SqlQueryFnT)dlsym(NULL, "_dvxSqlQuery"); + SqlNextFnT sqlNext = (SqlNextFnT)dlsym(NULL, "_dvxSqlNext"); + SqlFieldTextFnT sqlFieldText = (SqlFieldTextFnT)dlsym(NULL, "_dvxSqlFieldText"); + SqlFreeResultFnT sqlFreeResult = (SqlFreeResultFnT)dlsym(NULL, "_dvxSqlFreeResult"); + + if (!sqlOpen || !sqlClose || !sqlQuery || !sqlNext || !sqlFieldText || !sqlFreeResult) { + return 0; + } + + int32_t db = sqlOpen(dbName); + + if (db <= 0) { + return 0; + } + + int32_t rs = sqlQuery(db, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"); + + if (rs <= 0) { + sqlClose(db); + return 0; + } + + int32_t count = 0; + + while (sqlNext(rs) && count < maxNames) { + const char *name = sqlFieldText(rs, 0); + + if (name && name[0]) { + snprintf(names[count++], DSGN_MAX_NAME, "%s", name); + } + } + + sqlFreeResult(rs); + sqlClose(db); + return count; +} + + // ============================================================ // getPropType // ============================================================ @@ -103,7 +261,13 @@ static void onPrpClose(WindowT *win) { #define PROP_TYPE_INT WGT_IFACE_INT #define PROP_TYPE_BOOL WGT_IFACE_BOOL #define PROP_TYPE_ENUM WGT_IFACE_ENUM -#define PROP_TYPE_READONLY 255 +#define PROP_TYPE_READONLY 255 +#define PROP_TYPE_DATASOURCE 254 +#define PROP_TYPE_DATAFIELD 253 +#define PROP_TYPE_RECORDSRC 252 + +#define MAX_DATAFIELD_COLS 64 +#define MAX_TABLES 64 static uint8_t getPropType(const char *propName, const char *typeName) { // Read-only properties @@ -125,6 +289,9 @@ 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, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; } // Look up in the widget's interface descriptor if (typeName && typeName[0]) { @@ -314,6 +481,151 @@ static void onPropDblClick(WidgetT *w) { int32_t nextIdx = (curIdx + 1) % enumCount; snprintf(newValue, sizeof(newValue), "%s", pd->enumNames[nextIdx]); + } else if (propType == PROP_TYPE_DATASOURCE) { + // Show dropdown of Data control names on the form + int32_t formCtrlCount = (int32_t)arrlen(sDs->form->controls); + const char *dataNames[17]; + int32_t dataCount = 0; + + // First entry is "(none)" to clear binding + dataNames[dataCount++] = "(none)"; + + for (int32_t i = 0; i < formCtrlCount && dataCount < 16; i++) { + if (strcasecmp(sDs->form->controls[i].typeName, "Data") == 0) { + dataNames[dataCount++] = sDs->form->controls[i].name; + } + } + + if (dataCount <= 1) { + return; + } + + // Find current selection + int32_t defIdx = 0; + + for (int32_t i = 1; i < dataCount; i++) { + if (strcasecmp(dataNames[i], curValue) == 0) { + defIdx = i; + break; + } + } + + int32_t chosenIdx = 0; + + if (!dvxChoiceDialog(sPrpCtx, "DataSource", "Select Data control:", dataNames, dataCount, defIdx, &chosenIdx)) { + return; + } + + if (chosenIdx == 0) { + snprintf(newValue, sizeof(newValue), ""); + } else { + snprintf(newValue, sizeof(newValue), "%s", dataNames[chosenIdx]); + } + } else if (propType == PROP_TYPE_DATAFIELD) { + // Show dropdown of column names from the Data control's database + 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; + } + } + } + + char fieldNames[MAX_DATAFIELD_COLS][DSGN_MAX_NAME]; + int32_t fieldCount = getDataFieldNames(sDs, dataSrc, fieldNames, MAX_DATAFIELD_COLS); + + if (fieldCount <= 0) { + // No columns found — fall back to text input + char prompt[128]; + snprintf(prompt, sizeof(prompt), "%s:", propName); + snprintf(newValue, sizeof(newValue), "%s", curValue); + + if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, sizeof(newValue))) { + return; + } + } else { + const char *fieldPtrs[MAX_DATAFIELD_COLS]; + + for (int32_t i = 0; i < fieldCount; i++) { + fieldPtrs[i] = fieldNames[i]; + } + + int32_t defIdx = -1; + + for (int32_t i = 0; i < fieldCount; i++) { + if (strcasecmp(fieldNames[i], curValue) == 0) { + defIdx = i; + break; + } + } + + int32_t chosenIdx = 0; + + if (!dvxChoiceDialog(sPrpCtx, "DataField", "Select column:", fieldPtrs, fieldCount, defIdx, &chosenIdx)) { + return; + } + + snprintf(newValue, sizeof(newValue), "%s", fieldNames[chosenIdx]); + } + } else if (propType == PROP_TYPE_RECORDSRC) { + // Show dropdown of table names from the Data control's database + // Find DatabaseName on this control (which is a Data control) + int32_t selCount = (int32_t)arrlen(sDs->form->controls); + const char *dbName = ""; + + 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, "DatabaseName") == 0) { + dbName = selCtrl->props[i].value; + break; + } + } + } + + char tableNames[MAX_TABLES][DSGN_MAX_NAME]; + int32_t tableCount = getTableNames(dbName, tableNames, MAX_TABLES); + + if (tableCount <= 0) { + // No tables or can't open DB — fall back to text input + char prompt[128]; + snprintf(prompt, sizeof(prompt), "%s:", propName); + snprintf(newValue, sizeof(newValue), "%s", curValue); + + if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, sizeof(newValue))) { + return; + } + } else { + const char *tablePtrs[MAX_TABLES]; + + for (int32_t i = 0; i < tableCount; i++) { + tablePtrs[i] = tableNames[i]; + } + + int32_t defIdx = -1; + + for (int32_t i = 0; i < tableCount; i++) { + if (strcasecmp(tableNames[i], curValue) == 0) { + defIdx = i; + break; + } + } + + int32_t chosenIdx = 0; + + if (!dvxChoiceDialog(sPrpCtx, "RecordSource", "Select table:", tablePtrs, tableCount, defIdx, &chosenIdx)) { + return; + } + + snprintf(newValue, sizeof(newValue), "%s", tableNames[chosenIdx]); + } } else if (propType == PROP_TYPE_INT) { // Spinner dialog for integers char prompt[128]; diff --git a/core/dvxDialog.c b/core/dvxDialog.c index 9f8cedd..d2137ad 100644 --- a/core/dvxDialog.c +++ b/core/dvxDialog.c @@ -883,6 +883,140 @@ bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int } +// ============================================================ +// Choice dialog state +// ============================================================ + +static struct { + bool done; + bool accepted; + int32_t *outIdx; + WidgetT *listBox; +} sChoiceBox; + +static void cbOnClose(WindowT *win) { + (void)win; + sChoiceBox.done = true; +} + +static void cbOnOk(WidgetT *w) { + (void)w; + + if (sChoiceBox.listBox && sChoiceBox.outIdx) { + *sChoiceBox.outIdx = wgtListBoxGetSelected(sChoiceBox.listBox); + } + + sChoiceBox.accepted = true; + sChoiceBox.done = true; +} + +static void cbOnCancel(WidgetT *w) { + (void)w; + sChoiceBox.done = true; +} + +static void cbOnDblClick(WidgetT *w) { + (void)w; + cbOnOk(w); +} + + +// ============================================================ +// dvxChoiceDialog +// ============================================================ + +#define CB_DIALOG_WIDTH 200 +#define CB_LIST_HEIGHT 120 +#define CB_PADDING 8 + +bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, const char **items, int32_t itemCount, int32_t defaultIdx, int32_t *outIdx) { + if (!ctx || !items || itemCount <= 0 || !outIdx) { + return false; + } + + int32_t promptH = 0; + + if (prompt && prompt[0]) { + int32_t textMaxW = CB_DIALOG_WIDTH - CB_PADDING * 2; + promptH = wordWrapHeight(&ctx->font, prompt, textMaxW); + } + + int32_t contentH = CB_PADDING + promptH + CB_PADDING + CB_LIST_HEIGHT + CB_PADDING + BUTTON_HEIGHT + CB_PADDING; + int32_t contentW = CB_DIALOG_WIDTH; + + int32_t winX = (ctx->display.width - contentW) / 2 - CHROME_TOTAL_SIDE; + int32_t winY = (ctx->display.height - contentH) / 2 - CHROME_TOTAL_TOP; + + WindowT *win = dvxCreateWindow(ctx, title ? title : "Choose", + winX, winY, + contentW + CHROME_TOTAL_SIDE * 2, + contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM, + false); + + if (!win) { + return false; + } + + win->modal = true; + win->onClose = cbOnClose; + win->maxW = win->w; + win->maxH = win->h; + + sChoiceBox.done = false; + sChoiceBox.accepted = false; + sChoiceBox.outIdx = outIdx; + sChoiceBox.listBox = NULL; + + WidgetT *root = wgtInitWindow(ctx, win); + + if (root) { + if (prompt && prompt[0]) { + wgtLabel(root, prompt); + } + + WidgetT *lb = wgtListBox(root); + lb->weight = 100; + lb->minH = wgtPixels(CB_LIST_HEIGHT); + lb->onDblClick = cbOnDblClick; + wgtListBoxSetItems(lb, items, itemCount); + + if (defaultIdx >= 0 && defaultIdx < itemCount) { + wgtListBoxSetSelected(lb, defaultIdx); + } + + sChoiceBox.listBox = lb; + + WidgetT *btnRow = wgtHBox(root); + btnRow->align = AlignCenterE; + + WidgetT *okBtn = wgtButton(btnRow, "&OK"); + okBtn->minW = wgtPixels(BUTTON_WIDTH); + okBtn->minH = wgtPixels(BUTTON_HEIGHT); + okBtn->onClick = cbOnOk; + + WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); + cancelBtn->minW = wgtPixels(BUTTON_WIDTH); + cancelBtn->minH = wgtPixels(BUTTON_HEIGHT); + cancelBtn->onClick = cbOnCancel; + } + + dvxFitWindow(ctx, win); + + WindowT *prevModal = ctx->modalWindow; + ctx->modalWindow = win; + + while (!sChoiceBox.done && ctx->running) { + dvxUpdate(ctx); + } + + ctx->modalWindow = prevModal; + dvxDestroyWindow(ctx, win); + sChoiceBox.listBox = NULL; + + return sChoiceBox.accepted; +} + + // ============================================================ // ibOnOk // ============================================================ diff --git a/core/dvxDialog.h b/core/dvxDialog.h index 31f41ee..5d8b259 100644 --- a/core/dvxDialog.h +++ b/core/dvxDialog.h @@ -103,4 +103,15 @@ bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int int32_t dvxPromptSave(AppContextT *ctx, const char *title); +// ============================================================ +// Choice dialog +// ============================================================ +// +// Display a modal dialog with a listbox of choices. The user picks +// one item and clicks OK, or cancels. Returns true if a choice was +// made (selected index written to outIdx), false if cancelled. +// defaultIdx is the initially highlighted item (-1 for none). + +bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, const char **items, int32_t itemCount, int32_t defaultIdx, int32_t *outIdx); + #endif // DVX_DIALOG_H diff --git a/core/dvxWidget.h b/core/dvxWidget.h index bd8b7d1..d309a5f 100644 --- a/core/dvxWidget.h +++ b/core/dvxWidget.h @@ -471,6 +471,11 @@ WidgetT *wgtFind(WidgetT *root, const char *name); // Destroy a widget and all its children (removes from parent) void wgtDestroy(WidgetT *w); +// Default window resize handler installed by wgtInitWindow. Re-evaluates +// scrollbars and relayouts the widget tree. Call this from custom onResize +// handlers to chain to the widget system before firing application events. +void widgetOnResize(WindowT *win, int32_t newW, int32_t newH); + // ============================================================ // Tooltip // ============================================================ diff --git a/sql/Makefile b/sql/Makefile index ad962d0..703fab8 100644 --- a/sql/Makefile +++ b/sql/Makefile @@ -99,7 +99,7 @@ $(OBJDIR)/sqlite_opcodes.o: $(GEN_OPCODES_C) | $(OBJDIR) $(TARGET): $(OBJS) | $(LIBSDIR) $(DXE3GEN) -o $(LIBSDIR)/dvxsql.dxe \ - -E _dvxSql \ + -E _dvxSql -E _sqlite3 \ -U $(OBJS) mv $(LIBSDIR)/dvxsql.dxe $@ diff --git a/tools/mkwgticon.c b/tools/mkwgticon.c index efb088b..8eaae60 100644 --- a/tools/mkwgticon.c +++ b/tools/mkwgticon.c @@ -526,6 +526,28 @@ static void drawWrapbox(void) { } +static void drawDatactrl(void) { + clear(192, 192, 192); + // Navigation bar + rect(2, 8, 20, 8, 200, 200, 200); + box(2, 8, 20, 8, 128, 128, 128); + // |< button + vline(5, 10, 4, 0, 0, 0); + px(6, 11, 0, 0, 0); px(7, 12, 0, 0, 0); + px(6, 13, 0, 0, 0); px(7, 12, 0, 0, 0); + // > button + px(16, 11, 0, 0, 0); px(17, 12, 0, 0, 0); + px(16, 13, 0, 0, 0); + // >| button + vline(19, 10, 4, 0, 0, 0); + px(18, 11, 0, 0, 0); px(18, 13, 0, 0, 0); + // Database cylinder icon above + rect(7, 2, 10, 4, 255, 255, 200); + box(7, 2, 10, 4, 128, 128, 0); + hline(7, 3, 10, 128, 128, 0); +} + + int main(int argc, char **argv) { if (argc != 3) { fprintf(stderr, "Usage: mkwgticon \n"); @@ -566,6 +588,7 @@ int main(int argc, char **argv) { {"spacer", drawSpacer}, {"terminal", drawTerminal}, {"wrapbox", drawWrapbox}, + {"datactrl", drawDatactrl}, {NULL, NULL} }; diff --git a/widgets/Makefile b/widgets/Makefile index 6c33bea..f9d3b55 100644 --- a/widgets/Makefile +++ b/widgets/Makefile @@ -39,6 +39,7 @@ WIDGETS = \ terminal:ansiTerm:widgetAnsiTerm:terminal \ textinpt:textInput:widgetTextInput:textinpt \ timer:timer:widgetTimer:timer \ + datactrl:dataCtrl:widgetDataCtrl:datactr \ toolbar:toolbar:widgetToolbar:toolbar \ treeview:treeView:widgetTreeView:treeview \ wrapbox:wrapBox:widgetWrapBox:wrapbox @@ -48,6 +49,9 @@ WGT_NAMES = $(foreach w,$(WIDGETS),$(word 1,$(subst :, ,$w))) WGT_MODS = $(WGT_NAMES:%=$(WGTDIR)/%.wgt) OBJS = $(foreach w,$(WIDGETS),$(OBJDIR)/$(word 3,$(subst :, ,$w)).o) +# Per-widget extra DXE3GEN flags (e.g. additional -E exports for dlsym) +EXTRA_DXE_FLAGS_datactrl = -E _dataCtrl + DEPFILES = textinpt combobox spinner terminal listbox dropdown listview treeview WGT_DEPS = $(DEPFILES:%=$(WGTDIR)/%.dep) @@ -66,7 +70,7 @@ $(OBJDIR)/$(word 3,$(subst :, ,$1)).o: $(word 2,$(subst :, ,$1))/$(word 3,$(subs $$(CC) $$(CFLAGS) -c -o $$@ $$< $(WGTDIR)/$(word 1,$(subst :, ,$1)).wgt: $(OBJDIR)/$(word 3,$(subst :, ,$1)).o | $(WGTDIR) - $$(DXE3GEN) -o $(WGTDIR)/$(word 1,$(subst :, ,$1)).dxe -E _wgtRegister -U $$< + $$(DXE3GEN) -o $(WGTDIR)/$(word 1,$(subst :, ,$1)).dxe -E _wgtRegister $$(EXTRA_DXE_FLAGS_$(word 1,$(subst :, ,$1))) -U $$< mv $(WGTDIR)/$(word 1,$(subst :, ,$1)).dxe $$@ @if [ -f $(word 2,$(subst :, ,$1))/$(word 4,$(subst :, ,$1)).res ]; then \ cd $(word 2,$(subst :, ,$1)) && ../$(DVXRES) build ../$$@ $(word 4,$(subst :, ,$1)).res; \ diff --git a/widgets/dataCtrl/datactr.bmp b/widgets/dataCtrl/datactr.bmp new file mode 100644 index 0000000..e772057 --- /dev/null +++ b/widgets/dataCtrl/datactr.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6dd29a0449a163ca3b416da0ba4fcbff8bd5d3855ac23d5a933dba251a4a3f96 +size 1782 diff --git a/widgets/dataCtrl/datactr.res b/widgets/dataCtrl/datactr.res new file mode 100644 index 0000000..0a3fba6 --- /dev/null +++ b/widgets/dataCtrl/datactr.res @@ -0,0 +1,5 @@ +icon24 icon datactr.bmp +name text "Data" +author text "DVX Project" +description text "Database data control with navigation" +version text "1.0" diff --git a/widgets/dataCtrl/widgetDataCtrl.c b/widgets/dataCtrl/widgetDataCtrl.c new file mode 100644 index 0000000..62328ca --- /dev/null +++ b/widgets/dataCtrl/widgetDataCtrl.c @@ -0,0 +1,577 @@ +#define DVX_WIDGET_IMPL +// widgetDataCtrl.c -- VB3-style Data control for database binding +// +// A visible navigation bar that connects to a SQLite database via +// dvxSql* functions. Reads all rows from the RecordSource query +// into an in-memory cache for bidirectional navigation. Fires +// 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). + +#include "dvxWidgetPlugin.h" +#include "thirdparty/stb_ds_wrap.h" + +#include +#include +#include +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define DATA_HEIGHT 24 +#define DATA_BTN_W 20 +#define DATA_BORDER 2 +#define DATA_MAX_COLS 64 +#define DATA_MAX_FIELD 256 + +// ============================================================ +// Per-row cache entry +// ============================================================ + +typedef struct { + char **fields; // array of strdup'd strings, one per column + int32_t fieldCount; +} DataRowT; + +// ============================================================ +// Per-instance data +// ============================================================ + +typedef struct { + char databaseName[DATA_MAX_FIELD]; + char recordSource[DATA_MAX_FIELD]; + char caption[DATA_MAX_FIELD]; + + // Cached result set + DataRowT *rows; // stb_ds dynamic array + int32_t rowCount; + char **colNames; // stb_ds dynamic array of strdup'd names + int32_t colCount; + int32_t currentRow; // 0-based, -1 = no rows + + 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; +} 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; +} + + +// ============================================================ +// freeCache -- release the cached result set +// ============================================================ + +static void freeCache(DataCtrlDataT *d) { + for (int32_t i = 0; i < d->rowCount; i++) { + for (int32_t j = 0; j < d->rows[i].fieldCount; j++) { + free(d->rows[i].fields[j]); + } + + free(d->rows[i].fields); + } + + arrfree(d->rows); + d->rows = NULL; + d->rowCount = 0; + + for (int32_t i = 0; i < d->colCount; i++) { + free(d->colNames[i]); + } + + arrfree(d->colNames); + d->colNames = NULL; + d->colCount = 0; + d->currentRow = -1; + d->bof = true; + d->eof = true; +} + + +// ============================================================ +// dataCtrlRefresh -- execute query and cache all rows +// ============================================================ + +void dataCtrlRefresh(WidgetT *w) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + resolveSql(d); + + freeCache(d); + + if (!d->sqlOpen || !d->databaseName[0] || !d->recordSource[0]) { + return; + } + + int32_t db = d->sqlOpen(d->databaseName); + + if (db <= 0) { + return; + } + + // If recordSource doesn't start with SELECT, wrap it as "SELECT * FROM table" + char query[512]; + + if (strncasecmp(d->recordSource, "SELECT ", 7) == 0) { + snprintf(query, sizeof(query), "%s", d->recordSource); + } else { + snprintf(query, sizeof(query), "SELECT * FROM %s", d->recordSource); + } + + int32_t rs = d->sqlQuery(db, query); + + if (rs <= 0) { + d->sqlClose(db); + return; + } + + // Cache column names + d->colCount = d->sqlFieldCount(rs); + + for (int32_t i = 0; i < d->colCount; i++) { + const char *name = d->sqlFieldName(rs, i); + arrput(d->colNames, strdup(name ? name : "")); + } + + // Cache all rows + while (d->sqlNext(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); + row.fields[i] = strdup(text ? text : ""); + } + + arrput(d->rows, row); + } + + d->rowCount = (int32_t)arrlen(d->rows); + d->sqlFreeResult(rs); + d->sqlClose(db); + + if (d->rowCount > 0) { + d->currentRow = 0; + d->bof = false; + d->eof = false; + } + + // Auto-caption + if (!d->caption[0]) { + snprintf(d->caption, sizeof(d->caption), "%s", d->recordSource); + } + + wgtInvalidatePaint(w); +} + + +// ============================================================ +// Navigation methods +// ============================================================ + +static void fireReposition(WidgetT *w) { + wgtInvalidatePaint(w); + + if (w->onChange) { + w->onChange(w); + } +} + + +static void dataCtrlMoveFirst(WidgetT *w) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (d->rowCount <= 0) { + return; + } + + d->currentRow = 0; + d->bof = false; + d->eof = false; + fireReposition(w); +} + + +static void dataCtrlMovePrev(WidgetT *w) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (d->rowCount <= 0 || d->currentRow <= 0) { + d->bof = true; + return; + } + + d->currentRow--; + d->bof = (d->currentRow == 0); + d->eof = false; + fireReposition(w); +} + + +static void dataCtrlMoveNext(WidgetT *w) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (d->rowCount <= 0 || d->currentRow >= d->rowCount - 1) { + d->eof = true; + return; + } + + d->currentRow++; + d->bof = false; + d->eof = (d->currentRow >= d->rowCount - 1); + fireReposition(w); +} + + +static void dataCtrlMoveLast(WidgetT *w) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (d->rowCount <= 0) { + return; + } + + d->currentRow = d->rowCount - 1; + d->bof = false; + d->eof = false; + fireReposition(w); +} + + +// ============================================================ +// Public accessor: get field value from current row by column name +// ============================================================ + +// Exported for form runtime data binding (resolved via dlsym) +const char *dataCtrlGetField(WidgetT *w, const char *colName) { + if (!w || !w->data || !colName) { + 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) { + return d->rows[d->currentRow].fields[i]; + } + } + + return ""; +} + + +// ============================================================ +// Paint +// ============================================================ + +static void dataCtrlCalcMinSize(WidgetT *w, const BitmapFontT *font) { + (void)font; + w->calcMinW = DATA_BTN_W * 4 + 60; // 4 buttons + some caption space + w->calcMinH = DATA_HEIGHT; +} + + +static void dataCtrlPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + uint32_t face = colors->buttonFace; + uint32_t fg = colors->contentFg; + uint32_t hi = colors->windowHighlight; + uint32_t sh = colors->windowShadow; + + // Background + rectFill(disp, ops, w->x, w->y, w->w, w->h, face); + + // Outer bevel + BevelStyleT bevel; + bevel.highlight = hi; + bevel.shadow = sh; + bevel.face = face; + bevel.width = DATA_BORDER; + drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel); + + int32_t innerX = w->x + DATA_BORDER; + int32_t innerY = w->y + DATA_BORDER; + int32_t innerW = w->w - DATA_BORDER * 2; + int32_t innerH = w->h - DATA_BORDER * 2; + + // Button zones + int32_t btnH = innerH; + int32_t btnY = innerY; + + // |< button (MoveFirst) + BevelStyleT btnBevel = { hi, sh, face, 1 }; + drawBevel(disp, ops, innerX, btnY, DATA_BTN_W, btnH, &btnBevel); + // Draw |< glyph + int32_t cx = innerX + DATA_BTN_W / 2; + int32_t cy = btnY + btnH / 2; + drawVLine(disp, ops, cx - 3, cy - 4, 9, fg); + + for (int32_t i = 0; i < 4; i++) { + drawHLine(disp, ops, cx - 1 + i, cy - i, 1, fg); + drawHLine(disp, ops, cx - 1 + i, cy + i, 1, fg); + } + + // < button (MovePrev) + drawBevel(disp, ops, innerX + DATA_BTN_W, btnY, DATA_BTN_W, btnH, &btnBevel); + cx = innerX + DATA_BTN_W + DATA_BTN_W / 2; + + for (int32_t i = 0; i < 4; i++) { + drawHLine(disp, ops, cx + i, cy - i, 1, fg); + drawHLine(disp, ops, cx + i, cy + i, 1, fg); + } + + // > button (MoveNext) + int32_t rightX = innerX + innerW - DATA_BTN_W * 2; + drawBevel(disp, ops, rightX, btnY, DATA_BTN_W, btnH, &btnBevel); + cx = rightX + DATA_BTN_W / 2; + + for (int32_t i = 0; i < 4; i++) { + drawHLine(disp, ops, cx - i, cy - i, 1, fg); + drawHLine(disp, ops, cx - i, cy + i, 1, fg); + } + + // >| button (MoveLast) + drawBevel(disp, ops, rightX + DATA_BTN_W, btnY, DATA_BTN_W, btnH, &btnBevel); + cx = rightX + DATA_BTN_W + DATA_BTN_W / 2; + drawVLine(disp, ops, cx + 3, cy - 4, 9, fg); + + for (int32_t i = 0; i < 4; i++) { + drawHLine(disp, ops, cx - i, cy - i, 1, fg); + drawHLine(disp, ops, cx - i, cy + i, 1, fg); + } + + // Caption in center + int32_t captionX = innerX + DATA_BTN_W * 2 + 4; + int32_t captionW = innerW - DATA_BTN_W * 4 - 8; + const char *text = d->caption[0] ? d->caption : d->recordSource; + + if (text[0] && captionW > 0) { + int32_t tw = textWidth(font, text); + int32_t tx = captionX + (captionW - tw) / 2; + int32_t ty = innerY + (innerH - font->charHeight) / 2; + drawText(disp, ops, font, tx, ty, text, fg, face, false); + } +} + + +// ============================================================ +// Mouse handler +// ============================================================ + +static void dataCtrlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + (void)root; + (void)vy; + + sFocusedWidget = w; + + int32_t innerX = w->x + DATA_BORDER; + int32_t innerW = w->w - DATA_BORDER * 2; + int32_t relX = vx - innerX; + + if (relX < DATA_BTN_W) { + dataCtrlMoveFirst(w); + } else if (relX < DATA_BTN_W * 2) { + dataCtrlMovePrev(w); + } else if (relX >= innerW - DATA_BTN_W) { + dataCtrlMoveLast(w); + } else if (relX >= innerW - DATA_BTN_W * 2) { + dataCtrlMoveNext(w); + } +} + + +// ============================================================ +// Property getters/setters +// ============================================================ + +static const char *dataCtrlGetDatabaseName(const WidgetT *w) { + return ((DataCtrlDataT *)w->data)->databaseName; +} + +static void dataCtrlSetDatabaseName(WidgetT *w, const char *val) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + snprintf(d->databaseName, sizeof(d->databaseName), "%s", val ? val : ""); +} + +static const char *dataCtrlGetRecordSource(const WidgetT *w) { + return ((DataCtrlDataT *)w->data)->recordSource; +} + +static void dataCtrlSetRecordSource(WidgetT *w, const char *val) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + snprintf(d->recordSource, sizeof(d->recordSource), "%s", val ? val : ""); +} + +static const char *dataCtrlGetCaption(const WidgetT *w) { + return ((DataCtrlDataT *)w->data)->caption; +} + +static void dataCtrlSetCaption(WidgetT *w, const char *val) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + snprintf(d->caption, sizeof(d->caption), "%s", val ? val : ""); + wgtInvalidatePaint(w); +} + +static bool dataCtrlGetBof(const WidgetT *w) { + return ((DataCtrlDataT *)w->data)->bof; +} + +static bool dataCtrlGetEof(const WidgetT *w) { + return ((DataCtrlDataT *)w->data)->eof; +} + + +// ============================================================ +// Destroy +// ============================================================ + +static void dataCtrlDestroy(WidgetT *w) { + DataCtrlDataT *d = (DataCtrlDataT *)w->data; + + if (d) { + freeCache(d); + free(d); + } +} + + +// ============================================================ +// DXE registration +// ============================================================ + +static const WidgetClassT sClass = { + .version = WGT_CLASS_VERSION, + .flags = WCLASS_FOCUSABLE, + .handlers = { + [WGT_METHOD_PAINT] = (void *)dataCtrlPaint, + [WGT_METHOD_CALC_MIN_SIZE] = (void *)dataCtrlCalcMinSize, + [WGT_METHOD_ON_MOUSE] = (void *)dataCtrlOnMouse, + [WGT_METHOD_DESTROY] = (void *)dataCtrlDestroy, + } +}; + + +static WidgetT *dataCtrlCreate(WidgetT *parent) { + if (!parent) { + return NULL; + } + + WidgetT *w = widgetAlloc(parent, sTypeId); + + if (w) { + DataCtrlDataT *d = (DataCtrlDataT *)calloc(1, sizeof(DataCtrlDataT)); + + if (d) { + d->currentRow = -1; + d->bof = true; + d->eof = true; + } + + w->data = d; + w->minH = wgtPixels(DATA_HEIGHT); + } + + return w; +} + + +static const struct { + WidgetT *(*create)(WidgetT *parent); + void (*refresh)(WidgetT *w); + void (*moveFirst)(WidgetT *w); + void (*movePrev)(WidgetT *w); + void (*moveNext)(WidgetT *w); + void (*moveLast)(WidgetT *w); + const char *(*getField)(WidgetT *w, const char *colName); +} sApi = { + .create = dataCtrlCreate, + .refresh = dataCtrlRefresh, + .moveFirst = dataCtrlMoveFirst, + .movePrev = dataCtrlMovePrev, + .moveNext = dataCtrlMoveNext, + .moveLast = dataCtrlMoveLast, + .getField = dataCtrlGetField, +}; + +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 }, +}; + +static const WgtMethodDescT sMethods[] = { + { "Refresh", WGT_SIG_VOID, (void *)dataCtrlRefresh }, + { "MoveFirst", WGT_SIG_VOID, (void *)dataCtrlMoveFirst }, + { "MovePrevious", WGT_SIG_VOID, (void *)dataCtrlMovePrev }, + { "MoveNext", WGT_SIG_VOID, (void *)dataCtrlMoveNext }, + { "MoveLast", WGT_SIG_VOID, (void *)dataCtrlMoveLast }, +}; + +static const WgtEventDescT sEvents[] = { + { "Reposition" }, +}; + +static const WgtIfaceT sIface = { + .basName = "Data", + .props = sProps, + .propCount = 5, + .methods = sMethods, + .methodCount = 5, + .events = sEvents, + .eventCount = 1, + .createSig = WGT_CREATE_PARENT, + .isContainer = false, + .defaultEvent = "Reposition", + .namePrefix = "Data", +}; + + +void wgtRegister(void) { + sTypeId = wgtRegisterClass(&sClass); + wgtRegisterApi("data", &sApi); + wgtRegisterIface("data", &sIface); +} diff --git a/widgets/spinner/widgetSpinner.c b/widgets/spinner/widgetSpinner.c index 148502e..e76675b 100644 --- a/widgets/spinner/widgetSpinner.c +++ b/widgets/spinner/widgetSpinner.c @@ -5,6 +5,11 @@ // arrow buttons for numeric value entry. The user can either click // the arrows, use Up/Down keys, or type a number directly. // +// Supports two numeric modes: +// Integer mode (default): int32_t value, step, min, max +// Real mode (useReal=true): double value, step, min, max with +// configurable decimal places +// // Design: the widget has two modes -- display mode (showing the // formatted value) and edit mode (allowing free-form text input). // Edit mode is entered on the first text-modifying keystroke and @@ -16,8 +21,8 @@ // The text editing delegates to widgetTextEditOnKey() -- the same // shared single-line editing logic used by TextInput. This gives // the spinner cursor movement, selection, cut/copy/paste, and undo -// for free. Input validation filters non-digit characters before -// they reach the editor, and only allows minus at position 0. +// for free. Input validation filters non-numeric characters before +// they reach the editor. // // Undo uses a single-level swap buffer (same as TextInput): the // current state is copied to undoBuf before each mutation, and @@ -35,39 +40,84 @@ static int32_t sTypeId = -1; +#include #include #include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define SPINNER_BTN_W 14 +#define SPINNER_BORDER 2 +#define SPINNER_PAD 3 +#define SPINNER_BUF_SIZE 32 +#define SPINNER_DEFAULT_DECIMALS 2 + +// ============================================================ +// Per-instance data +// ============================================================ typedef struct { + // Integer mode int32_t value; int32_t minValue; int32_t maxValue; int32_t step; - char buf[16]; + + // Real mode + double realValue; + double realMin; + double realMax; + double realStep; + int32_t decimals; + bool useReal; + + // Text editing state + char buf[SPINNER_BUF_SIZE]; int32_t len; int32_t cursorPos; int32_t scrollOff; int32_t selStart; int32_t selEnd; - char undoBuf[16]; + char undoBuf[SPINNER_BUF_SIZE]; int32_t undoLen; int32_t undoCursor; bool editing; } SpinnerDataT; -#define SPINNER_BTN_W 14 -#define SPINNER_BORDER 2 -#define SPINNER_PAD 3 - // ============================================================ // Prototypes // ============================================================ -static void spinnerClampAndFormat(SpinnerDataT *d); -static void spinnerCommitEdit(SpinnerDataT *d); -static void spinnerFormat(SpinnerDataT *d); -static void spinnerStartEdit(SpinnerDataT *d); +static bool spinnerAllowMinus(const SpinnerDataT *d); +static void spinnerClampAndFormat(SpinnerDataT *d); +static void spinnerCommitEdit(SpinnerDataT *d); +static void spinnerFormat(SpinnerDataT *d); +static void spinnerStartEdit(SpinnerDataT *d); +static bool spinnerValidateBuffer(const SpinnerDataT *d); +static void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font); +static void widgetSpinnerDestroy(WidgetT *w); +static const char *widgetSpinnerGetText(const WidgetT *w); +static void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod); +static void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy); +static void widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void widgetSpinnerSetText(WidgetT *w, const char *text); + + +// ============================================================ +// spinnerAllowMinus -- can negative values be entered? +// ============================================================ + +static bool spinnerAllowMinus(const SpinnerDataT *d) { + if (d->useReal) { + return d->realMin < 0.0; + } + + return d->minValue < 0; +} // ============================================================ @@ -75,12 +125,22 @@ static void spinnerStartEdit(SpinnerDataT *d); // ============================================================ static void spinnerClampAndFormat(SpinnerDataT *d) { - if (d->value < d->minValue) { - d->value = d->minValue; - } + if (d->useReal) { + if (d->realValue < d->realMin) { + d->realValue = d->realMin; + } - if (d->value > d->maxValue) { - d->value = d->maxValue; + if (d->realValue > d->realMax) { + d->realValue = d->realMax; + } + } else { + if (d->value < d->minValue) { + d->value = d->minValue; + } + + if (d->value > d->maxValue) { + d->value = d->maxValue; + } } spinnerFormat(d); @@ -99,8 +159,12 @@ static void spinnerCommitEdit(SpinnerDataT *d) { d->editing = false; d->buf[d->len] = '\0'; - int32_t val = (int32_t)strtol(d->buf, NULL, 10); - d->value = val; + if (d->useReal) { + d->realValue = strtod(d->buf, NULL); + } else { + d->value = (int32_t)strtol(d->buf, NULL, 10); + } + spinnerClampAndFormat(d); } @@ -114,7 +178,12 @@ static void spinnerCommitEdit(SpinnerDataT *d) { // with the numeric value. The cursor-at-end position matches user // expectation after arrow-key increment/decrement. static void spinnerFormat(SpinnerDataT *d) { - d->len = snprintf(d->buf, sizeof(d->buf), "%d", (int)d->value); + if (d->useReal) { + d->len = snprintf(d->buf, sizeof(d->buf), "%.*f", (int)d->decimals, d->realValue); + } else { + d->len = snprintf(d->buf, sizeof(d->buf), "%d", (int)d->value); + } + d->cursorPos = d->len; d->scrollOff = 0; d->selStart = -1; @@ -142,21 +211,74 @@ static void spinnerStartEdit(SpinnerDataT *d) { } +// ============================================================ +// spinnerValidateBuffer -- check buffer contains valid number +// ============================================================ + +static bool spinnerValidateBuffer(const SpinnerDataT *d) { + bool allowMin = spinnerAllowMinus(d); + bool hadDot = false; + + for (int32_t i = 0; i < d->len; i++) { + char c = d->buf[i]; + + if (c == '-' && i == 0 && allowMin) { + continue; + } + + if (c == '.' && d->useReal && !hadDot) { + hadDot = true; + continue; + } + + if (c < '0' || c > '9') { + return false; + } + } + + return true; +} + + +// ============================================================ +// spinnerStep -- apply step in given direction (+1 or -1) +// ============================================================ + +static void spinnerApplyStep(SpinnerDataT *d, int32_t direction, int32_t multiplier) { + if (d->useReal) { + d->realValue += d->realStep * direction * multiplier; + } else { + d->value += d->step * direction * multiplier; + } + + spinnerClampAndFormat(d); +} + + // ============================================================ // widgetSpinnerCalcMinSize // ============================================================ -void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font) { +static void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = font->charWidth * 6 + SPINNER_PAD * 2 + SPINNER_BORDER * 2 + SPINNER_BTN_W; w->calcMinH = font->charHeight + SPINNER_PAD * 2 + SPINNER_BORDER * 2; } +// ============================================================ +// widgetSpinnerDestroy +// ============================================================ + +static void widgetSpinnerDestroy(WidgetT *w) { + free(w->data); +} + + // ============================================================ // widgetSpinnerGetText // ============================================================ -const char *widgetSpinnerGetText(const WidgetT *w) { +static const char *widgetSpinnerGetText(const WidgetT *w) { const SpinnerDataT *d = (const SpinnerDataT *)w->data; return d->buf; } @@ -174,15 +296,13 @@ const char *widgetSpinnerGetText(const WidgetT *w) { // // Page Up/Down use step*10 for coarser adjustment, matching the // convention used by Windows spin controls. -void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { +static void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { SpinnerDataT *d = (SpinnerDataT *)w->data; - int32_t step = d->step; // Up arrow -- increment if (key == (0x48 | 0x100)) { spinnerCommitEdit(d); - d->value += step; - spinnerClampAndFormat(d); + spinnerApplyStep(d, 1, 1); if (w->onChange) { w->onChange(w); @@ -195,8 +315,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { // Down arrow -- decrement if (key == (0x50 | 0x100)) { spinnerCommitEdit(d); - d->value -= step; - spinnerClampAndFormat(d); + spinnerApplyStep(d, -1, 1); if (w->onChange) { w->onChange(w); @@ -209,8 +328,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { // Page Up -- increment by step * 10 if (key == (0x49 | 0x100)) { spinnerCommitEdit(d); - d->value += step * 10; - spinnerClampAndFormat(d); + spinnerApplyStep(d, 1, 10); if (w->onChange) { w->onChange(w); @@ -223,8 +341,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { // Page Down -- decrement by step * 10 if (key == (0x51 | 0x100)) { spinnerCommitEdit(d); - d->value -= step * 10; - spinnerClampAndFormat(d); + spinnerApplyStep(d, -1, 10); if (w->onChange) { w->onChange(w); @@ -260,22 +377,32 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { return; } - // Filter: only allow digits, minus, and control keys through to text editor + // Filter: only allow digits, minus, dot (real mode), and control keys bool isDigit = (key >= '0' && key <= '9'); bool isMinus = (key == '-'); + bool isDot = (key == '.' && d->useReal); bool isControl = (key < 0x20) || (key & 0x100); - if (!isDigit && !isMinus && !isControl) { + if (!isDigit && !isMinus && !isDot && !isControl) { return; } // Minus only at position 0 (and only if min is negative) - if (isMinus && (d->cursorPos != 0 || d->minValue >= 0)) { + if (isMinus && (d->cursorPos != 0 || !spinnerAllowMinus(d))) { return; } + // Dot only once (check if buffer already has one) + if (isDot) { + for (int32_t i = 0; i < d->len; i++) { + if (d->buf[i] == '.') { + return; + } + } + } + // Enter edit mode on first text-modifying key - if (isDigit || isMinus || key == 8 || key == 127 || key == (0x53 | 0x100)) { + if (isDigit || isMinus || isDot || key == 8 || key == 127 || key == (0x53 | 0x100)) { spinnerStartEdit(d); } @@ -291,23 +418,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { w->w - SPINNER_BORDER * 2 - SPINNER_BTN_W); // Validate buffer after paste -- reject non-numeric content. - // Allow optional leading minus and digits only. - bool valid = true; - - for (int32_t i = 0; i < d->len; i++) { - char c = d->buf[i]; - - if (c == '-' && i == 0 && d->minValue < 0) { - continue; - } - - if (c < '0' || c > '9') { - valid = false; - break; - } - } - - if (!valid) { + if (!spinnerValidateBuffer(d)) { // Revert to the undo buffer (pre-paste state) memcpy(d->buf, d->undoBuf, sizeof(d->buf)); d->len = d->undoLen; @@ -332,7 +443,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { // Text area clicks compute cursor position from pixel offset using the // fixed-width font. Double-click selects all text (select-word doesn't // make sense for numbers), entering edit mode to allow replacement. -void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { +static void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { sFocusedWidget = hit; SpinnerDataT *d = (SpinnerDataT *)hit->data; @@ -342,14 +453,7 @@ void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { if (vx >= btnX) { // Click on button area spinnerCommitEdit(d); - - if (vy < midY) { - d->value += d->step; - } else { - d->value -= d->step; - } - - spinnerClampAndFormat(d); + spinnerApplyStep(d, (vy < midY) ? 1 : -1, 1); if (hit->onChange) { hit->onChange(hit); @@ -380,7 +484,7 @@ void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { // button area into the widget's right border so they visually merge // with the outer bevel -- this is why btnW is btnW + SPINNER_BORDER // in the drawBevel calls. -void widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { +static void widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { SpinnerDataT *d = (SpinnerDataT *)w->data; uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; @@ -467,23 +571,19 @@ void widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B // widgetSpinnerSetText // ============================================================ -void widgetSpinnerSetText(WidgetT *w, const char *text) { +static void widgetSpinnerSetText(WidgetT *w, const char *text) { SpinnerDataT *d = (SpinnerDataT *)w->data; - int32_t val = (int32_t)strtol(text, NULL, 10); - d->value = val; + + if (d->useReal) { + d->realValue = strtod(text, NULL); + } else { + d->value = (int32_t)strtol(text, NULL, 10); + } + spinnerClampAndFormat(d); } -// ============================================================ -// widgetSpinnerDestroy -// ============================================================ - -void widgetSpinnerDestroy(WidgetT *w) { - free(w->data); -} - - // ============================================================ // DXE registration // ============================================================ @@ -523,6 +623,8 @@ WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t ste d->maxValue = maxVal; d->step = step > 0 ? step : 1; d->value = minVal; + d->decimals = SPINNER_DEFAULT_DECIMALS; + d->realStep = 1.0; d->selStart = -1; d->selEnd = -1; spinnerFormat(d); @@ -532,6 +634,11 @@ WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t ste } +// ============================================================ +// Public API -- integer mode +// ============================================================ + + int32_t wgtSpinnerGetValue(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, 0); SpinnerDataT *d = (SpinnerDataT *)w->data; @@ -575,7 +682,102 @@ void wgtSpinnerSetValue(WidgetT *w, int32_t value) { // ============================================================ -// DXE registration +// Public API -- real mode +// ============================================================ + + +void wgtSpinnerSetRealMode(WidgetT *w, bool enable) { + VALIDATE_WIDGET_VOID(w, sTypeId); + SpinnerDataT *d = (SpinnerDataT *)w->data; + + d->useReal = enable; + spinnerClampAndFormat(d); + wgtInvalidate(w); +} + + +double wgtSpinnerGetRealValue(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0.0); + SpinnerDataT *d = (SpinnerDataT *)w->data; + + if (d->editing) { + spinnerCommitEdit(d); + } + + return d->realValue; +} + + +void wgtSpinnerSetRealValue(WidgetT *w, double value) { + VALIDATE_WIDGET_VOID(w, sTypeId); + SpinnerDataT *d = (SpinnerDataT *)w->data; + + d->realValue = value; + spinnerClampAndFormat(d); + wgtInvalidate(w); +} + + +void wgtSpinnerSetRealRange(WidgetT *w, double minVal, double maxVal) { + VALIDATE_WIDGET_VOID(w, sTypeId); + SpinnerDataT *d = (SpinnerDataT *)w->data; + + d->realMin = minVal; + d->realMax = maxVal; + spinnerClampAndFormat(d); + wgtInvalidate(w); +} + + +void wgtSpinnerSetRealStep(WidgetT *w, double step) { + VALIDATE_WIDGET_VOID(w, sTypeId); + SpinnerDataT *d = (SpinnerDataT *)w->data; + + d->realStep = step > 0.0 ? step : 0.01; +} + + +void wgtSpinnerSetDecimals(WidgetT *w, int32_t decimals) { + VALIDATE_WIDGET_VOID(w, sTypeId); + SpinnerDataT *d = (SpinnerDataT *)w->data; + + d->decimals = (decimals >= 0 && decimals <= 10) ? decimals : SPINNER_DEFAULT_DECIMALS; + spinnerFormat(d); + wgtInvalidate(w); +} + + +// ============================================================ +// Property getters/setters for WgtIfaceT +// ============================================================ + +static int32_t ifaceGetValue(const WidgetT *w) { + return wgtSpinnerGetValue(w); +} + +static void ifaceSetValue(WidgetT *w, int32_t v) { + wgtSpinnerSetValue(w, v); +} + +static bool ifaceGetRealMode(const WidgetT *w) { + return ((SpinnerDataT *)w->data)->useReal; +} + +static void ifaceSetRealMode(WidgetT *w, bool v) { + wgtSpinnerSetRealMode(w, v); +} + +static int32_t ifaceGetDecimals(const WidgetT *w) { + return ((SpinnerDataT *)w->data)->decimals; +} + +static void ifaceSetDecimals(WidgetT *w, int32_t v) { + wgtSpinnerSetDecimals(w, v); +} + + +// ============================================================ +// DXE API and interface registration // ============================================================ @@ -585,16 +787,30 @@ static const struct { int32_t (*getValue)(const WidgetT *w); void (*setRange)(WidgetT *w, int32_t minVal, int32_t maxVal); void (*setStep)(WidgetT *w, int32_t step); + void (*setRealMode)(WidgetT *w, bool enable); + double (*getRealValue)(const WidgetT *w); + void (*setRealValue)(WidgetT *w, double value); + void (*setRealRange)(WidgetT *w, double minVal, double maxVal); + void (*setRealStep)(WidgetT *w, double step); + void (*setDecimals)(WidgetT *w, int32_t decimals); } sApi = { - .create = wgtSpinner, - .setValue = wgtSpinnerSetValue, - .getValue = wgtSpinnerGetValue, - .setRange = wgtSpinnerSetRange, - .setStep = wgtSpinnerSetStep + .create = wgtSpinner, + .setValue = wgtSpinnerSetValue, + .getValue = wgtSpinnerGetValue, + .setRange = wgtSpinnerSetRange, + .setStep = wgtSpinnerSetStep, + .setRealMode = wgtSpinnerSetRealMode, + .getRealValue = wgtSpinnerGetRealValue, + .setRealValue = wgtSpinnerSetRealValue, + .setRealRange = wgtSpinnerSetRealRange, + .setRealStep = wgtSpinnerSetRealStep, + .setDecimals = wgtSpinnerSetDecimals, }; static const WgtPropDescT sProps[] = { - { "Value", WGT_IFACE_INT, (void *)wgtSpinnerGetValue, (void *)wgtSpinnerSetValue } + { "Value", WGT_IFACE_INT, (void *)ifaceGetValue, (void *)ifaceSetValue, NULL }, + { "RealMode", WGT_IFACE_BOOL, (void *)ifaceGetRealMode, (void *)ifaceSetRealMode, NULL }, + { "Decimals", WGT_IFACE_INT, (void *)ifaceGetDecimals, (void *)ifaceSetDecimals, NULL }, }; static const WgtMethodDescT sMethods[] = { @@ -605,16 +821,18 @@ static const WgtMethodDescT sMethods[] = { static const WgtIfaceT sIface = { .basName = "SpinButton", .props = sProps, - .propCount = 1, + .propCount = 3, .methods = sMethods, .methodCount = 2, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT_INT_INT, .createArgs = { 0, 100, 1 }, - .defaultEvent = "Change" + .defaultEvent = "Change", + .namePrefix = "Spin", }; + void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSpinner); wgtRegisterApi("spinner", &sApi); diff --git a/widgets/widgetDataCtrl.h b/widgets/widgetDataCtrl.h new file mode 100644 index 0000000..697da23 --- /dev/null +++ b/widgets/widgetDataCtrl.h @@ -0,0 +1,30 @@ +// widgetDataCtrl.h -- VB3-style Data control for database binding + +#ifndef WIDGET_DATACTRL_H +#define WIDGET_DATACTRL_H + +#include "dvxWidget.h" + +typedef struct { + WidgetT *(*create)(WidgetT *parent); + void (*refresh)(WidgetT *w); + void (*moveFirst)(WidgetT *w); + void (*movePrev)(WidgetT *w); + void (*moveNext)(WidgetT *w); + void (*moveLast)(WidgetT *w); + const char *(*getField)(WidgetT *w, const char *colName); +} 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) + +#endif // WIDGET_DATACTRL_H diff --git a/widgets/widgetSpinner.h b/widgets/widgetSpinner.h index 6aec79f..1b85624 100644 --- a/widgets/widgetSpinner.h +++ b/widgets/widgetSpinner.h @@ -10,6 +10,12 @@ typedef struct { int32_t (*getValue)(const WidgetT *w); void (*setRange)(WidgetT *w, int32_t minVal, int32_t maxVal); void (*setStep)(WidgetT *w, int32_t step); + void (*setRealMode)(WidgetT *w, bool enable); + double (*getRealValue)(const WidgetT *w); + void (*setRealValue)(WidgetT *w, double value); + void (*setRealRange)(WidgetT *w, double minVal, double maxVal); + void (*setRealStep)(WidgetT *w, double step); + void (*setDecimals)(WidgetT *w, int32_t decimals); } SpinnerApiT; static inline const SpinnerApiT *dvxSpinnerApi(void) { @@ -23,5 +29,11 @@ static inline const SpinnerApiT *dvxSpinnerApi(void) { #define wgtSpinnerGetValue(w) dvxSpinnerApi()->getValue(w) #define wgtSpinnerSetRange(w, minVal, maxVal) dvxSpinnerApi()->setRange(w, minVal, maxVal) #define wgtSpinnerSetStep(w, step) dvxSpinnerApi()->setStep(w, step) +#define wgtSpinnerSetRealMode(w, enable) dvxSpinnerApi()->setRealMode(w, enable) +#define wgtSpinnerGetRealValue(w) dvxSpinnerApi()->getRealValue(w) +#define wgtSpinnerSetRealValue(w, value) dvxSpinnerApi()->setRealValue(w, value) +#define wgtSpinnerSetRealRange(w, minVal, maxVal) dvxSpinnerApi()->setRealRange(w, minVal, maxVal) +#define wgtSpinnerSetRealStep(w, step) dvxSpinnerApi()->setRealStep(w, step) +#define wgtSpinnerSetDecimals(w, decimals) dvxSpinnerApi()->setDecimals(w, decimals) #endif // WIDGET_SPINNER_H