998 lines
28 KiB
C
998 lines
28 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.
|
|
//
|
|
// Depends on the dvxsql library DXE (via datactrl.dep) so all
|
|
// dvxSql* functions are resolved at load time.
|
|
|
|
#include "dvxWgtP.h"
|
|
#include "../../sql/dvxSql.h"
|
|
#include "thirdparty/stb_ds_wrap.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];
|
|
char keyColumn[DATA_MAX_FIELD];
|
|
|
|
// Master-detail linking
|
|
char masterSource[DATA_MAX_FIELD]; // name of master Data control
|
|
char masterField[DATA_MAX_FIELD]; // column in master to read
|
|
char detailField[DATA_MAX_FIELD]; // column in this table to filter
|
|
char masterValue[DATA_MAX_FIELD]; // current filter value (set by runtime)
|
|
|
|
// 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;
|
|
bool dirty; // current row has unsaved changes
|
|
bool isNewRow; // current row was created by AddNew
|
|
} DataCtrlDataT;
|
|
|
|
static int32_t sTypeId = -1;
|
|
|
|
// Forward declarations for functions referenced before definition
|
|
void dataCtrlUpdate(WidgetT *w);
|
|
|
|
|
|
// ============================================================
|
|
// 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;
|
|
|
|
|
|
freeCache(d);
|
|
|
|
if (!d->databaseName[0] || !d->recordSource[0]) {
|
|
return;
|
|
}
|
|
|
|
int32_t db = dvxSqlOpen(d->databaseName);
|
|
|
|
if (db <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Build query from RecordSource, with optional master-detail WHERE clause
|
|
char query[1024];
|
|
|
|
if (strncasecmp(d->recordSource, "SELECT ", 7) == 0) {
|
|
snprintf(query, sizeof(query), "%s", d->recordSource);
|
|
} else {
|
|
snprintf(query, sizeof(query), "SELECT * FROM %s", d->recordSource);
|
|
}
|
|
|
|
// Append WHERE filter for master-detail linking
|
|
if (d->detailField[0] && d->masterValue[0]) {
|
|
char escaped[DATA_MAX_FIELD * 2];
|
|
dvxSqlEscape(d->masterValue, escaped, sizeof(escaped));
|
|
|
|
int32_t len = (int32_t)strlen(query);
|
|
snprintf(query + len, sizeof(query) - len, " WHERE %s='%s'", d->detailField, escaped);
|
|
}
|
|
|
|
int32_t rs = dvxSqlQuery(db, query);
|
|
|
|
if (rs <= 0) {
|
|
dvxSqlClose(db);
|
|
return;
|
|
}
|
|
|
|
// Cache column names
|
|
d->colCount = dvxSqlFieldCount(rs);
|
|
|
|
for (int32_t i = 0; i < d->colCount; i++) {
|
|
const char *name = dvxSqlFieldName(rs, i);
|
|
arrput(d->colNames, strdup(name ? name : ""));
|
|
}
|
|
|
|
// Cache all rows
|
|
while (dvxSqlNext(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 = dvxSqlFieldText(rs, i);
|
|
row.fields[i] = strdup(text ? text : "");
|
|
}
|
|
|
|
arrput(d->rows, row);
|
|
}
|
|
|
|
d->rowCount = (int32_t)arrlen(d->rows);
|
|
dvxSqlFreeResult(rs);
|
|
dvxSqlClose(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 autoSave(WidgetT *w) {
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
|
|
if (d->dirty) {
|
|
dataCtrlUpdate(w);
|
|
}
|
|
}
|
|
|
|
|
|
static void dataCtrlMoveFirst(WidgetT *w) {
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
|
|
if (d->rowCount <= 0) {
|
|
return;
|
|
}
|
|
|
|
autoSave(w);
|
|
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;
|
|
}
|
|
|
|
autoSave(w);
|
|
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;
|
|
}
|
|
|
|
autoSave(w);
|
|
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;
|
|
}
|
|
|
|
autoSave(w);
|
|
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 "";
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dataCtrlSetField -- update a field value in the current cached row
|
|
// ============================================================
|
|
|
|
void dataCtrlSetField(WidgetT *w, const char *colName, const char *value) {
|
|
if (!w || !w->data || !colName || !value) {
|
|
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) {
|
|
free(d->rows[d->currentRow].fields[i]);
|
|
d->rows[d->currentRow].fields[i] = strdup(value);
|
|
d->dirty = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// findKeyCol -- locate the key column index
|
|
// ============================================================
|
|
|
|
static int32_t findKeyCol(const DataCtrlDataT *d) {
|
|
if (d->keyColumn[0]) {
|
|
for (int32_t i = 0; i < d->colCount; i++) {
|
|
if (strcasecmp(d->colNames[i], d->keyColumn) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// canWriteBack -- check if write operations are possible
|
|
// ============================================================
|
|
|
|
static bool canWriteBack(const DataCtrlDataT *d) {
|
|
if (!d->databaseName[0] || !d->recordSource[0]) {
|
|
return false;
|
|
}
|
|
|
|
// Write-back only for simple table names, not SELECT queries
|
|
if (strncasecmp(d->recordSource, "SELECT ", 7) == 0) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dataCtrlUpdate -- save the current row to the database
|
|
// ============================================================
|
|
//
|
|
// If the current row was created by AddNew, executes an INSERT.
|
|
// Otherwise executes an UPDATE using the KeyColumn to identify the row.
|
|
// Clears the dirty and isNewRow flags on success.
|
|
|
|
void dataCtrlUpdate(WidgetT *w) {
|
|
if (!w || !w->data) {
|
|
return;
|
|
}
|
|
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
|
|
if (!d->dirty || !canWriteBack(d)) {
|
|
return;
|
|
}
|
|
|
|
if (d->currentRow < 0 || d->currentRow >= d->rowCount || d->colCount == 0) {
|
|
return;
|
|
}
|
|
|
|
// Fire Validate event — return false cancels the write
|
|
if (w->onValidate && !w->onValidate(w)) {
|
|
return;
|
|
}
|
|
|
|
int32_t db = dvxSqlOpen(d->databaseName);
|
|
|
|
if (db <= 0) {
|
|
return;
|
|
}
|
|
|
|
char sql[2048];
|
|
int32_t pos = 0;
|
|
|
|
if (d->isNewRow) {
|
|
// INSERT INTO table (col1, col2, ...) VALUES ('val1', 'val2', ...)
|
|
pos = snprintf(sql, sizeof(sql), "INSERT INTO %s (", d->recordSource);
|
|
|
|
for (int32_t i = 0; i < d->colCount; i++) {
|
|
if (i > 0) {
|
|
pos += snprintf(sql + pos, sizeof(sql) - pos, ", ");
|
|
}
|
|
|
|
pos += snprintf(sql + pos, sizeof(sql) - pos, "%s", d->colNames[i]);
|
|
}
|
|
|
|
pos += snprintf(sql + pos, sizeof(sql) - pos, ") VALUES (");
|
|
|
|
for (int32_t i = 0; i < d->colCount; i++) {
|
|
if (i > 0) {
|
|
pos += snprintf(sql + pos, sizeof(sql) - pos, ", ");
|
|
}
|
|
|
|
char escaped[DATA_MAX_FIELD * 2];
|
|
dvxSqlEscape(d->rows[d->currentRow].fields[i], escaped, sizeof(escaped));
|
|
pos += snprintf(sql + pos, sizeof(sql) - pos, "'%s'", escaped);
|
|
}
|
|
|
|
pos += snprintf(sql + pos, sizeof(sql) - pos, ")");
|
|
} else {
|
|
// UPDATE table SET col1='val1', ... WHERE keyCol=keyVal
|
|
int32_t keyCol = findKeyCol(d);
|
|
const char *keyVal = d->rows[d->currentRow].fields[keyCol];
|
|
|
|
pos = snprintf(sql, sizeof(sql), "UPDATE %s SET ", d->recordSource);
|
|
|
|
bool first = true;
|
|
|
|
for (int32_t i = 0; i < d->colCount; i++) {
|
|
if (i == keyCol) {
|
|
continue;
|
|
}
|
|
|
|
if (!first) {
|
|
pos += snprintf(sql + pos, sizeof(sql) - pos, ", ");
|
|
}
|
|
|
|
char escaped[DATA_MAX_FIELD * 2];
|
|
dvxSqlEscape(d->rows[d->currentRow].fields[i], escaped, sizeof(escaped));
|
|
pos += snprintf(sql + pos, sizeof(sql) - pos, "%s='%s'", d->colNames[i], escaped);
|
|
first = false;
|
|
}
|
|
|
|
pos += snprintf(sql + pos, sizeof(sql) - pos, " WHERE %s=%s", d->colNames[keyCol], keyVal);
|
|
}
|
|
|
|
dvxSqlExec(db, sql);
|
|
dvxSqlClose(db);
|
|
d->dirty = false;
|
|
d->isNewRow = false;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dataCtrlUpdateRow -- legacy wrapper, calls dataCtrlUpdate
|
|
// ============================================================
|
|
|
|
void dataCtrlUpdateRow(WidgetT *w) {
|
|
dataCtrlUpdate(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dataCtrlAddNew -- append a blank row and move cursor to it
|
|
// ============================================================
|
|
|
|
void dataCtrlAddNew(WidgetT *w) {
|
|
if (!w || !w->data) {
|
|
return;
|
|
}
|
|
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
|
|
// Auto-save current row if dirty
|
|
if (d->dirty) {
|
|
dataCtrlUpdate(w);
|
|
}
|
|
|
|
if (d->colCount == 0) {
|
|
return;
|
|
}
|
|
|
|
// Append a blank row
|
|
DataRowT row;
|
|
row.fieldCount = d->colCount;
|
|
row.fields = (char **)malloc(d->colCount * sizeof(char *));
|
|
|
|
for (int32_t i = 0; i < d->colCount; i++) {
|
|
row.fields[i] = strdup("");
|
|
}
|
|
|
|
arrput(d->rows, row);
|
|
d->rowCount = (int32_t)arrlen(d->rows);
|
|
d->currentRow = d->rowCount - 1;
|
|
d->bof = false;
|
|
d->eof = false;
|
|
d->dirty = true;
|
|
d->isNewRow = true;
|
|
|
|
fireReposition(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dataCtrlDelete -- delete the current row from DB and cache
|
|
// ============================================================
|
|
|
|
void dataCtrlDelete(WidgetT *w) {
|
|
if (!w || !w->data) {
|
|
return;
|
|
}
|
|
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
|
|
if (d->currentRow < 0 || d->currentRow >= d->rowCount || d->colCount == 0) {
|
|
return;
|
|
}
|
|
|
|
// Delete from database (unless it's an unsaved new row)
|
|
if (!d->isNewRow && canWriteBack(d)) {
|
|
int32_t db = dvxSqlOpen(d->databaseName);
|
|
|
|
if (db > 0) {
|
|
int32_t keyCol = findKeyCol(d);
|
|
const char *keyVal = d->rows[d->currentRow].fields[keyCol];
|
|
|
|
char sql[512];
|
|
snprintf(sql, sizeof(sql), "DELETE FROM %s WHERE %s=%s",
|
|
d->recordSource, d->colNames[keyCol], keyVal);
|
|
dvxSqlExec(db, sql);
|
|
dvxSqlClose(db);
|
|
}
|
|
}
|
|
|
|
// Free the cached row
|
|
for (int32_t j = 0; j < d->rows[d->currentRow].fieldCount; j++) {
|
|
free(d->rows[d->currentRow].fields[j]);
|
|
}
|
|
|
|
free(d->rows[d->currentRow].fields);
|
|
arrdel(d->rows, d->currentRow);
|
|
d->rowCount = (int32_t)arrlen(d->rows);
|
|
|
|
// Adjust cursor
|
|
if (d->rowCount == 0) {
|
|
d->currentRow = -1;
|
|
d->bof = true;
|
|
d->eof = true;
|
|
} else {
|
|
if (d->currentRow >= d->rowCount) {
|
|
d->currentRow = d->rowCount - 1;
|
|
}
|
|
|
|
d->bof = (d->currentRow == 0);
|
|
d->eof = (d->currentRow >= d->rowCount - 1);
|
|
}
|
|
|
|
d->dirty = false;
|
|
d->isNewRow = false;
|
|
|
|
fireReposition(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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 *dataCtrlGetKeyColumn(const WidgetT *w) {
|
|
return ((DataCtrlDataT *)w->data)->keyColumn;
|
|
}
|
|
|
|
static void dataCtrlSetKeyColumn(WidgetT *w, const char *val) {
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
snprintf(d->keyColumn, sizeof(d->keyColumn), "%s", val ? val : "");
|
|
}
|
|
|
|
static const char *dataCtrlGetMasterSource(const WidgetT *w) {
|
|
return ((DataCtrlDataT *)w->data)->masterSource;
|
|
}
|
|
|
|
static void dataCtrlSetMasterSource(WidgetT *w, const char *val) {
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
snprintf(d->masterSource, sizeof(d->masterSource), "%s", val ? val : "");
|
|
}
|
|
|
|
static const char *dataCtrlGetMasterField(const WidgetT *w) {
|
|
return ((DataCtrlDataT *)w->data)->masterField;
|
|
}
|
|
|
|
static void dataCtrlSetMasterField(WidgetT *w, const char *val) {
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
snprintf(d->masterField, sizeof(d->masterField), "%s", val ? val : "");
|
|
}
|
|
|
|
// ============================================================
|
|
// Row-level accessors for DBGrid and other consumers
|
|
// ============================================================
|
|
|
|
int32_t dataCtrlGetRowCount(WidgetT *w) {
|
|
if (!w || !w->data) {
|
|
return 0;
|
|
}
|
|
|
|
return ((DataCtrlDataT *)w->data)->rowCount;
|
|
}
|
|
|
|
|
|
int32_t dataCtrlGetColCount(WidgetT *w) {
|
|
if (!w || !w->data) {
|
|
return 0;
|
|
}
|
|
|
|
return ((DataCtrlDataT *)w->data)->colCount;
|
|
}
|
|
|
|
|
|
const char *dataCtrlGetColName(WidgetT *w, int32_t col) {
|
|
if (!w || !w->data) {
|
|
return "";
|
|
}
|
|
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
|
|
if (col < 0 || col >= d->colCount) {
|
|
return "";
|
|
}
|
|
|
|
return d->colNames[col];
|
|
}
|
|
|
|
|
|
const char *dataCtrlGetCellText(WidgetT *w, int32_t row, int32_t col) {
|
|
if (!w || !w->data) {
|
|
return "";
|
|
}
|
|
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
|
|
if (row < 0 || row >= d->rowCount || col < 0 || col >= d->colCount) {
|
|
return "";
|
|
}
|
|
|
|
return d->rows[row].fields[col];
|
|
}
|
|
|
|
|
|
void dataCtrlSetCurrentRow(WidgetT *w, int32_t row) {
|
|
if (!w || !w->data) {
|
|
return;
|
|
}
|
|
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
|
|
if (row < 0 || row >= d->rowCount) {
|
|
return;
|
|
}
|
|
|
|
// Auto-save before moving
|
|
if (d->dirty) {
|
|
dataCtrlUpdate(w);
|
|
}
|
|
|
|
d->currentRow = row;
|
|
d->bof = (row == 0);
|
|
d->eof = (row >= d->rowCount - 1);
|
|
|
|
wgtInvalidatePaint(w);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
|
|
|
|
void dataCtrlSetMasterValue(WidgetT *w, const char *val) {
|
|
if (!w || !w->data) {
|
|
return;
|
|
}
|
|
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
snprintf(d->masterValue, sizeof(d->masterValue), "%s", val ? val : "");
|
|
}
|
|
|
|
static const char *dataCtrlGetDetailField(const WidgetT *w) {
|
|
return ((DataCtrlDataT *)w->data)->detailField;
|
|
}
|
|
|
|
static void dataCtrlSetDetailField(WidgetT *w, const char *val) {
|
|
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
|
|
snprintf(d->detailField, sizeof(d->detailField), "%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);
|
|
void (*setField)(WidgetT *w, const char *colName, const char *value);
|
|
void (*updateRow)(WidgetT *w);
|
|
void (*update)(WidgetT *w);
|
|
void (*addNew)(WidgetT *w);
|
|
void (*delete)(WidgetT *w);
|
|
void (*setMasterValue)(WidgetT *w, const char *val);
|
|
int32_t (*getRowCount)(WidgetT *w);
|
|
int32_t (*getColCount)(WidgetT *w);
|
|
const char *(*getColName)(WidgetT *w, int32_t col);
|
|
const char *(*getCellText)(WidgetT *w, int32_t row, int32_t col);
|
|
void (*setCurrentRow)(WidgetT *w, int32_t row);
|
|
} sApi = {
|
|
.create = dataCtrlCreate,
|
|
.refresh = dataCtrlRefresh,
|
|
.moveFirst = dataCtrlMoveFirst,
|
|
.movePrev = dataCtrlMovePrev,
|
|
.moveNext = dataCtrlMoveNext,
|
|
.moveLast = dataCtrlMoveLast,
|
|
.getField = dataCtrlGetField,
|
|
.setField = dataCtrlSetField,
|
|
.updateRow = dataCtrlUpdateRow,
|
|
.update = dataCtrlUpdate,
|
|
.addNew = dataCtrlAddNew,
|
|
.delete = dataCtrlDelete,
|
|
.setMasterValue = dataCtrlSetMasterValue,
|
|
.getRowCount = dataCtrlGetRowCount,
|
|
.getColCount = dataCtrlGetColCount,
|
|
.getColName = dataCtrlGetColName,
|
|
.getCellText = dataCtrlGetCellText,
|
|
.setCurrentRow = dataCtrlSetCurrentRow,
|
|
};
|
|
|
|
static const WgtPropDescT sProps[] = {
|
|
{ "DatabaseName", WGT_IFACE_STRING, (void *)dataCtrlGetDatabaseName, (void *)dataCtrlSetDatabaseName, NULL },
|
|
{ "RecordSource", WGT_IFACE_STRING, (void *)dataCtrlGetRecordSource, (void *)dataCtrlSetRecordSource, NULL },
|
|
{ "KeyColumn", WGT_IFACE_STRING, (void *)dataCtrlGetKeyColumn, (void *)dataCtrlSetKeyColumn, NULL },
|
|
{ "MasterSource", WGT_IFACE_STRING, (void *)dataCtrlGetMasterSource, (void *)dataCtrlSetMasterSource, NULL },
|
|
{ "MasterField", WGT_IFACE_STRING, (void *)dataCtrlGetMasterField, (void *)dataCtrlSetMasterField, NULL },
|
|
{ "DetailField", WGT_IFACE_STRING, (void *)dataCtrlGetDetailField, (void *)dataCtrlSetDetailField, 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[] = {
|
|
{ "AddNew", WGT_SIG_VOID, (void *)dataCtrlAddNew },
|
|
{ "Delete", WGT_SIG_VOID, (void *)dataCtrlDelete },
|
|
{ "MoveFirst", WGT_SIG_VOID, (void *)dataCtrlMoveFirst },
|
|
{ "MoveLast", WGT_SIG_VOID, (void *)dataCtrlMoveLast },
|
|
{ "MoveNext", WGT_SIG_VOID, (void *)dataCtrlMoveNext },
|
|
{ "MovePrevious", WGT_SIG_VOID, (void *)dataCtrlMovePrev },
|
|
{ "Refresh", WGT_SIG_VOID, (void *)dataCtrlRefresh },
|
|
{ "Update", WGT_SIG_VOID, (void *)dataCtrlUpdate },
|
|
};
|
|
|
|
static const WgtEventDescT sEvents[] = {
|
|
{ "Reposition" },
|
|
{ "Validate" },
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "Data",
|
|
.props = sProps,
|
|
.propCount = 9,
|
|
.methods = sMethods,
|
|
.methodCount = 8,
|
|
.events = sEvents,
|
|
.eventCount = 2,
|
|
.createSig = WGT_CREATE_PARENT,
|
|
.isContainer = false,
|
|
.defaultEvent = "Reposition",
|
|
.namePrefix = "Data",
|
|
};
|
|
|
|
|
|
void wgtRegister(void) {
|
|
sTypeId = wgtRegisterClass(&sClass);
|
|
wgtRegisterApi("data", &sApi);
|
|
wgtRegisterIface("data", &sIface);
|
|
}
|