#define DVX_WIDGET_IMPL // widgetDataCtrl.c -- VB3-style Data control for database binding // // A visible navigation bar that connects to a SQLite database via // dvxSql* functions. Reads all rows from the RecordSource query // into an in-memory cache for bidirectional navigation. Fires // Reposition events when the cursor moves so bound controls can // update. // // The control resolves dvxSql* functions via dlsym at first use, // keeping the widget DXE independent of the SQL library DXE at // link time (both are loaded by the same loader, so dlsym works). #include "dvxWidgetPlugin.h" #include "thirdparty/stb_ds_wrap.h" #include #include #include #include #include // ============================================================ // Constants // ============================================================ #define DATA_HEIGHT 24 #define DATA_BTN_W 20 #define DATA_BORDER 2 #define DATA_MAX_COLS 64 #define DATA_MAX_FIELD 256 // ============================================================ // Per-row cache entry // ============================================================ typedef struct { char **fields; // array of strdup'd strings, one per column int32_t fieldCount; } DataRowT; // ============================================================ // Per-instance data // ============================================================ typedef struct { char databaseName[DATA_MAX_FIELD]; char recordSource[DATA_MAX_FIELD]; char caption[DATA_MAX_FIELD]; // Cached result set DataRowT *rows; // stb_ds dynamic array int32_t rowCount; char **colNames; // stb_ds dynamic array of strdup'd names int32_t colCount; int32_t currentRow; // 0-based, -1 = no rows bool bof; bool eof; // SQL function pointers (resolved via dlsym) int32_t (*sqlOpen)(const char *); void (*sqlClose)(int32_t); int32_t (*sqlQuery)(int32_t, const char *); bool (*sqlNext)(int32_t); bool (*sqlEof)(int32_t); int32_t (*sqlFieldCount)(int32_t); const char *(*sqlFieldName)(int32_t, int32_t); const char *(*sqlFieldText)(int32_t, int32_t); void (*sqlFreeResult)(int32_t); bool sqlResolved; } DataCtrlDataT; static int32_t sTypeId = -1; // ============================================================ // resolveSql -- lazily resolve dvxSql* function pointers // ============================================================ static void resolveSql(DataCtrlDataT *d) { if (d->sqlResolved) { return; } d->sqlOpen = (int32_t (*)(const char *))dlsym(NULL, "_dvxSqlOpen"); d->sqlClose = (void (*)(int32_t))dlsym(NULL, "_dvxSqlClose"); d->sqlQuery = (int32_t (*)(int32_t, const char *))dlsym(NULL, "_dvxSqlQuery"); d->sqlNext = (bool (*)(int32_t))dlsym(NULL, "_dvxSqlNext"); d->sqlEof = (bool (*)(int32_t))dlsym(NULL, "_dvxSqlEof"); d->sqlFieldCount = (int32_t (*)(int32_t))dlsym(NULL, "_dvxSqlFieldCount"); d->sqlFieldName = (const char *(*)(int32_t, int32_t))dlsym(NULL, "_dvxSqlFieldName"); d->sqlFieldText = (const char *(*)(int32_t, int32_t))dlsym(NULL, "_dvxSqlFieldText"); d->sqlFreeResult = (void (*)(int32_t))dlsym(NULL, "_dvxSqlFreeResult"); d->sqlResolved = true; } // ============================================================ // freeCache -- release the cached result set // ============================================================ static void freeCache(DataCtrlDataT *d) { for (int32_t i = 0; i < d->rowCount; i++) { for (int32_t j = 0; j < d->rows[i].fieldCount; j++) { free(d->rows[i].fields[j]); } free(d->rows[i].fields); } arrfree(d->rows); d->rows = NULL; d->rowCount = 0; for (int32_t i = 0; i < d->colCount; i++) { free(d->colNames[i]); } arrfree(d->colNames); d->colNames = NULL; d->colCount = 0; d->currentRow = -1; d->bof = true; d->eof = true; } // ============================================================ // dataCtrlRefresh -- execute query and cache all rows // ============================================================ void dataCtrlRefresh(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; resolveSql(d); freeCache(d); if (!d->sqlOpen || !d->databaseName[0] || !d->recordSource[0]) { return; } int32_t db = d->sqlOpen(d->databaseName); if (db <= 0) { return; } // If recordSource doesn't start with SELECT, wrap it as "SELECT * FROM table" char query[512]; if (strncasecmp(d->recordSource, "SELECT ", 7) == 0) { snprintf(query, sizeof(query), "%s", d->recordSource); } else { snprintf(query, sizeof(query), "SELECT * FROM %s", d->recordSource); } int32_t rs = d->sqlQuery(db, query); if (rs <= 0) { d->sqlClose(db); return; } // Cache column names d->colCount = d->sqlFieldCount(rs); for (int32_t i = 0; i < d->colCount; i++) { const char *name = d->sqlFieldName(rs, i); arrput(d->colNames, strdup(name ? name : "")); } // Cache all rows while (d->sqlNext(rs)) { DataRowT row; row.fieldCount = d->colCount; row.fields = (char **)malloc(d->colCount * sizeof(char *)); for (int32_t i = 0; i < d->colCount; i++) { const char *text = d->sqlFieldText(rs, i); row.fields[i] = strdup(text ? text : ""); } arrput(d->rows, row); } d->rowCount = (int32_t)arrlen(d->rows); d->sqlFreeResult(rs); d->sqlClose(db); if (d->rowCount > 0) { d->currentRow = 0; d->bof = false; d->eof = false; } // Auto-caption if (!d->caption[0]) { snprintf(d->caption, sizeof(d->caption), "%s", d->recordSource); } wgtInvalidatePaint(w); } // ============================================================ // Navigation methods // ============================================================ static void fireReposition(WidgetT *w) { wgtInvalidatePaint(w); if (w->onChange) { w->onChange(w); } } static void dataCtrlMoveFirst(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; if (d->rowCount <= 0) { return; } d->currentRow = 0; d->bof = false; d->eof = false; fireReposition(w); } static void dataCtrlMovePrev(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; if (d->rowCount <= 0 || d->currentRow <= 0) { d->bof = true; return; } d->currentRow--; d->bof = (d->currentRow == 0); d->eof = false; fireReposition(w); } static void dataCtrlMoveNext(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; if (d->rowCount <= 0 || d->currentRow >= d->rowCount - 1) { d->eof = true; return; } d->currentRow++; d->bof = false; d->eof = (d->currentRow >= d->rowCount - 1); fireReposition(w); } static void dataCtrlMoveLast(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; if (d->rowCount <= 0) { return; } d->currentRow = d->rowCount - 1; d->bof = false; d->eof = false; fireReposition(w); } // ============================================================ // Public accessor: get field value from current row by column name // ============================================================ // Exported for form runtime data binding (resolved via dlsym) const char *dataCtrlGetField(WidgetT *w, const char *colName) { if (!w || !w->data || !colName) { return ""; } DataCtrlDataT *d = (DataCtrlDataT *)w->data; if (d->currentRow < 0 || d->currentRow >= d->rowCount) { return ""; } for (int32_t i = 0; i < d->colCount; i++) { if (strcasecmp(d->colNames[i], colName) == 0) { return d->rows[d->currentRow].fields[i]; } } return ""; } // ============================================================ // Paint // ============================================================ static void dataCtrlCalcMinSize(WidgetT *w, const BitmapFontT *font) { (void)font; w->calcMinW = DATA_BTN_W * 4 + 60; // 4 buttons + some caption space w->calcMinH = DATA_HEIGHT; } static void dataCtrlPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; uint32_t face = colors->buttonFace; uint32_t fg = colors->contentFg; uint32_t hi = colors->windowHighlight; uint32_t sh = colors->windowShadow; // Background rectFill(disp, ops, w->x, w->y, w->w, w->h, face); // Outer bevel BevelStyleT bevel; bevel.highlight = hi; bevel.shadow = sh; bevel.face = face; bevel.width = DATA_BORDER; drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel); int32_t innerX = w->x + DATA_BORDER; int32_t innerY = w->y + DATA_BORDER; int32_t innerW = w->w - DATA_BORDER * 2; int32_t innerH = w->h - DATA_BORDER * 2; // Button zones int32_t btnH = innerH; int32_t btnY = innerY; // |< button (MoveFirst) BevelStyleT btnBevel = { hi, sh, face, 1 }; drawBevel(disp, ops, innerX, btnY, DATA_BTN_W, btnH, &btnBevel); // Draw |< glyph int32_t cx = innerX + DATA_BTN_W / 2; int32_t cy = btnY + btnH / 2; drawVLine(disp, ops, cx - 3, cy - 4, 9, fg); for (int32_t i = 0; i < 4; i++) { drawHLine(disp, ops, cx - 1 + i, cy - i, 1, fg); drawHLine(disp, ops, cx - 1 + i, cy + i, 1, fg); } // < button (MovePrev) drawBevel(disp, ops, innerX + DATA_BTN_W, btnY, DATA_BTN_W, btnH, &btnBevel); cx = innerX + DATA_BTN_W + DATA_BTN_W / 2; for (int32_t i = 0; i < 4; i++) { drawHLine(disp, ops, cx + i, cy - i, 1, fg); drawHLine(disp, ops, cx + i, cy + i, 1, fg); } // > button (MoveNext) int32_t rightX = innerX + innerW - DATA_BTN_W * 2; drawBevel(disp, ops, rightX, btnY, DATA_BTN_W, btnH, &btnBevel); cx = rightX + DATA_BTN_W / 2; for (int32_t i = 0; i < 4; i++) { drawHLine(disp, ops, cx - i, cy - i, 1, fg); drawHLine(disp, ops, cx - i, cy + i, 1, fg); } // >| button (MoveLast) drawBevel(disp, ops, rightX + DATA_BTN_W, btnY, DATA_BTN_W, btnH, &btnBevel); cx = rightX + DATA_BTN_W + DATA_BTN_W / 2; drawVLine(disp, ops, cx + 3, cy - 4, 9, fg); for (int32_t i = 0; i < 4; i++) { drawHLine(disp, ops, cx - i, cy - i, 1, fg); drawHLine(disp, ops, cx - i, cy + i, 1, fg); } // Caption in center int32_t captionX = innerX + DATA_BTN_W * 2 + 4; int32_t captionW = innerW - DATA_BTN_W * 4 - 8; const char *text = d->caption[0] ? d->caption : d->recordSource; if (text[0] && captionW > 0) { int32_t tw = textWidth(font, text); int32_t tx = captionX + (captionW - tw) / 2; int32_t ty = innerY + (innerH - font->charHeight) / 2; drawText(disp, ops, font, tx, ty, text, fg, face, false); } } // ============================================================ // Mouse handler // ============================================================ static void dataCtrlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; (void)vy; sFocusedWidget = w; int32_t innerX = w->x + DATA_BORDER; int32_t innerW = w->w - DATA_BORDER * 2; int32_t relX = vx - innerX; if (relX < DATA_BTN_W) { dataCtrlMoveFirst(w); } else if (relX < DATA_BTN_W * 2) { dataCtrlMovePrev(w); } else if (relX >= innerW - DATA_BTN_W) { dataCtrlMoveLast(w); } else if (relX >= innerW - DATA_BTN_W * 2) { dataCtrlMoveNext(w); } } // ============================================================ // Property getters/setters // ============================================================ static const char *dataCtrlGetDatabaseName(const WidgetT *w) { return ((DataCtrlDataT *)w->data)->databaseName; } static void dataCtrlSetDatabaseName(WidgetT *w, const char *val) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; snprintf(d->databaseName, sizeof(d->databaseName), "%s", val ? val : ""); } static const char *dataCtrlGetRecordSource(const WidgetT *w) { return ((DataCtrlDataT *)w->data)->recordSource; } static void dataCtrlSetRecordSource(WidgetT *w, const char *val) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; snprintf(d->recordSource, sizeof(d->recordSource), "%s", val ? val : ""); } static const char *dataCtrlGetCaption(const WidgetT *w) { return ((DataCtrlDataT *)w->data)->caption; } static void dataCtrlSetCaption(WidgetT *w, const char *val) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; snprintf(d->caption, sizeof(d->caption), "%s", val ? val : ""); wgtInvalidatePaint(w); } static bool dataCtrlGetBof(const WidgetT *w) { return ((DataCtrlDataT *)w->data)->bof; } static bool dataCtrlGetEof(const WidgetT *w) { return ((DataCtrlDataT *)w->data)->eof; } // ============================================================ // Destroy // ============================================================ static void dataCtrlDestroy(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; if (d) { freeCache(d); free(d); } } // ============================================================ // DXE registration // ============================================================ static const WidgetClassT sClass = { .version = WGT_CLASS_VERSION, .flags = WCLASS_FOCUSABLE, .handlers = { [WGT_METHOD_PAINT] = (void *)dataCtrlPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)dataCtrlCalcMinSize, [WGT_METHOD_ON_MOUSE] = (void *)dataCtrlOnMouse, [WGT_METHOD_DESTROY] = (void *)dataCtrlDestroy, } }; static WidgetT *dataCtrlCreate(WidgetT *parent) { if (!parent) { return NULL; } WidgetT *w = widgetAlloc(parent, sTypeId); if (w) { DataCtrlDataT *d = (DataCtrlDataT *)calloc(1, sizeof(DataCtrlDataT)); if (d) { d->currentRow = -1; d->bof = true; d->eof = true; } w->data = d; w->minH = wgtPixels(DATA_HEIGHT); } return w; } static const struct { WidgetT *(*create)(WidgetT *parent); void (*refresh)(WidgetT *w); void (*moveFirst)(WidgetT *w); void (*movePrev)(WidgetT *w); void (*moveNext)(WidgetT *w); void (*moveLast)(WidgetT *w); const char *(*getField)(WidgetT *w, const char *colName); } sApi = { .create = dataCtrlCreate, .refresh = dataCtrlRefresh, .moveFirst = dataCtrlMoveFirst, .movePrev = dataCtrlMovePrev, .moveNext = dataCtrlMoveNext, .moveLast = dataCtrlMoveLast, .getField = dataCtrlGetField, }; static const WgtPropDescT sProps[] = { { "DatabaseName", WGT_IFACE_STRING, (void *)dataCtrlGetDatabaseName, (void *)dataCtrlSetDatabaseName, NULL }, { "RecordSource", WGT_IFACE_STRING, (void *)dataCtrlGetRecordSource, (void *)dataCtrlSetRecordSource, NULL }, { "Caption", WGT_IFACE_STRING, (void *)dataCtrlGetCaption, (void *)dataCtrlSetCaption, NULL }, { "BOF", WGT_IFACE_BOOL, (void *)dataCtrlGetBof, NULL, NULL }, { "EOF", WGT_IFACE_BOOL, (void *)dataCtrlGetEof, NULL, NULL }, }; static const WgtMethodDescT sMethods[] = { { "Refresh", WGT_SIG_VOID, (void *)dataCtrlRefresh }, { "MoveFirst", WGT_SIG_VOID, (void *)dataCtrlMoveFirst }, { "MovePrevious", WGT_SIG_VOID, (void *)dataCtrlMovePrev }, { "MoveNext", WGT_SIG_VOID, (void *)dataCtrlMoveNext }, { "MoveLast", WGT_SIG_VOID, (void *)dataCtrlMoveLast }, }; static const WgtEventDescT sEvents[] = { { "Reposition" }, }; static const WgtIfaceT sIface = { .basName = "Data", .props = sProps, .propCount = 5, .methods = sMethods, .methodCount = 5, .events = sEvents, .eventCount = 1, .createSig = WGT_CREATE_PARENT, .isContainer = false, .defaultEvent = "Reposition", .namePrefix = "Data", }; void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClass); wgtRegisterApi("data", &sApi); wgtRegisterIface("data", &sIface); }