577 lines
16 KiB
C
577 lines
16 KiB
C
#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);
|
|
}
|