labwc/src/menu/menu.c

1563 lines
41 KiB
C
Raw Normal View History

2021-09-24 21:45:48 +01:00
// SPDX-License-Identifier: GPL-2.0-only
2020-10-19 22:14:17 +01:00
#define _POSIX_C_SOURCE 200809L
2021-02-17 20:38:16 +00:00
#include <assert.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <signal.h>
2020-10-19 22:14:17 +01:00
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
2021-02-17 20:38:16 +00:00
#include <strings.h>
#include <wayland-server-core.h>
2021-07-22 21:30:17 +01:00
#include <wlr/util/log.h>
#include "action.h"
2021-02-17 20:38:16 +00:00
#include "common/buf.h"
config: support merging multiple config files Add the -m|--merge-config command line option to iterate backwards over XDG Base Dir paths and read config/theme files multiple times. For example if both ~/.config/labwc/rc.xml and /etc/xdg/labwc/rc.xml exist, the latter will be read first and then the former (if --merge-config is enabled). When $XDG_CONFIG_HOME is defined, make it replace (not augment) $HOME/.config. Similarly, make $XDG_CONFIG_DIRS replace /etc/xdg when defined. XDG Base Dir Spec does not specify whether or not an application (or a compositor!) should (a) define that only the file under the most important base directory should be used, or (b) define rules for merging the information from the different files. ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html In the case of labwc there is a use-case for both positions, just to be clear, the default behaviour, described by position (a) above, does NOT change. This change affects the following config/theme files: - rc.xml - menu.xml - autostart - environment - themerc - themerc-override - Theme buttons, for example max.xbm Instead of caching global config/theme directories, create lists of paths (e.g. '/home/foo/.config/labwc/rc.xml', '/etc/xdg/labwc/rc.xml', etc). This creates more common parsing logic and just reversing the direction of iteration and breaks early if config-merge is not wanted. Enable better fallback for themes. For example if a particular theme does not exist in $HOME/.local/share/themes, it will be searched for in ~/.themes/ and so on. This also applies to theme buttons which now fallback on an individual basis. Avoid using stat() in most situations and just go straight to fopen(). Fixes #1406
2024-01-09 22:00:45 +00:00
#include "common/dir.h"
2020-10-21 20:32:08 +01:00
#include "common/font.h"
#include "common/list.h"
#include "common/mem.h"
2021-02-17 20:38:16 +00:00
#include "common/nodename.h"
#include "common/scaled-font-buffer.h"
#include "common/scene-helpers.h"
#include "common/spawn.h"
2021-02-17 20:38:16 +00:00
#include "common/string-helpers.h"
2020-10-19 22:14:17 +01:00
#include "labwc.h"
#include "menu/menu.h"
2022-03-03 04:33:33 +01:00
#include "node.h"
#include "theme.h"
2020-10-19 22:14:17 +01:00
#define PIPEMENU_MAX_BUF_SIZE 1048576 /* 1 MiB */
#define PIPEMENU_TIMEOUT_IN_MS 4000 /* 4 seconds */
2021-02-17 20:38:16 +00:00
/* state-machine variables for processing <item></item> */
2021-09-24 22:14:04 +01:00
static bool in_item;
2021-02-17 20:38:16 +00:00
static struct menuitem *current_item;
static struct action *current_item_action;
2021-02-17 20:38:16 +00:00
static int menu_level;
static struct menu *current_menu;
static bool waiting_for_pipe_menu;
static struct menuitem *selected_item;
struct menu_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;
};
/* 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);
wl_list_init(&menu->menuitems);
menu->id = xstrdup(id);
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;
2022-02-19 02:05:38 +01:00
/* menu->size.height will be kept up to date by adding items */
menu->scene_tree = wlr_scene_tree_create(server->menu_tree);
2022-02-19 02:05:38 +01:00
wlr_scene_node_set_enabled(&menu->scene_tree->node, false);
return menu;
}
2022-02-19 02:05:38 +01:00
struct menu *
menu_get_by_id(struct server *server, const char *id)
{
2022-02-19 02:05:38 +01:00
if (!id) {
return NULL;
}
struct menu *menu;
wl_list_for_each(menu, &server->menus, link) {
if (!strcmp(menu->id, id)) {
return menu;
}
}
return NULL;
}
2020-10-19 22:14:17 +01:00
static void
menu_update_width(struct menu *menu)
{
struct menuitem *item;
struct theme *theme = menu->server->theme;
int max_width = theme->menu_min_width;
/* Get widest menu item, clamped by menu_max_width */
wl_list_for_each(item, &menu->menuitems, link) {
if (item->native_width > max_width) {
max_width = item->native_width < theme->menu_max_width
? item->native_width : theme->menu_max_width;
}
}
menu->size.width = max_width + 2 * theme->menu_item_padding_x;
/*
* TODO: This function is getting a bit unwieldy. Consider calculating
* the menu-window width up-front to avoid this post_processing() and
* second-bite-of-the-cherry stuff
*/
/* Update all items for the new size */
wl_list_for_each(item, &menu->menuitems, link) {
wlr_scene_rect_set_size(
2023-12-03 16:36:20 +08:00
wlr_scene_rect_from_node(item->normal.background),
menu->size.width, item->height);
/*
* Separator lines are special because they change width with
* the menu.
*/
if (item->type == LAB_MENU_SEPARATOR_LINE) {
int width = menu->size.width
- 2 * theme->menu_separator_padding_width
- 2 * theme->menu_item_padding_x;
wlr_scene_rect_set_size(
2023-12-03 16:36:20 +08:00
wlr_scene_rect_from_node(item->normal.text),
width, theme->menu_separator_line_thickness);
}
if (item->selectable) {
/* Only selectable items have item->selected.background */
wlr_scene_rect_set_size(
2023-12-03 16:36:20 +08:00
wlr_scene_rect_from_node(item->selected.background),
menu->size.width, item->height);
}
if (item->native_width > max_width || item->submenu || item->execute) {
scaled_font_buffer_set_max_width(item->normal.buffer,
max_width);
if (item->selectable) {
scaled_font_buffer_set_max_width(item->selected.buffer,
max_width);
}
}
}
}
static void
post_processing(struct server *server)
{
struct menu *menu;
wl_list_for_each(menu, &server->menus, link) {
menu_update_width(menu);
}
}
static void
validate_menu(struct menu *menu)
{
struct menuitem *item;
struct action *action, *action_tmp;
wl_list_for_each(item, &menu->menuitems, link) {
wl_list_for_each_safe(action, action_tmp, &item->actions, link) {
if (!action_is_valid(action)) {
wl_list_remove(&action->link);
action_free(action);
wlr_log(WLR_ERROR, "Removed invalid menu action");
}
}
}
}
static void
validate(struct server *server)
{
struct menu *menu;
wl_list_for_each(menu, &server->menus, link) {
validate_menu(menu);
}
}
2021-07-01 19:21:09 +01:00
static struct menuitem *
2022-08-02 22:00:24 +01:00
item_create(struct menu *menu, const char *text, bool show_arrow)
2020-10-19 22:14:17 +01:00
{
assert(menu);
assert(text);
2022-09-18 15:22:26 -04:00
struct menuitem *menuitem = znew(*menuitem);
2022-03-03 04:33:33 +01:00
menuitem->parent = menu;
menuitem->selectable = true;
menuitem->type = LAB_MENU_ITEM;
struct server *server = menu->server;
2021-02-21 21:54:40 +00:00
struct theme *theme = server->theme;
const char *arrow = show_arrow ? "" : NULL;
/* TODO: Consider setting this somewhere else */
2022-02-19 02:05:38 +01:00
if (!menu->item_height) {
menu->item_height = font_height(&rc.font_menuitem)
+ 2 * theme->menu_item_padding_y;
2022-02-19 02:05:38 +01:00
}
menuitem->height = menu->item_height;
2022-02-19 02:05:38 +01:00
int x, y;
menuitem->native_width = font_width(&rc.font_menuitem, text);
if (arrow) {
menuitem->native_width += font_width(&rc.font_menuitem, arrow);
}
2022-02-19 02:05:38 +01:00
2022-06-05 14:42:06 +02:00
/* Menu item root node */
menuitem->tree = wlr_scene_tree_create(menu->scene_tree);
2022-06-05 14:42:06 +02:00
node_descriptor_create(&menuitem->tree->node,
LAB_NODE_DESC_MENUITEM, menuitem);
/* Tree for each state to hold background and text buffer */
menuitem->normal.tree = wlr_scene_tree_create(menuitem->tree);
menuitem->selected.tree = wlr_scene_tree_create(menuitem->tree);
2022-06-05 14:42:06 +02:00
2022-02-19 02:05:38 +01:00
/* Item background nodes */
2022-06-05 14:42:06 +02:00
menuitem->normal.background = &wlr_scene_rect_create(
menuitem->normal.tree,
menu->size.width, menu->item_height,
2022-02-19 02:05:38 +01:00
theme->menu_items_bg_color)->node;
2022-06-05 14:42:06 +02:00
menuitem->selected.background = &wlr_scene_rect_create(
menuitem->selected.tree,
menu->size.width, menu->item_height,
2022-02-19 02:05:38 +01:00
theme->menu_items_active_bg_color)->node;
/* Font nodes */
menuitem->normal.buffer = scaled_font_buffer_create(menuitem->normal.tree);
menuitem->selected.buffer = scaled_font_buffer_create(menuitem->selected.tree);
if (!menuitem->normal.buffer || !menuitem->selected.buffer) {
wlr_log(WLR_ERROR, "Failed to create menu item '%s'", text);
2023-10-09 20:46:23 +01:00
/*
* Destroying the root node will destroy everything,
* including the node descriptor and scaled_font_buffers.
*/
wlr_scene_node_destroy(&menuitem->tree->node);
free(menuitem);
return NULL;
}
menuitem->normal.text = &menuitem->normal.buffer->scene_buffer->node;
menuitem->selected.text = &menuitem->selected.buffer->scene_buffer->node;
/* Font buffers */
scaled_font_buffer_update(menuitem->normal.buffer, text, menuitem->native_width,
&rc.font_menuitem, theme->menu_items_text_color,
theme->menu_items_bg_color, arrow);
scaled_font_buffer_update(menuitem->selected.buffer, text, menuitem->native_width,
&rc.font_menuitem, theme->menu_items_active_text_color,
theme->menu_items_active_bg_color, arrow);
2022-03-03 04:33:33 +01:00
2022-02-19 02:05:38 +01:00
/* Center font nodes */
x = theme->menu_item_padding_x;
y = (menu->item_height - menuitem->normal.buffer->height) / 2;
2022-02-19 02:05:38 +01:00
wlr_scene_node_set_position(menuitem->normal.text, x, y);
y = (menu->item_height - menuitem->selected.buffer->height) / 2;
2022-02-19 02:05:38 +01:00
wlr_scene_node_set_position(menuitem->selected.text, x, y);
/* Position the item in relation to its menu */
wlr_scene_node_set_position(&menuitem->tree->node, 0, menu->size.height);
2022-02-19 02:05:38 +01:00
/* Hide selected state */
2022-06-05 14:42:06 +02:00
wlr_scene_node_set_enabled(&menuitem->selected.tree->node, false);
2022-02-19 02:05:38 +01:00
2022-03-03 04:33:33 +01:00
/* Update menu extents */
menu->size.height += menuitem->height;
wl_list_append(&menu->menuitems, &menuitem->link);
wl_list_init(&menuitem->actions);
2020-10-19 22:14:17 +01:00
return menuitem;
}
static struct menuitem *
separator_create(struct menu *menu, const char *label)
{
2022-09-18 15:22:26 -04:00
struct menuitem *menuitem = znew(*menuitem);
menuitem->parent = menu;
menuitem->selectable = false;
menuitem->type = string_null_or_empty(label) ? LAB_MENU_SEPARATOR_LINE
: LAB_MENU_TITLE;
struct server *server = menu->server;
struct theme *theme = server->theme;
if (menuitem->type == LAB_MENU_TITLE) {
menuitem->height = menu->item_height;
menuitem->native_width = font_width(&rc.font_menuitem, label);
} else if (menuitem->type == LAB_MENU_SEPARATOR_LINE) {
menuitem->height = theme->menu_separator_line_thickness +
2 * theme->menu_separator_padding_height;
}
/* Menu item root node */
menuitem->tree = wlr_scene_tree_create(menu->scene_tree);
node_descriptor_create(&menuitem->tree->node,
LAB_NODE_DESC_MENUITEM, menuitem);
/* Tree to hold background and text/line buffer */
menuitem->normal.tree = wlr_scene_tree_create(menuitem->tree);
/* Item background nodes */
float *bg_color = menuitem->type == LAB_MENU_TITLE
? theme->menu_title_bg_color : theme->menu_items_bg_color;
menuitem->normal.background = &wlr_scene_rect_create(
menuitem->normal.tree,
menu->size.width, menuitem->height, bg_color)->node;
/* Draw separator line or title */
if (menuitem->type == LAB_MENU_TITLE) {
menuitem->normal.buffer = scaled_font_buffer_create(menuitem->normal.tree);
if (!menuitem->normal.buffer) {
wlr_log(WLR_ERROR, "Failed to create menu item '%s'", label);
wlr_scene_node_destroy(&menuitem->tree->node);
free(menuitem);
return NULL;
}
menuitem->normal.text = &menuitem->normal.buffer->scene_buffer->node;
/* Font buffer */
scaled_font_buffer_update(menuitem->normal.buffer, label,
menuitem->native_width, &rc.font_menuitem,
theme->menu_items_text_color, bg_color, /* arrow */ NULL);
/* Center font nodes */
int x, y;
x = theme->menu_item_padding_x;
y = (menu->item_height - menuitem->normal.buffer->height) / 2;
wlr_scene_node_set_position(menuitem->normal.text, x, y);
} else {
int nominal_width = theme->menu_min_width;
menuitem->normal.text = &wlr_scene_rect_create(
menuitem->normal.tree, nominal_width,
theme->menu_separator_line_thickness,
theme->menu_separator_color)->node;
wlr_scene_node_set_position(&menuitem->tree->node, 0, menu->size.height);
/* Vertically center-align separator line */
wlr_scene_node_set_position(menuitem->normal.text,
theme->menu_separator_padding_width
+ theme->menu_item_padding_x,
theme->menu_separator_padding_height);
}
wlr_scene_node_set_position(&menuitem->tree->node, 0, menu->size.height);
menu->size.height += menuitem->height;
wl_list_append(&menu->menuitems, &menuitem->link);
wl_list_init(&menuitem->actions);
return menuitem;
}
/*
* Handle the following:
* <item label="">
* <action name="">
* <command></command>
* </action>
* </item>
*/
static void
fill_item(char *nodename, char *content)
2021-02-17 20:38:16 +00:00
{
/*
* 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'
*/
2021-02-17 20:38:16 +00:00
string_truncate_at_pattern(nodename, ".item.menu");
string_truncate_at_pattern(nodename, ".item");
2021-02-17 20:38:16 +00:00
/* <item label=""> defines the start of a new item */
2021-02-17 20:38:16 +00:00
if (!strcmp(nodename, "label")) {
2022-08-02 22:00:24 +01:00
current_item = item_create(current_menu, content, false);
2022-01-06 03:24:32 +01:00
current_item_action = NULL;
} else if (!current_item) {
wlr_log(WLR_ERROR, "expect <item label=\"\"> element first. "
"nodename: '%s' content: '%s'", nodename, content);
} else if (!strcmp(nodename, "icon")) {
/*
* Do nothing as we don't support menu icons - just avoid
* logging errors if a menu.xml file contains icon="" entries.
*/
} else if (!strcmp(nodename, "name.action")) {
current_item_action = action_create(content);
if (current_item_action) {
wl_list_append(&current_item->actions,
&current_item_action->link);
}
} else if (!current_item_action) {
wlr_log(WLR_ERROR, "expect <action name=\"\"> element first. "
"nodename: '%s' content: '%s'", nodename, content);
} else {
action_arg_from_xml_node(current_item_action, nodename, content);
2021-02-17 20:38:16 +00:00
}
}
static void
item_destroy(struct menuitem *item)
{
if (item->pipe_ctx) {
item->pipe_ctx->item = NULL;
}
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);
}
/*
* We support XML CDATA for <command> in menu.xml in order to provide backward
* compatibility with obmenu-generator. For example:
*
* <menu id="" label="">
* <item label="">
* <action name="Execute">
* <command><![CDATA[xdg-open .]]></command>
* </action>
* </item>
* </menu>
*
* <execute> is an old, deprecated openbox variety of <command>. We support it
* for backward compatibility with old openbox-menu generators. It has the same
* function and <command>
*
* 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 !strncmp("command.action.", nodename, 15)
|| !strncmp("execute.action.", nodename, 15);
}
2021-02-17 20:38:16 +00:00
static void
entry(xmlNode *node, char *nodename, char *content)
2021-02-17 20:38:16 +00:00
{
if (!nodename) {
return;
}
xmlChar *cdata = NULL;
if (!content && nodename_supports_cdata(nodename)) {
cdata = xmlNodeGetContent(node);
}
if (!content && !cdata) {
2021-02-17 20:38:16 +00:00
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);
}
2021-02-17 20:38:16 +00:00
if (in_item) {
fill_item(nodename, content ? content : (char *)cdata);
2021-02-17 20:38:16 +00:00
}
xmlFree(cdata);
2021-02-17 20:38:16 +00:00
}
static void
process_node(xmlNode *node)
2021-02-17 20:38:16 +00:00
{
static char buffer[256];
char *content = (char *)node->content;
2021-02-17 20:38:16 +00:00
if (xmlIsBlankNode(node)) {
return;
}
char *name = nodename(node, buffer, sizeof(buffer));
entry(node, name, content);
2021-02-17 20:38:16 +00:00
}
static void xml_tree_walk(xmlNode *node, struct server *server);
2021-02-17 20:38:16 +00:00
static void
traverse(xmlNode *n, struct server *server)
2021-02-17 20:38:16 +00:00
{
2021-09-24 22:14:04 +01:00
xmlAttr *attr;
process_node(n);
2021-09-24 22:14:04 +01:00
for (attr = n->properties; attr; attr = attr->next) {
xml_tree_walk(attr->children, server);
}
xml_tree_walk(n->children, server);
}
static int
nr_parents(xmlNode *n)
{
assert(n);
int i = 0;
for (xmlNode *node = n->parent; node && i < INT_MAX; ++i) {
node = node->parent;
}
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 ID, LABEL and EXECUTE
* * Menuitem of submenu type - has ID only
*/
static void
handle_menu_element(xmlNode *n, struct server *server)
{
char *label = (char *)xmlGetProp(n, (const xmlChar *)"label");
char *execute = (char *)xmlGetProp(n, (const xmlChar *)"execute");
char *id = (char *)xmlGetProp(n, (const xmlChar *)"id");
if (execute && label && id) {
wlr_log(WLR_DEBUG, "pipemenu '%s:%s:%s'", id, label, execute);
if (!current_menu) {
/*
* We currently do not support pipemenus without a
* parent <item> such as the one the example below:
*
* <?xml version="1.0" encoding="UTF-8"?>
* <openbox_menu>
* <menu id="root-menu" label="foo" execute="bar"/>
* </openbox_menu>
*
* TODO: Consider supporting this
*/
wlr_log(WLR_ERROR,
"pipemenu '%s:%s:%s' has no parent <menu>",
id, label, execute);
goto error;
}
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.
*
* is_toplevel_static_menu_definition() catches:
* <openbox_menu>
* <menu id=""></menu>
* </openbox_menu>
*
* which is the highest level a menu can be defined at.
*
* Openbox spec requires a label="" defined here, but it is
* actually pointless so we handle it with or without the label
2024-03-08 21:59:20 +09:00
* attribute to make it easier for users to define "root-menu"
* and "client-menu".
*/
struct menu **submenu = NULL;
if (menu_level > 0) {
/*
* In a nested (inline) menu definition we need to
* create an item pointing to the new submenu
*/
2022-08-02 22:00:24 +01:00
current_item = item_create(current_menu, label, true);
if (current_item) {
submenu = &current_item->submenu;
} else {
submenu = NULL;
}
}
++menu_level;
current_menu = menu_create(server, id, label);
if (submenu) {
*submenu = current_menu;
}
traverse(n, server);
current_menu = current_menu->parent;
--menu_level;
} else if (id) {
/*
* <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) {
2022-08-02 22:00:24 +01:00
current_item = item_create(current_menu, menu->label, true);
if (current_item) {
current_item->submenu = menu;
}
} else {
wlr_log(WLR_ERROR, "no menu with id '%s'", id);
}
2021-02-17 20:38:16 +00:00
}
error:
2023-10-09 20:46:23 +01:00
free(label);
free(execute);
free(id);
2021-02-17 20:38:16 +00:00
}
/* This can be one of <separator> and <separator label=""> */
static void
handle_separator_element(xmlNode *n)
{
char *label = (char *)xmlGetProp(n, (const xmlChar *)"label");
current_item = separator_create(current_menu, label);
2023-10-09 20:46:23 +01:00
free(label);
}
2021-02-17 20:38:16 +00:00
static void
xml_tree_walk(xmlNode *node, struct server *server)
2021-02-17 20:38:16 +00:00
{
for (xmlNode *n = node; n && n->name; n = n->next) {
if (!strcasecmp((char *)n->name, "comment")) {
continue;
}
if (!strcasecmp((char *)n->name, "menu")) {
handle_menu_element(n, server);
continue;
}
if (!strcasecmp((char *)n->name, "separator")) {
handle_separator_element(n);
continue;
}
2021-02-17 20:38:16 +00:00
if (!strcasecmp((char *)n->name, "item")) {
if (!current_menu) {
wlr_log(WLR_ERROR,
"ignoring <item> without parent <menu>");
continue;
}
2021-02-17 20:38:16 +00:00
in_item = true;
traverse(n, server);
2021-02-17 20:38:16 +00:00
in_item = false;
continue;
}
traverse(n, server);
2021-02-17 20:38:16 +00:00
}
}
static bool
parse_buf(struct server *server, struct buf *buf)
{
xmlDoc *d = xmlParseMemory(buf->data, 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
*/
2021-02-17 20:38:16 +00:00
static void
parse_stream(struct server *server, FILE *stream)
2021-02-17 20:38:16 +00:00
{
char *line = NULL;
size_t len = 0;
struct buf b = BUF_INIT;
2021-02-17 20:38:16 +00:00
while (getline(&line, &len, stream) != -1) {
char *p = strrchr(line, '\n');
2022-01-09 05:48:27 +01:00
if (p) {
2021-02-17 20:38:16 +00:00
*p = '\0';
2022-01-09 05:48:27 +01:00
}
2021-02-17 20:38:16 +00:00
buf_add(&b, line);
}
free(line);
parse_buf(server, &b);
buf_reset(&b);
2021-02-17 20:38:16 +00:00
}
static void
parse_xml(const char *filename, struct server *server)
{
config: support merging multiple config files Add the -m|--merge-config command line option to iterate backwards over XDG Base Dir paths and read config/theme files multiple times. For example if both ~/.config/labwc/rc.xml and /etc/xdg/labwc/rc.xml exist, the latter will be read first and then the former (if --merge-config is enabled). When $XDG_CONFIG_HOME is defined, make it replace (not augment) $HOME/.config. Similarly, make $XDG_CONFIG_DIRS replace /etc/xdg when defined. XDG Base Dir Spec does not specify whether or not an application (or a compositor!) should (a) define that only the file under the most important base directory should be used, or (b) define rules for merging the information from the different files. ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html In the case of labwc there is a use-case for both positions, just to be clear, the default behaviour, described by position (a) above, does NOT change. This change affects the following config/theme files: - rc.xml - menu.xml - autostart - environment - themerc - themerc-override - Theme buttons, for example max.xbm Instead of caching global config/theme directories, create lists of paths (e.g. '/home/foo/.config/labwc/rc.xml', '/etc/xdg/labwc/rc.xml', etc). This creates more common parsing logic and just reversing the direction of iteration and breaks early if config-merge is not wanted. Enable better fallback for themes. For example if a particular theme does not exist in $HOME/.local/share/themes, it will be searched for in ~/.themes/ and so on. This also applies to theme buttons which now fallback on an individual basis. Avoid using stat() in most situations and just go straight to fopen(). Fixes #1406
2024-01-09 22:00:45 +00:00
struct wl_list paths;
paths_config_create(&paths, filename);
bool should_merge_config = rc.merge_config;
struct wl_list *(*iter)(struct wl_list *list);
iter = should_merge_config ? paths_get_prev : paths_get_next;
for (struct wl_list *elm = iter(&paths); elm != &paths; elm = iter(elm)) {
struct path *path = wl_container_of(elm, path, link);
FILE *stream = fopen(path->string, "r");
if (!stream) {
continue;
config: support merging multiple config files Add the -m|--merge-config command line option to iterate backwards over XDG Base Dir paths and read config/theme files multiple times. For example if both ~/.config/labwc/rc.xml and /etc/xdg/labwc/rc.xml exist, the latter will be read first and then the former (if --merge-config is enabled). When $XDG_CONFIG_HOME is defined, make it replace (not augment) $HOME/.config. Similarly, make $XDG_CONFIG_DIRS replace /etc/xdg when defined. XDG Base Dir Spec does not specify whether or not an application (or a compositor!) should (a) define that only the file under the most important base directory should be used, or (b) define rules for merging the information from the different files. ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html In the case of labwc there is a use-case for both positions, just to be clear, the default behaviour, described by position (a) above, does NOT change. This change affects the following config/theme files: - rc.xml - menu.xml - autostart - environment - themerc - themerc-override - Theme buttons, for example max.xbm Instead of caching global config/theme directories, create lists of paths (e.g. '/home/foo/.config/labwc/rc.xml', '/etc/xdg/labwc/rc.xml', etc). This creates more common parsing logic and just reversing the direction of iteration and breaks early if config-merge is not wanted. Enable better fallback for themes. For example if a particular theme does not exist in $HOME/.local/share/themes, it will be searched for in ~/.themes/ and so on. This also applies to theme buttons which now fallback on an individual basis. Avoid using stat() in most situations and just go straight to fopen(). Fixes #1406
2024-01-09 22:00:45 +00:00
}
wlr_log(WLR_INFO, "read menu file %s", path->string);
parse_stream(server, stream);
config: support merging multiple config files Add the -m|--merge-config command line option to iterate backwards over XDG Base Dir paths and read config/theme files multiple times. For example if both ~/.config/labwc/rc.xml and /etc/xdg/labwc/rc.xml exist, the latter will be read first and then the former (if --merge-config is enabled). When $XDG_CONFIG_HOME is defined, make it replace (not augment) $HOME/.config. Similarly, make $XDG_CONFIG_DIRS replace /etc/xdg when defined. XDG Base Dir Spec does not specify whether or not an application (or a compositor!) should (a) define that only the file under the most important base directory should be used, or (b) define rules for merging the information from the different files. ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html In the case of labwc there is a use-case for both positions, just to be clear, the default behaviour, described by position (a) above, does NOT change. This change affects the following config/theme files: - rc.xml - menu.xml - autostart - environment - themerc - themerc-override - Theme buttons, for example max.xbm Instead of caching global config/theme directories, create lists of paths (e.g. '/home/foo/.config/labwc/rc.xml', '/etc/xdg/labwc/rc.xml', etc). This creates more common parsing logic and just reversing the direction of iteration and breaks early if config-merge is not wanted. Enable better fallback for themes. For example if a particular theme does not exist in $HOME/.local/share/themes, it will be searched for in ~/.themes/ and so on. This also applies to theme buttons which now fallback on an individual basis. Avoid using stat() in most situations and just go straight to fopen(). Fixes #1406
2024-01-09 22:00:45 +00:00
fclose(stream);
if (!should_merge_config) {
break;
}
}
config: support merging multiple config files Add the -m|--merge-config command line option to iterate backwards over XDG Base Dir paths and read config/theme files multiple times. For example if both ~/.config/labwc/rc.xml and /etc/xdg/labwc/rc.xml exist, the latter will be read first and then the former (if --merge-config is enabled). When $XDG_CONFIG_HOME is defined, make it replace (not augment) $HOME/.config. Similarly, make $XDG_CONFIG_DIRS replace /etc/xdg when defined. XDG Base Dir Spec does not specify whether or not an application (or a compositor!) should (a) define that only the file under the most important base directory should be used, or (b) define rules for merging the information from the different files. ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html In the case of labwc there is a use-case for both positions, just to be clear, the default behaviour, described by position (a) above, does NOT change. This change affects the following config/theme files: - rc.xml - menu.xml - autostart - environment - themerc - themerc-override - Theme buttons, for example max.xbm Instead of caching global config/theme directories, create lists of paths (e.g. '/home/foo/.config/labwc/rc.xml', '/etc/xdg/labwc/rc.xml', etc). This creates more common parsing logic and just reversing the direction of iteration and breaks early if config-merge is not wanted. Enable better fallback for themes. For example if a particular theme does not exist in $HOME/.local/share/themes, it will be searched for in ~/.themes/ and so on. This also applies to theme buttons which now fallback on an individual basis. Avoid using stat() in most situations and just go straight to fopen(). Fixes #1406
2024-01-09 22:00:45 +00:00
paths_destroy(&paths);
}
2022-02-19 02:05:38 +01:00
static int
menu_get_full_width(struct menu *menu)
{
int width = menu->size.width - menu->server->theme->menu_overlap_x;
int child_width;
int max_child_width = 0;
struct menuitem *item;
wl_list_for_each(item, &menu->menuitems, link) {
2022-02-19 02:05:38 +01:00
if (!item->submenu) {
continue;
}
child_width = menu_get_full_width(item->submenu);
if (child_width > max_child_width) {
max_child_width = child_width;
}
}
return width + max_child_width;
}
/**
* get_submenu_position() - get output layout coordinates of menu window
* @item: the menuitem that triggers the submenu (static or dynamic)
*/
static struct wlr_box
get_submenu_position(struct menuitem *item, enum menu_align align)
{
struct wlr_box pos = { 0 };
struct menu *menu = item->parent;
struct theme *theme = menu->server->theme;
pos.x = menu->scene_tree->node.x;
pos.y = menu->scene_tree->node.y;
if (align & LAB_MENU_OPEN_RIGHT) {
pos.x += menu->size.width - theme->menu_overlap_x;
}
pos.y += item->tree->node.y - theme->menu_overlap_y;
return pos;
}
static void
2022-02-19 02:05:38 +01:00
menu_configure(struct menu *menu, int lx, int ly, enum menu_align align)
2020-10-19 22:14:17 +01:00
{
2021-11-08 17:36:39 +00:00
struct theme *theme = menu->server->theme;
2022-02-19 02:05:38 +01:00
/* Get output local coordinates + output usable area */
double ox = lx;
double oy = ly;
struct wlr_output *wlr_output = wlr_output_layout_output_at(
menu->server->output_layout, lx, ly);
struct output *output = wlr_output ? output_from_wlr_output(
menu->server, wlr_output) : NULL;
if (!output) {
wlr_log(WLR_ERROR,
"Failed to position menu %s (%s) and its submenus: "
"Not enough screen space", menu->id, menu->label);
return;
}
2022-02-19 02:05:38 +01:00
wlr_output_layout_output_coords(menu->server->output_layout,
wlr_output, &ox, &oy);
if (align == LAB_MENU_OPEN_AUTO) {
int full_width = menu_get_full_width(menu);
if (ox + full_width > output->usable_area.width) {
2022-02-19 02:05:38 +01:00
align = LAB_MENU_OPEN_LEFT;
} else {
align = LAB_MENU_OPEN_RIGHT;
}
}
if (oy + menu->size.height > output->usable_area.height) {
2022-02-19 02:05:38 +01:00
align &= ~LAB_MENU_OPEN_BOTTOM;
align |= LAB_MENU_OPEN_TOP;
} else {
align &= ~LAB_MENU_OPEN_TOP;
align |= LAB_MENU_OPEN_BOTTOM;
}
if (align & LAB_MENU_OPEN_LEFT) {
lx -= menu->size.width - theme->menu_overlap_x;
2022-02-19 02:05:38 +01:00
}
if (align & LAB_MENU_OPEN_TOP) {
ly -= menu->size.height;
if (menu->parent) {
/* For submenus adjust y to bottom left corner */
ly += menu->item_height;
}
2021-02-17 20:38:16 +00:00
}
2022-02-19 02:05:38 +01:00
wlr_scene_node_set_position(&menu->scene_tree->node, lx, ly);
2021-02-17 20:38:16 +00:00
/* Needed for pipemenus to inherit alignment */
menu->align = align;
2022-02-19 02:05:38 +01:00
struct menuitem *item;
wl_list_for_each(item, &menu->menuitems, link) {
2022-02-19 02:05:38 +01:00
if (!item->submenu) {
continue;
}
struct wlr_box pos = get_submenu_position(item, align);
menu_configure(item->submenu, pos.x, pos.y, align);
2022-02-19 02:05:38 +01:00
}
}
static void
menu_hide_submenu(struct server *server, const char *id)
{
struct menu *menu, *hide_menu;
hide_menu = menu_get_by_id(server, id);
if (!hide_menu) {
return;
}
wl_list_for_each(menu, &server->menus, link) {
bool should_reposition = false;
struct menuitem *item, *next;
wl_list_for_each_safe(item, next, &menu->menuitems, link) {
if (item->submenu == hide_menu) {
item_destroy(item);
should_reposition = true;
}
}
if (!should_reposition) {
continue;
}
/* Re-position items vertically */
menu->size.height = 0;
wl_list_for_each(item, &menu->menuitems, link) {
wlr_scene_node_set_position(&item->tree->node, 0,
menu->size.height);
menu->size.height += item->height;
}
}
}
static void
init_rootmenu(struct server *server)
{
struct menu *menu = menu_get_by_id(server, "root-menu");
2021-02-19 23:05:14 +00:00
2021-02-17 20:38:16 +00:00
/* Default menu if no menu.xml found */
2022-02-19 02:05:38 +01:00
if (!menu) {
2022-01-26 00:07:10 +01:00
current_menu = NULL;
2022-02-19 02:05:38 +01:00
menu = menu_create(server, "root-menu", "");
}
2022-02-19 02:05:38 +01:00
if (wl_list_empty(&menu->menuitems)) {
2022-08-02 22:00:24 +01:00
current_item = item_create(menu, _("Reconfigure"), false);
fill_item("name.action", "Reconfigure");
2022-08-02 22:00:24 +01:00
current_item = item_create(menu, _("Exit"), false);
fill_item("name.action", "Exit");
2021-02-17 20:38:16 +00:00
}
2020-10-19 22:14:17 +01:00
}
static void
init_windowmenu(struct server *server)
2022-01-26 00:07:10 +01:00
{
struct menu *menu = menu_get_by_id(server, "client-menu");
2022-01-26 00:07:10 +01:00
/* Default menu if no menu.xml found */
2022-02-19 02:05:38 +01:00
if (!menu) {
2022-01-26 00:07:10 +01:00
current_menu = NULL;
2022-02-19 02:05:38 +01:00
menu = menu_create(server, "client-menu", "");
2022-01-26 00:07:10 +01:00
}
2022-02-19 02:05:38 +01:00
if (wl_list_empty(&menu->menuitems)) {
2022-08-02 22:00:24 +01:00
current_item = item_create(menu, _("Minimize"), false);
2022-01-26 00:07:10 +01:00
fill_item("name.action", "Iconify");
2022-08-02 22:00:24 +01:00
current_item = item_create(menu, _("Maximize"), false);
2022-01-26 00:07:10 +01:00
fill_item("name.action", "ToggleMaximize");
2022-08-02 22:00:24 +01:00
current_item = item_create(menu, _("Fullscreen"), false);
2022-01-26 00:07:10 +01:00
fill_item("name.action", "ToggleFullscreen");
current_item = item_create(menu, _("Roll Up/Down"), false);
fill_item("name.action", "ToggleShade");
2022-08-02 22:00:24 +01:00
current_item = item_create(menu, _("Decorations"), false);
2022-01-26 00:07:10 +01:00
fill_item("name.action", "ToggleDecorations");
current_item = item_create(menu, _("Always on Top"), false);
2022-04-09 01:16:09 +02:00
fill_item("name.action", "ToggleAlwaysOnTop");
/* Workspace sub-menu */
struct menu *workspace_menu = menu_create(server, "workspaces", "");
current_item = item_create(workspace_menu, _("Move Left"), false);
/*
* <action name="SendToDesktop"><follow> is true by default so
* GoToDesktop will be called as part of the action.
*/
fill_item("name.action", "SendToDesktop");
fill_item("to.action", "left");
current_item = item_create(workspace_menu, _("Move Right"), false);
fill_item("name.action", "SendToDesktop");
fill_item("to.action", "right");
current_item = separator_create(workspace_menu, "");
current_item = item_create(workspace_menu,
_("Always on Visible Workspace"), false);
fill_item("name.action", "ToggleOmnipresent");
2022-08-02 22:00:24 +01:00
current_item = item_create(menu, _("Workspace"), true);
current_item->submenu = workspace_menu;
2022-08-02 22:00:24 +01:00
current_item = item_create(menu, _("Close"), false);
2022-01-26 00:07:10 +01:00
fill_item("name.action", "Close");
}
if (wl_list_length(&rc.workspace_config.workspaces) == 1) {
menu_hide_submenu(server, "workspaces");
}
2022-01-26 00:07:10 +01:00
}
void
menu_init(struct server *server)
{
wl_list_init(&server->menus);
parse_xml("menu.xml", server);
init_rootmenu(server);
init_windowmenu(server);
post_processing(server);
validate(server);
}
static void
nullify_item_pointing_to_this_menu(struct menu *menu)
{
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)
2020-10-22 19:43:27 +01:00
{
/* 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.
*/
wlr_scene_node_destroy(&menu->scene_tree->node);
wl_list_remove(&menu->link);
2024-07-08 11:09:20 -04:00
zfree(menu->id);
zfree(menu->label);
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);
/* Reset state vars for starting fresh when Reload is triggered */
current_item = NULL;
current_item_action = NULL;
current_menu = NULL;
}
2022-02-19 02:05:38 +01:00
/* Sets selection (or clears selection if passing NULL) */
static void
menu_set_selection(struct menu *menu, struct menuitem *item)
{
/* Clear old selection */
if (menu->selection.item) {
wlr_scene_node_set_enabled(
2022-06-05 14:42:06 +02:00
&menu->selection.item->normal.tree->node, true);
2022-02-19 02:05:38 +01:00
wlr_scene_node_set_enabled(
2022-06-05 14:42:06 +02:00
&menu->selection.item->selected.tree->node, false);
2022-02-19 02:05:38 +01:00
}
/* Set new selection */
if (item) {
2022-06-05 14:42:06 +02:00
wlr_scene_node_set_enabled(&item->normal.tree->node, false);
wlr_scene_node_set_enabled(&item->selected.tree->node, true);
2022-02-19 02:05:38 +01:00
}
menu->selection.item = item;
}
static void
close_all_submenus(struct menu *menu)
{
struct menuitem *item;
2022-11-03 19:58:21 +00:00
wl_list_for_each(item, &menu->menuitems, link) {
if (item->submenu) {
2022-04-04 20:53:36 +01:00
wlr_scene_node_set_enabled(
&item->submenu->scene_tree->node, false);
close_all_submenus(item->submenu);
}
2020-10-22 19:43:27 +01:00
}
2022-02-19 02:05:38 +01:00
menu->selection.menu = NULL;
2020-10-22 19:43:27 +01:00
}
/*
* 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)
{
if (!menu) {
wlr_log(WLR_ERROR, "Trying to close non exiting menu");
return;
}
_close(menu);
}
2020-10-19 22:14:17 +01:00
void
menu_open_root(struct menu *menu, int x, int y)
2020-10-19 22:14:17 +01:00
{
assert(menu);
2022-02-19 02:05:38 +01:00
if (menu->server->menu_current) {
menu_close(menu->server->menu_current);
destroy_pipemenus(menu->server);
2022-02-19 02:05:38 +01:00
}
close_all_submenus(menu);
2022-02-19 02:05:38 +01:00
menu_set_selection(menu, NULL);
menu_configure(menu, x, y, LAB_MENU_OPEN_AUTO);
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;
}
static void
create_pipe_menu(struct menu_pipe_context *ctx)
{
assert(ctx->item);
struct menu *pipe_parent = ctx->item->parent;
if (!pipe_parent) {
wlr_log(WLR_INFO, "[pipemenu %ld] invalid parent",
(long)ctx->pid);
return;
}
if (!pipe_parent->scene_tree->node.enabled) {
wlr_log(WLR_INFO, "[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);
enum menu_align align = ctx->item->parent->align;
struct wlr_box pos = get_submenu_position(ctx->item, align);
menu_configure(pipe_menu, pos.x, pos.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 menu_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);
buf_reset(&ctx->buf);
if (ctx->item) {
ctx->item->pipe_ctx = NULL;
}
free(ctx);
waiting_for_pipe_menu = false;
}
static int
handle_pipemenu_timeout(void *_ctx)
{
struct menu_pipe_context *ctx = _ctx;
wlr_log(WLR_ERROR, "[pipemenu %ld] timeout reached, killing %s",
(long)ctx->pid, ctx->item ? ctx->item->execute : "n/a");
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 menu_pipe_context *ctx = _ctx;
/* two 4k pages + 1 NULL byte */
char data[8193];
ssize_t size;
if (!ctx->item) {
/* parent menu item got destroyed in the meantime */
wlr_log(WLR_INFO, "[pipemenu %ld] parent menu item destroyed",
(long)ctx->pid);
kill(ctx->pid, SIGTERM);
goto clean_up;
}
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.data)) {
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;
}
if (item->pipe_ctx) {
wlr_log(WLR_ERROR, "item already has a pipe context attached");
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 menu_pipe_context *ctx = znew(*ctx);
ctx->server = item->parent->server;
ctx->item = item;
ctx->pid = pid;
ctx->pipe_fd = pipe_fd;
ctx->buf = BUF_INIT;
item->pipe_ctx = ctx;
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);
}
2020-10-19 22:14:17 +01:00
static void
menu_process_item_selection(struct menuitem *item)
{
assert(item);
/* Do not keep selecting the same item */
if (item == selected_item) {
return;
}
if (waiting_for_pipe_menu) {
return;
}
selected_item = item;
if (!item->selectable) {
return;
}
2022-02-19 02:05:38 +01:00
/* We are on an item that has new focus */
2022-03-03 04:33:33 +01:00
menu_set_selection(item->parent, item);
if (item->parent->selection.menu) {
/* Close old submenu tree */
menu_close(item->parent->selection.menu);
}
2021-08-09 17:28:39 +01:00
/* Pipemenu */
if (item->execute && !item->submenu) {
/* pipemenus are generated async */
parse_pipemenu(item);
return;
}
2022-03-03 04:33:33 +01:00
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 */
2022-03-03 04:33:33 +01:00
wlr_scene_node_set_enabled(
&item->submenu->scene_tree->node, true);
}
2022-03-03 04:33:33 +01:00
item->parent->selection.menu = item->submenu;
2020-10-19 22:14:17 +01:00
}
/* Get the deepest submenu with active item selection or the root menu itself */
static struct menu *
get_selection_leaf(struct server *server)
2020-10-19 22:14:17 +01:00
{
struct menu *menu = server->menu_current;
if (!menu) {
return NULL;
}
2022-02-19 02:05:38 +01:00
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 */
2022-02-19 02:05:38 +01:00
return false;
}
2022-03-03 04:33:33 +01:00
2023-10-09 20:46:23 +01:00
/*
* We close the menu here to provide a faster feedback to the user.
* We do that without resetting the input state so src/cursor.c
* can do its own clean up on the following RELEASE event.
*/
struct server *server = item->parent->server;
menu_close(server->menu_current);
server->input_mode = LAB_INPUT_STATE_PASSTHROUGH;
cursor_update_focus(server);
/*
* 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);
2022-03-03 04:33:33 +01:00
return true;
2020-10-19 22:14:17 +01:00
}
/* 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);
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);
}
2020-10-19 22:14:17 +01:00
void
menu_close_root(struct server *server)
2020-10-19 22:14:17 +01:00
{
assert(server->input_mode == LAB_INPUT_STATE_MENU);
if (server->menu_current) {
menu_close(server->menu_current);
server->menu_current = NULL;
destroy_pipemenus(server);
2020-10-19 22:14:17 +01:00
}
server->input_mode = LAB_INPUT_STATE_PASSTHROUGH;
2020-10-19 22:14:17 +01:00
}
2021-02-19 23:05:14 +00:00
void
2022-02-19 02:05:38 +01:00
menu_reconfigure(struct server *server)
2021-02-19 23:05:14 +00:00
{
menu_finish(server);
server->menu_current = NULL;
menu_init(server);
2021-02-19 23:05:14 +00:00
}