/* * Copyright (c) 2004 John Blitch * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * Tree-based table widget. This is a table display (comparable to the * AG_Table(3) widget), but its rows are instead stored in a tree structure * and rows and columns are identified numerically. */ #include #ifdef AG_WIDGETS #include #include #include #include #include /* * Worker function for FOREACH_VISIBLE_COLUMN(). * Return 0 to break the loop. */ typedef int (*VisibleForeachFn)(AG_Treetbl *tt, int x1, int x2, Uint32 visidx, void *, void *); #define VISROW(tt,i) ((tt)->visible.items[i].row) #define VISDEPTH(tt,i) ((tt)->visible.items[i].depth) AG_Treetbl * AG_TreetblNew(void *parent, Uint flags, AG_TreetblDataFn cellDataFn, AG_TreetblSortFn sortFn) { AG_Treetbl *tt; tt = Malloc(sizeof(AG_Treetbl)); AG_ObjectInit(tt, &agTreetblClass); tt->flags |= flags; if (flags & AG_TREETBL_HFILL) { WIDGET(tt)->flags |= AG_WIDGET_HFILL; } if (flags & AG_TREETBL_VFILL) { WIDGET(tt)->flags |= AG_WIDGET_VFILL; } tt->cellDataFn = cellDataFn; tt->sortFn = sortFn; AG_ObjectAttach(parent, tt); return (tt); } /* Timer for detecting double clicks. */ static Uint32 DoubleClickTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event) { AG_Treetbl *tt = AG_TREETBL_SELF(); tt->dblClicked = 0; /* TODO: if the cursor remains in the cell, activate a click-to-edit */ return (0); } static void FocusLost(AG_Event *_Nonnull event) { AG_Treetbl *tt = AG_TREETBL_SELF(); AG_DelTimer(tt, &tt->toDblClick); tt->dblClicked = 0; } static void FontChangedRow(AG_Treetbl *_Nonnull tt, AG_TreetblRow *_Nonnull row) { AG_TreetblRow *chld; Uint i; for (i = 0; i < tt->n; i++) { AG_TreetblCell *cell = &row->cell[i]; if (cell->textSu != -1) { AG_WidgetUnmapSurface(tt, cell->textSu); cell->textSu = -1; } } TAILQ_FOREACH(chld, &row->children, siblings) FontChangedRow(tt, chld); } static void FontChanged(AG_Event *_Nonnull event) { AG_Treetbl *tt = AG_TREETBL_SELF(); const AG_Font *font = WFONT(tt); AG_TreetblRow *row; Uint i; for (i = 0; i < tt->n; i++) { AG_TreetblCol *col = &tt->column[i]; if (col->labelSu != -1) { AG_WidgetUnmapSurface(tt, col->labelSu); col->labelSu = -1; } } tt->hCol = font->height; tt->hRow = font->height + 2; TAILQ_FOREACH(row, &tt->children, siblings) FontChangedRow(tt, row); } static void ScrollbarChanged(AG_Event *_Nonnull event) { AG_Treetbl *tt = AG_TREETBL_PTR(1); tt->visible.dirty = 1; AG_Redraw(tt); } /* Process mouse column resize */ static void ResizeColumn(AG_Treetbl *_Nonnull tt, int cid, int left) { AG_TreetblCol *col = &tt->column[cid]; int x = WIDGET(tt)->drv->mouse->x - WIDGET(tt)->rView.x1; col->w = (x - left) > 16 ? (x - left) : 16; AG_Redraw(tt); } /* * Return the number of visible descendants a given row has. must pass 0 for * i to get a meaningful result. */ static int CountVisibleChld(AG_TreetblRowQ *_Nonnull in, int i) { AG_TreetblRow *row; int j = i; TAILQ_FOREACH(row, in, siblings) { j++; if (row->flags & AG_TREETBL_ROW_EXPANDED && !TAILQ_EMPTY(&row->children)) j = CountVisibleChld(&row->children, j); } return (j); } /* Swap two columns. */ static void SwapColumns(AG_Treetbl *_Nonnull tt, Uint a, Uint b) { AG_TreetblCol colTmp; /* a little sanity checking never hurt */ if (a >= tt->n || b >= tt->n) return; if (a == b) return; colTmp = tt->column[a]; tt->column[a] = tt->column[b]; tt->column[b] = colTmp; tt->visible.dirty = 1; AG_Redraw(tt); } /* Process mouse column move. */ static void MoveColumn(AG_Treetbl *_Nonnull tt, Uint cid, int left) { AG_TreetblCol *col = &tt->column[cid]; const AG_TreetblCol *colLeft = &tt->column[cid-1]; const AG_TreetblCol *colRight = &tt->column[cid+1]; int x = WIDGET(tt)->drv->mouse->x - WIDGET(tt)->rView.x1; col->flags |= AG_TREETBL_COL_MOVING; if ((col->w < colLeft->w && x < left - colLeft->w + col->w) || (col->w >= colLeft->w && x < left)) { /* * Drag left */ if (cid > 0) { left -= colLeft->w; SwapColumns(tt, cid, cid-1); cid--; } } else if ((col->w <= colRight->w && x > left + colRight->w) || (col->w > colRight->w && x > left + col->w)) { /* * Drag right */ if (cid < tt->n-1) { left += colRight->w; SwapColumns(tt, cid, cid+1); cid++; } } AG_Redraw(tt); } /* * Loops through each column currently onscreen. calls foreachFn for each * one, passing along args returns the index of the first foreachFn() call to * return zero, or tt->n, whichever comes first. */ static void FOREACH_VISIBLE_COLUMN(AG_Treetbl *_Nonnull tt, VisibleForeachFn foreachFn, void *_Nullable arg1, void *_Nullable arg2) { int x = 0, first_col = -1, wCol; Uint i; first_col = -1; for (i = 0; i < tt->n; i++) { AG_TreetblCol *col = &tt->column[i]; if (first_col == -1 && x + col->w < tt->xOffs) { x += col->w; /* x = offset in table */ continue; } else if (first_col == -1) { first_col = i; x -= tt->xOffs; /* x = offset on screen */ } if (x >= tt->r.w) break; wCol = (x + col->w > tt->r.w) ? (tt->r.w - x) : col->w; if (foreachFn(tt, x, x+wCol, i, arg1, arg2) == 0) return; x += col->w; } } static void MouseButtonUp(void *obj, AG_MouseButton button, int x, int y) { AG_Treetbl *tt = obj; AG_TreetblCol *col = NULL; int left; Uint i; /* * Our goal here is to set/toggle a column's sorting mode if the * click fell on the header and the user did not drag it */ /* XXX - fix horiz sbar */ left = 0; for (i = 0; i < tt->n; i++) { col = &tt->column[i]; if (col->flags & AG_TREETBL_COL_SELECTED) { break; } left += col->w; } if (col == NULL) return; if (i < tt->n) { /* * if the column wasn't moved and the mouse is still within * the column's header, do sort work */ if (tt->flags & AG_TREETBL_SORT && !(col->flags & AG_TREETBL_COL_MOVING) && y < tt->hCol && x >= left && x < (left+col->w)) { if (col->flags & AG_TREETBL_COL_SORTING) { if (tt->sortMode == AG_TREETBL_SORT_DSC) { tt->sortMode = AG_TREETBL_SORT_ASC; } else { tt->sortMode = AG_TREETBL_SORT_DSC; } } AG_TreetblSetSortCol(tt, col); } col->flags &= ~(AG_TREETBL_COL_MOVING|AG_TREETBL_COL_SELECTED); } AG_Redraw(tt); } /* Process click on a column header. */ static int ClickedColumnHeader(AG_Treetbl *_Nonnull tt, int x1, int x2, Uint32 idx, void *_Nullable arg1, void *_Nullable arg2) { int x = *(int *)arg1; AG_TreetblCol *col = &tt->column[idx]; AG_TreetblCol *colLeft = &tt->column[idx-1]; if ((x < x1 || x >= x2) && idx != tt->n-1) return (1); /* click on the CENTER */ if (x >= x1+3 && x <= x2-3) { if (tt->flags & AG_TREETBL_REORDERCOLS) { MoveColumn(tt, idx, x1); } if (tt->flags & AG_TREETBL_REORDERCOLS || tt->flags & AG_TREETBL_SORT) { col->flags |= AG_TREETBL_COL_SELECTED; AG_Redraw(tt); } } /* click on the LEFT resize line */ else if (idx-1 < tt->n && colLeft->flags & AG_TREETBL_COL_RESIZABLE && idx > 0 && x < x1+3) { ResizeColumn(tt, idx-1, (x1 - colLeft->w)); } /* click on the RIGHT resize line */ else if (idx < tt->n && (col->flags & AG_TREETBL_COL_RESIZABLE) && (x > x2-3 || (idx == tt->n-1 && x < x2+3))) { ResizeColumn(tt, idx, x1); } return (0); } /* Clear the selection bit for all rows in and descended from the rowq */ static void DeselectAll(AG_TreetblRowQ *_Nonnull children) { AG_TreetblRow *row; TAILQ_FOREACH(row, children, siblings) { row->flags &= ~(AG_TREETBL_ROW_SELECTED); if (row->flags & AG_TREETBL_ROW_EXPANDED && !TAILQ_EMPTY(&row->children)) DeselectAll(&row->children); } } /* Process click over a row. */ static int ClickedRow(AG_Treetbl *_Nonnull tt, int x1, int x2, Uint32 idx, void *_Nullable arg1, void *_Nullable arg2) { AG_KeyMod kmod = AG_GetModState(tt); const int x = *(int *)arg1; const int y = *(int *)arg2; AG_TreetblRow *row = NULL; const int ts = (tt->hRow >> 1) + 1; Uint i, j, row_idx; int depth=0, px; if (x < x1 || x >= x2) return (1); /* Find the visible row under the cursor. */ for (i = 0, px = tt->hCol; i < tt->visible.count; i++, px += tt->hRow) { if (y >= px && y < px+tt->hRow) { if (VISROW(tt,i) == NULL) { i = tt->visible.count; } break; } } if (i != tt->visible.count) { row = VISROW(tt,i); depth = VISDEPTH(tt,i); } row_idx = i; /* Clicking on blank space clears the selection. */ if (row == NULL) { DeselectAll(&tt->children); /* AG_PostEvent(tt, "treetbl-selectclear", ""); */ return (0); } /* Check for a click on the +/- button, if applicable. */ if ((tt->column[idx].flags & AG_TREETBL_COL_EXPANDER) && !TAILQ_EMPTY(&row->children) && x > (x1+4+(depth*(ts+4))) && x < (x1+4+(depth*(ts+4))+ts)) { if (row->flags & AG_TREETBL_ROW_EXPANDED) { row->flags &= ~(AG_TREETBL_ROW_EXPANDED); /* * XXX DeselectAll could return count to save * redundant call to CountVisibleChld */ DeselectAll(&row->children); tt->nExpandedRows -= CountVisibleChld( &row->children, 0); } else { row->flags |= AG_TREETBL_ROW_EXPANDED; tt->nExpandedRows += CountVisibleChld(&row->children, 0); } tt->visible.dirty = 1; AG_Redraw(tt); return (0); } /* Handle command/control clicks and range selections. */ if (((tt->flags & AG_TREETBL_MULTITOGGLE) || kmod & AG_KEYMOD_META || kmod & AG_KEYMOD_CTRL)) { if (row->flags & AG_TREETBL_ROW_SELECTED) { row->flags &= ~(AG_TREETBL_ROW_SELECTED); AG_PostEvent(tt, "treetbl-deselect", "%p", row); AG_Redraw(tt); } else { if (!(tt->flags & AG_TREETBL_MULTI) && !(tt->flags & AG_TREETBL_MULTITOGGLE)) { DeselectAll(&tt->children); } row->flags |= AG_TREETBL_ROW_SELECTED; AG_PostEvent(tt, "treetbl-select", "%p", row); AG_Redraw(tt); } } else if (kmod & AG_KEYMOD_SHIFT) { for (j = 0; j < tt->visible.count; j++) { if (VISROW(tt,j) != NULL && (VISROW(tt,j)->flags & AG_TREETBL_ROW_SELECTED)) break; } if (j < tt->visible.count) { /* XXX the selection may start on an invisible row */ if (j < row_idx) { for (i = j; i <= row_idx; i++) { if (VISROW(tt,i) != NULL) VISROW(tt,i)->flags |= AG_TREETBL_ROW_SELECTED; } } else if (j > row_idx) { for (i = row_idx; i <= j; i++) { if (VISROW(tt,i) != NULL) VISROW(tt,i)->flags |= AG_TREETBL_ROW_SELECTED; } } else { if (VISROW(tt,row_idx) != NULL) VISROW(tt,row_idx)->flags |= AG_TREETBL_ROW_SELECTED; } AG_Redraw(tt); } } else { DeselectAll(&tt->children); if (!(row->flags & AG_TREETBL_ROW_SELECTED)) { row->flags |= AG_TREETBL_ROW_SELECTED; AG_PostEvent(tt, "treetbl-select", "%p", row); AG_Redraw(tt); } } /* Handle double-clicks. */ if (tt->dblClicked) { AG_DelTimer(tt, &tt->toDblClick); if(!TAILQ_EMPTY(&row->children)) { if (row->flags & AG_TREETBL_ROW_EXPANDED) { AG_TreetblCollapseRow(tt, row); } else { AG_TreetblExpandRow(tt, row); } } else { DeselectAll(&tt->children); row->flags |= AG_TREETBL_ROW_SELECTED; AG_Redraw(tt); } tt->dblClicked = 0; AG_PostEvent(tt, "treetbl-dblclick", "%p", row); } else { tt->dblClicked++; AG_AddTimer(tt, &tt->toDblClick, agMouseDblclickDelay, DoubleClickTimeout, NULL); } return (0); } static void MouseButtonDown(void *obj, AG_MouseButton button, int x, int y) { AG_Treetbl *tt = obj; if (!AG_WidgetIsFocused(tt)) AG_WidgetFocus(tt); switch (button) { case AG_MOUSE_WHEELUP: if ((tt->yOffs - 1) >= 0) { tt->yOffs--; tt->visible.dirty = 1; AG_Redraw(tt); } return; case AG_MOUSE_WHEELDOWN: if ((tt->yOffs < tt->yMax)) { tt->yOffs++; tt->visible.dirty = 1; AG_Redraw(tt); } return; default: break; } if (tt->hCol > 0 && y < tt->hCol) { /* a mouse down on the column header */ FOREACH_VISIBLE_COLUMN(tt, ClickedColumnHeader, &x, NULL); } else { /* a mouse down in the body */ FOREACH_VISIBLE_COLUMN(tt, ClickedRow, &x, &y); } } static void Init(void *_Nonnull obj) { AG_Treetbl *tt = obj; WIDGET(tt)->flags |= AG_WIDGET_FOCUSABLE | AG_WIDGET_USE_TEXT; tt->cellDataFn = NULL; tt->sortFn = NULL; tt->flags = 0; tt->r.x = 0; tt->r.y = 0; tt->r.w = 0; tt->r.h = 0; tt->xOffs = 0; tt->xMax = 0; tt->yOffs = 0; tt->yMax = 0; tt->hCol = agTextFontHeight; tt->hRow = agTextFontHeight + 2; tt->dblClicked = 0; tt->vBar = AG_ScrollbarNew(tt, AG_SCROLLBAR_VERT, AG_SCROLLBAR_EXCL); AG_SetInt(tt->vBar, "min", 0); AG_BindInt(tt->vBar, "max", &tt->yMax); AG_BindInt(tt->vBar, "value", &tt->yOffs); AG_SetEvent(tt->vBar, "scrollbar-changed", ScrollbarChanged, "%p", tt); tt->hBar = AG_ScrollbarNew(tt, AG_SCROLLBAR_HORIZ, AG_SCROLLBAR_EXCL); AG_SetInt(tt->hBar, "min", 0); AG_BindInt(tt->hBar, "max", &tt->xMax); AG_BindInt(tt->hBar, "value", &tt->xOffs); tt->n = 0; tt->sortMode = AG_TREETBL_SORT_NOT; tt->column = NULL; TAILQ_INIT(&tt->children); TAILQ_INIT(&tt->backstore); tt->nExpandedRows = 0; tt->lineScrollAmount = 5; tt->visible.redraw_rate = 0; tt->visible.redraw_last = AG_GetTicks(); tt->visible.count = 0; tt->visible.items = NULL; tt->wHint = 10; tt->hHint = tt->hCol + (tt->hRow << 2); AG_InitTimer(&tt->toDblClick, "dblClick", 0); AG_SetEvent(tt, "widget-lostfocus", FocusLost, NULL); AG_AddEvent(tt, "widget-hidden", FocusLost, NULL); AG_AddEvent(tt, "font-changed", FontChanged, NULL); } void AG_TreetblSizeHint(AG_Treetbl *tt, int w, int nrows) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); tt->wHint = w; tt->hHint = tt->hCol + tt->hRow*nrows; AG_ObjectUnlock(tt); } void AG_TreetblSetColHeight(AG_Treetbl *tt, int h) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); tt->hCol = h; AG_WidgetUpdate(tt); AG_Redraw(tt); } /* Insert a column in the table. */ AG_TreetblCol * AG_TreetblAddCol(AG_Treetbl *tt, int colID, const char *width, const char *text, ...) { AG_TreetblCol *col, *colsNew; va_list args; Uint i; if (colID == -1) return (NULL); AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); /* Check for existing column ID */ if ((tt->flags & AG_TREETBL_NODUPCHECKS) == 0) { for (i = 0; i < tt->n; i++) { if (tt->column[i].cid == colID) break; } if (i < tt->n) { AG_SetError("Existing column ID: %d", colID); goto fail; } } /* Allocate new column */ if ((colsNew = TryRealloc(tt->column, (tt->n+1)*sizeof(AG_TreetblCol))) == NULL) { goto fail; } tt->column = colsNew; col = &tt->column[tt->n]; col->tbl = tt; col->flags = 0; col->cid = colID; col->idx = tt->n; col->labelSu = -1; tt->n++; /* Use first column for expander controls by default. */ if (tt->n == 1) col->flags |= AG_TREETBL_COL_EXPANDER; /* Format the column header text. */ if (text == NULL) { col->label = Strdup(""); } else { va_start(args, text); Vasprintf(&col->label, text, args); va_end(args); } /* Set the default column width. */ if (width != NULL) { if (AG_WidgetParseSizeSpec(width, &col->w) == AG_WIDGET_PERCENT) col->w = col->w*WIDTH(tt)/100; } else { col->w = 6; col->flags |= AG_TREETBL_COL_FILL; } tt->visible.dirty = 1; AG_Redraw(tt); AG_ObjectUnlock(tt); return (col); fail: AG_ObjectUnlock(tt); return (NULL); } /* Select the column controlling the sorting. */ void AG_TreetblSetSortCol(AG_Treetbl *tt, AG_TreetblCol *col) { int i; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); for (i = 0; i < tt->n; i++) { tt->column[i].flags &= ~(AG_TREETBL_COL_SORTING); } col->flags |= AG_TREETBL_COL_SORTING; AG_Redraw(tt); AG_ObjectUnlock(tt); } /* Set the sorting order */ void AG_TreetblSetSortMode(AG_Treetbl *tt, enum ag_treetbl_sort_mode mode) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); tt->sortMode = mode; AG_Redraw(tt); } /* Select an alternate column for the expand/collapse controls. */ void AG_TreetblSetExpanderCol(AG_Treetbl *tt, AG_TreetblCol *col) { int i; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); for (i = 0; i < tt->n; i++) { tt->column[i].flags &= ~(AG_TREETBL_COL_EXPANDER); } col->flags |= AG_TREETBL_COL_EXPANDER; AG_Redraw(tt); AG_ObjectUnlock(tt); } /* Select a column. */ void AG_TreetblSelectCol(AG_Treetbl *tt, AG_TreetblCol *col) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); col->flags |= AG_TREETBL_COL_SELECTED; AG_Redraw(tt); AG_ObjectUnlock(tt); } /* Deselect a column. */ void AG_TreetblDeselectCol(AG_Treetbl *tt, AG_TreetblCol *col) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); col->flags &= ~(AG_TREETBL_COL_SELECTED); AG_Redraw(tt); AG_ObjectUnlock(tt); } /* Select a column by ID */ int AG_TreetblSelectColID(AG_Treetbl *tt, int colID) { int i; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); for (i = 0; i < tt->n; i++) { if (tt->column[i].cid == colID) break; } if (i == tt->n) { goto no_match; } tt->column[i].flags |= AG_TREETBL_COL_SELECTED; AG_Redraw(tt); AG_ObjectUnlock(tt); return (0); no_match: AG_ObjectUnlock(tt); return (-1); } /* Deselect a column by ID */ int AG_TreetblDeselectColID(AG_Treetbl *tt, int colID) { int i; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); for (i = 0; i < tt->n; i++) { if (tt->column[i].cid == colID) break; } if (i == tt->n) { goto no_match; } tt->column[i].flags &= ~(AG_TREETBL_COL_SELECTED); AG_Redraw(tt); AG_ObjectUnlock(tt); return (0); no_match: AG_ObjectUnlock(tt); return (-1); } void AG_TreetblSetRefreshRate(AG_Treetbl *tt, Uint ms) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); tt->visible.redraw_rate = ms; } /* * Return 1 if the row is visible return 0 if an ancestor row is collapsed, * hiding it. */ static int RowIsVisible(AG_TreetblRow *_Nonnull in) { AG_TreetblRow *row = in->parent; while (row != NULL) { if (!(row->flags & AG_TREETBL_ROW_EXPANDED)) { return (0); } row = row->parent; } return (1); } /* Insert a row in the table. */ AG_TreetblRow * AG_TreetblAddRow(AG_Treetbl *tt, AG_TreetblRow *pRow, int rowID, const char *argSpec, ...) { char argBuf[128], *pArg = argBuf; AG_TreetblRow *row; Uint i, arg, nArgs = 0; va_list ap; Strlcpy(argBuf, argSpec, sizeof(argBuf)); while (AG_Strsep(&pArg, ", ") != NULL) nArgs++; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); /* Check if row ID is already use */ if (!(tt->flags & AG_TREETBL_NODUPCHECKS) && AG_TreetblLookupRowRecurse(&tt->children, rowID)) { AG_SetError("Existing row ID: %d", rowID); goto fail; } if ((row = TryMalloc(sizeof(AG_TreetblRow))) == NULL) { goto fail; } row->tbl = tt; if ((row->cell = TryMalloc(sizeof(AG_TreetblCell)*tt->n)) == NULL) { Free(row); goto fail; } row->flags = 0; row->parent = pRow; TAILQ_INIT(&row->children); for (i = 0; i < tt->n; i++) { row->cell[i].text = NULL; row->cell[i].textSu = -1; } /* import static data */ va_start(ap, argSpec); for (arg = 0; arg < nArgs; arg++) { int colID = va_arg(ap, int); void *data = va_arg(ap, void *); AG_TreetblCell *cell; for (i = 0; i < tt->n; i++) { if (colID == tt->column[i].cid) break; } if (i == tt->n) { continue; } cell = &row->cell[i]; Free(cell->text); cell->text = (data != NULL) ? Strdup((char *)data) : Strdup("(null)"); if (cell->textSu != -1) { AG_WidgetUnmapSurface(tt, cell->textSu); cell->textSu = -1; } } va_end(ap); row->rid = rowID; if (pRow != NULL) { TAILQ_INSERT_TAIL(&pRow->children, row, siblings); } else { TAILQ_INSERT_TAIL(&tt->children, row, siblings); } /* increment scroll only if visible: */ if (RowIsVisible(row)) tt->nExpandedRows++; tt->visible.dirty = 1; AG_ObjectUnlock(tt); AG_Redraw(tt); return (row); fail: AG_ObjectUnlock(tt); return (NULL); } /* * Return a pointer if the row with the given identifer exists in or in a * descendant of the rowq. If it does not exist, return NULL. */ AG_TreetblRow * AG_TreetblLookupRowRecurse(AG_TreetblRowQ *searchIn, int rid) { AG_TreetblRow *row, *row2; AG_TAILQ_FOREACH(row, searchIn, siblings) { if (row->rid == rid) return (row); if (!AG_TAILQ_EMPTY(&row->children)) { row2 = AG_TreetblLookupRowRecurse(&row->children, rid); if (row2 != NULL) return (row2); } } return (NULL); } /* * Lookup a row by ID. * Return value is valid as long as Treetbl is locked. */ AG_TreetblRow *_Nullable AG_TreetblLookupRow(AG_Treetbl *tt, int rowID) { AG_TreetblRow *row; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); row = AG_TreetblLookupRowRecurse(&tt->children, rowID); AG_ObjectUnlock(tt); return (row); } /* Delete a row by ID */ int AG_TreetblDelRowID(AG_Treetbl *tt, int rowID) { AG_TreetblRow *row; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); if ((row = AG_TreetblLookupRow(tt,rowID)) == NULL) { return (-1); } AG_TreetblDelRow(tt, row); return (0); } /* Select a row by ID */ int AG_TreetblSelectRowID(AG_Treetbl *tt, int rowID) { AG_TreetblRow *row; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); if ((row = AG_TreetblLookupRow(tt, rowID)) == NULL) { return (-1); } AG_TreetblSelectRow(tt, row); return (0); } /* Deselect a row by ID */ int AG_TreetblDeselectRowID(AG_Treetbl *tt, int rowID) { AG_TreetblRow *row; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); if ((row = AG_TreetblLookupRow(tt, rowID)) == NULL) { return (-1); } AG_TreetblDeselectRow(tt, row); return (0); } /* Expand a row by ID */ int AG_TreetblExpandRowID(AG_Treetbl *tt, int rowID) { AG_TreetblRow *row; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); if ((row = AG_TreetblLookupRow(tt, rowID)) == NULL) { return (-1); } AG_TreetblExpandRow(tt, row); return (0); } /* Collapse a row by ID */ int AG_TreetblCollapseRowID(AG_Treetbl *tt, int rowID) { AG_TreetblRow *row; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); if ((row = AG_TreetblLookupRow(tt, rowID)) == NULL) { return (-1); } AG_TreetblCollapseRow(tt, row); return (0); } static void DestroyRow(AG_Treetbl *_Nonnull tt, AG_TreetblRow *_Nonnull row) { int i; for (i = 0; i < tt->n; i++) { AG_TreetblCell *cell = &row->cell[i]; if (cell->textSu != -1) { AG_WidgetUnmapSurface(tt, cell->textSu); } Free(cell->text); } Free(row->cell); free(row); } void AG_TreetblDelRow(AG_Treetbl *tt, AG_TreetblRow *row) { AG_TreetblRow *row1, *row2; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); /* first remove children */ row1 = TAILQ_FIRST(&row->children); while (row1 != NULL) { row2 = TAILQ_NEXT(row1, siblings); AG_TreetblDelRow(tt, row1); row1 = row2; } /* now that children are gone, remove this row */ if (RowIsVisible(row)) tt->nExpandedRows--; if (row->parent) { TAILQ_REMOVE(&row->parent->children, row, siblings); } else { TAILQ_REMOVE(&tt->children, row, siblings); } if (tt->flags & AG_TREETBL_POLLED) { TAILQ_INSERT_TAIL(&tt->backstore, row, backstore); goto out; } DestroyRow(tt, row); out: tt->visible.dirty = 1; AG_Redraw(tt); AG_ObjectUnlock(tt); } /* * Clear the rows. If AG_TREETBL_POLLED is in effect, the selection state * is remembered. */ void AG_TreetblClearRows(AG_Treetbl *tt) { AG_TreetblRow *row1, *row2; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); row1 = TAILQ_FIRST(&tt->children); while (row1 != NULL) { row2 = TAILQ_NEXT(row1, siblings); AG_TreetblDelRow(tt, row1); row1 = row2; } TAILQ_INIT(&tt->children); tt->nExpandedRows = 0; tt->visible.dirty = 1; AG_Redraw(tt); AG_ObjectUnlock(tt); } void AG_TreetblRestoreRows(AG_Treetbl *tt) { AG_TreetblRow *row, *nrow, *srow; int i; AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); for (row = TAILQ_FIRST(&tt->backstore); row != TAILQ_END(&tt->backstore); row = nrow) { nrow = TAILQ_NEXT(row, backstore); TAILQ_FOREACH(srow, &tt->children, siblings) { for (i = 0; i < tt->n; i++) { if (strcmp(row->cell[i].text, srow->cell[i].text) != 0) break; } if (i != tt->n) { continue; } if (row->flags & AG_TREETBL_ROW_SELECTED) { srow->flags |= AG_TREETBL_ROW_SELECTED; } else { srow->flags &= ~(AG_TREETBL_ROW_SELECTED); } } DestroyRow(tt, row); } TAILQ_INIT(&tt->backstore); AG_Redraw(tt); AG_ObjectUnlock(tt); } void AG_TreetblBegin(AG_Treetbl *tt) { AG_TreetblClearRows(tt); } void AG_TreetblEnd(AG_Treetbl *tt) { AG_TreetblRestoreRows(tt); } /* Select the given row. */ void AG_TreetblSelectRow(AG_Treetbl *tt, AG_TreetblRow *row) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); if (!(tt->flags & AG_TREETBL_MULTI) && !(tt->flags & AG_TREETBL_MULTITOGGLE)) { AG_TreetblDeselectRow(tt, NULL); } row->flags |= AG_TREETBL_ROW_SELECTED; AG_Redraw(tt); AG_ObjectUnlock(tt); } static void SelectAll(AG_TreetblRowQ *_Nonnull children) { AG_TreetblRow *row; TAILQ_FOREACH(row, children, siblings) { row->flags |= AG_TREETBL_ROW_SELECTED; if (!TAILQ_EMPTY(&row->children)) SelectAll(&row->children); } } /* Set the selection bit for all rows recursively. */ void AG_TreetblSelectAll(AG_Treetbl *tt, AG_TreetblRow *root) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); if (!(tt->flags & AG_TREETBL_MULTI) && !(tt->flags & AG_TREETBL_MULTITOGGLE)) goto out; if (root == NULL) { SelectAll(&tt->children); } else { SelectAll(&root->children); } out: AG_ObjectUnlock(tt); } /* Deselect the given row. */ void AG_TreetblDeselectRow(AG_Treetbl *tt, AG_TreetblRow *row) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); if (row == NULL) { DeselectAll(&tt->children); } else { DeselectAll(&row->children); } AG_ObjectUnlock(tt); } /* Expand a specified row for display. */ void AG_TreetblExpandRow(AG_Treetbl *tt, AG_TreetblRow *in) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); if (!(in->flags & AG_TREETBL_ROW_EXPANDED)) { in->flags |= AG_TREETBL_ROW_EXPANDED; if (RowIsVisible(in)) { tt->nExpandedRows += CountVisibleChld(&in->children, 0); tt->visible.dirty = 1; AG_Redraw(tt); } } AG_ObjectUnlock(tt); } /* Collapse a specified row from display. */ void AG_TreetblCollapseRow(AG_Treetbl *tt, AG_TreetblRow *in) { AG_OBJECT_ISA(tt, "AG_Widget:AG_Treetbl:*"); AG_ObjectLock(tt); if (in->flags & AG_TREETBL_ROW_EXPANDED) { in->flags &= ~(AG_TREETBL_ROW_EXPANDED); if (RowIsVisible(in)) { tt->nExpandedRows -= CountVisibleChld(&in->children, 0); tt->visible.dirty = 1; AG_Redraw(tt); } } AG_ObjectUnlock(tt); } static void Destroy(void *_Nonnull p) { AG_Treetbl *tt = p; AG_TreetblClearRows(tt); Free(tt->column); Free(tt->visible.items); } static void SizeRequest(void *_Nonnull obj, AG_SizeReq *_Nonnull r) { AG_Treetbl *tt = obj; AG_SizeReq rBar; int i; AG_WidgetSizeReq(tt->vBar, &rBar); r->w = tt->wHint + rBar.w; r->h = tt->hHint; if (tt->hBar != NULL) { AG_WidgetSizeReq(tt->hBar, &rBar); r->h += rBar.h; } for (i = 0; i < tt->n; i++) r->w += tt->column[i].w; } static int SizeAllocate(void *_Nonnull obj, const AG_SizeAlloc *_Nonnull a) { AG_Treetbl *tt = obj; AG_Scrollbar *hBar, *vBar; Uint rows_per_view, i; AG_SizeAlloc aBar; AG_SizeReq rBar; if (a->h < tt->hCol || a->w < 8) return (-1); tt->r.x = 0; tt->r.y = tt->hCol; tt->r.w = WIDTH(tt); tt->r.h = HEIGHT(tt) - tt->hCol; /* Size vertical scroll bar. */ vBar = tt->vBar; AG_WidgetSizeReq(vBar, &rBar); aBar.x = a->w - rBar.w; aBar.y = 0; aBar.w = rBar.w; aBar.h = a->h - aBar.w; AG_WidgetSizeAlloc(vBar, &aBar); tt->r.w -= WIDTH(vBar); /* Size horizontal scroll bar, if enabled. */ if ((hBar = tt->hBar) != NULL) { int col_w = 0; AG_WidgetSizeReq(hBar, &rBar); aBar.x = 0; aBar.y = a->h - rBar.h; aBar.w = a->w - WIDTH(vBar); aBar.h = rBar.h; AG_WidgetSizeAlloc(hBar, &aBar); tt->r.h -= HEIGHT(hBar); for (i = 0; i < tt->n; i++) { col_w += tt->column[i].w; } if (col_w > WIDTH(hBar)) { tt->xMax = col_w - WIDTH(hBar); if (tt->xOffs > tt->xMax) tt->xOffs = tt->xMax; } else { tt->xOffs = 0; } } /* Calculate widths for TREETBL_COL_FILL columns. */ { Uint fill_cols, nonfill_width, fill_width; fill_cols = 0; nonfill_width = 0; for (i = 0; i < tt->n; i++) { AG_TreetblCol *col = &tt->column[i]; if (col->flags & AG_TREETBL_COL_FILL) { fill_cols++; } else { nonfill_width += col->w; } } if (fill_cols == 0) fill_cols = 1; fill_width = (a->w - WIDTH(vBar) - nonfill_width) / fill_cols; for (i = 0; i < tt->n; i++) { AG_TreetblCol *col = &tt->column[i]; if (col->flags & AG_TREETBL_COL_FILL) col->w = fill_width; } } /* Calculate how many rows the view holds. */ { if (tt->r.h < tt->hCol) { tt->r.h = tt->hCol; } rows_per_view = tt->r.h/ tt->hRow; if (tt->r.h % tt->hRow) rows_per_view++; } if (tt->visible.count < rows_per_view) { /* visible increased */ tt->visible.items = Realloc(tt->visible.items, sizeof(struct ag_treetbl_rowdocket_item) * rows_per_view); for (i = tt->visible.count; i < rows_per_view; i++) VISROW(tt,i) = NULL; tt->visible.count = rows_per_view; tt->visible.dirty = 1; } else if (tt->visible.count > rows_per_view) { /* visible decreased */ tt->visible.items = Realloc(tt->visible.items, sizeof(struct ag_treetbl_rowdocket_item) * rows_per_view); tt->visible.count = rows_per_view; tt->visible.dirty = 1; } return (0); } static void DrawSubnodeIndicator(AG_Treetbl *_Nonnull tt, const AG_Rect *_Nonnull r, int isExpanded) { static AG_VectorElement expdSign[] = { { AG_VE_LINE, 3,5, 1,0, 0, NULL }, /* - */ { AG_VE_LINE, 1,7, 1,0, 0, NULL }, /* | */ }; const AG_Color *cLine = &WCOLOR(tt, LINE_COLOR); AG_Color c; AG_ColorRGBA_8(&c, 0,0,0, 64); AG_DrawRectBlended(tt, r, &c, AG_ALPHA_SRC, AG_ALPHA_ONE_MINUS_SRC); if (isExpanded) { AG_DrawVector(tt, 3,3, r, cLine, expdSign, 0,1); /* - */ } else { AG_DrawVector(tt, 3,3, r, cLine, expdSign, 0,2); /* + */ } } static void DrawDynamicColumn(AG_Treetbl *_Nonnull tt, Uint idx) { AG_TreetblCol *col = &tt->column[idx]; Uint i; for (i = 0; i < tt->visible.count; i++) { AG_TreetblRow *row = VISROW(tt,i); AG_TreetblCell *cell; char *sNew; if (row == NULL) break; if (!(row->flags & AG_TREETBL_ROW_DYNAMIC)) continue; cell = &row->cell[col->idx]; sNew = tt->cellDataFn(tt, col->cid, row->rid); if (sNew == NULL) { Free(cell->text); cell->text = NULL; if (cell->textSu != -1) { AG_WidgetUnmapSurface(tt, cell->textSu); cell->textSu = -1; } continue; /* Nothing to draw */ } if (cell->text != NULL && strcmp(cell->text, sNew) == 0) { free(sNew); continue; /* No change in text */ } Free(cell->text); cell->text = sNew; if (cell->textSu != -1) { AG_WidgetUnmapSurface(tt, cell->textSu); cell->textSu = -1; } } } /* Render a column header and cells. */ static int DrawColumn(AG_Treetbl *_Nonnull tt, int x1, int x2, Uint32 idx, void *_Nullable arg1, void *_Nullable arg2) { const int *update = (int *)arg1; AG_TreetblCol *col = &tt->column[idx]; AG_Rect rd; const int spacing = 4; Uint j; int y; /* Render the column header. */ if (tt->hCol > 0) { rd.x = x1; rd.y = 0; rd.w = col->w; rd.h = tt->hCol; if ((col->flags & (AG_TREETBL_COL_SELECTED | AG_TREETBL_COL_SORTING))) { AG_DrawBoxSunk(tt, &rd, &WCOLOR(tt, SELECTION_COLOR)); } else { AG_DrawBoxRaised(tt, &rd, &WCOLOR(tt, FG_COLOR)); } if (col->label[0] != '\0') { int xLbl; if (col->labelSu == -1) { col->labelSu = AG_WidgetMapSurface(tt, AG_TextRender(col->label)); } xLbl = (col->w >> 1) - (WSURFACE(tt,col->labelSu)->w >> 1); AG_WidgetBlitSurface(tt, col->labelSu, x1+xLbl, 0); } } /* Check for the need to update */ if (*update && (col->flags & AG_TREETBL_COL_DYNAMIC)) DrawDynamicColumn(tt, idx); /* Draw the cells under this column */ AG_PushClipRect(tt, &tt->r); y = tt->hCol; for (j = 0; j < tt->visible.count; j++) { int x = x1+spacing; AG_TreetblCell *cell; if (VISROW(tt,j) == NULL) { break; } if (col->flags & AG_TREETBL_COL_EXPANDER) { int tw = (tt->hRow >> 1); if ((tw & 1) == 0) { tw++; } x += VISDEPTH(tt,j) * (tw + spacing); if (!TAILQ_EMPTY(&VISROW(tt,j)->children)) { rd.x = x; rd.y = y + (tw >> 1); rd.w = tw; rd.h = tw; DrawSubnodeIndicator(tt, &rd, (VISROW(tt,j)->flags & AG_TREETBL_ROW_EXPANDED)); } x += tw + spacing; } cell = &VISROW(tt,j)->cell[col->idx]; if (cell->text != NULL) { if (cell->textSu == -1) { cell->textSu = AG_WidgetMapSurface(tt, AG_TextRender(cell->text)); } AG_WidgetBlitSurface(tt, cell->textSu, x, y+1); } y += tt->hRow; } AG_PopClipRect(tt); /* Fill the remaining space in column heading */ if (tt->hCol > 0 && idx == tt->n-1 && x2 < tt->r.w) { rd.x = x2; rd.y = 0; rd.w = tt->r.w - x2; rd.h = tt->hCol; AG_DrawBoxRaised(tt, &rd, &WCOLOR(tt, FG_COLOR)); } return (1); } /* * Recursive companion to ViewChanged. Finds the visible rows and populates * tt->visible.items array with them. */ static int ViewChangedRecurse(AG_Treetbl *_Nonnull tt, AG_TreetblRowQ *_Nonnull in, int depth, int filled, int *_Nonnull seen) { AG_TreetblRow *row; Uint x = 0; TAILQ_FOREACH(row, in, siblings) { if (*seen) { (*seen)--; } else { if ((filled+x) < tt->visible.count) { VISROW(tt,filled+x) = row; VISDEPTH(tt,filled+x) = depth; x++; } } if (row->flags & AG_TREETBL_ROW_EXPANDED && filled+x < tt->visible.count) x += ViewChangedRecurse(tt, &row->children, depth + 1, filled + x, seen); if (filled+x >= tt->visible.count) return (x); } return (x); } /* * Called after any addition or removal of visible rows. Rebuilds the array * of rows to be drawn (tt->visible). */ static void ViewChanged(AG_Treetbl *_Nonnull tt) { int rows_per_view, max, filled, value; Uint i; /* cancel double clicks if what's under it changes it */ AG_DelTimer(tt, &tt->toDblClick); tt->dblClicked = 0; rows_per_view = tt->r.h/tt->hRow; if (tt->r.h % tt->hRow) rows_per_view++; max = tt->nExpandedRows - rows_per_view; if (max < 0) { max = 0; } if (max && (tt->r.h % tt->hRow) < 16) { max++; } tt->yMax = max; if (tt->yOffs > max) tt->yOffs = max; /* locate visible rows */ value = tt->yOffs; filled = ViewChangedRecurse(tt, &tt->children, 0, 0, &value); /* blank empty rows */ for (i = filled; i < tt->visible.count; i++) VISROW(tt,i) = NULL; /* render dynamic columns */ for (i = 0; i < tt->n; i++) if (tt->column[i].flags & AG_TREETBL_COL_DYNAMIC) DrawDynamicColumn(tt, i); tt->visible.redraw_last = AG_GetTicks(); tt->visible.dirty = 0; } static void Draw(void *_Nonnull obj) { AG_Treetbl *tt = obj; Uint i; int w, hRow, y, update = 0; /* Before we draw, update if needed */ if (tt->visible.dirty) { ViewChanged(tt); } if (tt->visible.redraw_rate && AG_GetTicks() > tt->visible.redraw_last + tt->visible.redraw_rate) update = 1; AG_DrawBoxSunk(tt, &tt->r, &WCOLOR(tt, FG_COLOR)); w = tt->r.w; hRow = tt->hRow; AG_WidgetDraw(tt->vBar); if (tt->hBar != NULL) AG_WidgetDraw(tt->hBar); /* draw row selection hilites */ y = tt->hCol; for (i = 0; i < tt->visible.count; i++) { if (VISROW(tt,i) == NULL) { break; } if (VISROW(tt,i)->flags & AG_TREETBL_ROW_SELECTED) { AG_Rect rd; rd.x = 1; rd.y = y; rd.w = w-2; rd.h = hRow; AG_DrawBoxRaised(tt, &rd, &WCOLOR(tt, SELECTION_COLOR)); } y += hRow; } /* draw columns */ AG_PushBlendingMode(tt, AG_ALPHA_SRC, AG_ALPHA_ONE_MINUS_SRC); FOREACH_VISIBLE_COLUMN(tt, DrawColumn, &update, NULL); AG_PopBlendingMode(tt); if (update) tt->visible.redraw_last = AG_GetTicks(); } /* * Return a pointer to the currently selected row or NULL. * The Treetbl object must be locked. */ AG_TreetblRow * AG_TreetblSelectedRow(AG_Treetbl *tt) { AG_TreetblRow *row; TAILQ_FOREACH(row, &tt->children, siblings) { if (row->flags & AG_TREETBL_ROW_SELECTED) return (row); } return (NULL); } AG_WidgetClass agTreetblClass = { { "Agar(Widget:Treetbl)", sizeof(AG_Treetbl), { 1,0, AGC_TREETBL, 0xE030 }, Init, NULL, /* reset */ Destroy, NULL, /* load */ NULL, /* save */ NULL /* edit */ }, Draw, SizeRequest, SizeAllocate, MouseButtonDown, MouseButtonUp, NULL, /* mouse_motion */ NULL, /* key_down */ NULL, /* key_up */ NULL, /* touch */ NULL, /* ctrl */ NULL /* joy */ }; #endif /* AG_WIDGETS */