]> git.mdlowis.com Git - proto/labwc.git/commitdiff
src/menu/menu.c: support keyboard driven selection
authorConsolatis <35009135+Consolatis@users.noreply.github.com>
Wed, 30 Aug 2023 09:20:46 +0000 (11:20 +0200)
committerJohan Malm <johanmalm@users.noreply.github.com>
Tue, 5 Sep 2023 18:57:14 +0000 (19:57 +0100)
Fixes: #1058
Requested-by: @stefonarch
include/menu/menu.h
src/keyboard.c
src/menu/menu.c

index 490bcefb46c8bb2278b323a7343bf4b81c39b41a..fd614fe2d7c0a0f7a524525d5c086f0ee927e9f1 100644 (file)
@@ -62,6 +62,13 @@ struct menu {
        struct view *triggered_by_view;  /* may be NULL */
 };
 
+/* For keyboard support */
+void menu_item_select_next(struct server *server);
+void menu_item_select_previous(struct server *server);
+void menu_submenu_enter(struct server *server);
+void menu_submenu_leave(struct server *server);
+bool menu_call_selected_actions(struct server *server);
+
 void menu_init(struct server *server);
 void menu_finish(void);
 
index 8f2d321b19cce9c4e1de504090854fbf7d998edf..7a3190cc87f2c7751dcee7c58d0896a3cd08daa3 100644 (file)
@@ -6,6 +6,7 @@
 #include "idle.h"
 #include "key-state.h"
 #include "labwc.h"
+#include "menu/menu.h"
 #include "regions.h"
 #include "view.h"
 #include "workspaces.h"
@@ -125,6 +126,42 @@ struct keysyms {
        int nr_syms;
 };
 
+static void
+handle_menu_keys(struct server *server, struct keysyms *syms)
+{
+       assert(server->input_mode == LAB_INPUT_STATE_MENU);
+
+       for (int i = 0; i < syms->nr_syms; i++) {
+               switch (syms->syms[i]) {
+               case XKB_KEY_Down:
+                       menu_item_select_next(server);
+                       break;
+               case XKB_KEY_Up:
+                       menu_item_select_previous(server);
+                       break;
+               case XKB_KEY_Right:
+                       menu_submenu_enter(server);
+                       break;
+               case XKB_KEY_Left:
+                       menu_submenu_leave(server);
+                       break;
+               case XKB_KEY_Return:
+                       if (menu_call_selected_actions(server)) {
+                               menu_close_root(server);
+                               cursor_update_focus(server);
+                       }
+                       break;
+               case XKB_KEY_Escape:
+                       menu_close_root(server);
+                       cursor_update_focus(server);
+                       break;
+               default:
+                       continue;
+               }
+               break;
+       }
+}
+
 static bool
 handle_compositor_keybindings(struct keyboard *keyboard,
                struct wlr_keyboard_key_event *event)
@@ -212,6 +249,18 @@ handle_compositor_keybindings(struct keyboard *keyboard,
                }
        }
 
