menu: support pipe menus

See labwc-menu(5) for usage.

Co-authored-by: @Consolatis
This commit is contained in:
Johan Malm 2023-10-31 21:36:45 +00:00 committed by Johan Malm
parent e5488fefcb
commit f3b68b8fb5
3 changed files with 506 additions and 65 deletions

View file

@ -33,16 +33,21 @@ The menu file must be entirely enclosed within <openbox_menu> and
...some content...
</menu>
<!-- Pipemenu -->
<menu id="" label="" execute="COMMAND"/>
</menu>
```
*menu.id*
Each menu must be given an id, which is a unique identifier of the menu.
This id is used to refer to the menu in a ShowMenu action.
Default identifiers are "client-menu" for the titlebar context menu and
"root-menu" for the root window context menu.
Available localisation for the default "client-menu" is
only shown if no "client-menu" is present in menu.xml.
*menu.id* (when at toplevel)
Define a menu tree. Each menu must be given an id, which is a unique
identifier of the menu. This id is used to refer to the menu in a
ShowMenu action. Default identifiers are
- "root-menu" for the root window context menu
- "client-menu" for a window's titlebar context menu
*menu.id* (when nested under other *<menu>* element)
Link to a submenu defined elsewhere (by a *<menu id="">* at toplevel)
*menu.label*
The title of the menu, shown in its parent. A label must be given when
@ -58,6 +63,48 @@ The menu file must be entirely enclosed within <openbox_menu> and
*menu.separator*
Horizontal line.
*menu.execute*
Command to execute for pipe menu. See details below.
# PIPE MENUS
Pipe menus are menus generated dynamically based on output of scripts or
binaries. They are so-called because the output of the executable is piped to
the labwc menu.
For any *<menu id="" label="" execute="COMMAND"/>* entry in menu.xml, the
COMMAND will be executed the first time the item is selected (for example by
cursor or keyboard input). The XML output of the command will be parsed and
shown as a submenu. The content of pipemenus is cached until the whole menu
(not just the pipemenu) is closed.
The content of the output must be entirely enclosed within *<openbox_pipe_menu>*
tags. Inside these, menus are specified in the same way as static (normal)
menus, for example:
```
<openbox_pipe_menu>
<item label="Terminal">
<action name="Execute" command="xterm"/>
</item>
</openbox_pipe_menu>
```
Inline submenus and nested pipemenus are supported.
Note that it is the responsibility of the pipemenu executable to ensure that
ID attributes are unique. Duplicates are ignored.
When writing pipe menu scripts, make sure to escape XML special characters such
as "&" ("&amp;"), "<" ("&lt;"), and ">" ("&gt;").
# LOCALISATION
Available localisation for the default "client-menu" is only shown if no
"client-menu" is present in menu.xml. Any menu definition in menu.xml is
interpreted as a user-override.
# SEE ALSO
labwc(1), labwc-action(5), labwc-config(5), labwc-theme(5)

View file

@ -29,6 +29,8 @@ struct menu_scene {
struct menuitem {
struct wl_list actions;
char *execute;
char *id; /* needed for pipemenus */
struct menu *parent;
struct menu *submenu;
bool selectable;
@ -46,6 +48,7 @@ struct menu {
char *label;
int item_height;
struct menu *parent;
struct {
int width;
int height;
@ -57,6 +60,8 @@ struct menu {
struct menuitem *item;
} selection;
struct wlr_scene_tree *scene_tree;
bool is_pipemenu;
enum menu_align align;
/* Used to match a window-menu to the view that triggered it. */
struct view *triggered_by_view; /* may be NULL */

View file

@ -3,6 +3,7 @@
#include <assert.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -19,12 +20,16 @@
#include "common/nodename.h"
#include "common/scaled_font_buffer.h"
#include "common/scene-helpers.h"
#include "common/spawn.h"
#include "common/string-helpers.h"
#include "labwc.h"
#include "menu/menu.h"
#include "node.h"
#include "theme.h"
#define PIPEMENU_MAX_BUF_SIZE 1048576 /* 1 MiB */
#define PIPEMENU_TIMEOUT_IN_MS 4000 /* 4 seconds */
/* state-machine variables for processing <item></item> */
static bool in_item;
static struct menuitem *current_item;
@ -33,11 +38,30 @@ static struct action *current_item_action;
static int menu_level;
static struct menu *current_menu;
static bool waiting_for_pipe_menu;
static struct menuitem *selected_item;
/* TODO: split this whole file into parser.c and actions.c*/
static bool
is_unique_id(struct server *server, const char *id)
{
struct menu *menu;
wl_list_for_each(menu, &server->menus, link) {
if (!strcmp(menu->id, id)) {
return false;
}
}
return true;
}
static struct menu *
menu_create(struct server *server, const char *id, const char *label)
{
if (!is_unique_id(server, id)) {
wlr_log(WLR_ERROR, "menu id %s already exists", id);
}
struct menu *menu = znew(*menu);
wl_list_append(&server->menus, &menu->link);
@ -46,6 +70,7 @@ menu_create(struct server *server, const char *id, const char *label)
menu->label = xstrdup(label ? label : id);
menu->parent = current_menu;
menu->server = server;
menu->is_pipemenu = waiting_for_pipe_menu;
menu->size.width = server->theme->menu_min_width;
/* menu->size.height will be kept up to date by adding items */
menu->scene_tree = wlr_scene_tree_create(server->menu_tree);
@ -101,7 +126,7 @@ menu_update_width(struct menu *menu)
wlr_scene_rect_set_size(
wlr_scene_rect_from_node(item->selected.background),
menu->size.width, item->height);
if (item->native_width > max_width || item->submenu) {
if (item->native_width > max_width || item->submenu || item->execute) {
scaled_font_buffer_set_max_width(item->normal.buffer,
max_width);
scaled_font_buffer_set_max_width(item->selected.buffer,
@ -148,6 +173,9 @@ validate(struct server *server)
static struct menuitem *
item_create(struct menu *menu, const char *text, bool show_arrow)
{
assert(menu);
assert(text);
struct menuitem *menuitem = znew(*menuitem);
menuitem->parent = menu;
menuitem->selectable = true;
@ -283,7 +311,13 @@ separator_create(struct menu *menu, const char *label)
static void
fill_item(char *nodename, char *content)
{
/*
* Nodenames for most menu-items end with '.item.menu' but top-level
* pipemenu items do not have the associated <menu> element so merely
* end with a '.item'
*/
string_truncate_at_pattern(nodename, ".item.menu");
string_truncate_at_pattern(nodename, ".item");
/* <item label=""> defines the start of a new item */
if (!strcmp(nodename, "label")) {
@ -317,6 +351,8 @@ item_destroy(struct menuitem *item)
wl_list_remove(&item->link);
action_list_free(&item->actions);
wlr_scene_node_destroy(&item->tree->node);
free(item->execute);
free(item->id);
free(item);
}
@ -336,14 +372,23 @@ item_destroy(struct menuitem *item)
* for backward compatibility with old openbox-menu generators. It has the same
* function and <command>
*
* The match_glob() wildcard allows for nested menus giving nodenames with
* ...menu.menu... or ...menu.menu.menu... and so on.
* The following nodenames support CDATA.
* - command.action.item.*menu.openbox_menu
* - execute.action.item.*menu.openbox_menu
* - command.action.item.openbox_pipe_menu
* - execute.action.item.openbox_pipe_menu
* - command.action.item.*menu.openbox_pipe_menu
* - execute.action.item.*menu.openbox_pipe_menu
*
* The *menu allows nested menus with nodenames such as ...menu.menu... or
* ...menu.menu.menu... and so on. We could use match_glob() for all of the
* above but it seems simpler to just check the first three fields.
*/
static bool
nodename_supports_cdata(char *nodename)
{
return match_glob("command.action.item.*menu.openbox_menu", nodename)
|| match_glob("execute.action.item.*menu.openbox_menu", nodename);
return !strncmp("command.action.", nodename, 15)
|| !strncmp("execute.action.", nodename, 15);
}
static void
@ -360,6 +405,7 @@ entry(xmlNode *node, char *nodename, char *content)
return;
}
string_truncate_at_pattern(nodename, ".openbox_menu");
string_truncate_at_pattern(nodename, ".openbox_pipe_menu");
if (getenv("LABWC_DEBUG_MENU_NODENAMES")) {
printf("%s: %s\n", nodename, content ? content : (char *)cdata);
}
@ -407,10 +453,49 @@ nr_parents(xmlNode *n)
return i;
}
/*
* Return true for the highest level static menu definitions in the format
* below. We use the fact that the id-attribute has two nodal parents (<menu>
* and <openbox_menu>) as the test here.
*
* <openbox_menu>
* <menu id="">
* ...
* </menu>
* </openbox_menu>
*
* Return false for any other <menu id=""> element which could be either:
*
* (a) one found in a pipemenu; or
* (b) one that links to a submenu as follows (but is a child to another
* <menu> element.
*
* <menu id="root-menu">
* <!-- this links to a sub menu -->
* <menu id="submenu-defined-elsewhere"/>
* </menu>
*/
static bool
is_toplevel_static_menu_definition(xmlNode *n, char *id)
{
/*
* Catch <menu id=""> elements in pipemenus
*
* For pipemenus we cannot just rely on nr_parents() because they have
* their own hierarchy, so we just use the fact that a pipemenu cannot
* be the root-menu.
*/
if (menu_level) {
return false;
}
return id && nr_parents(n) == 2;
}
/*
* <menu> elements have three different roles:
* * Definition of (sub)menu - has ID, LABEL and CONTENT
* * Menuitem of pipemenu type - has EXECUTE and LABEL
* * Menuitem of pipemenu type - has ID, LABEL and EXECUTE
* * Menuitem of submenu type - has ID only
*/
static void
@ -420,18 +505,21 @@ handle_menu_element(xmlNode *n, struct server *server)
char *execute = (char *)xmlGetProp(n, (const xmlChar *)"execute");
char *id = (char *)xmlGetProp(n, (const xmlChar *)"id");
if (execute) {
wlr_log(WLR_ERROR, "we do not support pipemenus");
} else if ((label && id) || (id && nr_parents(n) == 2)) {
if (execute && label && id) {
wlr_log(WLR_DEBUG, "pipemenu '%s:%s:%s'", id, label, execute);
current_item = item_create(current_menu, label, /* arrow */ true);
current_item_action = NULL;
current_item->execute = xstrdup(execute);
current_item->id = xstrdup(id);
} else if ((label && id) || is_toplevel_static_menu_definition(n, id)) {
/*
* (label && id) refers to <menu id="" label=""> which is an
* inline menu definition.
*
* (id && nr_parents(n) == 2) refers to:
* is_toplevel_static_menu_definition() catches:
* <openbox_menu>
* <menu id="">
* </menu>
* </openbox>
* <menu id=""></menu>
* </openbox_menu>
*
* which is the highest level a menu can be defined at.
*
@ -463,9 +551,20 @@ handle_menu_element(xmlNode *n, struct server *server)
--menu_level;
} else if (id) {
/*
* <menu id=""> creates an entry which points to a menu
* defined elsewhere
* <menu id=""> (when inside another <menu> element) creates an
* entry which points to a menu defined elsewhere.
*
* This is only supported in static menus. Pipemenus need to use
* nested (inline) menu definitions, otherwise we could have a
* pipemenu opening the "root-menu" or similar.
*/
if (current_menu && current_menu->is_pipemenu) {
wlr_log(WLR_ERROR,
"cannot link to static menu from pipemenu");
goto error;
}
struct menu *menu = menu_get_by_id(server, id);
if (menu) {
current_item = item_create(current_menu, menu->label, true);
@ -476,6 +575,7 @@ handle_menu_element(xmlNode *n, struct server *server)
wlr_log(WLR_ERROR, "no menu with id '%s'", id);
}
}
error:
free(label);
free(execute);
free(id);
@ -515,13 +615,27 @@ xml_tree_walk(xmlNode *node, struct server *server)
}
}
static bool
parse_buf(struct server *server, struct buf *buf)
{
xmlDoc *d = xmlParseMemory(buf->buf, buf->len);
if (!d) {
wlr_log(WLR_ERROR, "xmlParseMemory()");
return false;
}
xml_tree_walk(xmlDocGetRootElement(d), server);
xmlFreeDoc(d);
xmlCleanupParser();
return true;
}
/*
* @stream can come from either of the following:
* - fopen() in the case of reading a file such as menu.xml
* - popen() when processing pipemenus
*/
static void
parse(struct server *server, FILE *stream)
parse_stream(struct server *server, FILE *stream)
{
char *line = NULL;
size_t len = 0;
@ -536,15 +650,7 @@ parse(struct server *server, FILE *stream)
buf_add(&b, line);
}
free(line);
xmlDoc *d = xmlParseMemory(b.buf, b.len);
if (!d) {
wlr_log(WLR_ERROR, "xmlParseMemory()");
goto err;
}
xml_tree_walk(xmlDocGetRootElement(d), server);
xmlFreeDoc(d);
xmlCleanupParser();
err:
parse_buf(server, &b);
free(b.buf);
}
@ -565,7 +671,7 @@ parse_xml(const char *filename, struct server *server)
return;
}
wlr_log(WLR_INFO, "read menu file %s", path->string);
parse(server, stream);
parse_stream(server, stream);
fclose(stream);
if (!should_merge_config) {
break;
@ -662,8 +768,9 @@ menu_configure(struct menu *menu, int lx, int ly, enum menu_align align)
}
wlr_scene_node_set_position(&menu->scene_tree->node, lx, ly);
int rel_y;
int new_lx, new_ly;
/* Needed for pipemenus to inherit alignment */
menu->align = align;
struct menuitem *item;
wl_list_for_each(item, &menu->menuitems, link) {
if (!item->submenu) {
@ -787,16 +894,41 @@ menu_init(struct server *server)
validate(server);
}
void
menu_finish(struct server *server)
static void
nullify_item_pointing_to_this_menu(struct menu *menu)
{
struct menu *menu, *tmp_menu;
wl_list_for_each_safe(menu, tmp_menu, &server->menus, link) {
struct menu *iter;
wl_list_for_each(iter, &menu->server->menus, link) {
struct menuitem *item;
wl_list_for_each(item, &iter->menuitems, link) {
if (item->submenu == menu) {
item->submenu = NULL;
/*
* Let's not return early here in case we have
* multiple items pointing to the same menu.
*/
}
}
/* This is important for pipe-menus */
if (iter->parent == menu) {
iter->parent = NULL;
}
}
}
static void
menu_free(struct menu *menu)
{
/* Keep items clean on pipemenu destruction */
nullify_item_pointing_to_this_menu(menu);
struct menuitem *item, *next;
wl_list_for_each_safe(item, next, &menu->menuitems, link) {
item_destroy(item);
}
/**
/*
* Destroying the root node will destroy everything,
* including node descriptors and scaled_font_buffers.
*/
@ -804,6 +936,32 @@ menu_finish(struct server *server)
wl_list_remove(&menu->link);
zfree(menu);
}
/**
* menu_free_from - free menu list starting from current point
* @from: point to free from (if NULL, all menus are freed)
*/
static void
menu_free_from(struct server *server, struct menu *from)
{
bool destroying = !from;
struct menu *menu, *tmp_menu;
wl_list_for_each_safe(menu, tmp_menu, &server->menus, link) {
if (menu == from) {
destroying = true;
}
if (!destroying) {
continue;
}
menu_free(menu);
}
}
void
menu_finish(struct server *server)
{
menu_free_from(server, NULL);
}
/* Sets selection (or clears selection if passing NULL) */
@ -839,6 +997,40 @@ close_all_submenus(struct menu *menu)
menu->selection.menu = NULL;
}
/*
* We only destroy pipemenus when closing the entire menu-tree so that pipemenu
* are cached (for as long as the menu is open). This drastically improves the
* felt performance when interacting with multiple pipe menus where a single
* item may be selected multiple times.
*/
static void
destroy_pipemenus(struct server *server)
{
wlr_log(WLR_DEBUG, "number of menus before close=%d",
wl_list_length(&server->menus));
struct menu *iter, *tmp;
wl_list_for_each_safe(iter, tmp, &server->menus, link) {
if (iter->is_pipemenu) {
menu_free(iter);
}
}
wlr_log(WLR_DEBUG, "number of menus after close=%d",
wl_list_length(&server->menus));
}
static void
_close(struct menu *menu)
{
wlr_scene_node_set_enabled(&menu->scene_tree->node, false);
menu_set_selection(menu, NULL);
if (menu->selection.menu) {
_close(menu->selection.menu);
menu->selection.menu = NULL;
}
}
static void
menu_close(struct menu *menu)
{
@ -846,12 +1038,7 @@ menu_close(struct menu *menu)
wlr_log(WLR_ERROR, "Trying to close non exiting menu");
return;
}
wlr_scene_node_set_enabled(&menu->scene_tree->node, false);
menu_set_selection(menu, NULL);
if (menu->selection.menu) {
menu_close(menu->selection.menu);
menu->selection.menu = NULL;
}
_close(menu);
}
void
@ -860,6 +1047,7 @@ menu_open_root(struct menu *menu, int x, int y)
assert(menu);
if (menu->server->menu_current) {
menu_close(menu->server->menu_current);
destroy_pipemenus(menu->server);
}
close_all_submenus(menu);
menu_set_selection(menu, NULL);
@ -867,6 +1055,191 @@ menu_open_root(struct menu *menu, int x, int y)
wlr_scene_node_set_enabled(&menu->scene_tree->node, true);
menu->server->menu_current = menu;
menu->server->input_mode = LAB_INPUT_STATE_MENU;
selected_item = NULL;
}
struct pipe_context {
struct server *server;
struct menuitem *item;
struct buf buf;
struct wl_event_source *event_read;
struct wl_event_source *event_timeout;
pid_t pid;
int pipe_fd;
};
static void
create_pipe_menu(struct pipe_context *ctx)
{
struct menu *pipe_parent = ctx->item->parent;
if (!pipe_parent) {
wlr_log(WLR_ERROR, "[pipemenu %ld] invalid parent",
(long)ctx->pid);
return;
}
if (!pipe_parent->scene_tree->node.enabled) {
wlr_log(WLR_ERROR, "[pipemenu %ld] parent menu already closed",
(long)ctx->pid);
return;
}
/*
* Pipemenus do not contain a toplevel <menu> element so we have to
* create that first `struct menu`.
*/
struct menu *pipe_menu = menu_create(ctx->server, ctx->item->id, /*label*/ NULL);
pipe_menu->is_pipemenu = true;
pipe_menu->triggered_by_view = pipe_parent->triggered_by_view;
pipe_menu->parent = pipe_parent;
menu_level++;
current_menu = pipe_menu;
if (!parse_buf(ctx->server, &ctx->buf)) {
menu_free(pipe_menu);
ctx->item->submenu = NULL;
goto restore_menus;
}
ctx->item->submenu = pipe_menu;
/*
* TODO: refactor validate() and post_processing() to only
* operate from current point onwards
*/
/* Set menu-widths before configuring */
post_processing(ctx->server);
/*
* TODO:
* (1) Combine this with the code in get_submenu_position()
* and/or menu_configure()
* (2) Take into account menu_overlap_{x,y}
*/
enum menu_align align = ctx->item->parent->align;
int x = pipe_parent->scene_tree->node.x;
int y = pipe_parent->scene_tree->node.y + ctx->item->tree->node.y;
if (align & LAB_MENU_OPEN_RIGHT) {
x += pipe_parent->size.width;
}
menu_configure(pipe_menu, x, y, align);
validate(ctx->server);
/* Finally open the new submenu tree */
wlr_scene_node_set_enabled(&pipe_menu->scene_tree->node, true);
pipe_parent->selection.menu = pipe_menu;
restore_menus:
current_menu = pipe_parent;
menu_level--;
}
static void
pipemenu_ctx_destroy(struct pipe_context *ctx)
{
wl_event_source_remove(ctx->event_read);
wl_event_source_remove(ctx->event_timeout);
spawn_piped_close(ctx->pid, ctx->pipe_fd);
free(ctx->buf.buf);
free(ctx);
waiting_for_pipe_menu = false;
}
static int
handle_pipemenu_timeout(void *_ctx)
{
struct pipe_context *ctx = _ctx;
wlr_log(WLR_ERROR, "[pipemenu %ld] timeout reached, killing %s",
(long)ctx->pid, ctx->item->execute);
kill(ctx->pid, SIGTERM);
pipemenu_ctx_destroy(ctx);
return 0;
}
static bool
starts_with_less_than(const char *s)
{
return (s + strspn(s, " \t\r\n"))[0] == '<';
}
static int
handle_pipemenu_readable(int fd, uint32_t mask, void *_ctx)
{
struct pipe_context *ctx = _ctx;
/* two 4k pages + 1 NULL byte */
char data[8193];
ssize_t size;
do {
/* leave space for terminating NULL byte */
size = read(fd, data, sizeof(data) - 1);
} while (size == -1 && errno == EINTR);
if (size == -1) {
wlr_log_errno(WLR_ERROR, "[pipemenu %ld] failed to read data (%s)",
(long)ctx->pid, ctx->item->execute);
goto clean_up;
}
/* Limit pipemenu buffer to 1 MiB for safety */
if (ctx->buf.len + size > PIPEMENU_MAX_BUF_SIZE) {
wlr_log(WLR_ERROR, "[pipemenu %ld] too big (> %d bytes); killing %s",
(long)ctx->pid, PIPEMENU_MAX_BUF_SIZE, ctx->item->execute);
kill(ctx->pid, SIGTERM);
goto clean_up;
}
wlr_log(WLR_DEBUG, "[pipemenu %ld] read %ld bytes of data", (long)ctx->pid, size);
if (size) {
data[size] = '\0';
buf_add(&ctx->buf, data);
return 0;
}
/* Guard against badly formed data such as binary input */
if (!starts_with_less_than(ctx->buf.buf)) {
wlr_log(WLR_ERROR, "expect xml data to start with '<'; abort pipemenu");
goto clean_up;
}
create_pipe_menu(ctx);
clean_up:
pipemenu_ctx_destroy(ctx);
return 0;
}
static void
parse_pipemenu(struct menuitem *item)
{
if (!is_unique_id(item->parent->server, item->id)) {
wlr_log(WLR_ERROR, "duplicate id '%s'; abort pipemenu", item->id);
return;
}
int pipe_fd = 0;
pid_t pid = spawn_piped(item->execute, &pipe_fd);
if (pid <= 0) {
wlr_log(WLR_ERROR, "Failed to spawn pipe menu process %s", item->execute);
return;
}
waiting_for_pipe_menu = true;
struct pipe_context *ctx = znew(*ctx);
ctx->server = item->parent->server;
ctx->item = item;
ctx->pid = pid;
ctx->pipe_fd = pipe_fd;
buf_init(&ctx->buf);
ctx->event_read = wl_event_loop_add_fd(ctx->server->wl_event_loop,
pipe_fd, WL_EVENT_READABLE, handle_pipemenu_readable, ctx);
ctx->event_timeout = wl_event_loop_add_timer(ctx->server->wl_event_loop,
handle_pipemenu_timeout, ctx);
wl_event_source_timer_update(ctx->event_timeout, PIPEMENU_TIMEOUT_IN_MS);
wlr_log(WLR_DEBUG, "[pipemenu %ld] executed: %s", (long)ctx->pid, ctx->item->execute);
}
static void
@ -875,23 +1248,33 @@ menu_process_item_selection(struct menuitem *item)
assert(item);
/* Do not keep selecting the same item */
static struct menuitem *last;
if (item == last) {
if (item == selected_item) {
return;
}
last = item;
if (waiting_for_pipe_menu) {
return;
}
selected_item = item;
if (!item->selectable) {
return;
}
/* We are on an item that has new mouse-focus */
/* We are on an item that has new focus */
menu_set_selection(item->parent, item);
if (item->parent->selection.menu) {
/* Close old submenu tree */
menu_close(item->parent->selection.menu);
}
/* Pipemenu */
if (item->execute && !item->submenu) {
/* pipemenus are generated async */
parse_pipemenu(item);
return;
}
if (item->submenu) {
/* Sync the triggering view */
item->submenu->triggered_by_view = item->parent->triggered_by_view;
@ -901,6 +1284,7 @@ menu_process_item_selection(struct menuitem *item)
wlr_scene_node_set_enabled(
&item->submenu->scene_tree->node, true);
}
item->parent->selection.menu = item->submenu;
}
@ -967,20 +1351,24 @@ menu_execute_item(struct menuitem *item)
* We do that without resetting the input state so src/cursor.c
* can do its own clean up on the following RELEASE event.
*/
menu_close(item->parent->server->menu_current);
item->parent->server->menu_current = NULL;
struct server *server = item->parent->server;
menu_close_root(server);
menu_close(server->menu_current);
server->input_mode = LAB_INPUT_STATE_PASSTHROUGH;
cursor_update_focus(server);
/*
* We call the actions after menu_close_root() so that virtual keyboard
* We call the actions after closing the menu so that virtual keyboard
* input is sent to the focused_surface instead of being absorbed by the
* menu. Consider for example: `wlrctl keyboard type abc`
*
* We cannot call menu_close_root() directly here because it does both
* menu_close() and destroy_pipemenus() which we have to handle
* before/after action_run() respectively.
*/
actions_run(item->parent->triggered_by_view, server, &item->actions, 0);
server->menu_current = NULL;
destroy_pipemenus(server);
return true;
}
@ -1068,6 +1456,7 @@ menu_close_root(struct server *server)
if (server->menu_current) {
menu_close(server->menu_current);
server->menu_current = NULL;
destroy_pipemenus(server);
}
server->input_mode = LAB_INPUT_STATE_PASSTHROUGH;
}