Data binding!

This commit is contained in:
Scott Duensing 2026-04-04 20:00:25 -05:00
parent d3898707f9
commit 827d73fbd1
16 changed files with 1506 additions and 102 deletions

View file

@ -13,6 +13,7 @@
#include "thirdparty/stb_ds_wrap.h"
#include <ctype.h>
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -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);

View file

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

View file

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

View file

@ -13,6 +13,7 @@
#include "widgetSplitter.h"
#include "widgetTreeView.h"
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -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];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

BIN
widgets/dataCtrl/datactr.bmp (Stored with Git LFS) Normal file

Binary file not shown.

View file

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

View file

@ -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 <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// 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);
}

View file

@ -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 <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// 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);

30
widgets/widgetDataCtrl.h Normal file
View file

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

View file

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