1947 lines
62 KiB
C
1947 lines
62 KiB
C
#define DVX_WIDGET_IMPL
|
|
// widgetListView.c -- ListView (multi-column list) widget
|
|
//
|
|
// A multi-column list with clickable/sortable column headers, horizontal
|
|
// and vertical scrolling, column resize by dragging header borders, and
|
|
// multi-select support. This is the most complex widget in the toolkit.
|
|
//
|
|
// Data model: cell data is stored as a flat array of const char* pointers
|
|
// in row-major order (cellData[row * colCount + col]). This external data
|
|
// model (pointers, not copies) keeps memory usage minimal and avoids
|
|
// copying strings that the application already owns.
|
|
//
|
|
// ListView state is heap-allocated separately (ListViewDataT*) rather than
|
|
// inlined in the widget union because the state is too large for a union
|
|
// member -- it includes per-column resolved widths, sort index, selection
|
|
// bits, and numerous scroll/drag state fields.
|
|
//
|
|
// Sort: clicking a column header toggles sort direction. Sorting uses an
|
|
// indirection array (sortIndex) rather than rearranging the cellData. This
|
|
// is critical because:
|
|
// 1. The application owns cellData and may not expect it to be mutated
|
|
// 2. Multi-select selBits are indexed by data row, not display row --
|
|
// rearranging data would invalidate all selection state
|
|
// 3. The indirection allows O(1) display<->data row mapping
|
|
//
|
|
// Insertion sort is used because it's stable (preserves original order for
|
|
// equal keys) and has good performance for the typical case of partially-sorted
|
|
// data. For the row counts typical in a DOS GUI (hundreds, not millions),
|
|
// insertion sort's O(n^2) worst case is acceptable and the constant factors
|
|
// are lower than quicksort.
|
|
//
|
|
// Column widths support four sizing modes via a tagged integer:
|
|
// - 0: auto-size (scan all data for widest string)
|
|
// - positive small: character count (resolved via font->charWidth)
|
|
// - positive large: pixel width
|
|
// - negative: percentage of parent width
|
|
// The resolution is done lazily in resolveColumnWidths, which is called on
|
|
// first paint or mouse event after data changes.
|
|
//
|
|
// Column resize: dragging a column header border (3px hot zone) resizes the
|
|
// column. The drag state (resizeCol, resizeStartX, resizeOrigW, resizeDragging)
|
|
// is stored in the ListView's private data. The core only keeps sResizeListView
|
|
// to route drag events. A minimum column width (LISTVIEW_MIN_COL_W) prevents
|
|
// columns from collapsing to zero.
|
|
//
|
|
// Horizontal scrolling is needed when total column width exceeds the widget's
|
|
// inner width. Both headers and data rows are offset by scrollPosH. The
|
|
// horizontal scrollbar appears automatically when needed, and its presence
|
|
// may trigger the vertical scrollbar to appear (and vice versa) -- this
|
|
// two-pass logic handles the interdependency correctly.
|
|
|
|
#include "dvxWgtP.h"
|
|
#include "../listhelp/listHelp.h"
|
|
|
|
#define LISTVIEW_BORDER 2
|
|
#define LISTVIEW_MIN_COL_W 20
|
|
|
|
static int32_t sTypeId = -1;
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "stb_ds_wrap.h"
|
|
|
|
#define LISTVIEW_MAX_COLS 16
|
|
#define LISTVIEW_PAD 3
|
|
#define LISTVIEW_MIN_ROWS 4
|
|
#define LISTVIEW_COL_PAD 6
|
|
#define LISTVIEW_SORT_W 10
|
|
|
|
// ListView private data -- heap-allocated per widget, stored in w->data.
|
|
// This replaces the old as.listView union pointer. The struct includes
|
|
// column resize drag state that was previously in core globals.
|
|
typedef struct {
|
|
const ListViewColT *cols;
|
|
int32_t colCount;
|
|
const char **cellData;
|
|
int32_t rowCount;
|
|
int32_t selectedIdx;
|
|
int32_t scrollPos;
|
|
int32_t scrollPosH;
|
|
int32_t sortCol;
|
|
ListViewSortE sortDir;
|
|
int32_t resolvedColW[LISTVIEW_MAX_COLS];
|
|
int32_t totalColW;
|
|
int32_t *sortIndex;
|
|
bool multiSelect;
|
|
int32_t anchorIdx;
|
|
uint8_t *selBits;
|
|
bool reorderable;
|
|
int32_t dragIdx;
|
|
int32_t dropIdx;
|
|
void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir);
|
|
// Column resize drag state (moved from core globals)
|
|
int32_t resizeCol;
|
|
int32_t resizeStartX;
|
|
int32_t resizeOrigW;
|
|
bool resizeDragging;
|
|
int32_t sbDragOrient;
|
|
int32_t sbDragOff;
|
|
// Owned cell storage for AddItem/RemoveRow/Clear/SetCell
|
|
char **ownedCells; // stb_ds dynamic array of strdup'd strings
|
|
bool ownsCells; // true if cellData points to ownedCells
|
|
int32_t nextCell; // next cell position for AddItem (row-major index)
|
|
} ListViewDataT;
|
|
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void allocListViewSelBits(WidgetT *w);
|
|
static void listViewBuildSortIndex(WidgetT *w);
|
|
static void listViewSyncOwned(WidgetT *w);
|
|
static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font);
|
|
static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY);
|
|
void wgtListViewSelectAll(WidgetT *w);
|
|
|
|
|
|
// ============================================================
|
|
// allocListViewSelBits
|
|
// ============================================================
|
|
|
|
static void allocListViewSelBits(WidgetT *w) {
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (lv->selBits) {
|
|
free(lv->selBits);
|
|
lv->selBits = NULL;
|
|
}
|
|
|
|
int32_t count = lv->rowCount;
|
|
|
|
if (count > 0 && lv->multiSelect) {
|
|
lv->selBits = (uint8_t *)calloc(count, 1);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// listViewBuildSortIndex
|
|
// ============================================================
|
|
//
|
|
// Build or rebuild the sortIndex array. Uses insertion sort
|
|
// (stable) on the sort column. If no sort is active, frees
|
|
// the index so paint uses natural order.
|
|
|
|
// Build the sort indirection array. Rather than rearranging cellData (which
|
|
// the application owns and indexes into), we maintain a separate array that
|
|
// maps display-row -> data-row. Paint and keyboard navigation work in display-row
|
|
// space, then map through sortIndex to get the data row for cell lookups and
|
|
// selection operations. This decoupling is essential because selBits are
|
|
// indexed by data row -- sorting must not invalidate selection state.
|
|
static void listViewBuildSortIndex(WidgetT *w) {
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
int32_t rowCount = lv->rowCount;
|
|
int32_t sortCol = lv->sortCol;
|
|
int32_t colCount = lv->colCount;
|
|
|
|
// No sort active -- clear index
|
|
if (sortCol < 0 || lv->sortDir == ListViewSortNoneE || rowCount <= 0) {
|
|
if (lv->sortIndex) {
|
|
free(lv->sortIndex);
|
|
lv->sortIndex = NULL;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Allocate or reuse
|
|
int32_t *idx = lv->sortIndex;
|
|
|
|
if (!idx) {
|
|
idx = (int32_t *)malloc(rowCount * sizeof(int32_t));
|
|
|
|
if (!idx) {
|
|
return;
|
|
}
|
|
|
|
lv->sortIndex = idx;
|
|
}
|
|
|
|
// Initialize identity
|
|
for (int32_t i = 0; i < rowCount; i++) {
|
|
idx[i] = i;
|
|
}
|
|
|
|
// Insertion sort -- stable (equal elements keep their original order),
|
|
// O(n^2) worst case but with very low constant factors. For typical
|
|
// ListView row counts (10s to low 100s), this beats quicksort because
|
|
// there's no recursion overhead, no stack usage, and the inner loop
|
|
// is a simple pointer chase. On a 486, the predictable memory access
|
|
// pattern also helps since insertion sort is cache-friendly (sequential
|
|
// access to adjacent elements).
|
|
bool ascending = (lv->sortDir == ListViewSortAscE);
|
|
|
|
for (int32_t i = 1; i < rowCount; i++) {
|
|
int32_t key = idx[i];
|
|
const char *keyStr = lv->cellData[key * colCount + sortCol];
|
|
|
|
if (!keyStr) {
|
|
keyStr = "";
|
|
}
|
|
|
|
int32_t j = i - 1;
|
|
|
|
while (j >= 0) {
|
|
const char *jStr = lv->cellData[idx[j] * colCount + sortCol];
|
|
|
|
if (!jStr) {
|
|
jStr = "";
|
|
}
|
|
|
|
int32_t cmp = strcmp(jStr, keyStr);
|
|
|
|
if (!ascending) {
|
|
cmp = -cmp;
|
|
}
|
|
|
|
if (cmp <= 0) {
|
|
break;
|
|
}
|
|
|
|
idx[j + 1] = idx[j];
|
|
j--;
|
|
}
|
|
|
|
idx[j + 1] = key;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// listViewSyncOwned
|
|
// ============================================================
|
|
|
|
// Sync cellData/rowCount from ownedCells after mutation.
|
|
// Rebuilds sort index and selection bits, forces column width recalculation.
|
|
static void listViewSyncOwned(WidgetT *w) {
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
int32_t cellCount = (int32_t)arrlen(lv->ownedCells);
|
|
|
|
lv->cellData = (const char **)lv->ownedCells;
|
|
lv->rowCount = (lv->colCount > 0) ? cellCount / lv->colCount : 0;
|
|
lv->totalColW = 0;
|
|
lv->ownsCells = true;
|
|
|
|
listViewBuildSortIndex(w);
|
|
allocListViewSelBits(w);
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// resolveColumnWidths
|
|
// ============================================================
|
|
//
|
|
// Resolve tagged column sizes (pixels, chars, percent, or auto)
|
|
// to actual pixel widths. Auto-sized columns (width==0) scan
|
|
// cellData for the widest string in that column.
|
|
|
|
// Resolve tagged column widths to actual pixel values. Column widths in the
|
|
// ListViewColT definition can be specified as:
|
|
// 0 = auto (scan data for widest string, include header)
|
|
// positive = passed to wgtResolveSize (handles px, chars, or percent)
|
|
//
|
|
// Auto-sizing scans all rows for the widest string in that column, which
|
|
// is O(rows * cols) but only runs once per data change (totalColW is reset
|
|
// to 0 when data changes, triggering recalculation on next paint/mouse).
|
|
static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font) {
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
int32_t colCount = lv->colCount;
|
|
int32_t parentW = w->w - LISTVIEW_BORDER * 2;
|
|
|
|
if (parentW < 0) {
|
|
parentW = 0;
|
|
}
|
|
|
|
int32_t totalW = 0;
|
|
|
|
for (int32_t c = 0; c < colCount; c++) {
|
|
int32_t taggedW = lv->cols[c].width;
|
|
|
|
if (taggedW == 0) {
|
|
// Auto-size: scan data for widest string in this column
|
|
int32_t maxLen = (int32_t)strlen(lv->cols[c].title);
|
|
|
|
for (int32_t r = 0; r < lv->rowCount; r++) {
|
|
const char *cell = lv->cellData[r * colCount + c];
|
|
|
|
if (cell) {
|
|
int32_t slen = (int32_t)strlen(cell);
|
|
|
|
if (slen > maxLen) {
|
|
maxLen = slen;
|
|
}
|
|
}
|
|
}
|
|
|
|
lv->resolvedColW[c] = maxLen * font->charWidth + LISTVIEW_COL_PAD;
|
|
} else {
|
|
lv->resolvedColW[c] = wgtResolveSize(taggedW, parentW, font->charWidth);
|
|
}
|
|
|
|
totalW += lv->resolvedColW[c];
|
|
}
|
|
|
|
lv->totalColW = totalW;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListViewColBorderHit
|
|
// ============================================================
|
|
//
|
|
// Returns true if (vx, vy) is on a column header border (resize zone).
|
|
// Coordinates are in widget/virtual space (scroll-adjusted).
|
|
|
|
// Test if a point is on a column header border (within 3px). Used by the
|
|
// cursor-shape logic to show a resize cursor when hovering over column borders.
|
|
// The 3px zone on each side of the border line makes it easy to grab even
|
|
// on low-resolution displays where pixel-precise clicking is difficult.
|
|
bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) {
|
|
if (!w || w->type != sTypeId) {
|
|
return false;
|
|
}
|
|
|
|
const ListViewDataT *lv = (const ListViewDataT *)w->data;
|
|
|
|
if (lv->colCount == 0) {
|
|
return false;
|
|
}
|
|
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t headerH = font->charHeight + 4;
|
|
int32_t headerTop = w->y + LISTVIEW_BORDER;
|
|
|
|
if (vy < headerTop || vy >= headerTop + headerH) {
|
|
return false;
|
|
}
|
|
|
|
int32_t colX = w->x + LISTVIEW_BORDER - lv->scrollPosH;
|
|
|
|
for (int32_t c = 0; c < lv->colCount; c++) {
|
|
int32_t border = colX + lv->resolvedColW[c];
|
|
|
|
if (vx >= border - 3 && vx <= border + 3) {
|
|
return true;
|
|
}
|
|
|
|
colX += lv->resolvedColW[c];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListViewDestroy
|
|
// ============================================================
|
|
|
|
// Free all heap-allocated ListView state. The sortIndex and selBits are
|
|
// optional (may be NULL if no sort or single-select), so we check before
|
|
// freeing. The ListViewDataT struct itself is always freed.
|
|
void widgetListViewDestroy(WidgetT *w) {
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(lv->ownedCells); i++) {
|
|
free(lv->ownedCells[i]);
|
|
}
|
|
arrfree(lv->ownedCells);
|
|
free(lv->selBits);
|
|
free(lv->sortIndex);
|
|
free(lv);
|
|
w->data = NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListView
|
|
// ============================================================
|
|
|
|
// ============================================================
|
|
// widgetListViewCalcMinSize
|
|
// ============================================================
|
|
|
|
void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
int32_t headerH = font->charHeight + 4;
|
|
int32_t minW = font->charWidth * 12 + LISTVIEW_BORDER * 2 + WGT_SB_W;
|
|
|
|
w->calcMinW = minW;
|
|
w->calcMinH = headerH + LISTVIEW_MIN_ROWS * font->charHeight + LISTVIEW_BORDER * 2;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListViewOnKey
|
|
// ============================================================
|
|
|
|
// Key handling must account for the sort indirection layer. Navigation happens
|
|
// in display-row space (what the user sees on screen), but selection and data
|
|
// access happen in data-row space (the original cellData indices). This means:
|
|
// 1. Convert selectedIdx (data row) to display row via linear search in sortIndex
|
|
// 2. Navigate in display space (widgetNavigateIndex)
|
|
// 3. Convert back to data row via sortIndex[displayRow]
|
|
// 4. Update selectedIdx and selBits using data-row indices
|
|
//
|
|
// Shift-select for sorted views is particularly tricky: the range must be
|
|
// computed in display-row space (what the user sees as contiguous) but applied
|
|
// to the data-row selBits. This means the selected data rows may not be
|
|
// contiguous in the original data order.
|
|
void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (!w || w->type != sTypeId || lv->rowCount == 0) {
|
|
return;
|
|
}
|
|
|
|
bool multi = lv->multiSelect;
|
|
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
|
bool ctrl = (mod & KEY_MOD_CTRL) != 0;
|
|
int32_t rowCount = lv->rowCount;
|
|
int32_t *sortIdx = lv->sortIndex;
|
|
|
|
// Enter -- activate selected item (same as double-click)
|
|
if (key == '\r' || key == '\n') {
|
|
if (w->onDblClick && lv->selectedIdx >= 0) {
|
|
w->onDblClick(w);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Ctrl+A -- select all (multi-select only)
|
|
if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) {
|
|
wgtListViewSelectAll(w);
|
|
return;
|
|
}
|
|
|
|
// Space -- toggle current item (multi-select only)
|
|
if (multi && key == ' ') {
|
|
int32_t sel = lv->selectedIdx;
|
|
|
|
if (sel >= 0 && lv->selBits) {
|
|
lv->selBits[sel] ^= 1;
|
|
lv->anchorIdx = sel;
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Find current display row from selectedIdx (data row)
|
|
int32_t displaySel = -1;
|
|
|
|
if (lv->selectedIdx >= 0) {
|
|
if (sortIdx) {
|
|
for (int32_t i = 0; i < rowCount; i++) {
|
|
if (sortIdx[i] == lv->selectedIdx) {
|
|
displaySel = i;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
displaySel = lv->selectedIdx;
|
|
}
|
|
}
|
|
|
|
// Compute visible rows for page up/down
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t headerH = font->charHeight + 4;
|
|
int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH;
|
|
int32_t visibleRows = innerH / font->charHeight;
|
|
|
|
if (visibleRows < 1) {
|
|
visibleRows = 1;
|
|
}
|
|
|
|
int32_t newDisplaySel = widgetNavigateIndex(key, displaySel, rowCount, visibleRows);
|
|
|
|
if (newDisplaySel < 0) {
|
|
return;
|
|
}
|
|
|
|
displaySel = newDisplaySel;
|
|
|
|
// Convert display row back to data row
|
|
int32_t newDataRow = sortIdx ? sortIdx[displaySel] : displaySel;
|
|
|
|
if (newDataRow == lv->selectedIdx) {
|
|
return;
|
|
}
|
|
|
|
lv->selectedIdx = newDataRow;
|
|
|
|
// Update multi-select
|
|
if (multi && lv->selBits) {
|
|
if (shift) {
|
|
// Shift+arrow: range from anchor to new cursor (in data-row space)
|
|
memset(lv->selBits, 0, rowCount);
|
|
|
|
// Convert anchor to display row, then select display range mapped to data rows
|
|
int32_t anchorDisplay = -1;
|
|
int32_t anchor = lv->anchorIdx;
|
|
|
|
if (sortIdx && anchor >= 0) {
|
|
for (int32_t i = 0; i < rowCount; i++) {
|
|
if (sortIdx[i] == anchor) {
|
|
anchorDisplay = i;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
anchorDisplay = anchor;
|
|
}
|
|
|
|
// Select all display rows in range, mapping to data rows
|
|
int32_t lo = anchorDisplay < displaySel ? anchorDisplay : displaySel;
|
|
int32_t hi = anchorDisplay > displaySel ? anchorDisplay : displaySel;
|
|
|
|
if (lo < 0) {
|
|
lo = 0;
|
|
}
|
|
|
|
if (hi >= rowCount) {
|
|
hi = rowCount - 1;
|
|
}
|
|
|
|
for (int32_t i = lo; i <= hi; i++) {
|
|
int32_t dr = sortIdx ? sortIdx[i] : i;
|
|
lv->selBits[dr] = 1;
|
|
}
|
|
}
|
|
// Plain arrow: just move cursor, leave selections untouched
|
|
}
|
|
|
|
// Scroll to keep selection visible (in display-row space)
|
|
if (displaySel >= 0) {
|
|
if (displaySel < lv->scrollPos) {
|
|
lv->scrollPos = displaySel;
|
|
} else if (displaySel >= lv->scrollPos + visibleRows) {
|
|
lv->scrollPos = displaySel - visibleRows + 1;
|
|
}
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListViewOnMouse
|
|
// ============================================================
|
|
|
|
// Mouse handling is the most complex part of ListView. It handles four
|
|
// distinct hit regions (checked in priority order):
|
|
// 1. Vertical scrollbar (right edge, if visible)
|
|
// 2. Horizontal scrollbar (bottom edge, if visible)
|
|
// 3. Dead corner (when both scrollbars present -- no action)
|
|
// 4. Column headers: resize drag (border +-3px) or sort toggle (click)
|
|
// 5. Data rows: item selection with Ctrl/Shift modifiers, double-click,
|
|
// and drag-reorder initiation
|
|
//
|
|
// The scrollbar visibility calculation is repeated here (matching paint)
|
|
// because scrollbar presence depends on content/widget dimensions which
|
|
// may have changed since the last paint.
|
|
void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
|
sFocusedWidget = hit;
|
|
ListViewDataT *lv = (ListViewDataT *)hit->data;
|
|
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
const BitmapFontT *font = &ctx->font;
|
|
|
|
// Resolve column widths if needed
|
|
if (lv->totalColW == 0 && lv->colCount > 0) {
|
|
resolveColumnWidths(hit, font);
|
|
}
|
|
|
|
int32_t headerH = font->charHeight + 4;
|
|
int32_t innerH = hit->h - LISTVIEW_BORDER * 2 - headerH;
|
|
int32_t innerW = hit->w - LISTVIEW_BORDER * 2;
|
|
int32_t visibleRows = innerH / font->charHeight;
|
|
int32_t totalColW = lv->totalColW;
|
|
bool needVSb = (lv->rowCount > visibleRows);
|
|
bool needHSb = false;
|
|
|
|
// Scrollbar interdependency: adding a vertical scrollbar reduces innerW,
|
|
// which may cause columns to overflow and require a horizontal scrollbar.
|
|
// Adding a horizontal scrollbar reduces innerH, which may require a
|
|
// vertical scrollbar. This two-pass check handles the mutual dependency.
|
|
if (needVSb) {
|
|
innerW -= WGT_SB_W;
|
|
}
|
|
|
|
if (totalColW > innerW) {
|
|
needHSb = true;
|
|
innerH -= WGT_SB_W;
|
|
visibleRows = innerH / font->charHeight;
|
|
|
|
if (!needVSb && lv->rowCount > visibleRows) {
|
|
needVSb = true;
|
|
innerW -= WGT_SB_W;
|
|
}
|
|
}
|
|
|
|
if (visibleRows < 1) {
|
|
visibleRows = 1;
|
|
}
|
|
|
|
// Clamp scroll positions
|
|
int32_t maxScrollV = lv->rowCount - visibleRows;
|
|
int32_t maxScrollH = totalColW - innerW;
|
|
|
|
if (maxScrollV < 0) {
|
|
maxScrollV = 0;
|
|
}
|
|
|
|
if (maxScrollH < 0) {
|
|
maxScrollH = 0;
|
|
}
|
|
|
|
lv->scrollPos = clampInt(lv->scrollPos, 0, maxScrollV);
|
|
lv->scrollPosH = clampInt(lv->scrollPosH, 0, maxScrollH);
|
|
|
|
// Check vertical scrollbar
|
|
if (needVSb) {
|
|
int32_t sbX = hit->x + hit->w - LISTVIEW_BORDER - WGT_SB_W;
|
|
int32_t sbY = hit->y + LISTVIEW_BORDER + headerH;
|
|
|
|
if (vx >= sbX && vy >= sbY && vy < sbY + innerH) {
|
|
int32_t relY = vy - sbY;
|
|
ScrollHitE sh = widgetScrollbarHitTest(innerH, relY, lv->rowCount, visibleRows, lv->scrollPos);
|
|
|
|
if (sh == ScrollHitArrowDecE) {
|
|
if (lv->scrollPos > 0) {
|
|
lv->scrollPos--;
|
|
}
|
|
} else if (sh == ScrollHitArrowIncE) {
|
|
if (lv->scrollPos < maxScrollV) {
|
|
lv->scrollPos++;
|
|
}
|
|
} else if (sh == ScrollHitPageDecE) {
|
|
lv->scrollPos -= visibleRows;
|
|
lv->scrollPos = clampInt(lv->scrollPos, 0, maxScrollV);
|
|
} else if (sh == ScrollHitPageIncE) {
|
|
lv->scrollPos += visibleRows;
|
|
lv->scrollPos = clampInt(lv->scrollPos, 0, maxScrollV);
|
|
} else if (sh == ScrollHitThumbE) {
|
|
int32_t trackLen = innerH - WGT_SB_W * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, lv->rowCount, visibleRows, lv->scrollPos, &thumbPos, &thumbSize);
|
|
|
|
sDragWidget = hit;
|
|
lv->sbDragOrient = 0;
|
|
lv->sbDragOff = relY - WGT_SB_W - thumbPos;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check horizontal scrollbar
|
|
if (needHSb) {
|
|
int32_t sbX = hit->x + LISTVIEW_BORDER;
|
|
int32_t sbY = hit->y + hit->h - LISTVIEW_BORDER - WGT_SB_W;
|
|
|
|
if (vy >= sbY && vx >= sbX && vx < sbX + innerW) {
|
|
int32_t relX = vx - sbX;
|
|
int32_t pageSize = innerW - font->charWidth;
|
|
|
|
if (pageSize < font->charWidth) {
|
|
pageSize = font->charWidth;
|
|
}
|
|
|
|
ScrollHitE sh = widgetScrollbarHitTest(innerW, relX, totalColW, innerW, lv->scrollPosH);
|
|
|
|
if (sh == ScrollHitArrowDecE) {
|
|
lv->scrollPosH -= font->charWidth;
|
|
} else if (sh == ScrollHitArrowIncE) {
|
|
lv->scrollPosH += font->charWidth;
|
|
} else if (sh == ScrollHitPageDecE) {
|
|
lv->scrollPosH -= pageSize;
|
|
} else if (sh == ScrollHitPageIncE) {
|
|
lv->scrollPosH += pageSize;
|
|
} else if (sh == ScrollHitThumbE) {
|
|
int32_t trackLen = innerW - WGT_SB_W * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, totalColW, innerW, lv->scrollPosH, &thumbPos, &thumbSize);
|
|
|
|
sDragWidget = hit;
|
|
lv->sbDragOrient = 1;
|
|
lv->sbDragOff = relX - WGT_SB_W - thumbPos;
|
|
}
|
|
|
|
lv->scrollPosH = clampInt(lv->scrollPosH, 0, maxScrollH);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check dead corner (both scrollbars present)
|
|
if (needVSb && needHSb) {
|
|
int32_t cornerX = hit->x + hit->w - LISTVIEW_BORDER - WGT_SB_W;
|
|
int32_t cornerY = hit->y + hit->h - LISTVIEW_BORDER - WGT_SB_W;
|
|
|
|
if (vx >= cornerX && vy >= cornerY) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check column header area
|
|
int32_t headerTop = hit->y + LISTVIEW_BORDER;
|
|
|
|
if (vy >= headerTop && vy < headerTop + headerH) {
|
|
// Check for column border resize (3px zone on each side of border)
|
|
int32_t colX = hit->x + LISTVIEW_BORDER - lv->scrollPosH;
|
|
|
|
for (int32_t c = 0; c < lv->colCount; c++) {
|
|
int32_t cw = lv->resolvedColW[c];
|
|
int32_t border = colX + cw;
|
|
|
|
if (vx >= border - 3 && vx <= border + 3 && c < lv->colCount) {
|
|
if (multiClickDetect(vx, vy) >= 2) {
|
|
// Double-click on column border: auto-size to fit content
|
|
int32_t maxLen = (int32_t)strlen(lv->cols[c].title);
|
|
|
|
for (int32_t r = 0; r < lv->rowCount; r++) {
|
|
const char *cell = lv->cellData[r * lv->colCount + c];
|
|
|
|
if (cell) {
|
|
int32_t slen = (int32_t)strlen(cell);
|
|
|
|
if (slen > maxLen) {
|
|
maxLen = slen;
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t newW = maxLen * font->charWidth + LISTVIEW_COL_PAD;
|
|
|
|
if (newW < LISTVIEW_MIN_COL_W) {
|
|
newW = LISTVIEW_MIN_COL_W;
|
|
}
|
|
|
|
lv->resolvedColW[c] = newW;
|
|
|
|
// Recalculate totalColW
|
|
int32_t total = 0;
|
|
|
|
for (int32_t tc = 0; tc < lv->colCount; tc++) {
|
|
total += lv->resolvedColW[tc];
|
|
}
|
|
|
|
lv->totalColW = total;
|
|
wgtInvalidatePaint(hit);
|
|
} else {
|
|
// Start column resize drag (deferred until mouse moves)
|
|
sDragWidget = hit;
|
|
lv->sbDragOrient = -1;
|
|
lv->resizeCol = c;
|
|
lv->resizeStartX = vx;
|
|
lv->resizeOrigW = cw;
|
|
lv->resizeDragging = false;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
colX += cw;
|
|
}
|
|
|
|
// Not on a border -- check for sort click (disabled when reorderable
|
|
// since sorting conflicts with manual ordering). Clicking a column
|
|
// toggles between ascending/descending; clicking a different column
|
|
// starts ascending.
|
|
if (!lv->reorderable) {
|
|
colX = hit->x + LISTVIEW_BORDER - lv->scrollPosH;
|
|
|
|
for (int32_t c = 0; c < lv->colCount; c++) {
|
|
int32_t cw = lv->resolvedColW[c];
|
|
|
|
if (vx >= colX && vx < colX + cw) {
|
|
// Toggle sort direction for this column
|
|
if (lv->sortCol == c) {
|
|
if (lv->sortDir == ListViewSortAscE) {
|
|
lv->sortDir = ListViewSortDescE;
|
|
} else {
|
|
lv->sortDir = ListViewSortAscE;
|
|
}
|
|
} else {
|
|
lv->sortCol = c;
|
|
lv->sortDir = ListViewSortAscE;
|
|
}
|
|
|
|
listViewBuildSortIndex(hit);
|
|
|
|
if (lv->onHeaderClick) {
|
|
lv->onHeaderClick(hit, c, lv->sortDir);
|
|
}
|
|
|
|
wgtInvalidatePaint(hit);
|
|
return;
|
|
}
|
|
|
|
colX += cw;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Click on data area
|
|
int32_t dataTop = headerTop + headerH;
|
|
int32_t relY = vy - dataTop;
|
|
|
|
if (relY < 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t clickedRow = relY / font->charHeight;
|
|
int32_t displayRow = lv->scrollPos + clickedRow;
|
|
|
|
if (displayRow >= 0 && displayRow < lv->rowCount) {
|
|
int32_t dataRow = lv->sortIndex ? lv->sortIndex[displayRow] : displayRow;
|
|
lv->selectedIdx = dataRow;
|
|
|
|
bool multi = lv->multiSelect;
|
|
bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0;
|
|
bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0;
|
|
|
|
if (multi && lv->selBits) {
|
|
if (ctrl) {
|
|
// Ctrl+click: toggle item, update anchor
|
|
lv->selBits[dataRow] ^= 1;
|
|
lv->anchorIdx = dataRow;
|
|
} else if (shift) {
|
|
// Shift+click: range from anchor to clicked (in display-row space)
|
|
memset(lv->selBits, 0, lv->rowCount);
|
|
|
|
int32_t anchorDisplay = -1;
|
|
int32_t anchor = lv->anchorIdx;
|
|
|
|
if (lv->sortIndex && anchor >= 0) {
|
|
for (int32_t i = 0; i < lv->rowCount; i++) {
|
|
if (lv->sortIndex[i] == anchor) {
|
|
anchorDisplay = i;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
anchorDisplay = anchor;
|
|
}
|
|
|
|
int32_t lo = anchorDisplay < displayRow ? anchorDisplay : displayRow;
|
|
int32_t hi = anchorDisplay > displayRow ? anchorDisplay : displayRow;
|
|
|
|
if (lo < 0) {
|
|
lo = 0;
|
|
}
|
|
|
|
if (hi >= lv->rowCount) {
|
|
hi = lv->rowCount - 1;
|
|
}
|
|
|
|
for (int32_t i = lo; i <= hi; i++) {
|
|
int32_t dr = lv->sortIndex ? lv->sortIndex[i] : i;
|
|
lv->selBits[dr] = 1;
|
|
}
|
|
} else {
|
|
// Plain click: select only this item, update anchor
|
|
memset(lv->selBits, 0, lv->rowCount);
|
|
lv->selBits[dataRow] = 1;
|
|
lv->anchorIdx = dataRow;
|
|
}
|
|
}
|
|
|
|
if (hit->onChange) {
|
|
hit->onChange(hit);
|
|
}
|
|
|
|
// Double-click: fire onDblClick
|
|
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
|
|
hit->onDblClick(hit);
|
|
}
|
|
|
|
// Initiate drag-reorder if enabled (not from modifier clicks)
|
|
if (lv->reorderable && !shift && !ctrl) {
|
|
lv->dragIdx = dataRow;
|
|
lv->dropIdx = dataRow;
|
|
sDragWidget = hit;
|
|
}
|
|
}
|
|
|
|
wgtInvalidatePaint(hit);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListViewPaint
|
|
// ============================================================
|
|
|
|
// Paint: the most involved paint function in the widget toolkit. Renders
|
|
// in layers:
|
|
// 1. Outer sunken bevel border
|
|
// 2. Column headers (clipped to content width, scrolled by scrollPosH):
|
|
// each header is a raised bevel button with centered text and optional
|
|
// sort indicator (up/down triangle)
|
|
// 3. Data rows (clipped to data area, scrolled both H and V):
|
|
// background fill, then per-cell text rendering with alignment, then
|
|
// selection highlight, then cursor focus rect in multi-select mode
|
|
// 4. Drag-reorder insertion line (if active)
|
|
// 5. Scrollbars (V and/or H, with dead corner fill)
|
|
// 6. Outer focus rect
|
|
//
|
|
// Clip rectangles are saved/restored around the header and data sections to
|
|
// prevent rendering from bleeding into the scrollbar or border areas. This
|
|
// is cheaper than per-cell clipping and handles horizontal scroll correctly
|
|
// (partially visible columns at the edges are clipped by the display's clip
|
|
// rect rather than by the draw calls).
|
|
void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
|
|
|
// Resolve column widths if needed
|
|
if (lv->totalColW == 0 && lv->colCount > 0) {
|
|
resolveColumnWidths(w, font);
|
|
}
|
|
|
|
int32_t headerH = font->charHeight + 4;
|
|
int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH;
|
|
int32_t innerW = w->w - LISTVIEW_BORDER * 2;
|
|
int32_t visibleRows = innerH / font->charHeight;
|
|
int32_t totalColW = lv->totalColW;
|
|
bool needVSb = (lv->rowCount > visibleRows);
|
|
bool needHSb = false;
|
|
|
|
if (needVSb) {
|
|
innerW -= WGT_SB_W;
|
|
}
|
|
|
|
if (totalColW > innerW) {
|
|
needHSb = true;
|
|
innerH -= WGT_SB_W;
|
|
visibleRows = innerH / font->charHeight;
|
|
|
|
if (!needVSb && lv->rowCount > visibleRows) {
|
|
needVSb = true;
|
|
innerW -= WGT_SB_W;
|
|
}
|
|
}
|
|
|
|
if (visibleRows < 1) {
|
|
visibleRows = 1;
|
|
}
|
|
|
|
// Sunken border
|
|
BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, 2);
|
|
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
|
|
|
// Clamp scroll positions
|
|
int32_t maxScrollV = lv->rowCount - visibleRows;
|
|
int32_t maxScrollH = totalColW - innerW;
|
|
|
|
if (maxScrollV < 0) {
|
|
maxScrollV = 0;
|
|
}
|
|
|
|
if (maxScrollH < 0) {
|
|
maxScrollH = 0;
|
|
}
|
|
|
|
lv->scrollPos = clampInt(lv->scrollPos, 0, maxScrollV);
|
|
lv->scrollPosH = clampInt(lv->scrollPosH, 0, maxScrollH);
|
|
|
|
int32_t baseX = w->x + LISTVIEW_BORDER;
|
|
int32_t baseY = w->y + LISTVIEW_BORDER;
|
|
int32_t colCount = lv->colCount;
|
|
|
|
// ---- Draw column headers ----
|
|
{
|
|
// Clip headers to the content area width
|
|
int32_t oldClipX = d->clipX;
|
|
int32_t oldClipY = d->clipY;
|
|
int32_t oldClipW = d->clipW;
|
|
int32_t oldClipH = d->clipH;
|
|
setClipRect(d, baseX, baseY, innerW, headerH);
|
|
|
|
int32_t hdrX = baseX - lv->scrollPosH;
|
|
BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1);
|
|
|
|
for (int32_t c = 0; c < colCount; c++) {
|
|
int32_t cw = lv->resolvedColW[c];
|
|
|
|
// Draw raised button for header
|
|
drawBevel(d, ops, hdrX, baseY, cw, headerH, &hdrBevel);
|
|
|
|
// Header text
|
|
int32_t textX = hdrX + LISTVIEW_PAD;
|
|
int32_t textY = baseY + 2;
|
|
int32_t availTextW = cw - LISTVIEW_PAD * 2;
|
|
|
|
// Reserve space for sort indicator if this is the sort column
|
|
if (c == lv->sortCol && lv->sortDir != ListViewSortNoneE) {
|
|
availTextW -= LISTVIEW_SORT_W;
|
|
}
|
|
|
|
if (lv->cols[c].title) {
|
|
int32_t titleLen = (int32_t)strlen(lv->cols[c].title);
|
|
int32_t titleW = titleLen * font->charWidth;
|
|
|
|
if (titleW > availTextW) {
|
|
titleLen = availTextW / font->charWidth;
|
|
}
|
|
|
|
if (titleLen > 0) {
|
|
drawTextN(d, ops, font, textX, textY, lv->cols[c].title, titleLen, colors->contentFg, colors->windowFace, true);
|
|
}
|
|
}
|
|
|
|
// Sort indicator: a small filled triangle (4px tall) in the header.
|
|
// Up triangle for ascending, down triangle for descending. Drawn as
|
|
// horizontal lines of increasing/decreasing width, same technique as
|
|
// the dropdown arrow glyph.
|
|
if (c == lv->sortCol && lv->sortDir != ListViewSortNoneE) {
|
|
int32_t cx = hdrX + cw - LISTVIEW_SORT_W / 2 - LISTVIEW_PAD;
|
|
int32_t cy = baseY + headerH / 2;
|
|
|
|
if (lv->sortDir == ListViewSortAscE) {
|
|
// Up triangle (ascending)
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, colors->contentFg);
|
|
}
|
|
} else {
|
|
// Down triangle (descending)
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
drawHLine(d, ops, cx - 3 + i, cy - 1 + i, 7 - i * 2, colors->contentFg);
|
|
}
|
|
}
|
|
}
|
|
|
|
hdrX += cw;
|
|
}
|
|
|
|
// Fill any remaining header space to the right of columns
|
|
if (hdrX < baseX + innerW) {
|
|
drawBevel(d, ops, hdrX, baseY, baseX + innerW - hdrX, headerH, &hdrBevel);
|
|
}
|
|
|
|
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
|
}
|
|
|
|
// ---- Draw data rows ----
|
|
{
|
|
int32_t dataY = baseY + headerH;
|
|
|
|
// Set clip rect to data area
|
|
int32_t oldClipX = d->clipX;
|
|
int32_t oldClipY = d->clipY;
|
|
int32_t oldClipW = d->clipW;
|
|
int32_t oldClipH = d->clipH;
|
|
setClipRect(d, baseX, dataY, innerW, innerH);
|
|
|
|
int32_t scrollPos = lv->scrollPos;
|
|
int32_t *sortIdx = lv->sortIndex;
|
|
|
|
// Fill entire data area background first. This is done as a single
|
|
// rectFill rather than per-row because it's cheaper to fill once and
|
|
// then overdraw selected rows than to compute and fill each unselected
|
|
// row's background separately.
|
|
rectFill(d, ops, baseX, dataY, innerW, innerH, bg);
|
|
|
|
bool multi = lv->multiSelect;
|
|
uint8_t *selBits = lv->selBits;
|
|
|
|
for (int32_t i = 0; i < visibleRows && (scrollPos + i) < lv->rowCount; i++) {
|
|
int32_t displayRow = scrollPos + i;
|
|
int32_t dataRow = sortIdx ? sortIdx[displayRow] : displayRow;
|
|
int32_t iy = dataY + i * font->charHeight;
|
|
uint32_t ifg = fg;
|
|
uint32_t ibg = bg;
|
|
|
|
bool selected = false;
|
|
|
|
if (multi && selBits) {
|
|
selected = selBits[dataRow] != 0;
|
|
} else {
|
|
selected = (dataRow == lv->selectedIdx);
|
|
}
|
|
|
|
if (selected) {
|
|
ifg = colors->menuHighlightFg;
|
|
ibg = colors->menuHighlightBg;
|
|
rectFill(d, ops, baseX, iy, innerW, font->charHeight, ibg);
|
|
}
|
|
|
|
// Draw each cell
|
|
int32_t cellX = baseX - lv->scrollPosH;
|
|
|
|
for (int32_t c = 0; c < colCount; c++) {
|
|
int32_t cw = lv->resolvedColW[c];
|
|
const char *cell = lv->cellData[dataRow * colCount + c];
|
|
|
|
if (cell) {
|
|
int32_t cellLen = (int32_t)strlen(cell);
|
|
int32_t maxChars = (cw - LISTVIEW_PAD * 2) / font->charWidth;
|
|
|
|
if (maxChars < 0) {
|
|
maxChars = 0;
|
|
}
|
|
|
|
if (cellLen > maxChars) {
|
|
cellLen = maxChars;
|
|
}
|
|
|
|
int32_t tx = cellX + LISTVIEW_PAD;
|
|
|
|
// Column alignment: left (default), right, or center.
|
|
// Right/center alignment compute the available width and
|
|
// offset the text start position accordingly. This is done
|
|
// per-cell rather than using a general-purpose aligned draw
|
|
// function to keep the inner loop simple and avoid extra
|
|
// function call overhead per cell.
|
|
if (lv->cols[c].align == ListViewAlignRightE) {
|
|
int32_t renderedW = cellLen * font->charWidth;
|
|
int32_t availW = cw - LISTVIEW_PAD * 2;
|
|
tx = cellX + LISTVIEW_PAD + (availW - renderedW);
|
|
} else if (lv->cols[c].align == ListViewAlignCenterE) {
|
|
int32_t renderedW = cellLen * font->charWidth;
|
|
int32_t availW = cw - LISTVIEW_PAD * 2;
|
|
tx = cellX + LISTVIEW_PAD + (availW - renderedW) / 2;
|
|
}
|
|
|
|
if (cellLen > 0) {
|
|
drawTextN(d, ops, font, tx, iy, cell, cellLen, ifg, ibg, true);
|
|
}
|
|
}
|
|
|
|
cellX += cw;
|
|
}
|
|
|
|
// Draw cursor focus rect in multi-select mode (on top of text)
|
|
if (multi && dataRow == lv->selectedIdx && w == sFocusedWidget) {
|
|
drawFocusRect(d, ops, baseX, iy, innerW, font->charHeight, fg);
|
|
}
|
|
}
|
|
|
|
// Draw drag-reorder insertion indicator
|
|
if (lv->reorderable && lv->dragIdx >= 0 && lv->dropIdx >= 0) {
|
|
int32_t drop = lv->dropIdx;
|
|
int32_t lineY = dataY + (drop - lv->scrollPos) * font->charHeight;
|
|
|
|
if (lineY >= dataY && lineY <= dataY + innerH) {
|
|
drawHLine(d, ops, baseX, lineY, innerW, fg);
|
|
drawHLine(d, ops, baseX, lineY + 1, innerW, fg);
|
|
}
|
|
}
|
|
|
|
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
|
}
|
|
|
|
// ---- Draw scrollbars ----
|
|
if (needVSb) {
|
|
int32_t sbX = w->x + w->w - LISTVIEW_BORDER - WGT_SB_W;
|
|
int32_t sbY = w->y + LISTVIEW_BORDER + headerH;
|
|
widgetDrawScrollbarV(d, ops, colors, sbX, sbY, innerH, lv->rowCount, visibleRows, lv->scrollPos);
|
|
}
|
|
|
|
if (needHSb) {
|
|
int32_t sbX = w->x + LISTVIEW_BORDER;
|
|
int32_t sbY = w->y + w->h - LISTVIEW_BORDER - WGT_SB_W;
|
|
widgetDrawScrollbarH(d, ops, colors, sbX, sbY, innerW, totalColW, innerW, lv->scrollPosH);
|
|
|
|
// Fill the dead corner when both scrollbars are present. This is the
|
|
// small square at the intersection of the two scrollbars (bottom-right)
|
|
// that doesn't belong to either. Without filling it, stale content
|
|
// from previous paints would show through.
|
|
if (needVSb) {
|
|
rectFill(d, ops, sbX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace);
|
|
}
|
|
}
|
|
|
|
if (w == sFocusedWidget) {
|
|
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Public accessors (kept here due to static helper dependencies)
|
|
// ============================================================
|
|
|
|
void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (count > LISTVIEW_MAX_COLS) {
|
|
count = LISTVIEW_MAX_COLS;
|
|
}
|
|
|
|
lv->cols = cols;
|
|
lv->colCount = count;
|
|
lv->totalColW = 0;
|
|
|
|
if (lv->rowCount > 0) {
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
|
|
if (ctx) {
|
|
resolveColumnWidths(w, &ctx->font);
|
|
}
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (lv->sortIndex) {
|
|
free(lv->sortIndex);
|
|
lv->sortIndex = NULL;
|
|
}
|
|
|
|
lv->cellData = cellData;
|
|
lv->rowCount = rowCount;
|
|
lv->totalColW = 0;
|
|
|
|
if (lv->selectedIdx >= rowCount) {
|
|
lv->selectedIdx = rowCount > 0 ? 0 : -1;
|
|
}
|
|
|
|
if (lv->selectedIdx < 0 && rowCount > 0) {
|
|
lv->selectedIdx = 0;
|
|
}
|
|
|
|
lv->anchorIdx = lv->selectedIdx;
|
|
|
|
listViewBuildSortIndex(w);
|
|
allocListViewSelBits(w);
|
|
|
|
if (lv->selBits && lv->selectedIdx >= 0) {
|
|
lv->selBits[lv->selectedIdx] = 1;
|
|
}
|
|
|
|
if (lv->colCount > 0) {
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
|
|
if (ctx) {
|
|
resolveColumnWidths(w, &ctx->font);
|
|
}
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
void wgtListViewSetMultiSelect(WidgetT *w, bool multi) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
lv->multiSelect = multi;
|
|
allocListViewSelBits(w);
|
|
|
|
if (lv->selBits && lv->selectedIdx >= 0) {
|
|
lv->selBits[lv->selectedIdx] = 1;
|
|
}
|
|
}
|
|
|
|
|
|
void wgtListViewSetReorderable(WidgetT *w, bool reorderable) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
lv->reorderable = reorderable;
|
|
|
|
if (reorderable) {
|
|
lv->sortCol = -1;
|
|
lv->sortDir = ListViewSortNoneE;
|
|
|
|
if (lv->sortIndex) {
|
|
free(lv->sortIndex);
|
|
lv->sortIndex = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
lv->sortCol = col;
|
|
lv->sortDir = dir;
|
|
listViewBuildSortIndex(w);
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListViewScrollDragUpdate
|
|
// ============================================================
|
|
|
|
// Handle column resize drag. orient == -1 means column resize (sent by
|
|
// widgetEvent.c when sResizeListView is set). The drag is deferred: we
|
|
// don't start modifying column widths until the mouse actually moves from
|
|
// the initial click position. This prevents accidental resizes from stray
|
|
// clicks on column borders.
|
|
static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
// orient == -1 means column resize drag
|
|
if (orient == -1) {
|
|
if (!lv->resizeDragging) {
|
|
if (mouseX == lv->resizeStartX) {
|
|
return;
|
|
}
|
|
lv->resizeDragging = true;
|
|
}
|
|
|
|
int32_t delta = mouseX - lv->resizeStartX;
|
|
int32_t newW = lv->resizeOrigW + delta;
|
|
|
|
if (newW < LISTVIEW_MIN_COL_W) {
|
|
newW = LISTVIEW_MIN_COL_W;
|
|
}
|
|
|
|
if (newW != lv->resolvedColW[lv->resizeCol]) {
|
|
lv->resolvedColW[lv->resizeCol] = newW;
|
|
|
|
int32_t total = 0;
|
|
|
|
for (int32_t c = 0; c < lv->colCount; c++) {
|
|
total += lv->resolvedColW[c];
|
|
}
|
|
|
|
lv->totalColW = total;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
|
|
int32_t headerH = font->charHeight + 4;
|
|
int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH;
|
|
int32_t innerW = w->w - LISTVIEW_BORDER * 2;
|
|
int32_t visibleRows = innerH / font->charHeight;
|
|
int32_t totalColW = lv->totalColW;
|
|
bool needVSb = (lv->rowCount > visibleRows);
|
|
|
|
if (needVSb) {
|
|
innerW -= WGT_SB_W;
|
|
}
|
|
|
|
if (totalColW > innerW) {
|
|
innerH -= WGT_SB_W;
|
|
visibleRows = innerH / font->charHeight;
|
|
|
|
if (!needVSb && lv->rowCount > visibleRows) {
|
|
needVSb = true;
|
|
innerW -= WGT_SB_W;
|
|
}
|
|
}
|
|
|
|
if (visibleRows < 1) {
|
|
visibleRows = 1;
|
|
}
|
|
|
|
if (orient == 0) {
|
|
// Vertical scrollbar drag
|
|
int32_t maxScroll = lv->rowCount - visibleRows;
|
|
|
|
if (maxScroll <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t trackLen = innerH - WGT_SB_W * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, lv->rowCount, visibleRows, lv->scrollPos, &thumbPos, &thumbSize);
|
|
|
|
int32_t sbY = w->y + LISTVIEW_BORDER + headerH;
|
|
int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff;
|
|
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
|
|
lv->scrollPos = clampInt(newScroll, 0, maxScroll);
|
|
} else if (orient == 1) {
|
|
// Horizontal scrollbar drag
|
|
int32_t maxScroll = totalColW - innerW;
|
|
|
|
if (maxScroll <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t trackLen = innerW - WGT_SB_W * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, totalColW, innerW, lv->scrollPosH, &thumbPos, &thumbSize);
|
|
|
|
int32_t sbX = w->x + LISTVIEW_BORDER;
|
|
int32_t relMouse = mouseX - sbX - WGT_SB_W - dragOff;
|
|
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
|
|
lv->scrollPosH = clampInt(newScroll, 0, maxScroll);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
// ============================================================
|
|
// widgetListViewGetCursorShape
|
|
// ============================================================
|
|
|
|
int32_t widgetListViewGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy) {
|
|
if (widgetListViewColBorderHit(w, vx, vy)) {
|
|
return CURSOR_RESIZE_H;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static void widgetListViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
|
(void)root;
|
|
(void)x;
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t headerH = font->charHeight + 4;
|
|
int32_t dataY = w->y + LISTVIEW_BORDER + headerH;
|
|
int32_t relY = y - dataY;
|
|
int32_t dropIdx = lv->scrollPos + relY / font->charHeight;
|
|
|
|
if (dropIdx < 0) {
|
|
dropIdx = 0;
|
|
}
|
|
|
|
if (dropIdx > lv->rowCount) {
|
|
dropIdx = lv->rowCount;
|
|
}
|
|
|
|
lv->dropIdx = dropIdx;
|
|
|
|
// Auto-scroll near edges
|
|
int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH;
|
|
int32_t visibleRows = innerH / font->charHeight;
|
|
|
|
if (relY < font->charHeight && lv->scrollPos > 0) {
|
|
lv->scrollPos--;
|
|
} else if (relY >= (visibleRows - 1) * font->charHeight && lv->scrollPos < lv->rowCount - visibleRows) {
|
|
lv->scrollPos++;
|
|
}
|
|
}
|
|
|
|
|
|
static void widgetListViewReorderDrop(WidgetT *w) {
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
int32_t from = lv->dragIdx;
|
|
int32_t to = lv->dropIdx;
|
|
|
|
lv->dragIdx = -1;
|
|
lv->dropIdx = -1;
|
|
|
|
if (from < 0 || to < 0 || from == to || from == to - 1) {
|
|
return;
|
|
}
|
|
|
|
if (from < 0 || from >= lv->rowCount) {
|
|
return;
|
|
}
|
|
|
|
// Move row data by shifting the cellData pointers.
|
|
// cellData is row-major: cellData[row * colCount + col].
|
|
int32_t cols = lv->colCount;
|
|
const char **moving = (const char **)malloc(cols * sizeof(const char *));
|
|
|
|
if (!moving) {
|
|
return;
|
|
}
|
|
|
|
// Save the moving row
|
|
for (int32_t c = 0; c < cols; c++) {
|
|
moving[c] = lv->cellData[from * cols + c];
|
|
}
|
|
|
|
// Save the moving row's selection bit
|
|
uint8_t movingSel = (lv->selBits && from < lv->rowCount) ? lv->selBits[from] : 0;
|
|
|
|
if (to > from) {
|
|
// Moving down: shift rows up
|
|
for (int32_t r = from; r < to - 1; r++) {
|
|
for (int32_t c = 0; c < cols; c++) {
|
|
((const char **)lv->cellData)[r * cols + c] = lv->cellData[(r + 1) * cols + c];
|
|
}
|
|
|
|
if (lv->selBits) {
|
|
lv->selBits[r] = lv->selBits[r + 1];
|
|
}
|
|
}
|
|
|
|
for (int32_t c = 0; c < cols; c++) {
|
|
((const char **)lv->cellData)[(to - 1) * cols + c] = moving[c];
|
|
}
|
|
|
|
if (lv->selBits) {
|
|
lv->selBits[to - 1] = movingSel;
|
|
}
|
|
|
|
lv->selectedIdx = to - 1;
|
|
} else {
|
|
// Moving up: shift rows down
|
|
for (int32_t r = from; r > to; r--) {
|
|
for (int32_t c = 0; c < cols; c++) {
|
|
((const char **)lv->cellData)[r * cols + c] = lv->cellData[(r - 1) * cols + c];
|
|
}
|
|
|
|
if (lv->selBits) {
|
|
lv->selBits[r] = lv->selBits[r - 1];
|
|
}
|
|
}
|
|
|
|
for (int32_t c = 0; c < cols; c++) {
|
|
((const char **)lv->cellData)[to * cols + c] = moving[c];
|
|
}
|
|
|
|
if (lv->selBits) {
|
|
lv->selBits[to] = movingSel;
|
|
}
|
|
|
|
lv->selectedIdx = to;
|
|
}
|
|
|
|
free(moving);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListViewOnDragEnd
|
|
// ============================================================
|
|
|
|
static void widgetListViewOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
|
(void)root;
|
|
(void)x;
|
|
(void)y;
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (lv->dragIdx >= 0) {
|
|
widgetListViewReorderDrop(w);
|
|
lv->dragIdx = -1;
|
|
}
|
|
|
|
lv->resizeDragging = false;
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListViewOnDragUpdate
|
|
// ============================================================
|
|
|
|
static void widgetListViewOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (lv->resizeDragging) {
|
|
widgetListViewScrollDragUpdate(w, -1, 0, x, y);
|
|
} else if (lv->dragIdx >= 0) {
|
|
widgetListViewReorderUpdate(w, root, x, y);
|
|
} else {
|
|
widgetListViewScrollDragUpdate(w, lv->sbDragOrient, lv->sbDragOff, x, y);
|
|
}
|
|
}
|
|
|
|
|
|
static const WidgetClassT sClassListView = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)widgetListViewPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetListViewCalcMinSize,
|
|
[WGT_METHOD_ON_MOUSE] = (void *)widgetListViewOnMouse,
|
|
[WGT_METHOD_ON_KEY] = (void *)widgetListViewOnKey,
|
|
[WGT_METHOD_DESTROY] = (void *)widgetListViewDestroy,
|
|
[WGT_METHOD_GET_CURSOR_SHAPE] = (void *)widgetListViewGetCursorShape,
|
|
[WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetListViewOnDragUpdate,
|
|
[WGT_METHOD_ON_DRAG_END] = (void *)widgetListViewOnDragEnd,
|
|
}
|
|
};
|
|
|
|
|
|
// ============================================================
|
|
// Widget creation functions and accessors
|
|
// ============================================================
|
|
|
|
|
|
WidgetT *wgtListView(WidgetT *parent) {
|
|
WidgetT *w = widgetAlloc(parent, sTypeId);
|
|
|
|
if (!w) {
|
|
return NULL;
|
|
}
|
|
|
|
ListViewDataT *lv = (ListViewDataT *)calloc(1, sizeof(ListViewDataT));
|
|
|
|
if (!lv) {
|
|
free(w);
|
|
return NULL;
|
|
}
|
|
|
|
lv->selectedIdx = -1;
|
|
lv->anchorIdx = -1;
|
|
lv->sortCol = -1;
|
|
lv->sortDir = ListViewSortNoneE;
|
|
lv->dragIdx = -1;
|
|
lv->dropIdx = -1;
|
|
lv->resizeCol = -1;
|
|
w->data = lv;
|
|
w->weight = 100;
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
void wgtListViewAddItem(WidgetT *w, const char *text) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (lv->colCount <= 0) {
|
|
return;
|
|
}
|
|
|
|
// If at a row boundary, add colCount empty cells to start a new row
|
|
if (lv->nextCell % lv->colCount == 0) {
|
|
for (int32_t c = 0; c < lv->colCount; c++) {
|
|
arrput(lv->ownedCells, strdup(""));
|
|
}
|
|
}
|
|
|
|
// Replace the next empty cell with the provided text
|
|
int32_t idx = lv->nextCell;
|
|
free(lv->ownedCells[idx]);
|
|
lv->ownedCells[idx] = strdup(text ? text : "");
|
|
lv->nextCell++;
|
|
|
|
listViewSyncOwned(w);
|
|
}
|
|
|
|
|
|
void wgtListViewClear(WidgetT *w) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(lv->ownedCells); i++) {
|
|
free(lv->ownedCells[i]);
|
|
}
|
|
arrsetlen(lv->ownedCells, 0);
|
|
lv->nextCell = 0;
|
|
lv->selectedIdx = -1;
|
|
lv->scrollPos = 0;
|
|
lv->scrollPosH = 0;
|
|
listViewSyncOwned(w);
|
|
}
|
|
|
|
|
|
void wgtListViewClearSelection(WidgetT *w) {
|
|
if (!w || w->type != sTypeId) {
|
|
return;
|
|
}
|
|
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (!lv->selBits) {
|
|
return;
|
|
}
|
|
|
|
memset(lv->selBits, 0, lv->rowCount);
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
const char *wgtListViewGetCell(const WidgetT *w, int32_t row, int32_t col) {
|
|
if (!w || w->type != sTypeId) {
|
|
return "";
|
|
}
|
|
|
|
const ListViewDataT *lv = (const ListViewDataT *)w->data;
|
|
|
|
if (row < 0 || row >= lv->rowCount || col < 0 || col >= lv->colCount) {
|
|
return "";
|
|
}
|
|
|
|
const char *cell = lv->cellData[row * lv->colCount + col];
|
|
return cell ? cell : "";
|
|
}
|
|
|
|
|
|
int32_t wgtListViewGetRowCount(const WidgetT *w) {
|
|
if (!w || w->type != sTypeId) {
|
|
return 0;
|
|
}
|
|
|
|
const ListViewDataT *lv = (const ListViewDataT *)w->data;
|
|
return lv->rowCount;
|
|
}
|
|
|
|
|
|
int32_t wgtListViewGetSelected(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTypeId, -1);
|
|
const ListViewDataT *lv = (const ListViewDataT *)w->data;
|
|
|
|
return lv->selectedIdx;
|
|
}
|
|
|
|
|
|
bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET(w, sTypeId, false);
|
|
const ListViewDataT *lv = (const ListViewDataT *)w->data;
|
|
|
|
if (!lv->multiSelect) {
|
|
return idx == lv->selectedIdx;
|
|
}
|
|
|
|
if (!lv->selBits || idx < 0 || idx >= lv->rowCount) {
|
|
return false;
|
|
}
|
|
|
|
return lv->selBits[idx] != 0;
|
|
}
|
|
|
|
|
|
void wgtListViewRemoveRow(WidgetT *w, int32_t row) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (row < 0 || row >= lv->rowCount || lv->colCount <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Remove colCount cells starting at row * colCount
|
|
int32_t base = row * lv->colCount;
|
|
|
|
for (int32_t c = 0; c < lv->colCount; c++) {
|
|
free(lv->ownedCells[base + c]);
|
|
}
|
|
|
|
// Shift remaining cells down by colCount positions
|
|
int32_t totalCells = (int32_t)arrlen(lv->ownedCells);
|
|
|
|
for (int32_t i = base; i < totalCells - lv->colCount; i++) {
|
|
lv->ownedCells[i] = lv->ownedCells[i + lv->colCount];
|
|
}
|
|
|
|
arrsetlen(lv->ownedCells, totalCells - lv->colCount);
|
|
|
|
// Adjust nextCell if it was past the removed row
|
|
if (lv->nextCell > base + lv->colCount) {
|
|
lv->nextCell -= lv->colCount;
|
|
} else if (lv->nextCell > base) {
|
|
lv->nextCell = base;
|
|
}
|
|
|
|
listViewSyncOwned(w);
|
|
|
|
if (lv->selectedIdx >= lv->rowCount) {
|
|
lv->selectedIdx = lv->rowCount > 0 ? lv->rowCount - 1 : -1;
|
|
}
|
|
}
|
|
|
|
|
|
void wgtListViewSelectAll(WidgetT *w) {
|
|
if (!w || w->type != sTypeId) {
|
|
return;
|
|
}
|
|
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (!lv->selBits) {
|
|
return;
|
|
}
|
|
|
|
memset(lv->selBits, 1, lv->rowCount);
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
void wgtListViewSetCell(WidgetT *w, int32_t row, int32_t col, const char *text) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (row < 0 || row >= lv->rowCount || col < 0 || col >= lv->colCount) {
|
|
return;
|
|
}
|
|
|
|
if (!lv->ownsCells) {
|
|
return;
|
|
}
|
|
|
|
int32_t idx = row * lv->colCount + col;
|
|
free(lv->ownedCells[idx]);
|
|
lv->ownedCells[idx] = strdup(text ? text : "");
|
|
|
|
// cellData pointer may have been invalidated by prior arrput; refresh it
|
|
lv->cellData = (const char **)lv->ownedCells;
|
|
lv->totalColW = 0;
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
lv->onHeaderClick = cb;
|
|
}
|
|
|
|
|
|
void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
if (!lv->selBits || idx < 0 || idx >= lv->rowCount) {
|
|
return;
|
|
}
|
|
|
|
lv->selBits[idx] = selected ? 1 : 0;
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
void wgtListViewSetSelected(WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListViewDataT *lv = (ListViewDataT *)w->data;
|
|
|
|
lv->selectedIdx = idx;
|
|
lv->anchorIdx = idx;
|
|
|
|
if (lv->selBits) {
|
|
memset(lv->selBits, 0, lv->rowCount);
|
|
|
|
if (idx >= 0 && idx < lv->rowCount) {
|
|
lv->selBits[idx] = 1;
|
|
}
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
|
|
static const struct {
|
|
WidgetT *(*create)(WidgetT *parent);
|
|
void (*setColumns)(WidgetT *w, const ListViewColT *cols, int32_t count);
|
|
void (*setData)(WidgetT *w, const char **cellData, int32_t rowCount);
|
|
int32_t (*getSelected)(const WidgetT *w);
|
|
void (*setSelected)(WidgetT *w, int32_t idx);
|
|
void (*setSort)(WidgetT *w, int32_t col, ListViewSortE dir);
|
|
void (*setHeaderClickCallback)(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir));
|
|
void (*setMultiSelect)(WidgetT *w, bool multi);
|
|
bool (*isItemSelected)(const WidgetT *w, int32_t idx);
|
|
void (*setItemSelected)(WidgetT *w, int32_t idx, bool selected);
|
|
void (*selectAll)(WidgetT *w);
|
|
void (*clearSelection)(WidgetT *w);
|
|
void (*setReorderable)(WidgetT *w, bool reorderable);
|
|
void (*addItem)(WidgetT *w, const char *text);
|
|
void (*removeRow)(WidgetT *w, int32_t row);
|
|
void (*clear)(WidgetT *w);
|
|
const char *(*getCell)(const WidgetT *w, int32_t row, int32_t col);
|
|
void (*setCell)(WidgetT *w, int32_t row, int32_t col, const char *text);
|
|
int32_t (*getRowCount)(const WidgetT *w);
|
|
} sApi = {
|
|
.create = wgtListView,
|
|
.setColumns = wgtListViewSetColumns,
|
|
.setData = wgtListViewSetData,
|
|
.getSelected = wgtListViewGetSelected,
|
|
.setSelected = wgtListViewSetSelected,
|
|
.setSort = wgtListViewSetSort,
|
|
.setHeaderClickCallback = wgtListViewSetHeaderClickCallback,
|
|
.setMultiSelect = wgtListViewSetMultiSelect,
|
|
.isItemSelected = wgtListViewIsItemSelected,
|
|
.setItemSelected = wgtListViewSetItemSelected,
|
|
.selectAll = wgtListViewSelectAll,
|
|
.clearSelection = wgtListViewClearSelection,
|
|
.setReorderable = wgtListViewSetReorderable,
|
|
.addItem = wgtListViewAddItem,
|
|
.removeRow = wgtListViewRemoveRow,
|
|
.clear = wgtListViewClear,
|
|
.getCell = wgtListViewGetCell,
|
|
.setCell = wgtListViewSetCell,
|
|
.getRowCount = wgtListViewGetRowCount
|
|
};
|
|
|
|
static const WgtPropDescT sProps[] = {
|
|
{ "ListIndex", WGT_IFACE_INT, (void *)wgtListViewGetSelected, (void *)wgtListViewSetSelected, NULL }
|
|
};
|
|
|
|
static const WgtMethodDescT sMethods[] = {
|
|
{ "AddItem", WGT_SIG_STR, (void *)wgtListViewAddItem },
|
|
{ "Clear", WGT_SIG_VOID, (void *)wgtListViewClear },
|
|
{ "ClearSelection", WGT_SIG_VOID, (void *)wgtListViewClearSelection },
|
|
{ "GetCell", WGT_SIG_RET_STR_INT_INT, (void *)wgtListViewGetCell },
|
|
{ "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListViewIsItemSelected },
|
|
{ "RemoveItem", WGT_SIG_INT, (void *)wgtListViewRemoveRow },
|
|
{ "RowCount", WGT_SIG_RET_INT, (void *)wgtListViewGetRowCount },
|
|
{ "SelectAll", WGT_SIG_VOID, (void *)wgtListViewSelectAll },
|
|
{ "SetCell", WGT_SIG_INT_INT_STR, (void *)wgtListViewSetCell },
|
|
{ "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListViewSetItemSelected },
|
|
{ "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListViewSetMultiSelect },
|
|
{ "SetReorderable", WGT_SIG_BOOL, (void *)wgtListViewSetReorderable },
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "ListView",
|
|
.props = sProps,
|
|
.propCount = 1,
|
|
.methods = sMethods,
|
|
.methodCount = 12,
|
|
.events = NULL,
|
|
.eventCount = 0,
|
|
.createSig = WGT_CREATE_PARENT,
|
|
.defaultEvent = "Click"
|
|
};
|
|
|
|
void wgtRegister(void) {
|
|
sTypeId = wgtRegisterClass(&sClassListView);
|
|
wgtRegisterApi("listview", &sApi);
|
|
wgtRegisterIface("listview", &sIface);
|
|
}
|