+       if (server->input_mode == LAB_INPUT_STATE_MENU) {
+               /*
+                * Usually, release events are already caught via _press_event_was_bound().
+                * But to be on the safe side we will simply ignore them here as well.
+                */
+               if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
+                       handle_menu_keys(server, &translated);
+               }
+               handled = true;
+               goto out;
+       }
+
        if (server->osd_state.cycle_view) {
                if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
                        for (int i = 0; i < translated.nr_syms; i++) {
index 4b3b6286c06d37232d8698556b7b5a3f0df43702..5c7712f17dbddd18a68b33d915185be4471c7f7c 100644 (file)
@@ -37,6 +37,8 @@ static struct menu *current_menu;
 static struct menu *menus;
 static int nr_menus, alloc_menus;
 
+/* TODO: split this whole file into parser.c and actions.c*/
+
 static struct menu *
 menu_create(struct server *server, const char *id, const char *label)
 {
@@ -841,22 +843,14 @@ menu_open(struct menu *menu, int x, int y)
        menu->server->input_mode = LAB_INPUT_STATE_MENU;
 }
 
-void
-menu_process_cursor_motion(struct wlr_scene_node *node)
+static void
+menu_process_item_selection(struct menuitem *item)
 {
-       assert(node && node->data);
-       struct menuitem *item = node_menuitem_from_node(node);
-
-       /* This function shall only be called for menu nodes */
        assert(item);
 
        if (!item->selectable) {
                return;
        }
-       if (node == &item->selected.tree->node) {
-               /* We are on an already selected item */
-               return;
-       }
 
        /* We are on an item that has new mouse-focus */
        menu_set_selection(item->parent, item);
@@ -868,6 +862,8 @@ menu_process_cursor_motion(struct wlr_scene_node *node)
        if (item->submenu) {
                /* Sync the triggering view */
                item->submenu->triggered_by_view = item->parent->triggered_by_view;
+               /* Ensure the submenu has its parent set correctly */
+               item->submenu->parent = item->parent;
                /* And open the new submenu tree */
                wlr_scene_node_set_enabled(
                        &item->submenu->scene_tree->node, true);
@@ -875,14 +871,61 @@ menu_process_cursor_motion(struct wlr_scene_node *node)
        item->parent->selection.menu = item->submenu;
 }
 
-bool
-menu_call_actions(struct wlr_scene_node *node)
+/* Get the deepest submenu with active item selection or the root menu itself */
+static struct menu *
+get_selection_leaf(struct server *server)
 {
-       assert(node && node->data);
-       struct menuitem *item = node_menuitem_from_node(node);
+       struct menu *menu = server->menu_current;
+       if (!menu) {
+               return NULL;
+       }
 
-       if (item->submenu) {
-               /* We received a click on an item that just opens a submenu */
+       while (menu->selection.menu) {
+               if (!menu->selection.menu->selection.item) {
+                       return menu;
+               }
+               menu = menu->selection.menu;
+       }
+
+       return menu;
+}
+
+/* Selects the next or previous sibling of the currently selected item */
+static void
+menu_item_select(struct server *server, bool forward)
+{
+       struct menu *menu = get_selection_leaf(server);
+       if (!menu) {
+               return;
+       }
+
+       struct menuitem *item = NULL;
+       struct menuitem *selection = menu->selection.item;
+       struct wl_list *start = selection ? &selection->link : &menu->menuitems;
+       struct wl_list *current = start;
+       while (!item || !item->selectable) {
+               current = forward ? current->next : current->prev;
+               if (current == start) {
+                       return;
+               }
+               if (current == &menu->menuitems) {
+                       /* Allow wrap around */
+                       item = NULL;
+                       continue;
+               }
+               item = wl_container_of(current, item, link);
+       }
+
+       menu_process_item_selection(item);
+}
+
+static bool
+menu_execute_item(struct menuitem *item)
+{
+       assert(item);
+
+       if (item->submenu || !item->selectable) {
+               /* We received a click on a separator or item that just opens a submenu */
                return false;
        }
 
@@ -900,6 +943,89 @@ menu_call_actions(struct wlr_scene_node *node)
        return true;
 }
 
+/* Keyboard based selection */
+void
+menu_item_select_next(struct server *server)
+{
+       menu_item_select(server, /* forward */ true);
+}
+
+void
+menu_item_select_previous(struct server *server)
+{
+       menu_item_select(server, /* forward */ false);
+}
+
+bool
+menu_call_selected_actions(struct server *server)
+{
+       struct menu *menu = get_selection_leaf(server);
+       if (!menu || !menu->selection.item) {
+               return false;
+       }
+
+       return menu_execute_item(menu->selection.item);
+}
+
+/* Selects the first item on the submenu attached to the current selection */
+void
+menu_submenu_enter(struct server *server)
+{
+       struct menu *menu = get_selection_leaf(server);
+       if (!menu || !menu->selection.menu) {
+               return;
+       }
+
+       struct wl_list *start = &menu->selection.menu->menuitems;
+       struct wl_list *current = start;
+       struct menuitem *item = NULL;
+       while (!item || !item->selectable) {
+               current = current->next;
+               if (current == start) {
+                       return;
+               }
+               item = wl_container_of(current, item, link);
+       }
+
+       menu_process_item_selection(item);
+}
+
+/* Re-selects the selected item on the parent menu of the current selection */
+void
+menu_submenu_leave(struct server *server)
+{
+       struct menu *menu = get_selection_leaf(server);
+       if (!menu || !menu->parent || !menu->parent->selection.item) {
+               return;
+       }
+
+       menu_process_item_selection(menu->parent->selection.item);
+}
+
+/* Mouse based selection */
+void
+menu_process_cursor_motion(struct wlr_scene_node *node)
+{
+       assert(node && node->data);
+       struct menuitem *item = node_menuitem_from_node(node);
+
+       if (item->selectable && node == &item->selected.tree->node) {
+               /* We are on an already selected item */
+               return;
+       }
+
+       menu_process_item_selection(item);
+}
+
+bool
+menu_call_actions(struct wlr_scene_node *node)
+{
+       assert(node && node->data);
+       struct menuitem *item = node_menuitem_from_node(node);
+
+       return menu_execute_item(item);
+}
+
 void
 menu_close_root(struct server *server)
 {