menu: implement menu accelerators

Menu accelerators are one-letter mnemonics to quickly select/exec
items from the current menu. For each menu item, the accelerator is
defined as the first character of the item label, converted to
lowercase. A different accelerator can be explicitly defined in
menu.xml with the special '_' character before the target letter.

- Add a field `accelerator` to the `menuitem` struct
- Implement `menu_item_select_by_accelerator()`

Example:
The accelerator for an item with the label "e_macs" is 'm'.
This commit is contained in:
Alex Chernika 2026-04-10 14:52:24 +02:00 committed by Johan Malm
parent 07a0a4e59b
commit 3632c6703a
4 changed files with 190 additions and 8 deletions

View file

@ -148,6 +148,21 @@ obmenu-generator with the menu generator of your choice):
</openbox_menu>
```
# ACCELERATORS / MNEMONICS
Menu accelerators are one-letter mnemonics to quickly select/exec items from
the current menu. For each menu item, the accelerator is defined as the first
character of the item label, converted to lowercase. A different accelerator
can be explicitly defined in menu.xml with the special '\_' character before the
target letter. Accelerators can be any unicode character and are not limited to
ASCII. A usual underscore can be shown by duplicating it.
If the menu only contains a single instance of the pressed accelerator the item
will be executed directly. Otherwise, all matching items are cycled through.
Example:
The accelerator for an item with the label "e_Macs" is 'm'.
# LOCALISATION
Available localisation for the default "client-menu" is only shown if no

View file

@ -23,9 +23,11 @@ struct menuitem {
char *text;
char *icon_name;
const char *arrow;
uint32_t accelerator;
struct menu *parent;
struct menu *submenu;
bool selectable;
bool use_markup;
enum menuitem_type type;
int native_width;
struct wlr_scene_tree *tree;
@ -66,6 +68,19 @@ struct menu {
/* For keyboard support */
void menu_item_select_next(void);
void menu_item_select_previous(void);
/**
* menu_item_select_by_accelerator - selects the next menu item with
* a matching accelerator, starting after the current selection
*
* @accelerator a shortcut to quickly select/open an item, defined in menu.xml
* with an underscore in the item label before the target letter.
*
* Return: a boolean value that represents whether the newly selected item
* needs to be executed.
*/
bool menu_item_select_by_accelerator(uint32_t accelerator);
void menu_submenu_enter(void);
void menu_submenu_leave(void);
bool menu_call_selected_actions(void);
@ -100,7 +115,7 @@ void menu_open_root(struct menu *menu, int x, int y);
void menu_process_cursor_motion(struct wlr_scene_node *node);
/**
* menu_close_root- close root menu
* menu_close_root - close root menu
*
* This function will close server.menu_current and set it to NULL.
* Asserts that server.input_mode is set to LAB_INPUT_STATE_MENU.

View file

@ -444,15 +444,24 @@ handle_menu_keys(struct keysyms *syms)
break;
case XKB_KEY_Return:
case XKB_KEY_KP_Enter:
menu_call_selected_actions();
if (!menu_call_selected_actions()) {
menu_submenu_enter();
};
break;
case XKB_KEY_Escape:
menu_close_root();
cursor_update_focus();
break;
default:
continue;
}
default: {
uint32_t accelerator = xkb_keysym_to_utf32(syms->syms[i]);
if (accelerator == 0) {
continue;
}
if (menu_item_select_by_accelerator(accelerator)) {
menu_call_selected_actions();
}
break;
}}
break;
}
}

View file

@ -2,12 +2,16 @@
#define _POSIX_C_SOURCE 200809L
#include "menu/menu.h"
#include <assert.h>
#include <ctype.h>
#include <libxml/parser.h>
#include <locale.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <uchar.h>
#include <unistd.h>
#include <wctype.h>
#include <wlr/types/wlr_scene.h>
#include <wlr/types/wlr_xdg_shell.h>
#include <wlr/util/log.h>
@ -129,6 +133,91 @@ validate(void)
}
}
static uint32_t
get_unicode_char(const char *first_byte, size_t *out_bytes)
{
if (!first_byte || first_byte[0] == '\0') {
*out_bytes = 0;
return 0;
}
/* Temporarily set locale to UTF-8 */
locale_t utf8_locale = newlocale(LC_CTYPE_MASK, "C.UTF-8", (locale_t)0);
locale_t old_locale = (locale_t)0;
if (utf8_locale != (locale_t)0) {
old_locale = uselocale(utf8_locale);
}
uint32_t result = 0;
char32_t codepoint = 0;
mbstate_t state = {0};
size_t bytes = mbrtoc32(&codepoint, first_byte, 4, &state);
if (bytes > 0 && bytes <= 4) {
*out_bytes = bytes;
result = (uint32_t)towlower((wint_t)codepoint);
} else {
*out_bytes = 1;
result = (uint32_t)(unsigned char)first_byte[0];
}
/* Restore previous locale */
if (utf8_locale != (locale_t)0) {
uselocale(old_locale);
freelocale(utf8_locale);
}
return result;
}
static void
item_parse_accelerator(struct menuitem *item, const char *text)
{
const char *accel_ptr = NULL;
char *underscore = strchr(text, '_');
while (underscore) {
if (underscore[1] == '_') {
/* Ignore escaped underscores */
underscore = strchr(underscore + 2, '_');
} else if (underscore[1] != '\0') {
/* Found a valid accelerator */
accel_ptr = underscore + 1;
break;
} else {
/* Ignore empty accelertor */
break;
}
}
size_t bytes = 0;
if (!accel_ptr) {
item->text = xstrdup(text);
item->accelerator = get_unicode_char(text, &bytes);
} else {
item->use_markup = true;
item->accelerator = get_unicode_char(accel_ptr, &bytes);
item->text = strdup_printf("%.*s<u>%.*s</u>%s",
/* Prefix length + prefix */
(int)(accel_ptr - 1 - text), text,
/* Accelerator (utf-8 byte) length + accelerator */
(int)bytes, accel_ptr,
/* Remainder */
accel_ptr + bytes);
}
/* Remove undescores used for escaping */
char *src = item->text;
char *dst = item->text;
while (*src) {
if (*src == '_' && *(src + 1) == '_') {
*dst++ = '_';
src += 2;
} else {
*dst++ = *src++;
}
}
*dst = '\0';
}
static struct menuitem *
item_create(struct menu *menu, const char *text, const char *icon_name, bool show_arrow)
{
@ -140,8 +229,8 @@ item_create(struct menu *menu, const char *text, const char *icon_name, bool sho
menuitem->parent = menu;
menuitem->selectable = true;
menuitem->type = LAB_MENU_ITEM;
menuitem->text = xstrdup(text);
menuitem->arrow = show_arrow ? "" : NULL;
item_parse_accelerator(menuitem, text);
#if HAVE_LIBSFDO
if (rc.menu_show_icons && !string_null_or_empty(icon_name)) {
@ -212,8 +301,8 @@ item_create_scene_for_state(struct menuitem *item, float *text_color,
/* Create label */
struct scaled_font_buffer *label_buffer = scaled_font_buffer_create(tree);
assert(label_buffer);
scaled_font_buffer_update(label_buffer, item->text, label_max_width,
&rc.font_menuitem, text_color, bg_color);
scaled_font_buffer_update_markup(label_buffer, item->text, label_max_width,
&rc.font_menuitem, text_color, bg_color, item->use_markup);
/* Vertically center and left-align label */
int x = theme->menu_items_padding_x + icon_width;
int y = (theme->menu_item_height - label_buffer->height) / 2;
@ -1460,6 +1549,60 @@ menu_item_select_previous(void)
menu_item_select(/* forward */ false);
}
bool
menu_item_select_by_accelerator(uint32_t accelerator)
{
struct menu *menu = get_selection_leaf();
if (!menu || wl_list_empty(&menu->menuitems)) {
return false;
}
bool needs_exec = false;
bool matched = false;
struct menuitem *selection = menu->selection.item;
struct wl_list *start = selection ? &selection->link : &menu->menuitems;
struct wl_list *current = start;
struct menuitem *item = NULL;
struct menuitem *next_selection = NULL;
do {
current = current->next;
if (current == &menu->menuitems) {
/* Allow wrap around */
continue;
}
item = wl_container_of(current, item, link);
if (item->accelerator == accelerator) {
if (!matched) {
/* Found first match */
next_selection = item;
needs_exec = true;
matched = true;
} else {
/*
* Found another match,
* cycle selection instead of executing
*/
needs_exec = false;
break;
}
}
} while (current != start);
if (!next_selection) {
return false;
}
menu_process_item_selection(next_selection);
if (needs_exec && next_selection->submenu) {
/* Since we can't execute a submenu, enter it. */
needs_exec = false;
menu_submenu_enter();
}
return needs_exec;
}
bool
menu_call_selected_actions(void)
{