Data binding!
This commit is contained in:
parent
d3898707f9
commit
827d73fbd1
16 changed files with 1506 additions and 102 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
134
core/dvxDialog.c
134
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
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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 $@
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
BIN
widgets/dataCtrl/datactr.bmp
(Stored with Git LFS)
Normal file
Binary file not shown.
5
widgets/dataCtrl/datactr.res
Normal file
5
widgets/dataCtrl/datactr.res
Normal 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"
|
||||
577
widgets/dataCtrl/widgetDataCtrl.c
Normal file
577
widgets/dataCtrl/widgetDataCtrl.c
Normal 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);
|
||||
}
|
||||
|
|
@ -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
30
widgets/widgetDataCtrl.h
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue