1
0
mirror of https://github.com/gryf/wmaker.git synced 2026-03-19 17:23:33 +01:00
Files
wmaker/WINGs/wlist.c
David Maciejak 80079cd343 WINGs: improve wlist widget
This patch is adding keyboard control to the wlist widget
(up/down/pgup/pgdw/home/end) and typeahead list search.
That component is for example used in the wmaker icon chooser,
and WPrefs keyboard shortcut, font conf panels.
2026-02-16 17:55:21 +00:00

1628 lines
40 KiB
C

#include "WINGsP.h"
#include <ctype.h>
#include <strings.h>
const char *WMListDidScrollNotification = "WMListDidScrollNotification";
const 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;
int 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 */
WMHandlerID *selectID; /* for selecting items in list while scrolling */
WMHandlerID *typeaheadID; /* for clearing typeahead buffer */
WMScroller *vScroller;
Pixmap doubleBuffer;
char *typeahead;
int typeaheadLen;
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
#define SCROLL_DELAY 100
#define TYPEAHEAD_CLEAR_DELAY 700
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(void *data);
static void scrollForwardSelecting(void *data);
static void scrollBackwardSelecting(void *data);
static void vScrollCallBack(WMWidget * scroller, void *self);
static void toggleItemSelection(WMList * lPtr, int index);
static void jumpToFirstItemWithPrefix(WMList * lPtr, const char *prefix, int prefixLen);
static void typeaheadTimeout(void *data);
static void updateGeometry(WMList * lPtr);
static void didResizeList(W_ViewDelegate * self, WMView * view);
static void unselectAllListItems(WMList * lPtr, WMListItem * exceptThis);
static W_ViewDelegate _ListViewDelegate = {
NULL,
NULL,
didResizeList,
NULL,
NULL
};
static void updateDoubleBufferPixmap(WMList * lPtr)
{
WMView *view = lPtr->view;
WMScreen *scr = view->screen;
if (!view->flags.realized)
return;
if (lPtr->doubleBuffer)
XFreePixmap(scr->display, lPtr->doubleBuffer);
lPtr->doubleBuffer =
XCreatePixmap(scr->display, view->window, view->size.width, lPtr->itemHeight, scr->depth);
}
static void realizeObserver(void *self, WMNotification * not)
{
/* Parameter not used, but tell the compiler that it is ok */
(void) not;
updateDoubleBufferPixmap(self);
}
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));
lPtr->typeahead = NULL;
lPtr->typeaheadLen = 0;
lPtr->typeaheadID = NULL;
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
| KeyPressMask, 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);
WMAddNotificationObserver(realizeObserver, lPtr, WMViewRealizedNotification, lPtr->view);
return lPtr;
}
void WMSetListAllowMultipleSelection(WMList * lPtr, Bool flag)
{
lPtr->flags.allowMultipleSelection = ((flag == 0) ? 0 : 1);
}
void WMSetListAllowEmptySelection(WMList * lPtr, Bool flag)
{
lPtr->flags.allowEmptySelection = ((flag == 0) ? 0 : 1);
}
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, const char *text)
{
WMListItem *item;
CHECK_CLASS(lPtr, WC_List);
item = wmalloc(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;
}
void WMRemoveListItem(WMList * lPtr, int row)
{
WMListItem *item;
int topItem = lPtr->topItem;
int selNotify = 0;
CHECK_CLASS(lPtr, WC_List);
/*wassertr(row>=0 && row<WMGetArrayItemCount(lPtr->items)); */
if (row < 0 || row >= WMGetArrayItemCount(lPtr->items))
return;
item = WMGetFromArray(lPtr->items, row);
if (item->selected) {
WMRemoveFromArray(lPtr->selectedItems, item);
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);
}
}
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;
updateDoubleBufferPixmap(lPtr);
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->selectID) {
WMDeleteTimerHandler(lPtr->selectID);
lPtr->selectID = NULL;
}
if (lPtr->typeaheadID) {
WMDeleteTimerHandler(lPtr->typeaheadID);
lPtr->typeaheadID = NULL;
}
if (lPtr->typeahead) {
lPtr->typeahead[0] = '\0';
lPtr->typeaheadLen = 0;
}
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 oldTopItem = lPtr->topItem;
int itemCount = WMGetArrayItemCount(lPtr->items);
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 WSDecrementWheel:
scrollByAmount(lPtr, -lPtr->fullFitLines / 3);
break;
case WSIncrementWheel:
scrollByAmount(lPtr, lPtr->fullFitLines / 3);
break;
case WSKnob:
lPtr->topItem = WMGetScrollerValue(lPtr->vScroller) * (float)(itemCount - lPtr->fullFitLines);
if (oldTopItem != lPtr->topItem)
paintList(lPtr); /* use updateScroller(lPtr) here? -Dan */
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;
Display *display = scr->display;
int width, height, x, y, tlen;
WMListItem *itemPtr;
Drawable d = lPtr->doubleBuffer;
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;
tlen = strlen(itemPtr->text);
if (lPtr->flags.userDrawn) {
WMRect rect;
int flags;
rect.size.width = width;
rect.size.height = height;
rect.pos.x = 0;
rect.pos.y = 0;
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, d, itemPtr->text, flags, &rect);
XCopyArea(display, d, view->window, scr->copyGC, 0, 0, width, height, x, y);
} else {
WMColor *back = (itemPtr->selected ? scr->white : view->backColor);
XFillRectangle(display, d, WMColorGC(back), 0, 0, width, height);
W_PaintText(view, d, scr->normalFont, 4, 0, width, WALeft, scr->black, False, itemPtr->text, tlen);
XCopyArea(display, d, view->window, scr->copyGC, 0, 0, width, height, x, y);
}
if ((index - 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);
}
}
static void paintList(List * lPtr)
{
W_Screen *scrPtr = lPtr->view->screen;
int i, lim, itemCount;
if (!lPtr->view->flags.mapped)
return;
itemCount = WMGetArrayItemCount(lPtr->items);
if (itemCount > 0) {
if (lPtr->topItem + lPtr->fullFitLines + lPtr->flags.dontFitAll > itemCount) {
lim = itemCount - 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(void *data)
{
List *lPtr = (List *) data;
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 scrollForwardSelecting(void *data)
{
List *lPtr = (List *) data;
int lastSelected;
lastSelected = lPtr->topItem + lPtr->fullFitLines + lPtr->flags.dontFitAll - 1;
if (lastSelected >= WMGetArrayItemCount(lPtr->items) - 1) {
lPtr->selectID = NULL;
if (lPtr->flags.dontFitAll)
scrollByAmount(lPtr, 1);
return;
}
/* selecting NEEDS to be done before scrolling to avoid flickering */
if (lPtr->flags.allowMultipleSelection) {
WMListItem *item;
WMRange range;
item = WMGetFromArray(lPtr->selectedItems, 0);
range.position = WMGetFirstInArray(lPtr->items, item);
if (lastSelected + 1 >= range.position) {
range.count = lastSelected - range.position + 2;
} else {
range.count = lastSelected - range.position;
}
WMSetListSelectionToRange(lPtr, range);
} else {
WMSelectListItem(lPtr, lastSelected + 1);
}
scrollByAmount(lPtr, 1);
lPtr->selectID = WMAddTimerHandler(SCROLL_DELAY, scrollForwardSelecting, lPtr);
}
static void scrollBackwardSelecting(void *data)
{
List *lPtr = (List *) data;
if (lPtr->topItem < 1) {
lPtr->selectID = NULL;
return;
}
/* selecting NEEDS to be done before scrolling to avoid flickering */
if (lPtr->flags.allowMultipleSelection) {
WMListItem *item;
WMRange range;
item = WMGetFromArray(lPtr->selectedItems, 0);
range.position = WMGetFirstInArray(lPtr->items, item);
if (lPtr->topItem - 1 >= range.position) {
range.count = lPtr->topItem - range.position;
} else {
range.count = lPtr->topItem - range.position - 2;
}
WMSetListSelectionToRange(lPtr, range);
} else {
WMSelectListItem(lPtr, lPtr->topItem - 1);
}
scrollByAmount(lPtr, -1);
lPtr->selectID = WMAddTimerHandler(SCROLL_DELAY, scrollBackwardSelecting, lPtr);
}
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(const void *item, const void *title)
{
const WMListItem *wl_item = item;
const char *s_title = title;
return (strcmp(wl_item->text, s_title) == 0 ? 1 : 0);
}
int WMFindRowOfListItemWithTitle(WMList * lPtr, const char *title)
{
/*
* We explicitely discard the 'const' attribute here because the
* call-back function handler must not be made with a const
* attribute, but our local call-back function (above) does have
* it properly set, so we're consistent
*/
return WMFindInArray(lPtr->items, matchTitle, (char *) title);
}
void WMSelectListItem(WMList * lPtr, int row)
{
WMListItem *item;
if (row >= WMGetArrayItemCount(lPtr->items))
return;
if (row < 0) {
/* row = -1 will deselects all for backward compatibility.
* will be removed later. -Dan */
WMUnselectAllListItems(lPtr);
return;
}
item = WMGetFromArray(lPtr->items, row);
if (item->selected)
return; /* Return if already selected */
if (!lPtr->flags.allowMultipleSelection) {
/* unselect previous selected items */
unselectAllListItems(lPtr, NULL);
}
/* select item */
item->selected = 1;
WMAddToArray(lPtr->selectedItems, item);
if (lPtr->view->flags.mapped && row >= lPtr->topItem && row <= lPtr->topItem + lPtr->fullFitLines) {
paintItem(lPtr, row);
}
WMPostNotificationName(WMListSelectionDidChangeNotification, lPtr, NULL);
}
void WMUnselectListItem(WMList * lPtr, int row)
{
WMListItem *item = WMGetFromArray(lPtr->items, row);
if (!item || !item->selected)
return;
if (!lPtr->flags.allowEmptySelection && WMGetArrayItemCount(lPtr->selectedItems) <= 1) {
return;
}
item->selected = 0;
WMRemoveFromArray(lPtr->selectedItems, item);
if (lPtr->view->flags.mapped && row >= lPtr->topItem && row <= lPtr->topItem + lPtr->fullFitLines) {
paintItem(lPtr, row);
}
WMPostNotificationName(WMListSelectionDidChangeNotification, lPtr, NULL);
}
void WMSelectListItemsInRange(WMList * lPtr, WMRange range)
{
WMListItem *item;
int position = range.position, k = 1, notify = 0;
int total = WMGetArrayItemCount(lPtr->items);
if (!lPtr->flags.allowMultipleSelection)
return;
if (range.count == 0)
return; /* Nothing to select */
if (range.count < 0) {
range.count = -range.count;
k = -1;
}
for (; range.count > 0 && position >= 0 && position < total; range.count--) {
item = WMGetFromArray(lPtr->items, position);
if (!item->selected) {
item->selected = 1;
WMAddToArray(lPtr->selectedItems, item);
if (lPtr->view->flags.mapped && position >= lPtr->topItem
&& position <= lPtr->topItem + lPtr->fullFitLines) {
paintItem(lPtr, position);
}
notify = 1;
}
position += k;
}
if (notify) {
WMPostNotificationName(WMListSelectionDidChangeNotification, lPtr, NULL);
}
}
void WMSetListSelectionToRange(WMList * lPtr, WMRange range)
{
WMListItem *item;
int mark1, mark2, i, k;
int position = range.position, notify = 0;
int total = WMGetArrayItemCount(lPtr->items);
if (!lPtr->flags.allowMultipleSelection)
return;
if (range.count == 0) {
WMUnselectAllListItems(lPtr);
return;
}
if (range.count < 0) {
mark1 = range.position + range.count + 1;
mark2 = range.position + 1;
range.count = -range.count;
k = -1;
} else {
mark1 = range.position;
mark2 = range.position + range.count;
k = 1;
}
if (mark1 > total)
mark1 = total;
if (mark2 < 0)
mark2 = 0;
WMEmptyArray(lPtr->selectedItems);
for (i = 0; i < mark1; 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);
}
notify = 1;
}
}
for (; range.count > 0 && position >= 0 && position < total; range.count--) {
item = WMGetFromArray(lPtr->items, position);
if (!item->selected) {
item->selected = 1;
if (lPtr->view->flags.mapped && position >= lPtr->topItem
&& position <= lPtr->topItem + lPtr->fullFitLines) {
paintItem(lPtr, position);
}
notify = 1;
}
WMAddToArray(lPtr->selectedItems, item);
position += k;
}
for (i = mark2; i < total; 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);
}
notify = 1;
}
}
if (notify) {
WMPostNotificationName(WMListSelectionDidChangeNotification, lPtr, NULL);
}
}
void WMSelectAllListItems(WMList * lPtr)
{
int i, itemCount;
WMListItem *item;
if (!lPtr->flags.allowMultipleSelection)
return;
if (WMGetArrayItemCount(lPtr->items) == WMGetArrayItemCount(lPtr->selectedItems)) {
return; /* All items are selected already */
}
WMFreeArray(lPtr->selectedItems);
lPtr->selectedItems = WMCreateArrayWithArray(lPtr->items);
itemCount = WMGetArrayItemCount(lPtr->items);
for (i = 0; i < itemCount; i++) {
item = WMGetFromArray(lPtr->items, i);
if (!item->selected) {
item->selected = 1;
if (lPtr->view->flags.mapped && i >= lPtr->topItem
&& i <= lPtr->topItem + lPtr->fullFitLines) {
paintItem(lPtr, i);
}
}
}
WMPostNotificationName(WMListSelectionDidChangeNotification, lPtr, NULL);
}
/*
* Be careful from where you call this function! It doesn't honor the
* allowEmptySelection flag and doesn't send a notification about selection
* change! You need to manage these in the functions from where you call it.
*
* This will unselect all items if exceptThis is NULL, else will keep
* exceptThis selected.
* Make sure that exceptThis is one of the already selected items if not NULL!
*
*/
static void unselectAllListItems(WMList * lPtr, WMListItem * exceptThis)
{
int i, itemCount;
WMListItem *item;
itemCount = WMGetArrayItemCount(lPtr->items);
for (i = 0; i < itemCount; i++) {
item = WMGetFromArray(lPtr->items, i);
if (item != exceptThis && item->selected) {
item->selected = 0;
if (lPtr->view->flags.mapped && i >= lPtr->topItem
&& i <= lPtr->topItem + lPtr->fullFitLines) {
paintItem(lPtr, i);
}
}
}
WMEmptyArray(lPtr->selectedItems);
if (exceptThis != NULL) {
exceptThis->selected = 1;
WMAddToArray(lPtr->selectedItems, exceptThis);
}
}
void WMUnselectAllListItems(WMList * lPtr)
{
int keep;
WMListItem *keepItem;
keep = lPtr->flags.allowEmptySelection ? 0 : 1;
if (WMGetArrayItemCount(lPtr->selectedItems) == keep)
return;
keepItem = (keep == 1 ? WMGetFromArray(lPtr->selectedItems, 0) : NULL);
unselectAllListItems(lPtr, keepItem);
WMPostNotificationName(WMListSelectionDidChangeNotification, lPtr, NULL);
}
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 toggleItemSelection(WMList * lPtr, int index)
{
WMListItem *item = WMGetFromArray(lPtr->items, index);
if (item && item->selected) {
WMUnselectListItem(lPtr, index);
} else {
WMSelectListItem(lPtr, index);
}
}
static int findItemWithPrefix(List * lPtr, const char *prefix, int prefixLen)
{
if (prefixLen <= 0)
return -1;
int i, itemCount;
itemCount = WMGetArrayItemCount(lPtr->items);
for (i = 0; i < itemCount; i++) {
WMListItem *item = WMGetFromArray(lPtr->items, i);
if (!item || !item->text || item->text[0] == '\0')
continue;
if (strncasecmp(item->text, prefix, prefixLen) == 0)
return i;
}
return -1;
}
static void jumpToFirstItemWithPrefix(WMList * lPtr, const char *prefix, int prefixLen)
{
int index, oldTop, visibleCount;
index = findItemWithPrefix(lPtr, prefix, prefixLen);
if (index < 0)
return;
if (lPtr->flags.allowMultipleSelection) {
WMRange range;
range.position = index;
range.count = 1;
WMSetListSelectionToRange(lPtr, range);
} else {
WMSelectListItem(lPtr, index);
/* Trigger action callback */
if (lPtr->action)
(*lPtr->action) (lPtr, lPtr->clientData);
}
visibleCount = lPtr->fullFitLines + lPtr->flags.dontFitAll;
if (visibleCount < 1)
visibleCount = 1;
oldTop = lPtr->topItem;
if (index < lPtr->topItem) {
lPtr->topItem = index;
} else {
if (lPtr->flags.dontFitAll) {
if (lPtr->fullFitLines <= 0) {
lPtr->topItem = index;
} else {
int lastFullyVisible = lPtr->topItem + lPtr->fullFitLines - 1;
if (index > lastFullyVisible)
lPtr->topItem = index - lPtr->fullFitLines + 1;
}
} else if (index >= lPtr->topItem + visibleCount) {
lPtr->topItem = index - visibleCount + 1;
}
}
if (lPtr->topItem < 0)
lPtr->topItem = 0;
if (lPtr->view->flags.realized && lPtr->topItem != oldTop)
updateScroller(lPtr);
}
static void typeaheadTimeout(void *data)
{
List *lPtr = (List *) data;
lPtr->typeaheadID = NULL;
if (lPtr->typeahead) {
lPtr->typeahead[0] = '\0';
lPtr->typeaheadLen = 0;
}
}
static void handleActionEvents(XEvent * event, void *data)
{
List *lPtr = (List *) data;
int tmp, height;
int topItem = lPtr->topItem;
static int lastClicked = -1, prevItem = -1;
CHECK_CLASS(data, WC_List);
switch (event->type) {
case ButtonRelease:
/* Ignore mouse wheel events, they're not "real" button events */
if (event->xbutton.button == WINGsConfiguration.mouseWheelUp ||
event->xbutton.button == WINGsConfiguration.mouseWheelDown) {
break;
}
lPtr->flags.buttonPressed = 0;
if (lPtr->selectID) {
WMDeleteTimerHandler(lPtr->selectID);
lPtr->selectID = NULL;
}
tmp = getItemIndexAt(lPtr, event->xbutton.y);
if (tmp >= 0) {
if (lPtr->action)
(*lPtr->action) (lPtr, lPtr->clientData);
}
if (!(event->xbutton.state & ShiftMask))
lastClicked = prevItem = tmp;
break;
case EnterNotify:
if (lPtr->selectID) {
WMDeleteTimerHandler(lPtr->selectID);
lPtr->selectID = NULL;
}
WMSetFocusToWidget(lPtr);
break;
case LeaveNotify:
height = WMWidgetHeight(lPtr);
if (lPtr->flags.buttonPressed && !lPtr->selectID) {
if (event->xcrossing.y >= height) {
lPtr->selectID = WMAddTimerHandler(SCROLL_DELAY, scrollForwardSelecting, lPtr);
} else if (event->xcrossing.y <= 0) {
lPtr->selectID = WMAddTimerHandler(SCROLL_DELAY, scrollBackwardSelecting, lPtr);
}
}
WMWidget *parentWidget = WMWidgetOfView(lPtr->view->parent);
if (parentWidget)
WMSetFocusToWidget(parentWidget);
break;
case ButtonPress:
if (event->xbutton.x <= WMWidgetWidth(lPtr->vScroller))
break;
if (event->xbutton.button == WINGsConfiguration.mouseWheelDown ||
event->xbutton.button == WINGsConfiguration.mouseWheelUp) {
int amount = 0;
if (event->xbutton.state & ControlMask) {
amount = lPtr->fullFitLines - (1 - lPtr->flags.dontFitAll) - 1;
} else if (event->xbutton.state & ShiftMask) {
amount = 1;
} else {
amount = lPtr->fullFitLines / 3;
if (amount == 0)
amount++;
}
if (event->xbutton.button == WINGsConfiguration.mouseWheelUp)
amount = -amount;
scrollByAmount(lPtr, amount);
break;
}
tmp = getItemIndexAt(lPtr, event->xbutton.y);
lPtr->flags.buttonPressed = 1;
if (tmp >= 0) {
if (tmp == lastClicked && WMIsDoubleClick(event)) {
WMSelectListItem(lPtr, tmp);
if (lPtr->doubleAction)
(*lPtr->doubleAction) (lPtr, lPtr->doubleClientData);
} else {
if (!lPtr->flags.allowMultipleSelection) {
if (event->xbutton.state & ControlMask) {
toggleItemSelection(lPtr, tmp);
} else {
WMSelectListItem(lPtr, tmp);
}
} else {
WMRange range;
WMListItem *lastSel;
if (event->xbutton.state & ControlMask) {
toggleItemSelection(lPtr, tmp);
} else if (event->xbutton.state & ShiftMask) {
if (WMGetArrayItemCount(lPtr->selectedItems) == 0) {
WMSelectListItem(lPtr, tmp);
} else {
lastSel = WMGetFromArray(lPtr->items, lastClicked);
range.position = WMGetFirstInArray(lPtr->items, lastSel);
if (tmp >= range.position)
range.count = tmp - range.position + 1;
else
range.count = tmp - range.position - 1;
WMSetListSelectionToRange(lPtr, range);
}
} else {
range.position = tmp;
range.count = 1;
WMSetListSelectionToRange(lPtr, range);
}
}
}
}
if (!(event->xbutton.state & ShiftMask))
lastClicked = prevItem = tmp;
break;
case MotionNotify:
height = WMWidgetHeight(lPtr);
if (lPtr->selectID && event->xmotion.y > 0 && event->xmotion.y < height) {
WMDeleteTimerHandler(lPtr->selectID);
lPtr->selectID = NULL;
}
if (lPtr->flags.buttonPressed && !lPtr->selectID) {
if (event->xmotion.y <= 0) {
lPtr->selectID = WMAddTimerHandler(SCROLL_DELAY, scrollBackwardSelecting, lPtr);
break;
} else if (event->xmotion.y >= height) {
lPtr->selectID = WMAddTimerHandler(SCROLL_DELAY, scrollForwardSelecting, lPtr);
break;
}
tmp = getItemIndexAt(lPtr, event->xmotion.y);
if (tmp >= 0 && tmp != prevItem) {
if (lPtr->flags.allowMultipleSelection) {
WMRange range;
range.position = lastClicked;
if (tmp >= lastClicked)
range.count = tmp - lastClicked + 1;
else
range.count = tmp - lastClicked - 1;
WMSetListSelectionToRange(lPtr, range);
} else {
WMSelectListItem(lPtr, tmp);
}
}
prevItem = tmp;
}
break;
case KeyPress:
char buffer[16];
KeySym ksym;
Status status;
int len;
WMScreen *scr = lPtr->view->screen;
XWindowAttributes wattr;
if (event->xkey.state & (ControlMask | Mod1Mask))
break;
if (!(XGetWindowAttributes(scr->display, lPtr->view->window, &wattr) && wattr.map_state == IsViewable))
break;
len = W_LookupString(lPtr->view, &event->xkey, buffer, (int)sizeof(buffer) - 1, &ksym, &status);
if (len < 0)
break;
if (len > 0)
buffer[len] = '\0';
/* Handle navigation keys */
switch (ksym) {
case XK_Up: {
int newRow;
int itemCount = WMGetArrayItemCount(lPtr->items);
int cur = WMGetListSelectedItemRow(lPtr);
if (lPtr->flags.allowMultipleSelection && WMGetArrayItemCount(lPtr->selectedItems) > 0) {
WMListItem *lastSel = WMGetFromArray(lPtr->selectedItems, WMGetArrayItemCount(lPtr->selectedItems) - 1);
if (lastSel)
cur = WMGetFirstInArray(lPtr->items, lastSel);
}
if (cur == WLNotFound)
cur = lPtr->topItem;
newRow = cur - 1;
if (newRow < 0)
newRow = 0;
if (newRow != cur && itemCount > 0) {
if (lPtr->flags.allowMultipleSelection) {
if (event->xkey.state & ShiftMask) {
WMRange range;
int anchor = WMGetListSelectedItemRow(lPtr);
if (anchor == WLNotFound || WMGetArrayItemCount(lPtr->selectedItems) == 0) {
anchor = cur;
}
range.position = anchor;
if (newRow >= anchor)
range.count = newRow - anchor + 1;
else
range.count = newRow - anchor - 1;
WMSetListSelectionToRange(lPtr, range);
} else {
WMRange range = { .position = newRow, .count = 1 };
WMSetListSelectionToRange(lPtr, range);
lastClicked = newRow;
}
} else {
WMSelectListItem(lPtr, newRow);
lastClicked = newRow;
}
/* Ensure visibility */
if (newRow < lPtr->topItem) {
lPtr->topItem = newRow;
if (lPtr->view->flags.realized)
updateScroller(lPtr);
}
/* Trigger action callback */
if (lPtr->action)
(*lPtr->action) (lPtr, lPtr->clientData);
}
break;
}
case XK_Down: {
int newRow;
int itemCount = WMGetArrayItemCount(lPtr->items);
int cur = WMGetListSelectedItemRow(lPtr);
if (lPtr->flags.allowMultipleSelection && WMGetArrayItemCount(lPtr->selectedItems) > 0) {
WMListItem *lastSel = WMGetFromArray(lPtr->selectedItems, WMGetArrayItemCount(lPtr->selectedItems) - 1);
if (lastSel)
cur = WMGetFirstInArray(lPtr->items, lastSel);
}
if (cur == WLNotFound)
cur = lPtr->topItem;
newRow = cur + 1;
if (newRow >= itemCount)
newRow = itemCount - 1;
if (newRow != cur && itemCount > 0) {
if (lPtr->flags.allowMultipleSelection) {
if (event->xkey.state & ShiftMask) {
WMRange range;
int anchor = WMGetListSelectedItemRow(lPtr);
if (anchor == WLNotFound || WMGetArrayItemCount(lPtr->selectedItems) == 0) {
anchor = cur;
}
range.position = anchor;
if (newRow >= anchor)
range.count = newRow - anchor + 1;
else
range.count = newRow - anchor - 1;
WMSetListSelectionToRange(lPtr, range);
} else {
WMRange range = { .position = newRow, .count = 1 };
WMSetListSelectionToRange(lPtr, range);
lastClicked = newRow;
}
} else {
WMSelectListItem(lPtr, newRow);
lastClicked = newRow;
}
/* Ensure visibility */
if (newRow > (lPtr->topItem + lPtr->fullFitLines - 1)) {
lPtr->topItem = newRow - lPtr->fullFitLines + 1;
if (lPtr->topItem < 0)
lPtr->topItem = 0;
/* Ensure we don't scroll past the end */
if (lPtr->topItem + lPtr->fullFitLines > itemCount)
lPtr->topItem = itemCount - lPtr->fullFitLines;
if (lPtr->topItem < 0)
lPtr->topItem = 0;
if (lPtr->view->flags.realized)
updateScroller(lPtr);
}
/* Trigger action callback */
if (lPtr->action)
(*lPtr->action) (lPtr, lPtr->clientData);
}
break;
}
case XK_Page_Up: {
int newRow;
int page = lPtr->fullFitLines > 0 ? lPtr->fullFitLines : 1;
int cur = WMGetListSelectedItemRow(lPtr);
if (cur == WLNotFound)
cur = lPtr->topItem;
newRow = cur - page;
if (newRow < 0)
newRow = 0;
if (newRow != cur) {
if (lPtr->flags.allowMultipleSelection) {
if (event->xkey.state & ShiftMask) {
WMRange range;
int anchor = WMGetListSelectedItemRow(lPtr);
if (anchor == WLNotFound || WMGetArrayItemCount(lPtr->selectedItems) == 0) {
anchor = cur;
}
range.position = anchor;
if (newRow >= anchor)
range.count = newRow - anchor + 1;
else
range.count = newRow - anchor - 1;
WMSetListSelectionToRange(lPtr, range);
} else {
WMRange range = { .position = newRow, .count = 1 };
WMSetListSelectionToRange(lPtr, range);
lastClicked = newRow;
}
} else {
WMSelectListItem(lPtr, newRow);
lastClicked = newRow;
}
if (newRow < lPtr->topItem) {
lPtr->topItem = newRow;
if (lPtr->view->flags.realized)
updateScroller(lPtr);
}
/* Trigger action callback */
if (lPtr->action)
(*lPtr->action) (lPtr, lPtr->clientData);
}
break;
}
case XK_Page_Down: {
int newRow;
int page = lPtr->fullFitLines > 0 ? lPtr->fullFitLines : 1;
int itemCount = WMGetArrayItemCount(lPtr->items);
int cur = WMGetListSelectedItemRow(lPtr);
if (cur == WLNotFound)
cur = lPtr->topItem;
newRow = cur + page;
if (newRow >= itemCount)
newRow = itemCount - 1;
if (newRow != cur && itemCount > 0) {
if (lPtr->flags.allowMultipleSelection) {
if (event->xkey.state & ShiftMask) {
WMRange range;
int anchor = WMGetListSelectedItemRow(lPtr);
if (anchor == WLNotFound || WMGetArrayItemCount(lPtr->selectedItems) == 0) {
anchor = cur;
}
range.position = anchor;
if (newRow >= anchor)
range.count = newRow - anchor + 1;
else
range.count = newRow - anchor - 1;
WMSetListSelectionToRange(lPtr, range);
} else {
WMRange range = { .position = newRow, .count = 1 };
WMSetListSelectionToRange(lPtr, range);
lastClicked = newRow;
}
} else {
WMSelectListItem(lPtr, newRow);
lastClicked = newRow;
}
/* Ensure visibility */
if (newRow > (lPtr->topItem + lPtr->fullFitLines - 1)) {
lPtr->topItem = newRow - lPtr->fullFitLines + 1;
if (lPtr->topItem < 0)
lPtr->topItem = 0;
/* Ensure we don't scroll past the end */
if (lPtr->topItem + lPtr->fullFitLines > itemCount)
lPtr->topItem = itemCount - lPtr->fullFitLines;
if (lPtr->topItem < 0)
lPtr->topItem = 0;
if (lPtr->view->flags.realized)
updateScroller(lPtr);
}
/* Trigger action callback */
if (lPtr->action)
(*lPtr->action) (lPtr, lPtr->clientData);
}
break;
}
case XK_Home: {
int itemCount = WMGetArrayItemCount(lPtr->items);
if (itemCount > 0) {
int newRow = 0;
if (lPtr->flags.allowMultipleSelection) {
if (event->xkey.state & ShiftMask) {
WMRange range;
int anchor = WMGetListSelectedItemRow(lPtr);
if (anchor == WLNotFound || WMGetArrayItemCount(lPtr->selectedItems) == 0) {
anchor = lPtr->topItem;
}
range.position = anchor;
if (newRow >= anchor)
range.count = newRow - anchor + 1;
else
range.count = newRow - anchor - 1;
WMSetListSelectionToRange(lPtr, range);
} else {
WMRange range = { .position = newRow, .count = 1 };
WMSetListSelectionToRange(lPtr, range);
lastClicked = newRow;
}
} else {
WMSelectListItem(lPtr, newRow);
lastClicked = newRow;
}
if (newRow < lPtr->topItem) {
lPtr->topItem = newRow;
if (lPtr->view->flags.realized)
updateScroller(lPtr);
}
/* Trigger action callback */
if (lPtr->action)
(*lPtr->action) (lPtr, lPtr->clientData);
}
break;
}
case XK_End: {
int itemCount = WMGetArrayItemCount(lPtr->items);
if (itemCount > 0) {
int newRow = itemCount - 1;
if (lPtr->flags.allowMultipleSelection) {
if (event->xkey.state & ShiftMask) {
WMRange range;
int anchor = WMGetListSelectedItemRow(lPtr);
if (anchor == WLNotFound || WMGetArrayItemCount(lPtr->selectedItems) == 0) {
anchor = lPtr->topItem;
}
range.position = anchor;
if (newRow >= anchor)
range.count = newRow - anchor + 1;
else
range.count = newRow - anchor - 1;
WMSetListSelectionToRange(lPtr, range);
} else {
WMRange range = { .position = newRow, .count = 1 };
WMSetListSelectionToRange(lPtr, range);
lastClicked = newRow;
}
} else {
WMSelectListItem(lPtr, newRow);
lastClicked = newRow;
}
/* Ensure the last item is fully visible */
lPtr->topItem = itemCount - lPtr->fullFitLines;
if (lPtr->topItem < 0)
lPtr->topItem = 0;
if (lPtr->view->flags.realized)
updateScroller(lPtr);
/* Trigger action callback */
if (lPtr->action)
(*lPtr->action) (lPtr, lPtr->clientData);
}
break;
}
default:
break;
}
/* If we handled a navigation keysym, avoid falling through to typeahead logic */
if (ksym == XK_Up || ksym == XK_Down || ksym == XK_Page_Up || ksym == XK_Page_Down || ksym == XK_Home || ksym == XK_End)
break;
if (len <= 0)
break;
buffer[len] = '\0';
if (ksym == XK_Escape) {
if (lPtr->typeaheadID) {
WMDeleteTimerHandler(lPtr->typeaheadID);
lPtr->typeaheadID = NULL;
}
if (lPtr->typeahead) {
lPtr->typeahead[0] = '\0';
lPtr->typeaheadLen = 0;
}
break;
}
if (ksym == XK_BackSpace) {
if (lPtr->typeaheadLen > 0 && lPtr->typeahead) {
lPtr->typeaheadLen--;
lPtr->typeahead[lPtr->typeaheadLen] = '\0';
}
} else if (len == 1 && isalnum((unsigned char)buffer[0])) {
if (!lPtr->typeahead) {
lPtr->typeahead = wmalloc(2);
lPtr->typeaheadLen = 0;
}
lPtr->typeahead = wrealloc(lPtr->typeahead, lPtr->typeaheadLen + 2);
lPtr->typeahead[lPtr->typeaheadLen] = buffer[0];
lPtr->typeaheadLen++;
lPtr->typeahead[lPtr->typeaheadLen] = '\0';
} else {
break;
}
if (lPtr->typeaheadLen > 0)
jumpToFirstItemWithPrefix(lPtr, lPtr->typeahead, lPtr->typeaheadLen);
if (lPtr->typeaheadID) {
WMDeleteTimerHandler(lPtr->typeaheadID);
lPtr->typeaheadID = NULL;
}
if (lPtr->typeaheadLen > 0)
lPtr->typeaheadID = WMAddTimerHandler(TYPEAHEAD_CLEAR_DELAY, typeaheadTimeout, lPtr);
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;
/* Parameter not used, but tell the compiler that it is ok */
(void) self;
WMResizeWidget(lPtr->vScroller, 1, view->size.height - 2);
updateDoubleBufferPixmap(lPtr);
updateGeometry(lPtr);
}
static void destroyList(List * lPtr)
{
if (lPtr->idleID)
WMDeleteIdleHandler(lPtr->idleID);
lPtr->idleID = NULL;
if (lPtr->selectID)
WMDeleteTimerHandler(lPtr->selectID);
lPtr->selectID = NULL;
if (lPtr->typeaheadID)
WMDeleteTimerHandler(lPtr->typeaheadID);
lPtr->typeaheadID = NULL;
if (lPtr->selectedItems)
WMFreeArray(lPtr->selectedItems);
if (lPtr->items)
WMFreeArray(lPtr->items);
if (lPtr->doubleBuffer)
XFreePixmap(lPtr->view->screen->display, lPtr->doubleBuffer);
if (lPtr->typeahead)
wfree(lPtr->typeahead);
WMRemoveNotificationObserver(lPtr);
wfree(lPtr);
}