david.m...@gmail.com unread, Feb 16, 2026, 11:04:55 AM (8 days ago) Feb 16
Sign in to reply to author
Sign in to forward
You do not have permission to delete messages in this group
Either email addresses are anonymous for this group or you need the view member email addresses permission to view the original message
to Window Maker Development
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. --- WINGs/wlist.c | 488 +++++++++++++++++++++++++++++++++++++++++++++++++- src/dialog.c | 1 + 2 files changed, 488 insertions(+), 1 deletion(-) diff --git a/WINGs/wlist.c b/WINGs/wlist.c index 8350095b..4e6591cf 100644 --- a/WINGs/wlist.c +++ b/WINGs/wlist.c @@ -1,5 +1,7 @@ #include "WINGsP.h" +#include <ctype.h> +#include <strings.h> const char *WMListDidScrollNotification = "WMListDidScrollNotification"; const char *WMListSelectionDidChangeNotification = "WMListSelectionDidChangeNotification"; @@ -27,11 +29,15 @@ typedef struct W_List { 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; @@ -48,6 +54,7 @@ typedef struct W_List { #define DEFAULT_HEIGHT 150 #define SCROLL_DELAY 100 +#define TYPEAHEAD_CLEAR_DELAY 700 static void destroyList(List * lPtr); static void paintList(List * lPtr); @@ -62,6 +69,8 @@ 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); @@ -113,6 +122,9 @@ WMList *WMCreateList(WMWidget * parent) W_Screen *scrPtr = W_VIEW(parent)->screen; lPtr = wmalloc(sizeof(List)); + lPtr->typeahead = NULL; + lPtr->typeaheadLen = 0; + lPtr->typeaheadID = NULL; lPtr->widgetClass = WC_List; @@ -129,7 +141,8 @@ WMList *WMCreateList(WMWidget * parent) | ClientMessageMask, handleEvents, lPtr); WMCreateEventHandler(lPtr->view, ButtonPressMask | ButtonReleaseMask - | EnterWindowMask | LeaveWindowMask | ButtonMotionMask, handleActionEvents, lPtr); + | EnterWindowMask | LeaveWindowMask | ButtonMotionMask + | KeyPressMask, handleActionEvents, lPtr); lPtr->itemHeight = WMFontHeight(scrPtr->normalFont) + 1; @@ -288,6 +301,14 @@ void WMClearList(WMList * lPtr) 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); } @@ -923,6 +944,85 @@ static void toggleItemSelection(WMList * lPtr, int 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; @@ -962,6 +1062,7 @@ static void handleActionEvents(XEvent * event, void *data) WMDeleteTimerHandler(lPtr->selectID); lPtr->selectID = NULL; } + WMSetFocusToWidget(lPtr); break; case LeaveNotify: @@ -973,6 +1074,9 @@ static void handleActionEvents(XEvent * event, void *data) lPtr->selectID = WMAddTimerHandler(SCROLL_DELAY, scrollBackwardSelecting, lPtr); } } + WMWidget *parentWidget = WMWidgetOfView(lPtr->view->parent); + if (parentWidget) + WMSetFocusToWidget(parentWidget); break; case ButtonPress: @@ -1079,6 +1183,381 @@ static void handleActionEvents(XEvent * event, void *data) 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); @@ -1126,6 +1605,10 @@ static void destroyList(List * lPtr) WMDeleteTimerHandler(lPtr->selectID); lPtr->selectID = NULL; + if (lPtr->typeaheadID) + WMDeleteTimerHandler(lPtr->typeaheadID); + lPtr->typeaheadID = NULL; + if (lPtr->selectedItems) WMFreeArray(lPtr->selectedItems); @@ -1135,6 +1618,9 @@ static void destroyList(List * lPtr) if (lPtr->doubleBuffer) XFreePixmap(lPtr->view->screen->display, lPtr->doubleBuffer); + if (lPtr->typeahead) + wfree(lPtr->typeahead); + WMRemoveNotificationObserver(lPtr); wfree(lPtr); diff --git a/src/dialog.c b/src/dialog.c index 98e0b515..82c20965 100644 --- a/src/dialog.c +++ b/src/dialog.c @@ -1083,6 +1083,7 @@ Bool wIconChooserDialog(WScreen *scr, char **file, const char *instance, const c WMMapWidget(panel->win); wWindowMap(wwin); + wSetFocusTo(scr, wwin); while (!panel->done) { XEvent event; -- 2.43.0
0001-WINGs-improve-wlist-widget.patch