[PATCH] WINGs: improve wlist widget

5 views
Skip to first unread message

david.m...@gmail.com

unread,
Feb 16, 2026, 11:04:55 AM (8 days ago) Feb 16
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
Reply all
Reply to author
Forward
0 new messages