#include "WINGsP.h" char *WMListDidScrollNotification = "WMListDidScrollNotification"; char *WMListSelectionDidChangeNotification = "WMListSelectionDidChangeNotification"; typedef struct W_List { W_Class widgetClass; W_View *view; WMArray *items; /* list of WMListItem */ WMArray *selectedItems; /* list of selected WMListItems */ short itemHeight; short topItem; /* index of first visible item */ short fullFitLines; /* no of lines that fit entirely */ void *clientData; WMAction *action; void *doubleClientData; WMAction *doubleAction; WMListDrawProc *draw; WMHandlerID *idleID; /* for updating the scroller after adding elements */ WMScroller *vScroller; struct { unsigned int allowMultipleSelection:1; unsigned int allowEmptySelection:1; unsigned int userDrawn:1; unsigned int userItemHeight:1; unsigned int dontFitAll:1; /* 1 = last item won't be fully visible */ unsigned int redrawPending:1; unsigned int buttonPressed:1; unsigned int buttonWasPressed:1; } flags; } List; #define DEFAULT_WIDTH 150 #define DEFAULT_HEIGHT 150 static void destroyList(List *lPtr); static void paintList(List *lPtr); static void handleEvents(XEvent *event, void *data); static void handleActionEvents(XEvent *event, void *data); static void updateScroller(List *lPtr); static void vScrollCallBack(WMWidget *scroller, void *self); static void updateGeometry(WMList *lPtr); static void didResizeList(); W_ViewDelegate _ListViewDelegate = { NULL, NULL, didResizeList, NULL, NULL }; static void releaseItem(void *data) { WMListItem *item = (WMListItem*)data; if (item->text) wfree(item->text); wfree(item); } WMList* WMCreateList(WMWidget *parent) { List *lPtr; W_Screen *scrPtr = W_VIEW(parent)->screen; lPtr = wmalloc(sizeof(List)); memset(lPtr, 0, sizeof(List)); lPtr->widgetClass = WC_List; lPtr->view = W_CreateView(W_VIEW(parent)); if (!lPtr->view) { wfree(lPtr); return NULL; } lPtr->view->self = lPtr; lPtr->view->delegate = &_ListViewDelegate; WMCreateEventHandler(lPtr->view, ExposureMask|StructureNotifyMask |ClientMessageMask, handleEvents, lPtr); WMCreateEventHandler(lPtr->view, ButtonPressMask|ButtonReleaseMask |EnterWindowMask|LeaveWindowMask|ButtonMotionMask, handleActionEvents, lPtr); lPtr->itemHeight = WMFontHeight(scrPtr->normalFont) + 1; lPtr->items = WMCreateArrayWithDestructor(4, releaseItem); lPtr->selectedItems = WMCreateArray(4); /* create the vertical scroller */ lPtr->vScroller = WMCreateScroller(lPtr); WMMoveWidget(lPtr->vScroller, 1, 1); WMSetScrollerArrowsPosition(lPtr->vScroller, WSAMaxEnd); WMSetScrollerAction(lPtr->vScroller, vScrollCallBack, lPtr); /* make the scroller map itself when it's realized */ WMMapWidget(lPtr->vScroller); W_ResizeView(lPtr->view, DEFAULT_WIDTH, DEFAULT_HEIGHT); return lPtr; } void WMSetListAllowMultipleSelection(WMList *lPtr, Bool flag) { lPtr->flags.allowMultipleSelection = flag ? 1 : 0; } void WMSetListAllowEmptySelection(WMList *lPtr, Bool flag) { lPtr->flags.allowEmptySelection = flag ? 1 : 0; } static int comparator(const void *a, const void *b) { return (strcmp((*(WMListItem**)a)->text, (*(WMListItem**)b)->text)); } void WMSortListItems(WMList *lPtr) { WMSortArray(lPtr->items, comparator); paintList(lPtr); } void WMSortListItemsWithComparer(WMList *lPtr, WMCompareDataProc *func) { WMSortArray(lPtr->items, func); paintList(lPtr); } WMListItem* WMInsertListItem(WMList *lPtr, int row, char *text) { WMListItem *item; CHECK_CLASS(lPtr, WC_List); item = wmalloc(sizeof(WMListItem)); memset(item, 0, sizeof(WMListItem)); item->text = wstrdup(text); row = WMIN(row, WMGetArrayItemCount(lPtr->items)); if (row < 0) WMAddToArray(lPtr->items, item); else WMInsertInArray(lPtr->items, row, item); /* update the scroller when idle, so that we don't waste time * updating it when another item is going to be added later */ if (!lPtr->idleID) { lPtr->idleID = WMAddIdleHandler((WMCallback*)updateScroller, lPtr); } return item; } int WMRemoveListItem(WMList *lPtr, int row) { WMListItem *item; int topItem = lPtr->topItem; int selNotify = 0; CHECK_CLASS(lPtr, WC_List); if (row < 0 || row >= WMGetArrayItemCount(lPtr->items)) return 0; item = WMGetFromArray(lPtr->items, row); if (item->selected) { WMRemoveFromArray(lPtr->selectedItems, item); //WMUnselectListItem(lPtr, row); selNotify = 1; } if (row <= lPtr->topItem+lPtr->fullFitLines+lPtr->flags.dontFitAll) lPtr->topItem--; if (lPtr->topItem < 0) lPtr->topItem = 0; WMDeleteFromArray(lPtr->items, row); if (!lPtr->idleID) { lPtr->idleID = WMAddIdleHandler((WMCallback*)updateScroller, lPtr); } if (lPtr->topItem != topItem) { WMPostNotificationName(WMListDidScrollNotification, lPtr, NULL); } if (selNotify) { WMPostNotificationName(WMListSelectionDidChangeNotification, lPtr, NULL); } return 1; } WMListItem* WMGetListItem(WMList *lPtr, int row) { return WMGetFromArray(lPtr->items, row); } WMArray* WMGetListItems(WMList *lPtr) { return lPtr->items; } void WMSetListUserDrawProc(WMList *lPtr, WMListDrawProc *proc) { lPtr->flags.userDrawn = 1; lPtr->draw = proc; } void WMSetListUserDrawItemHeight(WMList *lPtr, unsigned short height) { assert(height > 0); lPtr->flags.userItemHeight = 1; lPtr->itemHeight = height; updateGeometry(lPtr); } void WMClearList(WMList *lPtr) { int selNo = WMGetArrayItemCount(lPtr->selectedItems); WMEmptyArray(lPtr->selectedItems); WMEmptyArray(lPtr->items); lPtr->topItem = 0; if (!lPtr->idleID) { WMDeleteIdleHandler(lPtr->idleID); lPtr->idleID = NULL; } if (lPtr->view->flags.realized) { updateScroller(lPtr); } if (selNo > 0) { WMPostNotificationName(WMListSelectionDidChangeNotification, lPtr, NULL); } } void WMSetListAction(WMList *lPtr, WMAction *action, void *clientData) { lPtr->action = action; lPtr->clientData = clientData; } void WMSetListDoubleAction(WMList *lPtr, WMAction *action, void *clientData) { lPtr->doubleAction = action; lPtr->doubleClientData = clientData; } WMArray* WMGetListSelectedItems(WMList *lPtr) { return lPtr->selectedItems; } WMListItem* WMGetListSelectedItem(WMList *lPtr) { return WMGetFromArray(lPtr->selectedItems, 0); } int WMGetListSelectedItemRow(WMList *lPtr) { WMListItem *item = WMGetFromArray(lPtr->selectedItems, 0); return (item!=NULL ? WMGetFirstInArray(lPtr->items, item) : WLNotFound); } int WMGetListItemHeight(WMList *lPtr) { return lPtr->itemHeight; } void WMSetListPosition(WMList *lPtr, int row) { lPtr->topItem = row; if (lPtr->topItem + lPtr->fullFitLines > WMGetArrayItemCount(lPtr->items)) lPtr->topItem = WMGetArrayItemCount(lPtr->items) - lPtr->fullFitLines; if (lPtr->topItem < 0) lPtr->topItem = 0; if (lPtr->view->flags.realized) updateScroller(lPtr); } void WMSetListBottomPosition(WMList *lPtr, int row) { if (WMGetArrayItemCount(lPtr->items) > lPtr->fullFitLines) { lPtr->topItem = row - lPtr->fullFitLines; if (lPtr->topItem < 0) lPtr->topItem = 0; if (lPtr->view->flags.realized) updateScroller(lPtr); } } int WMGetListNumberOfRows(WMList *lPtr) { return WMGetArrayItemCount(lPtr->items); } int WMGetListPosition(WMList *lPtr) { return lPtr->topItem; } Bool WMListAllowsMultipleSelection(WMList *lPtr) { return lPtr->flags.allowMultipleSelection; } Bool WMListAllowsEmptySelection(WMList *lPtr) { return lPtr->flags.allowEmptySelection; } static void scrollByAmount(WMList *lPtr, int amount) { int itemCount = WMGetArrayItemCount(lPtr->items); if ((amount < 0 && lPtr->topItem > 0) || (amount > 0 && (lPtr->topItem + lPtr->fullFitLines < itemCount))) { lPtr->topItem += amount; if (lPtr->topItem < 0) lPtr->topItem = 0; if (lPtr->topItem + lPtr->fullFitLines > itemCount) lPtr->topItem = itemCount - lPtr->fullFitLines; updateScroller(lPtr); } } static void vScrollCallBack(WMWidget *scroller, void *self) { WMList *lPtr = (WMList*)self; int height; int oldTopItem = lPtr->topItem; int itemCount = WMGetArrayItemCount(lPtr->items); height = lPtr->view->size.height - 4; switch (WMGetScrollerHitPart((WMScroller*)scroller)) { case WSDecrementLine: scrollByAmount(lPtr, -1); break; case WSIncrementLine: scrollByAmount(lPtr, 1); break; case WSDecrementPage: scrollByAmount(lPtr, -lPtr->fullFitLines+(1-lPtr->flags.dontFitAll)+1); break; case WSIncrementPage: scrollByAmount(lPtr, lPtr->fullFitLines-(1-lPtr->flags.dontFitAll)-1); break; case WSKnob: lPtr->topItem = WMGetScrollerValue(lPtr->vScroller) * (float)(itemCount - lPtr->fullFitLines); if (oldTopItem != lPtr->topItem) paintList(lPtr); // use updateScroller(lPtr) here? break; case WSKnobSlot: case WSNoPart: default: /* do nothing */ break; } if (lPtr->topItem != oldTopItem) WMPostNotificationName(WMListDidScrollNotification, lPtr, NULL); } static void paintItem(List *lPtr, int index) { WMView *view = lPtr->view; W_Screen *scr = view->screen; int width, height, x, y; WMListItem *itemPtr; itemPtr = WMGetFromArray(lPtr->items, index); width = lPtr->view->size.width - 2 - 19; height = lPtr->itemHeight; x = 19; y = 2 + (index-lPtr->topItem) * lPtr->itemHeight + 1; if (lPtr->flags.userDrawn) { WMRect rect; int flags; rect.size.width = width; rect.size.height = height; rect.pos.x = x; rect.pos.y = y; flags = itemPtr->uflags; if (itemPtr->disabled) flags |= WLDSDisabled; if (itemPtr->selected) flags |= WLDSSelected; if (itemPtr->isBranch) flags |= WLDSIsBranch; if (lPtr->draw) (*lPtr->draw)(lPtr, index, view->window, itemPtr->text, flags, &rect); } else { if (itemPtr->selected) { XFillRectangle(scr->display, view->window, WMColorGC(scr->white), x, y, width, height); } else { XClearArea(scr->display, view->window, x, y, width, height, False); } W_PaintText(view, view->window, scr->normalFont, x+4, y, width, WALeft, WMColorGC(scr->black), False, itemPtr->text, strlen(itemPtr->text)); } } static void paintList(List *lPtr) { W_Screen *scrPtr = lPtr->view->screen; int i, lim; if (!lPtr->view->flags.mapped) return; if (WMGetArrayItemCount(lPtr->items) > 0) { if (lPtr->topItem+lPtr->fullFitLines+lPtr->flags.dontFitAll > WMGetArrayItemCount(lPtr->items)) { lim = WMGetArrayItemCount(lPtr->items) - lPtr->topItem; XClearArea(scrPtr->display, lPtr->view->window, 19, 2+lim*lPtr->itemHeight, lPtr->view->size.width-21, lPtr->view->size.height-lim*lPtr->itemHeight-3, False); } else { lim = lPtr->fullFitLines + lPtr->flags.dontFitAll; } for (i = lPtr->topItem; i < lPtr->topItem + lim; i++) { paintItem(lPtr, i); } } else { XClearWindow(scrPtr->display, lPtr->view->window); } W_DrawRelief(scrPtr, lPtr->view->window, 0, 0, lPtr->view->size.width, lPtr->view->size.height, WRSunken); } #if 0 static void scrollTo(List *lPtr, int newTop) { } #endif static void updateScroller(List *lPtr) { float knobProportion, floatValue, tmp; int count = WMGetArrayItemCount(lPtr->items); if (lPtr->idleID) WMDeleteIdleHandler(lPtr->idleID); lPtr->idleID = NULL; paintList(lPtr); if (count == 0 || count <= lPtr->fullFitLines) WMSetScrollerParameters(lPtr->vScroller, 0, 1); else { tmp = lPtr->fullFitLines; knobProportion = tmp/(float)count; floatValue = (float)lPtr->topItem/(float)(count - lPtr->fullFitLines); WMSetScrollerParameters(lPtr->vScroller, floatValue, knobProportion); } } static void handleEvents(XEvent *event, void *data) { List *lPtr = (List*)data; CHECK_CLASS(data, WC_List); switch (event->type) { case Expose: if (event->xexpose.count!=0) break; paintList(lPtr); break; case DestroyNotify: destroyList(lPtr); break; } } static int matchTitle(void *item, void *title) { return (strcmp(((WMListItem*)item)->text, (char*)title)==0 ? 1 : 0); } int WMFindRowOfListItemWithTitle(WMList *lPtr, char *title) { return WMFindInArray(lPtr->items, matchTitle, title); } void WMSelectListItem(WMList *lPtr, int row) { WMListItem *item, *oldSelected; if (row >= WMGetArrayItemCount(lPtr->items)) return; if (row < 0) { /* Should row = -1 deselect all or just do nothing ?. Check it. -Dan */ if (!lPtr->flags.allowMultipleSelection) { WMUnselectAllListItems(lPtr); } return; } item = WMGetFromArray(lPtr->items, row); if (item->selected) return; /* Return if already selected */ oldSelected = WMGetFromArray(lPtr->selectedItems, 0); /* unselect previous selected item if case */ if (!lPtr->flags.allowMultipleSelection && oldSelected) { int oldSelectedRow = WMGetListSelectedItemRow(lPtr); // better call WMUnselectAllListItems() here? -Dan oldSelected->selected = 0; WMDeleteFromArray(lPtr->selectedItems, 0); // This is faster and have the same effect in the single selected case // but may leave xxx->selected flags set after a multi->single selected // switch //WMEmptyArray(lPtr->selectedItems); if (lPtr->view->flags.mapped && oldSelectedRow>=lPtr->topItem && oldSelectedRow<=lPtr->topItem+lPtr->fullFitLines) { paintItem(lPtr, oldSelectedRow); } } /* select item */ item->selected = 1; WMAddToArray(lPtr->selectedItems, item); if (lPtr->view->flags.mapped) { paintItem(lPtr, row); if ((row-lPtr->topItem+lPtr->fullFitLines)*lPtr->itemHeight > lPtr->view->size.height-2) W_DrawRelief(lPtr->view->screen, lPtr->view->window, 0, 0, lPtr->view->size.width, lPtr->view->size.height, WRSunken); } WMPostNotificationName(WMListSelectionDidChangeNotification, lPtr, NULL); } // make them return an int void WMUnselectListItem(WMList *lPtr, int row) { WMListItem *item = WMGetFromArray(lPtr->items, row); if (!item || !item->selected) return; // also add check for allowEmptySelection item->selected = 0; WMRemoveFromArray(lPtr->selectedItems, item); if (lPtr->view->flags.mapped && row>=lPtr->topItem && row<=lPtr->topItem+lPtr->fullFitLines) { paintItem(lPtr, row); } } void WMSelectAllListItems(WMList *lPtr) { int i; WMListItem *item; if (!lPtr->flags.allowMultipleSelection) return; // implement some WMDuplicateArray() ? for (i=0; iitems); i++) { item = WMGetFromArray(lPtr->items, i); if (!item->selected) { item->selected = 1; WMAddToArray(lPtr->selectedItems, item); if (lPtr->view->flags.mapped && i>=lPtr->topItem && i<=lPtr->topItem+lPtr->fullFitLines) { paintItem(lPtr, i); } } } } void WMUnselectAllListItems(WMList *lPtr) { int i; WMListItem *item; // check for allowEmptySelection for (i=0; iitems); i++) { item = WMGetFromArray(lPtr->items, i); if (item->selected) { item->selected = 0; if (lPtr->view->flags.mapped && i>=lPtr->topItem && i<=lPtr->topItem+lPtr->fullFitLines) { paintItem(lPtr, i); } } } WMEmptyArray(lPtr->selectedItems); } static int getItemIndexAt(List *lPtr, int clickY) { int index; index = (clickY - 2) / lPtr->itemHeight + lPtr->topItem; if (index < 0 || index >= WMGetArrayItemCount(lPtr->items)) return -1; return index; } static void handleActionEvents(XEvent *event, void *data) { List *lPtr = (List*)data; int tmp; int topItem = lPtr->topItem; static int oldClicked = -1; CHECK_CLASS(data, WC_List); switch (event->type) { case ButtonRelease: #define CHECK_WHEEL_PATCH #ifdef CHECK_WHEEL_PATCH /* Ignore mouse wheel events, they're not "real" button events */ if (event->xbutton.button == WINGsConfiguration.mouseWheelUp || event->xbutton.button == WINGsConfiguration.mouseWheelDown) { break; } #endif lPtr->flags.buttonPressed = 0; tmp = getItemIndexAt(lPtr, event->xbutton.y); if (/*tmp == lPtr->selectedItem && */tmp >= 0) { if (lPtr->action) (*lPtr->action)(lPtr, lPtr->clientData); } oldClicked = tmp; break; case EnterNotify: lPtr->flags.buttonPressed = lPtr->flags.buttonWasPressed; lPtr->flags.buttonWasPressed = 0; break; case LeaveNotify: lPtr->flags.buttonWasPressed = lPtr->flags.buttonPressed; lPtr->flags.buttonPressed = 0; break; case ButtonPress: if (event->xbutton.x > WMWidgetWidth(lPtr->vScroller)) { #ifdef CHECK_WHEEL_PATCH if (event->xbutton.button == WINGsConfiguration.mouseWheelDown || event->xbutton.button == WINGsConfiguration.mouseWheelUp) { int amount = 0; if (event->xbutton.state & ShiftMask) { amount = lPtr->fullFitLines-(1-lPtr->flags.dontFitAll)-1; } else if (event->xbutton.state & ControlMask) { amount = 1; } else { amount = lPtr->fullFitLines / 3; if (amount == 0) amount++; } if (event->xbutton.button == WINGsConfiguration.mouseWheelUp) amount = -amount; scrollByAmount(lPtr, amount); break; } #endif tmp = getItemIndexAt(lPtr, event->xbutton.y); lPtr->flags.buttonPressed = 1; if (tmp >= 0) { if (tmp == oldClicked && WMIsDoubleClick(event)) { WMSelectListItem(lPtr, tmp); if (lPtr->doubleAction) (*lPtr->doubleAction)(lPtr, lPtr->doubleClientData); } else { WMSelectListItem(lPtr, tmp); } } oldClicked = tmp; } break; case MotionNotify: if (lPtr->flags.buttonPressed) { tmp = getItemIndexAt(lPtr, event->xmotion.y); if (tmp>=0 /*&& tmp != lPtr->selectedItem*/) { WMSelectListItem(lPtr, tmp); } } break; } if (lPtr->topItem != topItem) WMPostNotificationName(WMListDidScrollNotification, lPtr, NULL); } static void updateGeometry(WMList *lPtr) { lPtr->fullFitLines = (lPtr->view->size.height - 4) / lPtr->itemHeight; if (lPtr->fullFitLines * lPtr->itemHeight < lPtr->view->size.height - 4) { lPtr->flags.dontFitAll = 1; } else { lPtr->flags.dontFitAll = 0; } if (WMGetArrayItemCount(lPtr->items) - lPtr->topItem <= lPtr->fullFitLines) { lPtr->topItem = WMGetArrayItemCount(lPtr->items) - lPtr->fullFitLines; if (lPtr->topItem < 0) lPtr->topItem = 0; } updateScroller(lPtr); } static void didResizeList(W_ViewDelegate *self, WMView *view) { WMList *lPtr = (WMList*)view->self; WMResizeWidget(lPtr->vScroller, 1, view->size.height-2); updateGeometry(lPtr); } static void destroyList(List *lPtr) { if (lPtr->idleID) WMDeleteIdleHandler(lPtr->idleID); lPtr->idleID = NULL; WMFreeArray(lPtr->items); wfree(lPtr); }