1221 lines
34 KiB
C
1221 lines
34 KiB
C
#define DVX_WIDGET_IMPL
|
|
// widgetDbGrid.c -- Database grid widget for tabular record display
|
|
//
|
|
// A read-only grid that displays all records from a Data control in a
|
|
// scrollable, sortable table. Columns auto-populate from the Data
|
|
// control's column names and can be hidden, resized, and reordered
|
|
// by clicking headers. Selecting a row syncs the Data control's
|
|
// cursor position.
|
|
//
|
|
// The grid reads directly from the Data control's cached rows via
|
|
// the row-level accessor API (getRowCount, getCellText, etc.) so
|
|
// there is no separate copy of the data. The grid is refreshed by
|
|
// calling dbGridRefresh() which re-reads the Data control's state.
|
|
//
|
|
// Depends on the datactrl widget DXE for the DataCtrlApiT.
|
|
|
|
#include "dvxWgtP.h"
|
|
#include "../dataCtrl/dataCtrl.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
|
|
// ============================================================
|
|
// Constants
|
|
// ============================================================
|
|
|
|
#define DBGRID_BORDER 2
|
|
#define DBGRID_MAX_COLS 32
|
|
#define DBGRID_MIN_COL_W 20
|
|
#define DBGRID_COL_PAD 6
|
|
#define DBGRID_PAD 3
|
|
#define DBGRID_SORT_W 10
|
|
#define DBGRID_MIN_ROWS 3
|
|
#define DBGRID_MAX_NAME 64
|
|
#define DBGRID_RESIZE_ZONE 3
|
|
|
|
// ============================================================
|
|
// Sort direction (matches ListView for consistency)
|
|
// ============================================================
|
|
|
|
#define SORT_NONE 0
|
|
#define SORT_ASC 1
|
|
#define SORT_DESC 2
|
|
|
|
// ============================================================
|
|
// Column definition
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
char fieldName[DBGRID_MAX_NAME];
|
|
char header[DBGRID_MAX_NAME];
|
|
int32_t width; // tagged size (0 = auto)
|
|
int32_t align; // 0=left, 1=center, 2=right
|
|
bool visible;
|
|
int32_t dataCol; // index into Data control's columns (-1 = unresolved)
|
|
} DbGridColT;
|
|
|
|
// ============================================================
|
|
// Per-instance data
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
WidgetT *dataWidget; // the Data control (set by form runtime)
|
|
|
|
// Column configuration
|
|
DbGridColT columns[DBGRID_MAX_COLS];
|
|
int32_t colCount;
|
|
|
|
// Display state
|
|
int32_t selectedRow; // data-row index (-1 = none)
|
|
int32_t scrollPos; // vertical scroll (row units)
|
|
int32_t scrollPosH; // horizontal scroll (pixel units)
|
|
int32_t resolvedW[DBGRID_MAX_COLS];
|
|
int32_t totalColW; // 0 = needs recalc
|
|
bool gridLines;
|
|
|
|
// Sort state
|
|
int32_t sortCol; // visible-column index (-1 = none)
|
|
int32_t sortDir; // SORT_NONE/ASC/DESC
|
|
int32_t *sortIndex; // display-row -> data-row mapping (NULL = natural)
|
|
int32_t sortCount; // length of sortIndex
|
|
|
|
// Column resize drag state
|
|
int32_t resizeCol;
|
|
int32_t resizeStartX;
|
|
int32_t resizeOrigW;
|
|
bool resizeDragging;
|
|
|
|
// Scrollbar drag state
|
|
int32_t sbDragOrient; // 0=vert, 1=horiz, -1=col resize
|
|
int32_t sbDragOff;
|
|
} DbGridDataT;
|
|
|
|
static int32_t sTypeId = -1;
|
|
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static int32_t colBorderHit(WidgetT *w, int32_t vx, int32_t vy);
|
|
static void dbGridBuildSortIndex(WidgetT *w);
|
|
static void dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
static void dbGridDestroy(WidgetT *w);
|
|
static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my);
|
|
static void dbGridOnDragEnd(WidgetT *w);
|
|
static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
|
|
static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod);
|
|
static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
|
static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static int32_t getDataRowCount(const DbGridDataT *d);
|
|
static void resolveColumns(WidgetT *w, const BitmapFontT *font);
|
|
|
|
|
|
// ============================================================
|
|
// Helpers
|
|
// ============================================================
|
|
|
|
static int32_t getDataRowCount(const DbGridDataT *d) {
|
|
if (!d->dataWidget) {
|
|
return 0;
|
|
}
|
|
|
|
return wgtDataCtrlGetRowCount(d->dataWidget);
|
|
}
|
|
|
|
|
|
// Map a data-row to display-row (linear search in sortIndex)
|
|
static int32_t dataRowToDisplay(const DbGridDataT *d, int32_t dataRow) {
|
|
if (!d->sortIndex) {
|
|
return dataRow;
|
|
}
|
|
|
|
for (int32_t i = 0; i < d->sortCount; i++) {
|
|
if (d->sortIndex[i] == dataRow) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return dataRow;
|
|
}
|
|
|
|
|
|
// Map a display-row to data-row
|
|
static int32_t displayToDataRow(const DbGridDataT *d, int32_t dispRow) {
|
|
if (!d->sortIndex || dispRow < 0 || dispRow >= d->sortCount) {
|
|
return dispRow;
|
|
}
|
|
|
|
return d->sortIndex[dispRow];
|
|
}
|
|
|
|
|
|
// Get cell text for a display-row and visible-column index
|
|
static const char *getCellText(const DbGridDataT *d, int32_t dispRow, int32_t visCol) {
|
|
if (!d->dataWidget || visCol < 0 || visCol >= d->colCount || !d->columns[visCol].visible) {
|
|
return "";
|
|
}
|
|
|
|
int32_t dataRow = displayToDataRow(d, dispRow);
|
|
int32_t dataCol = d->columns[visCol].dataCol;
|
|
|
|
if (dataCol < 0) {
|
|
return "";
|
|
}
|
|
|
|
return wgtDataCtrlGetCellText(d->dataWidget, dataRow, dataCol);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// resolveColumns -- compute pixel widths for each column
|
|
// ============================================================
|
|
|
|
static void resolveColumns(WidgetT *w, const BitmapFontT *font) {
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
|
|
if (d->totalColW > 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t rowCount = getDataRowCount(d);
|
|
|
|
d->totalColW = 0;
|
|
|
|
for (int32_t c = 0; c < d->colCount; c++) {
|
|
if (!d->columns[c].visible) {
|
|
d->resolvedW[c] = 0;
|
|
continue;
|
|
}
|
|
|
|
if (d->columns[c].width > 0) {
|
|
d->resolvedW[c] = wgtResolveSize(d->columns[c].width, 0, font->charWidth);
|
|
} else {
|
|
// Auto-size: find widest content
|
|
int32_t maxW = textWidth(font, d->columns[c].header) + DBGRID_COL_PAD;
|
|
|
|
for (int32_t r = 0; r < rowCount; r++) {
|
|
int32_t dataCol = d->columns[c].dataCol;
|
|
|
|
if (dataCol >= 0) {
|
|
const char *text = wgtDataCtrlGetCellText(d->dataWidget, r, dataCol);
|
|
int32_t tw = textWidth(font, text) + DBGRID_COL_PAD;
|
|
|
|
if (tw > maxW) {
|
|
maxW = tw;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add space for sort indicator on header
|
|
maxW += DBGRID_SORT_W;
|
|
d->resolvedW[c] = maxW;
|
|
}
|
|
|
|
if (d->resolvedW[c] < DBGRID_MIN_COL_W) {
|
|
d->resolvedW[c] = DBGRID_MIN_COL_W;
|
|
}
|
|
|
|
d->totalColW += d->resolvedW[c];
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dbGridBuildSortIndex -- sort rows by the current sort column
|
|
// ============================================================
|
|
|
|
static void dbGridBuildSortIndex(WidgetT *w) {
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
|
|
free(d->sortIndex);
|
|
d->sortIndex = NULL;
|
|
d->sortCount = 0;
|
|
|
|
if (d->sortDir == SORT_NONE || d->sortCol < 0 || !d->dataWidget) {
|
|
return;
|
|
}
|
|
|
|
int32_t rowCount = getDataRowCount(d);
|
|
|
|
if (rowCount <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t dataCol = d->columns[d->sortCol].dataCol;
|
|
|
|
if (dataCol < 0) {
|
|
return;
|
|
}
|
|
|
|
d->sortIndex = (int32_t *)malloc(rowCount * sizeof(int32_t));
|
|
d->sortCount = rowCount;
|
|
|
|
for (int32_t i = 0; i < rowCount; i++) {
|
|
d->sortIndex[i] = i;
|
|
}
|
|
|
|
// Insertion sort (stable, good for small N)
|
|
for (int32_t i = 1; i < rowCount; i++) {
|
|
int32_t key = d->sortIndex[i];
|
|
const char *keyStr = wgtDataCtrlGetCellText(d->dataWidget, key, dataCol);
|
|
int32_t j = i - 1;
|
|
|
|
while (j >= 0) {
|
|
int32_t cmpRow = d->sortIndex[j];
|
|
const char *cmpStr = wgtDataCtrlGetCellText(d->dataWidget, cmpRow, dataCol);
|
|
int32_t cmp = strcasecmp(keyStr, cmpStr);
|
|
|
|
if (d->sortDir == SORT_DESC) {
|
|
cmp = -cmp;
|
|
}
|
|
|
|
if (cmp >= 0) {
|
|
break;
|
|
}
|
|
|
|
d->sortIndex[j + 1] = d->sortIndex[j];
|
|
j--;
|
|
}
|
|
|
|
d->sortIndex[j + 1] = key;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Scrollbar geometry helpers (matches ListView pattern)
|
|
// ============================================================
|
|
|
|
static void computeLayout(WidgetT *w, const BitmapFontT *font, int32_t *outInnerW, int32_t *outInnerH, int32_t *outHeaderH, int32_t *outVisRows, bool *outNeedV, bool *outNeedH) {
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
|
|
int32_t headerH = font->charHeight + 4;
|
|
int32_t innerW = w->w - DBGRID_BORDER * 2;
|
|
int32_t innerH = w->h - DBGRID_BORDER * 2 - headerH;
|
|
int32_t rowH = font->charHeight + 2;
|
|
int32_t rowCount = getDataRowCount(d);
|
|
int32_t visRows = innerH / rowH;
|
|
|
|
resolveColumns(w, font);
|
|
|
|
bool needV = (rowCount > visRows);
|
|
|
|
if (needV) {
|
|
innerW -= WGT_SB_W;
|
|
}
|
|
|
|
bool needH = (d->totalColW > innerW);
|
|
|
|
if (needH) {
|
|
innerH -= WGT_SB_W;
|
|
visRows = innerH / rowH;
|
|
|
|
if (!needV && rowCount > visRows) {
|
|
needV = true;
|
|
innerW -= WGT_SB_W;
|
|
}
|
|
}
|
|
|
|
if (visRows < 0) {
|
|
visRows = 0;
|
|
}
|
|
|
|
*outInnerW = innerW;
|
|
*outInnerH = innerH;
|
|
*outHeaderH = headerH;
|
|
*outVisRows = visRows;
|
|
*outNeedV = needV;
|
|
*outNeedH = needH;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// colBorderHit -- check if mouse X is on a column border
|
|
// ============================================================
|
|
|
|
static int32_t colBorderHit(WidgetT *w, int32_t vx, int32_t vy) {
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
(void)vy;
|
|
|
|
int32_t x = w->x + DBGRID_BORDER - d->scrollPosH;
|
|
|
|
for (int32_t c = 0; c < d->colCount; c++) {
|
|
if (!d->columns[c].visible) {
|
|
continue;
|
|
}
|
|
|
|
x += d->resolvedW[c];
|
|
|
|
if (vx >= x - DBGRID_RESIZE_ZONE && vx <= x + DBGRID_RESIZE_ZONE) {
|
|
return c;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dbGridCalcMinSize
|
|
// ============================================================
|
|
|
|
static void dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
w->calcMinW = font->charWidth * 20 + DBGRID_BORDER * 2;
|
|
w->calcMinH = (font->charHeight + 2) * (DBGRID_MIN_ROWS + 1) + DBGRID_BORDER * 2;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dbGridDestroy
|
|
// ============================================================
|
|
|
|
static void dbGridDestroy(WidgetT *w) {
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
|
|
if (d) {
|
|
free(d->sortIndex);
|
|
free(d);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dbGridGetCursorShape
|
|
// ============================================================
|
|
|
|
static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my) {
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
|
|
if (!ctx) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t headerH = ctx->font.charHeight + 4;
|
|
int32_t headerTop = w->y + DBGRID_BORDER;
|
|
|
|
if (my >= headerTop && my < headerTop + headerH) {
|
|
resolveColumns(w, &ctx->font);
|
|
|
|
if (colBorderHit(w, mx, my) >= 0) {
|
|
return CURSOR_RESIZE_H;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dbGridOnKey
|
|
// ============================================================
|
|
|
|
static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|
(void)mod;
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
int32_t rowCount = getDataRowCount(d);
|
|
|
|
if (rowCount <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t innerW, innerH, headerH, visRows;
|
|
bool needV, needH;
|
|
computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
|
|
|
|
int32_t dispRow = dataRowToDisplay(d, d->selectedRow);
|
|
int32_t newDisp = dispRow;
|
|
|
|
// Up arrow
|
|
if (key == (0x48 | 0x100)) {
|
|
newDisp = dispRow > 0 ? dispRow - 1 : 0;
|
|
}
|
|
|
|
// Down arrow
|
|
if (key == (0x50 | 0x100)) {
|
|
newDisp = dispRow < rowCount - 1 ? dispRow + 1 : rowCount - 1;
|
|
}
|
|
|
|
// Page Up
|
|
if (key == (0x49 | 0x100)) {
|
|
newDisp = dispRow - visRows;
|
|
|
|
if (newDisp < 0) {
|
|
newDisp = 0;
|
|
}
|
|
}
|
|
|
|
// Page Down
|
|
if (key == (0x51 | 0x100)) {
|
|
newDisp = dispRow + visRows;
|
|
|
|
if (newDisp >= rowCount) {
|
|
newDisp = rowCount - 1;
|
|
}
|
|
}
|
|
|
|
// Home
|
|
if (key == (0x47 | 0x100)) {
|
|
newDisp = 0;
|
|
}
|
|
|
|
// End
|
|
if (key == (0x4F | 0x100)) {
|
|
newDisp = rowCount - 1;
|
|
}
|
|
|
|
// Enter = activate (fire DblClick)
|
|
if (key == '\r' || key == '\n') {
|
|
if (w->onDblClick) {
|
|
w->onDblClick(w);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (newDisp != dispRow) {
|
|
int32_t newDataRow = displayToDataRow(d, newDisp);
|
|
d->selectedRow = newDataRow;
|
|
|
|
// Auto-scroll
|
|
if (newDisp < d->scrollPos) {
|
|
d->scrollPos = newDisp;
|
|
} else if (newDisp >= d->scrollPos + visRows) {
|
|
d->scrollPos = newDisp - visRows + 1;
|
|
}
|
|
|
|
// Sync Data control cursor
|
|
if (d->dataWidget) {
|
|
wgtDataCtrlSetCurrentRow(d->dataWidget, newDataRow);
|
|
}
|
|
|
|
if (w->onClick) {
|
|
w->onClick(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dbGridOnMouse
|
|
// ============================================================
|
|
|
|
static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
sFocusedWidget = w;
|
|
|
|
int32_t innerW, innerH, headerH, visRows;
|
|
bool needV, needH;
|
|
computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
|
|
|
|
int32_t rowH = ctx->font.charHeight + 2;
|
|
int32_t headerTop = w->y + DBGRID_BORDER;
|
|
int32_t dataTop = headerTop + headerH;
|
|
int32_t rowCount = getDataRowCount(d);
|
|
|
|
// Vertical scrollbar click
|
|
if (needV && vx >= w->x + w->w - DBGRID_BORDER - WGT_SB_W) {
|
|
int32_t sbY = dataTop;
|
|
int32_t sbH = innerH;
|
|
int32_t maxScroll = rowCount - visRows;
|
|
|
|
if (maxScroll < 0) {
|
|
maxScroll = 0;
|
|
}
|
|
|
|
int32_t relY = vy - sbY;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(sbH, WGT_SB_W, maxScroll, d->scrollPos, &thumbPos, &thumbSize);
|
|
|
|
if (relY < WGT_SB_W) {
|
|
// Up arrow
|
|
if (d->scrollPos > 0) {
|
|
d->scrollPos--;
|
|
}
|
|
} else if (relY >= sbH - WGT_SB_W) {
|
|
// Down arrow
|
|
if (d->scrollPos < maxScroll) {
|
|
d->scrollPos++;
|
|
}
|
|
} else if (relY >= thumbPos && relY < thumbPos + thumbSize) {
|
|
// Thumb drag
|
|
d->sbDragOrient = 0;
|
|
d->sbDragOff = relY - thumbPos;
|
|
} else if (relY < thumbPos) {
|
|
d->scrollPos -= visRows;
|
|
|
|
if (d->scrollPos < 0) {
|
|
d->scrollPos = 0;
|
|
}
|
|
} else {
|
|
d->scrollPos += visRows;
|
|
|
|
if (d->scrollPos > maxScroll) {
|
|
d->scrollPos = maxScroll;
|
|
}
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Horizontal scrollbar click
|
|
if (needH && vy >= w->y + w->h - DBGRID_BORDER - WGT_SB_W) {
|
|
int32_t sbX = w->x + DBGRID_BORDER;
|
|
int32_t sbW = innerW;
|
|
int32_t maxScroll = d->totalColW - innerW;
|
|
|
|
if (maxScroll < 0) {
|
|
maxScroll = 0;
|
|
}
|
|
|
|
int32_t relX = vx - sbX;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(sbW, WGT_SB_W, maxScroll, d->scrollPosH, &thumbPos, &thumbSize);
|
|
|
|
if (relX < WGT_SB_W) {
|
|
d->scrollPosH -= 20;
|
|
|
|
if (d->scrollPosH < 0) {
|
|
d->scrollPosH = 0;
|
|
}
|
|
} else if (relX >= sbW - WGT_SB_W) {
|
|
d->scrollPosH += 20;
|
|
|
|
if (d->scrollPosH > maxScroll) {
|
|
d->scrollPosH = maxScroll;
|
|
}
|
|
} else if (relX >= thumbPos && relX < thumbPos + thumbSize) {
|
|
d->sbDragOrient = 1;
|
|
d->sbDragOff = relX - thumbPos;
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Column header area
|
|
if (vy >= headerTop && vy < dataTop) {
|
|
resolveColumns(w, &ctx->font);
|
|
|
|
// Check column border for resize
|
|
int32_t borderCol = colBorderHit(w, vx, vy);
|
|
|
|
if (borderCol >= 0) {
|
|
d->resizeCol = borderCol;
|
|
d->resizeStartX = vx;
|
|
d->resizeOrigW = d->resolvedW[borderCol];
|
|
d->resizeDragging = false;
|
|
d->sbDragOrient = -1;
|
|
return;
|
|
}
|
|
|
|
// Header click = sort
|
|
int32_t x = w->x + DBGRID_BORDER - d->scrollPosH;
|
|
|
|
for (int32_t c = 0; c < d->colCount; c++) {
|
|
if (!d->columns[c].visible) {
|
|
continue;
|
|
}
|
|
|
|
if (vx >= x && vx < x + d->resolvedW[c]) {
|
|
if (d->sortCol == c) {
|
|
d->sortDir = (d->sortDir == SORT_ASC) ? SORT_DESC : SORT_ASC;
|
|
} else {
|
|
d->sortCol = c;
|
|
d->sortDir = SORT_ASC;
|
|
}
|
|
|
|
dbGridBuildSortIndex(w);
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
x += d->resolvedW[c];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Data row click
|
|
if (vy >= dataTop && vy < dataTop + innerH) {
|
|
int32_t relY = vy - dataTop;
|
|
int32_t dispRow = d->scrollPos + relY / rowH;
|
|
|
|
if (dispRow >= 0 && dispRow < rowCount) {
|
|
int32_t dataRow = displayToDataRow(d, dispRow);
|
|
d->selectedRow = dataRow;
|
|
|
|
// Sync Data control cursor
|
|
if (d->dataWidget) {
|
|
wgtDataCtrlSetCurrentRow(d->dataWidget, dataRow);
|
|
}
|
|
|
|
if (w->onClick) {
|
|
w->onClick(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dbGridOnDragUpdate
|
|
// ============================================================
|
|
|
|
static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
(void)root;
|
|
(void)y;
|
|
|
|
// Column resize drag
|
|
if (d->sbDragOrient == -1) {
|
|
d->resizeDragging = true;
|
|
int32_t delta = x - d->resizeStartX;
|
|
int32_t newW = d->resizeOrigW + delta;
|
|
|
|
if (newW < DBGRID_MIN_COL_W) {
|
|
newW = DBGRID_MIN_COL_W;
|
|
}
|
|
|
|
d->totalColW += (newW - d->resolvedW[d->resizeCol]);
|
|
d->resolvedW[d->resizeCol] = newW;
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Vertical scrollbar thumb drag
|
|
if (d->sbDragOrient == 0) {
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
int32_t innerW, innerH, headerH, visRows;
|
|
bool needV, needH;
|
|
computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
|
|
|
|
int32_t rowCount = getDataRowCount(d);
|
|
int32_t maxScroll = rowCount - visRows;
|
|
|
|
if (maxScroll < 1) {
|
|
return;
|
|
}
|
|
|
|
int32_t sbY = w->y + DBGRID_BORDER + headerH;
|
|
int32_t sbH = innerH;
|
|
int32_t relY = y - sbY - d->sbDragOff;
|
|
int32_t track = sbH - WGT_SB_W * 2;
|
|
|
|
if (track > 0) {
|
|
d->scrollPos = (relY - WGT_SB_W) * maxScroll / track;
|
|
|
|
if (d->scrollPos < 0) {
|
|
d->scrollPos = 0;
|
|
}
|
|
|
|
if (d->scrollPos > maxScroll) {
|
|
d->scrollPos = maxScroll;
|
|
}
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Horizontal scrollbar thumb drag
|
|
if (d->sbDragOrient == 1) {
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
int32_t innerW, innerH, headerH, visRows;
|
|
bool needV, needH;
|
|
computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
|
|
|
|
int32_t maxScroll = d->totalColW - innerW;
|
|
|
|
if (maxScroll < 1) {
|
|
return;
|
|
}
|
|
|
|
int32_t sbX = w->x + DBGRID_BORDER;
|
|
int32_t sbW = innerW;
|
|
int32_t relX = x - sbX - d->sbDragOff;
|
|
int32_t track = sbW - WGT_SB_W * 2;
|
|
|
|
if (track > 0) {
|
|
d->scrollPosH = (relX - WGT_SB_W) * maxScroll / track;
|
|
|
|
if (d->scrollPosH < 0) {
|
|
d->scrollPosH = 0;
|
|
}
|
|
|
|
if (d->scrollPosH > maxScroll) {
|
|
d->scrollPosH = maxScroll;
|
|
}
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dbGridOnDragEnd
|
|
// ============================================================
|
|
|
|
static void dbGridOnDragEnd(WidgetT *w) {
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
d->sbDragOrient = -2;
|
|
d->resizeDragging = false;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dbGridPaint
|
|
// ============================================================
|
|
|
|
static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
|
|
int32_t innerW, innerH, headerH, visRows;
|
|
bool needV, needH;
|
|
computeLayout(w, font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
|
|
|
|
int32_t rowH = font->charHeight + 2;
|
|
int32_t rowCount = getDataRowCount(d);
|
|
|
|
// Outer sunken border
|
|
BevelStyleT bevel = { colors->windowShadow, colors->windowHighlight, colors->contentBg, DBGRID_BORDER };
|
|
drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel);
|
|
|
|
int32_t contentX = w->x + DBGRID_BORDER;
|
|
int32_t contentY = w->y + DBGRID_BORDER;
|
|
|
|
// Save clip rect
|
|
int32_t oldClipX = disp->clipX;
|
|
int32_t oldClipY = disp->clipY;
|
|
int32_t oldClipW = disp->clipW;
|
|
int32_t oldClipH = disp->clipH;
|
|
|
|
// ---- Column Headers ----
|
|
setClipRect(disp, contentX, contentY, innerW, headerH);
|
|
|
|
int32_t hx = contentX - d->scrollPosH;
|
|
|
|
for (int32_t c = 0; c < d->colCount; c++) {
|
|
if (!d->columns[c].visible) {
|
|
continue;
|
|
}
|
|
|
|
int32_t cw = d->resolvedW[c];
|
|
|
|
if (hx + cw > contentX - 20 && hx < contentX + innerW + 20) {
|
|
BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1);
|
|
drawBevel(disp, ops, hx, contentY, cw, headerH, &hdrBevel);
|
|
|
|
// Header text (centered)
|
|
const char *hdr = d->columns[c].header;
|
|
int32_t tw = textWidth(font, hdr);
|
|
int32_t tx = hx + (cw - tw) / 2;
|
|
int32_t ty = contentY + (headerH - font->charHeight) / 2;
|
|
drawText(disp, ops, font, tx, ty, hdr, colors->contentFg, colors->buttonFace, false);
|
|
|
|
// Sort indicator
|
|
if (c == d->sortCol && d->sortDir != SORT_NONE) {
|
|
int32_t sx = hx + cw - DBGRID_SORT_W;
|
|
int32_t sy = contentY + headerH / 2;
|
|
|
|
if (d->sortDir == SORT_ASC) {
|
|
for (int32_t i = 0; i < 3; i++) {
|
|
drawHLine(disp, ops, sx + 3 - i, sy - 1 + i, 1 + i * 2, colors->contentFg);
|
|
}
|
|
} else {
|
|
for (int32_t i = 0; i < 3; i++) {
|
|
drawHLine(disp, ops, sx + 3 - i, sy + 1 - i, 1 + i * 2, colors->contentFg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
hx += cw;
|
|
}
|
|
|
|
// Fill remaining header space
|
|
if (hx < contentX + innerW) {
|
|
BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1);
|
|
drawBevel(disp, ops, hx, contentY, contentX + innerW - hx, headerH, &hdrBevel);
|
|
}
|
|
|
|
// ---- Data Rows ----
|
|
int32_t dataY = contentY + headerH;
|
|
setClipRect(disp, contentX, dataY, innerW, innerH);
|
|
|
|
// Background fill
|
|
rectFill(disp, ops, contentX, dataY, innerW, innerH, colors->contentBg);
|
|
|
|
for (int32_t i = 0; i < visRows && d->scrollPos + i < rowCount; i++) {
|
|
int32_t dispRow = d->scrollPos + i;
|
|
int32_t dataRow = displayToDataRow(d, dispRow);
|
|
int32_t ry = dataY + i * rowH;
|
|
|
|
// Alternating row background
|
|
if (i % 2 == 1) {
|
|
uint32_t altBg = colors->contentBg - 0x080808;
|
|
rectFill(disp, ops, contentX, ry, innerW, rowH, altBg);
|
|
}
|
|
|
|
// Selection highlight
|
|
bool selected = (dataRow == d->selectedRow);
|
|
|
|
if (selected) {
|
|
rectFill(disp, ops, contentX, ry, innerW, rowH, colors->menuHighlightBg);
|
|
}
|
|
|
|
uint32_t fg = selected ? colors->menuHighlightFg : colors->contentFg;
|
|
uint32_t bg = selected ? colors->menuHighlightBg : (i % 2 == 1 ? colors->contentBg - 0x080808 : colors->contentBg);
|
|
|
|
// Draw cells
|
|
int32_t cx = contentX - d->scrollPosH;
|
|
|
|
for (int32_t c = 0; c < d->colCount; c++) {
|
|
if (!d->columns[c].visible) {
|
|
continue;
|
|
}
|
|
|
|
int32_t cw = d->resolvedW[c];
|
|
|
|
if (cx + cw > contentX && cx < contentX + innerW) {
|
|
const char *text = getCellText(d, dispRow, c);
|
|
int32_t tw = textWidth(font, text);
|
|
int32_t tx;
|
|
|
|
if (d->columns[c].align == 1) {
|
|
tx = cx + (cw - tw) / 2;
|
|
} else if (d->columns[c].align == 2) {
|
|
tx = cx + cw - tw - DBGRID_PAD;
|
|
} else {
|
|
tx = cx + DBGRID_PAD;
|
|
}
|
|
|
|
int32_t ty = ry + (rowH - font->charHeight) / 2;
|
|
drawText(disp, ops, font, tx, ty, text, fg, bg, false);
|
|
}
|
|
|
|
// Grid line
|
|
if (d->gridLines && cx + cw > contentX && cx + cw <= contentX + innerW) {
|
|
drawVLine(disp, ops, cx + cw - 1, ry, rowH, colors->windowShadow);
|
|
}
|
|
|
|
cx += cw;
|
|
}
|
|
|
|
// Horizontal grid line
|
|
if (d->gridLines) {
|
|
drawHLine(disp, ops, contentX, ry + rowH - 1, innerW, colors->windowShadow);
|
|
}
|
|
}
|
|
|
|
setClipRect(disp, oldClipX, oldClipY, oldClipW, oldClipH);
|
|
|
|
// ---- Scrollbars ----
|
|
if (needV) {
|
|
int32_t sbX = contentX + innerW;
|
|
int32_t sbY = dataY;
|
|
widgetDrawScrollbarV(disp, ops, colors, sbX, sbY, innerH, rowCount, visRows, d->scrollPos);
|
|
}
|
|
|
|
if (needH) {
|
|
int32_t sbX = contentX;
|
|
int32_t sbY = dataY + innerH;
|
|
widgetDrawScrollbarH(disp, ops, colors, sbX, sbY, innerW, d->totalColW, innerW, d->scrollPosH);
|
|
|
|
if (needV) {
|
|
rectFill(disp, ops, contentX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace);
|
|
}
|
|
}
|
|
|
|
// Focus rect
|
|
if (w == sFocusedWidget) {
|
|
drawFocusRect(disp, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Public API
|
|
// ============================================================
|
|
|
|
static WidgetT *dbGridCreate(WidgetT *parent) {
|
|
if (!parent) {
|
|
return NULL;
|
|
}
|
|
|
|
WidgetT *w = widgetAlloc(parent, sTypeId);
|
|
|
|
if (w) {
|
|
DbGridDataT *d = (DbGridDataT *)calloc(1, sizeof(DbGridDataT));
|
|
|
|
if (d) {
|
|
d->selectedRow = -1;
|
|
d->sortCol = -1;
|
|
d->sbDragOrient = -2;
|
|
d->gridLines = true;
|
|
}
|
|
|
|
w->data = d;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// Set the Data control widget that this grid reads from.
|
|
// Also auto-populates columns from the Data control's column names.
|
|
void dbGridSetDataWidget(WidgetT *w, WidgetT *dataWidget) {
|
|
if (!w || !w->data) {
|
|
return;
|
|
}
|
|
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
d->dataWidget = dataWidget;
|
|
d->totalColW = 0;
|
|
|
|
// Auto-populate columns
|
|
if (dataWidget) {
|
|
int32_t colCount = wgtDataCtrlGetColCount(dataWidget);
|
|
|
|
if (colCount > DBGRID_MAX_COLS) {
|
|
colCount = DBGRID_MAX_COLS;
|
|
}
|
|
|
|
d->colCount = colCount;
|
|
|
|
for (int32_t i = 0; i < colCount; i++) {
|
|
const char *name = wgtDataCtrlGetColName(dataWidget, i);
|
|
snprintf(d->columns[i].fieldName, DBGRID_MAX_NAME, "%s", name);
|
|
snprintf(d->columns[i].header, DBGRID_MAX_NAME, "%s", name);
|
|
d->columns[i].width = 0;
|
|
d->columns[i].align = 0;
|
|
d->columns[i].visible = true;
|
|
d->columns[i].dataCol = i;
|
|
}
|
|
}
|
|
|
|
// Reset sort
|
|
free(d->sortIndex);
|
|
d->sortIndex = NULL;
|
|
d->sortCount = 0;
|
|
d->sortCol = -1;
|
|
d->sortDir = SORT_NONE;
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
// Refresh the grid display (e.g. after Data control refresh)
|
|
void dbGridRefresh(WidgetT *w) {
|
|
if (!w || !w->data) {
|
|
return;
|
|
}
|
|
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
d->totalColW = 0;
|
|
|
|
// Rebuild sort index if sorting is active
|
|
if (d->sortDir != SORT_NONE) {
|
|
dbGridBuildSortIndex(w);
|
|
}
|
|
|
|
// Clamp scroll and selection
|
|
int32_t rowCount = getDataRowCount(d);
|
|
|
|
if (d->scrollPos >= rowCount) {
|
|
d->scrollPos = rowCount > 0 ? rowCount - 1 : 0;
|
|
}
|
|
|
|
if (d->selectedRow >= rowCount) {
|
|
d->selectedRow = rowCount > 0 ? rowCount - 1 : -1;
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
// Hide or show a column by field name
|
|
void dbGridSetColumnVisible(WidgetT *w, const char *fieldName, bool visible) {
|
|
if (!w || !w->data || !fieldName) {
|
|
return;
|
|
}
|
|
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
|
|
for (int32_t i = 0; i < d->colCount; i++) {
|
|
if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) {
|
|
d->columns[i].visible = visible;
|
|
d->totalColW = 0;
|
|
wgtInvalidate(w);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Set a column's header text
|
|
void dbGridSetColumnHeader(WidgetT *w, const char *fieldName, const char *header) {
|
|
if (!w || !w->data || !fieldName || !header) {
|
|
return;
|
|
}
|
|
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
|
|
for (int32_t i = 0; i < d->colCount; i++) {
|
|
if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) {
|
|
snprintf(d->columns[i].header, DBGRID_MAX_NAME, "%s", header);
|
|
d->totalColW = 0;
|
|
wgtInvalidate(w);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Set a column's width (tagged size, 0 = auto)
|
|
void dbGridSetColumnWidth(WidgetT *w, const char *fieldName, int32_t width) {
|
|
if (!w || !w->data || !fieldName) {
|
|
return;
|
|
}
|
|
|
|
DbGridDataT *d = (DbGridDataT *)w->data;
|
|
|
|
for (int32_t i = 0; i < d->colCount; i++) {
|
|
if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) {
|
|
d->columns[i].width = width;
|
|
d->totalColW = 0;
|
|
wgtInvalidate(w);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Get the selected data-row index (-1 = none)
|
|
int32_t dbGridGetSelectedRow(const WidgetT *w) {
|
|
if (!w || !w->data) {
|
|
return -1;
|
|
}
|
|
|
|
return ((DbGridDataT *)w->data)->selectedRow;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Property getters/setters
|
|
// ============================================================
|
|
|
|
static bool dbGridGetGridLines(const WidgetT *w) {
|
|
return ((DbGridDataT *)w->data)->gridLines;
|
|
}
|
|
|
|
static void dbGridSetGridLines(WidgetT *w, bool val) {
|
|
((DbGridDataT *)w->data)->gridLines = val;
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
static const WidgetClassT sClass = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)dbGridPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)dbGridCalcMinSize,
|
|
[WGT_METHOD_ON_MOUSE] = (void *)dbGridOnMouse,
|
|
[WGT_METHOD_ON_KEY] = (void *)dbGridOnKey,
|
|
[WGT_METHOD_DESTROY] = (void *)dbGridDestroy,
|
|
[WGT_METHOD_GET_CURSOR_SHAPE] = (void *)dbGridGetCursorShape,
|
|
[WGT_METHOD_ON_DRAG_UPDATE] = (void *)dbGridOnDragUpdate,
|
|
[WGT_METHOD_ON_DRAG_END] = (void *)dbGridOnDragEnd,
|
|
}
|
|
};
|
|
|
|
|
|
static const struct {
|
|
WidgetT *(*create)(WidgetT *parent);
|
|
void (*setDataWidget)(WidgetT *w, WidgetT *dataWidget);
|
|
void (*refresh)(WidgetT *w);
|
|
void (*setColumnVisible)(WidgetT *w, const char *fieldName, bool visible);
|
|
void (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header);
|
|
void (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width);
|
|
int32_t (*getSelectedRow)(const WidgetT *w);
|
|
} sApi = {
|
|
.create = dbGridCreate,
|
|
.setDataWidget = dbGridSetDataWidget,
|
|
.refresh = dbGridRefresh,
|
|
.setColumnVisible = dbGridSetColumnVisible,
|
|
.setColumnHeader = dbGridSetColumnHeader,
|
|
.setColumnWidth = dbGridSetColumnWidth,
|
|
.getSelectedRow = dbGridGetSelectedRow,
|
|
};
|
|
|
|
static const WgtPropDescT sProps[] = {
|
|
{ "GridLines", WGT_IFACE_BOOL, (void *)dbGridGetGridLines, (void *)dbGridSetGridLines, NULL },
|
|
};
|
|
|
|
static const WgtMethodDescT sMethods[] = {
|
|
{ "Refresh", WGT_SIG_VOID, (void *)dbGridRefresh },
|
|
};
|
|
|
|
static const WgtEventDescT sEvents[] = {
|
|
{ "Click" },
|
|
{ "DblClick" },
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "DBGrid",
|
|
.props = sProps,
|
|
.propCount = 1,
|
|
.methods = sMethods,
|
|
.methodCount = 1,
|
|
.events = sEvents,
|
|
.eventCount = 2,
|
|
.createSig = WGT_CREATE_PARENT,
|
|
.isContainer = false,
|
|
.defaultEvent = "DblClick",
|
|
.namePrefix = "DBGrid",
|
|
};
|
|
|
|
|
|
void wgtRegister(void) {
|
|
sTypeId = wgtRegisterClass(&sClass);
|
|
wgtRegisterApi("dbgrid", &sApi);
|
|
wgtRegisterIface("dbgrid", &sIface);
|
|
}
|