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>
|
2023-10-31 21:36:45 +00:00
|
|
|
|
#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>
|
2022-06-12 21:22:01 +02:00
|
|
|
|
#include <wayland-server-core.h>
|
2024-11-26 06:42:08 +09:00
|
|
|
|
#include <wlr/types/wlr_xdg_shell.h>
|
2021-07-22 21:30:17 +01:00
|
|
|
|
#include <wlr/util/log.h>
|
2022-10-05 08:43:56 +02:00
|
|
|
|
#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"
|
2022-10-05 08:43:56 +02:00
|
|
|
|
#include "common/list.h"
|
2024-11-10 18:54:01 +09:00
|
|
|
|
#include "common/macros.h"
|
2022-09-16 18:41:02 -04:00
|
|
|
|
#include "common/mem.h"
|
2021-02-17 20:38:16 +00:00
|
|
|
|
#include "common/nodename.h"
|
2024-05-22 09:56:16 +09:00
|
|
|
|
#include "common/scaled-font-buffer.h"
|
2025-01-06 07:34:16 +01:00
|
|
|
|
#include "common/scaled-icon-buffer.h"
|
2024-11-24 14:40:40 +09:00
|
|
|
|
#include "common/scaled-rect-buffer.h"
|
2022-12-05 14:38:16 +01:00
|
|
|
|
#include "common/scene-helpers.h"
|
2023-10-31 21:36:45 +00:00
|
|
|
|
#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"
|
2024-09-17 06:52:10 -05:00
|
|
|
|
#include "workspaces.h"
|
|
|
|
|
|
#include "view.h"
|
2022-03-03 04:33:33 +01:00
|
|
|
|
#include "node.h"
|
2022-10-05 08:43:56 +02:00
|
|
|
|
#include "theme.h"
|
2020-10-19 22:14:17 +01:00
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
#define PIPEMENU_MAX_BUF_SIZE 1048576 /* 1 MiB */
|
|
|
|
|
|
#define PIPEMENU_TIMEOUT_IN_MS 4000 /* 4 seconds */
|
|
|
|
|
|
|
2025-01-06 07:34:16 +01:00
|
|
|
|
#define ICON_SIZE (rc.theme->menu_item_height - 2 * rc.theme->menu_items_padding_y)
|
|
|
|
|
|
|
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;
|
2022-01-05 09:11:24 +01:00
|
|
|
|
static struct action *current_item_action;
|
2021-02-17 20:38:16 +00:00
|
|
|
|
|
2021-11-08 17:20:37 +00:00
|
|
|
|
static int menu_level;
|
2021-11-02 18:31:19 +00:00
|
|
|
|
static struct menu *current_menu;
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
static bool waiting_for_pipe_menu;
|
|
|
|
|
|
static struct menuitem *selected_item;
|
|
|
|
|
|
|
2024-08-20 18:09:17 +02:00
|
|
|
|
struct menu_pipe_context {
|
|
|
|
|
|
struct server *server;
|
|
|
|
|
|
struct menuitem *item;
|
2024-10-14 19:46:03 +01:00
|
|
|
|
struct menu *top_level_menu;
|
2024-08-20 18:09:17 +02:00
|
|
|
|
struct buf buf;
|
|
|
|
|
|
struct wl_event_source *event_read;
|
|
|
|
|
|
struct wl_event_source *event_timeout;
|
|
|
|
|
|
pid_t pid;
|
|
|
|
|
|
int pipe_fd;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2023-08-30 11:20:46 +02:00
|
|
|
|
/* TODO: split this whole file into parser.c and actions.c*/
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-02 18:31:19 +00:00
|
|
|
|
static struct menu *
|
|
|
|
|
|
menu_create(struct server *server, const char *id, const char *label)
|
|
|
|
|
|
{
|
2023-10-31 21:36:45 +00:00
|
|
|
|
if (!is_unique_id(server, id)) {
|
|
|
|
|
|
wlr_log(WLR_ERROR, "menu id %s already exists", id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-09 20:59:04 +01:00
|
|
|
|
struct menu *menu = znew(*menu);
|
|
|
|
|
|
wl_list_append(&server->menus, &menu->link);
|
|
|
|
|
|
|
2021-11-02 18:31:19 +00:00
|
|
|
|
wl_list_init(&menu->menuitems);
|
2022-09-16 18:41:02 -04:00
|
|
|
|
menu->id = xstrdup(id);
|
|
|
|
|
|
menu->label = xstrdup(label ? label : id);
|
2021-11-08 17:20:37 +00:00
|
|
|
|
menu->parent = current_menu;
|
2021-11-02 18:31:19 +00:00
|
|
|
|
menu->server = server;
|
2023-10-31 21:36:45 +00:00
|
|
|
|
menu->is_pipemenu = waiting_for_pipe_menu;
|
2022-12-05 14:38:16 +01:00
|
|
|
|
menu->size.width = server->theme->menu_min_width;
|
2021-11-02 18:31:19 +00:00
|
|
|
|
return menu;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-02-19 02:05:38 +01:00
|
|
|
|
struct menu *
|
2023-10-09 20:59:04 +01:00
|
|
|
|
menu_get_by_id(struct server *server, const char *id)
|
2021-11-02 18:31:19 +00:00
|
|
|
|
{
|
2022-02-19 02:05:38 +01:00
|
|
|
|
if (!id) {
|
|
|
|
|
|
return NULL;
|
|
|
|
|
|
}
|
2021-11-02 18:31:19 +00:00
|
|
|
|
struct menu *menu;
|
2023-10-09 20:59:04 +01:00
|
|
|
|
wl_list_for_each(menu, &server->menus, link) {
|
2021-11-02 18:31:19 +00:00
|
|
|
|
if (!strcmp(menu->id, id)) {
|
|
|
|
|
|
return menu;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return NULL;
|
|
|
|
|
|
}
|
2020-10-19 22:14:17 +01:00
|
|
|
|
|
2023-05-13 16:27:46 +02:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2023-10-09 20:59:04 +01:00
|
|
|
|
struct menu *menu;
|
|
|
|
|
|
wl_list_for_each(menu, &server->menus, link) {
|
|
|
|
|
|
validate_menu(menu);
|
2023-05-13 16:27:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
{
|
2023-10-31 21:36:45 +00: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;
|
2022-06-22 21:07:25 +01:00
|
|
|
|
menuitem->selectable = true;
|
2024-05-09 21:20:15 +01:00
|
|
|
|
menuitem->type = LAB_MENU_ITEM;
|
2024-11-10 18:54:01 +09:00
|
|
|
|
menuitem->text = xstrdup(text);
|
|
|
|
|
|
menuitem->arrow = show_arrow ? "›" : NULL;
|
2021-08-07 08:35:46 +01:00
|
|
|
|
|
2022-12-05 14:38:16 +01:00
|
|
|
|
menuitem->native_width = font_width(&rc.font_menuitem, text);
|
2024-11-10 18:54:01 +09:00
|
|
|
|
if (menuitem->arrow) {
|
|
|
|
|
|
menuitem->native_width += font_width(&rc.font_menuitem, menuitem->arrow);
|
2022-12-05 14:38:16 +01:00
|
|
|
|
}
|
2022-02-19 02:05:38 +01:00
|
|
|
|
|
2024-11-10 18:54:01 +09:00
|
|
|
|
wl_list_append(&menu->menuitems, &menuitem->link);
|
|
|
|
|
|
wl_list_init(&menuitem->actions);
|
|
|
|
|
|
return menuitem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-03 15:58:07 +09:00
|
|
|
|
static struct wlr_scene_tree *
|
|
|
|
|
|
item_create_scene_for_state(struct menuitem *item, float *text_color,
|
|
|
|
|
|
float *bg_color)
|
|
|
|
|
|
{
|
|
|
|
|
|
struct menu *menu = item->parent;
|
|
|
|
|
|
struct theme *theme = menu->server->theme;
|
|
|
|
|
|
|
|
|
|
|
|
/* Tree to hold background and label buffers */
|
|
|
|
|
|
struct wlr_scene_tree *tree = wlr_scene_tree_create(item->tree);
|
|
|
|
|
|
|
2025-01-06 07:34:16 +01:00
|
|
|
|
int icon_width = 0;
|
|
|
|
|
|
int icon_size = ICON_SIZE;
|
|
|
|
|
|
if (item->parent->has_icons) {
|
|
|
|
|
|
icon_width = theme->menu_items_padding_x + icon_size;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-03 15:58:07 +09:00
|
|
|
|
/* Create background */
|
|
|
|
|
|
int bg_width = menu->size.width
|
|
|
|
|
|
- 2 * theme->menu_border_width;
|
|
|
|
|
|
wlr_scene_rect_create(tree, bg_width, theme->menu_item_height, bg_color);
|
|
|
|
|
|
|
|
|
|
|
|
int arrow_width = item->arrow ?
|
|
|
|
|
|
font_width(&rc.font_menuitem, item->arrow) : 0;
|
2025-01-06 07:34:16 +01:00
|
|
|
|
int label_max_width = bg_width - 2 * theme->menu_items_padding_x - arrow_width - icon_width;
|
|
|
|
|
|
|
|
|
|
|
|
/* Create icon */
|
|
|
|
|
|
if (item->icon_name) {
|
|
|
|
|
|
struct scaled_icon_buffer *icon_buffer = scaled_icon_buffer_create(
|
|
|
|
|
|
tree, menu->server, icon_size, icon_size);
|
|
|
|
|
|
scaled_icon_buffer_set_icon_name(icon_buffer, item->icon_name);
|
|
|
|
|
|
wlr_scene_node_set_position(&icon_buffer->scene_buffer->node,
|
|
|
|
|
|
theme->menu_items_padding_x, theme->menu_items_padding_y);
|
|
|
|
|
|
}
|
2024-12-03 15:58:07 +09:00
|
|
|
|
|
|
|
|
|
|
/* Create label */
|
|
|
|
|
|
struct scaled_font_buffer *label_buffer = scaled_font_buffer_create(tree);
|
|
|
|
|
|
assert(label_buffer);
|
2024-12-03 16:09:40 +09:00
|
|
|
|
scaled_font_buffer_update(label_buffer, item->text, label_max_width,
|
|
|
|
|
|
&rc.font_menuitem, text_color, bg_color);
|
2024-12-03 15:58:07 +09:00
|
|
|
|
/* Vertically center and left-align label */
|
2025-01-06 07:34:16 +01:00
|
|
|
|
int x = theme->menu_items_padding_x + icon_width;
|
2024-12-03 15:58:07 +09:00
|
|
|
|
int y = (theme->menu_item_height - label_buffer->height) / 2;
|
|
|
|
|
|
wlr_scene_node_set_position(&label_buffer->scene_buffer->node, x, y);
|
|
|
|
|
|
|
2024-12-03 16:09:40 +09:00
|
|
|
|
if (!item->arrow) {
|
|
|
|
|
|
return tree;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Create arrow for submenu items */
|
|
|
|
|
|
struct scaled_font_buffer *arrow_buffer = scaled_font_buffer_create(tree);
|
|
|
|
|
|
assert(arrow_buffer);
|
|
|
|
|
|
scaled_font_buffer_update(arrow_buffer, item->arrow, -1,
|
|
|
|
|
|
&rc.font_menuitem, text_color, bg_color);
|
|
|
|
|
|
/* Vertically center and right-align arrow */
|
|
|
|
|
|
x += label_max_width;
|
|
|
|
|
|
y = (theme->menu_item_height - label_buffer->height) / 2;
|
|
|
|
|
|
wlr_scene_node_set_position(&arrow_buffer->scene_buffer->node, x, y);
|
|
|
|
|
|
|
2024-12-03 15:58:07 +09:00
|
|
|
|
return tree;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-10 18:54:01 +09:00
|
|
|
|
static void
|
|
|
|
|
|
item_create_scene(struct menuitem *menuitem, int *item_y)
|
|
|
|
|
|
{
|
|
|
|
|
|
assert(menuitem);
|
|
|
|
|
|
assert(menuitem->type == LAB_MENU_ITEM);
|
|
|
|
|
|
struct menu *menu = menuitem->parent;
|
|
|
|
|
|
struct theme *theme = menu->server->theme;
|
|
|
|
|
|
|
2022-06-05 14:42:06 +02:00
|
|
|
|
/* Menu item root node */
|
2022-06-05 15:17:35 +02:00
|
|
|
|
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);
|
|
|
|
|
|
|
2024-12-03 15:58:07 +09:00
|
|
|
|
/* Create scenes for unselected/selected states */
|
|
|
|
|
|
menuitem->normal_tree = item_create_scene_for_state(menuitem,
|
2024-11-10 18:54:01 +09:00
|
|
|
|
theme->menu_items_text_color,
|
2024-12-03 15:58:07 +09:00
|
|
|
|
theme->menu_items_bg_color);
|
|
|
|
|
|
menuitem->selected_tree = item_create_scene_for_state(menuitem,
|
2024-11-10 18:54:01 +09:00
|
|
|
|
theme->menu_items_active_text_color,
|
2024-12-03 15:58:07 +09:00
|
|
|
|
theme->menu_items_active_bg_color);
|
|
|
|
|
|
/* Hide selected state */
|
|
|
|
|
|
wlr_scene_node_set_enabled(&menuitem->selected_tree->node, false);
|
2022-02-19 02:05:38 +01:00
|
|
|
|
|
|
|
|
|
|
/* Position the item in relation to its menu */
|
2024-11-24 14:40:40 +09:00
|
|
|
|
wlr_scene_node_set_position(&menuitem->tree->node,
|
|
|
|
|
|
theme->menu_border_width, *item_y);
|
2024-11-10 18:54:01 +09:00
|
|
|
|
*item_y += theme->menu_item_height;
|
2020-10-19 22:14:17 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-06-22 21:07:25 +01:00
|
|
|
|
static struct menuitem *
|
|
|
|
|
|
separator_create(struct menu *menu, const char *label)
|
|
|
|
|
|
{
|
2022-09-18 15:22:26 -04:00
|
|
|
|
struct menuitem *menuitem = znew(*menuitem);
|
2022-06-22 21:07:25 +01:00
|
|
|
|
menuitem->parent = menu;
|
|
|
|
|
|
menuitem->selectable = false;
|
2024-05-09 21:20:15 +01:00
|
|
|
|
menuitem->type = string_null_or_empty(label) ? LAB_MENU_SEPARATOR_LINE
|
|
|
|
|
|
: LAB_MENU_TITLE;
|
|
|
|
|
|
if (menuitem->type == LAB_MENU_TITLE) {
|
2024-11-10 18:54:01 +09:00
|
|
|
|
menuitem->text = xstrdup(label);
|
2024-08-21 12:27:07 -05:00
|
|
|
|
menuitem->native_width = font_width(&rc.font_menuheader, label);
|
2024-05-09 21:20:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-10 18:54:01 +09:00
|
|
|
|
wl_list_append(&menu->menuitems, &menuitem->link);
|
|
|
|
|
|
wl_list_init(&menuitem->actions);
|
|
|
|
|
|
return menuitem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
|
separator_create_scene(struct menuitem *menuitem, int *item_y)
|
|
|
|
|
|
{
|
|
|
|
|
|
assert(menuitem);
|
|
|
|
|
|
assert(menuitem->type == LAB_MENU_SEPARATOR_LINE);
|
|
|
|
|
|
struct menu *menu = menuitem->parent;
|
|
|
|
|
|
struct theme *theme = menu->server->theme;
|
2024-11-24 14:40:40 +09:00
|
|
|
|
int bg_width = menu->size.width - 2 * theme->menu_border_width;
|
|
|
|
|
|
int bg_height = theme->menu_separator_line_thickness
|
2024-11-10 18:54:01 +09:00
|
|
|
|
+ 2 * theme->menu_separator_padding_height;
|
|
|
|
|
|
|
2024-05-09 21:20:15 +01:00
|
|
|
|
/* Menu item root node */
|
2022-06-22 21:07:25 +01:00
|
|
|
|
menuitem->tree = wlr_scene_tree_create(menu->scene_tree);
|
|
|
|
|
|
node_descriptor_create(&menuitem->tree->node,
|
|
|
|
|
|
LAB_NODE_DESC_MENUITEM, menuitem);
|
2024-05-09 21:20:15 +01:00
|
|
|
|
|
2024-11-10 18:54:01 +09:00
|
|
|
|
/* Tree to hold background and line buffer */
|
2024-12-03 15:58:07 +09:00
|
|
|
|
menuitem->normal_tree = wlr_scene_tree_create(menuitem->tree);
|
2022-06-22 21:07:25 +01:00
|
|
|
|
|
2024-05-09 21:20:15 +01:00
|
|
|
|
/* Item background nodes */
|
2024-12-03 15:58:07 +09:00
|
|
|
|
wlr_scene_rect_create(menuitem->normal_tree, bg_width, bg_height,
|
|
|
|
|
|
theme->menu_items_bg_color);
|
2024-05-09 21:20:15 +01:00
|
|
|
|
|
2024-11-10 18:54:01 +09:00
|
|
|
|
/* Draw separator line */
|
2024-11-24 14:40:40 +09:00
|
|
|
|
int line_width = bg_width - 2 * theme->menu_separator_padding_width;
|
2024-12-03 15:58:07 +09:00
|
|
|
|
struct wlr_scene_rect *line_rect = wlr_scene_rect_create(
|
|
|
|
|
|
menuitem->normal_tree, line_width,
|
2024-11-10 18:54:01 +09:00
|
|
|
|
theme->menu_separator_line_thickness,
|
2024-12-03 15:58:07 +09:00
|
|
|
|
theme->menu_separator_color);
|
2024-11-10 18:54:01 +09:00
|
|
|
|
|
|
|
|
|
|
/* Vertically center-align separator line */
|
2024-12-03 15:58:07 +09:00
|
|
|
|
wlr_scene_node_set_position(&line_rect->node,
|
2024-11-10 18:54:01 +09:00
|
|
|
|
theme->menu_separator_padding_width,
|
|
|
|
|
|
theme->menu_separator_padding_height);
|
2024-11-24 14:40:40 +09:00
|
|
|
|
wlr_scene_node_set_position(&menuitem->tree->node,
|
|
|
|
|
|
theme->menu_border_width, *item_y);
|
|
|
|
|
|
*item_y += bg_height;
|
2024-11-10 18:54:01 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
|
title_create_scene(struct menuitem *menuitem, int *item_y)
|
|
|
|
|
|
{
|
|
|
|
|
|
assert(menuitem);
|
|
|
|
|
|
assert(menuitem->type == LAB_MENU_TITLE);
|
|
|
|
|
|
struct menu *menu = menuitem->parent;
|
|
|
|
|
|
struct theme *theme = menu->server->theme;
|
|
|
|
|
|
float *bg_color = theme->menu_title_bg_color;
|
|
|
|
|
|
float *text_color = theme->menu_title_text_color;
|
2024-11-24 14:40:40 +09:00
|
|
|
|
int bg_width = menu->size.width - 2 * theme->menu_border_width;
|
2024-11-10 18:54:01 +09:00
|
|
|
|
|
|
|
|
|
|
/* 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 buffer */
|
2024-12-03 15:58:07 +09:00
|
|
|
|
menuitem->normal_tree = wlr_scene_tree_create(menuitem->tree);
|
2024-11-10 18:54:01 +09:00
|
|
|
|
|
|
|
|
|
|
/* Background */
|
2024-12-03 15:58:07 +09:00
|
|
|
|
wlr_scene_rect_create(menuitem->normal_tree,
|
|
|
|
|
|
bg_width, theme->menu_header_height, bg_color);
|
2024-11-10 18:54:01 +09:00
|
|
|
|
|
|
|
|
|
|
/* Draw separator title */
|
2024-12-03 15:58:07 +09:00
|
|
|
|
struct scaled_font_buffer *title_font_buffer =
|
|
|
|
|
|
scaled_font_buffer_create(menuitem->normal_tree);
|
|
|
|
|
|
assert(title_font_buffer);
|
|
|
|
|
|
scaled_font_buffer_update(title_font_buffer, menuitem->text,
|
2024-11-24 14:40:40 +09:00
|
|
|
|
bg_width - 2 * theme->menu_items_padding_x,
|
2024-12-03 16:09:40 +09:00
|
|
|
|
&rc.font_menuheader, text_color, bg_color);
|
2024-11-10 18:54:01 +09:00
|
|
|
|
|
|
|
|
|
|
int title_x = 0;
|
2024-11-13 05:14:53 +09:00
|
|
|
|
switch (theme->menu_title_text_justify) {
|
|
|
|
|
|
case LAB_JUSTIFY_CENTER:
|
2024-11-24 14:40:40 +09:00
|
|
|
|
title_x = (bg_width - menuitem->native_width) / 2;
|
2024-11-10 18:54:01 +09:00
|
|
|
|
title_x = MAX(title_x, 0);
|
2024-11-13 05:14:53 +09:00
|
|
|
|
break;
|
|
|
|
|
|
case LAB_JUSTIFY_LEFT:
|
2024-11-10 18:54:01 +09:00
|
|
|
|
title_x = theme->menu_items_padding_x;
|
2024-11-13 05:14:53 +09:00
|
|
|
|
break;
|
|
|
|
|
|
case LAB_JUSTIFY_RIGHT:
|
2024-11-24 14:40:40 +09:00
|
|
|
|
title_x = bg_width - menuitem->native_width
|
2024-11-13 05:14:53 +09:00
|
|
|
|
- theme->menu_items_padding_x;
|
|
|
|
|
|
break;
|
2024-05-09 21:20:15 +01:00
|
|
|
|
}
|
2024-12-03 15:58:07 +09:00
|
|
|
|
int title_y = (theme->menu_header_height - title_font_buffer->height) / 2;
|
|
|
|
|
|
wlr_scene_node_set_position(&title_font_buffer->scene_buffer->node,
|
|
|
|
|
|
title_x, title_y);
|
2022-06-22 21:07:25 +01:00
|
|
|
|
|
2024-11-24 14:40:40 +09:00
|
|
|
|
wlr_scene_node_set_position(&menuitem->tree->node,
|
|
|
|
|
|
theme->menu_border_width, *item_y);
|
2024-11-10 18:54:01 +09:00
|
|
|
|
*item_y += theme->menu_header_height;
|
2022-06-22 21:07:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-14 08:14:11 +09:00
|
|
|
|
/* (Re)creates the scene of the menu */
|
2024-11-10 18:51:48 +09:00
|
|
|
|
static void
|
2024-11-10 18:54:01 +09:00
|
|
|
|
menu_update_scene(struct menu *menu)
|
2024-11-10 18:51:48 +09:00
|
|
|
|
{
|
|
|
|
|
|
struct menuitem *item;
|
|
|
|
|
|
struct theme *theme = menu->server->theme;
|
2024-11-10 18:54:01 +09:00
|
|
|
|
|
|
|
|
|
|
if (menu->scene_tree) {
|
|
|
|
|
|
wlr_scene_node_destroy(&menu->scene_tree->node);
|
|
|
|
|
|
wl_list_for_each(item, &menu->menuitems, link) {
|
|
|
|
|
|
item->tree = NULL;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
menu->scene_tree = wlr_scene_tree_create(menu->server->menu_tree);
|
|
|
|
|
|
wlr_scene_node_set_enabled(&menu->scene_tree->node, false);
|
2024-11-10 18:51:48 +09:00
|
|
|
|
|
2024-11-13 05:34:44 +09:00
|
|
|
|
/* Menu width is the maximum item width, capped by menu.width.{min,max} */
|
|
|
|
|
|
menu->size.width = 0;
|
2024-11-10 18:51:48 +09:00
|
|
|
|
wl_list_for_each(item, &menu->menuitems, link) {
|
2024-11-24 14:40:40 +09:00
|
|
|
|
int width = item->native_width
|
|
|
|
|
|
+ 2 * theme->menu_items_padding_x
|
|
|
|
|
|
+ 2 * theme->menu_border_width;
|
|
|
|
|
|
menu->size.width = MAX(menu->size.width, width);
|
2024-11-10 18:51:48 +09:00
|
|
|
|
}
|
2025-01-06 07:34:16 +01:00
|
|
|
|
|
|
|
|
|
|
if (menu->has_icons) {
|
|
|
|
|
|
menu->size.width += theme->menu_items_padding_x + ICON_SIZE;
|
|
|
|
|
|
}
|
2024-11-13 05:34:44 +09:00
|
|
|
|
menu->size.width = MAX(menu->size.width, theme->menu_min_width);
|
|
|
|
|
|
menu->size.width = MIN(menu->size.width, theme->menu_max_width);
|
2024-11-10 18:51:48 +09:00
|
|
|
|
|
|
|
|
|
|
/* Update all items for the new size */
|
2024-11-24 14:40:40 +09:00
|
|
|
|
int item_y = theme->menu_border_width;
|
2024-11-10 18:51:48 +09:00
|
|
|
|
wl_list_for_each(item, &menu->menuitems, link) {
|
2024-11-10 18:54:01 +09:00
|
|
|
|
assert(!item->tree);
|
|
|
|
|
|
switch (item->type) {
|
|
|
|
|
|
case LAB_MENU_ITEM:
|
|
|
|
|
|
item_create_scene(item, &item_y);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case LAB_MENU_SEPARATOR_LINE:
|
|
|
|
|
|
separator_create_scene(item, &item_y);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case LAB_MENU_TITLE:
|
|
|
|
|
|
title_create_scene(item, &item_y);
|
|
|
|
|
|
break;
|
2024-11-10 18:51:48 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-11-24 14:40:40 +09:00
|
|
|
|
menu->size.height = item_y + theme->menu_border_width;
|
|
|
|
|
|
|
|
|
|
|
|
float transparent[4] = {0};
|
|
|
|
|
|
struct scaled_rect_buffer *bg_buffer = scaled_rect_buffer_create(
|
|
|
|
|
|
menu->scene_tree, menu->size.width, menu->size.height,
|
|
|
|
|
|
theme->menu_border_width, transparent,
|
|
|
|
|
|
theme->menu_border_color);
|
|
|
|
|
|
assert(bg_buffer);
|
|
|
|
|
|
wlr_scene_node_lower_to_bottom(&bg_buffer->scene_buffer->node);
|
2024-11-10 18:51:48 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
|
post_processing(struct server *server)
|
|
|
|
|
|
{
|
2024-11-14 08:14:11 +09:00
|
|
|
|
/*
|
|
|
|
|
|
* Create menu scene after all of its contents is determined
|
|
|
|
|
|
* (e.g. when finished reading menu.xml or received output from
|
|
|
|
|
|
* pipemenu program).
|
|
|
|
|
|
*/
|
2024-11-10 18:51:48 +09:00
|
|
|
|
struct menu *menu;
|
|
|
|
|
|
wl_list_for_each(menu, &server->menus, link) {
|
2024-11-14 08:14:11 +09:00
|
|
|
|
if (!menu->scene_tree) {
|
|
|
|
|
|
menu_update_scene(menu);
|
|
|
|
|
|
}
|
2024-11-10 18:51:48 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-02 18:31:19 +00:00
|
|
|
|
/*
|
|
|
|
|
|
* 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
|
|
|
|
{
|
2023-10-31 21:36:45 +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");
|
2023-10-31 21:36:45 +00:00
|
|
|
|
string_truncate_at_pattern(nodename, ".item");
|
2021-02-17 20:38:16 +00:00
|
|
|
|
|
2021-11-02 18:31:19 +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;
|
2022-01-05 09:11:24 +01:00
|
|
|
|
} else if (!current_item) {
|
|
|
|
|
|
wlr_log(WLR_ERROR, "expect <item label=\"\"> element first. "
|
|
|
|
|
|
"nodename: '%s' content: '%s'", nodename, content);
|
2022-05-06 21:52:46 +01:00
|
|
|
|
} else if (!strcmp(nodename, "icon")) {
|
2025-01-06 07:34:16 +01:00
|
|
|
|
#if HAVE_LIBSFDO
|
2025-01-27 17:19:20 +01:00
|
|
|
|
if (rc.menu_show_icons && !string_null_or_empty(content)) {
|
2025-01-06 07:34:16 +01:00
|
|
|
|
xstrdup_replace(current_item->icon_name, content);
|
|
|
|
|
|
current_menu->has_icons = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
2022-01-05 09:11:24 +01:00
|
|
|
|
} else if (!strcmp(nodename, "name.action")) {
|
|
|
|
|
|
current_item_action = action_create(content);
|
2023-01-29 04:06:46 +01:00
|
|
|
|
if (current_item_action) {
|
|
|
|
|
|
wl_list_append(¤t_item->actions,
|
|
|
|
|
|
¤t_item_action->link);
|
|
|
|
|
|
}
|
2022-01-05 09:11:24 +01:00
|
|
|
|
} else if (!current_item_action) {
|
|
|
|
|
|
wlr_log(WLR_ERROR, "expect <action name=\"\"> element first. "
|
|
|
|
|
|
"nodename: '%s' content: '%s'", nodename, content);
|
2022-12-10 15:28:25 +01:00
|
|
|
|
} else {
|
|
|
|
|
|
action_arg_from_xml_node(current_item_action, nodename, content);
|
2021-02-17 20:38:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-06-15 01:49:55 +02:00
|
|
|
|
static void
|
|
|
|
|
|
item_destroy(struct menuitem *item)
|
|
|
|
|
|
{
|
2024-08-20 18:09:17 +02:00
|
|
|
|
if (item->pipe_ctx) {
|
|
|
|
|
|
item->pipe_ctx->item = NULL;
|
|
|
|
|
|
}
|
2022-06-15 01:49:55 +02:00
|
|
|
|
wl_list_remove(&item->link);
|
|
|
|
|
|
action_list_free(&item->actions);
|
2024-11-14 18:16:43 +09:00
|
|
|
|
if (item->tree) {
|
|
|
|
|
|
wlr_scene_node_destroy(&item->tree->node);
|
|
|
|
|
|
}
|
2023-10-31 21:36:45 +00:00
|
|
|
|
free(item->execute);
|
|
|
|
|
|
free(item->id);
|
2024-11-10 18:54:01 +09:00
|
|
|
|
free(item->text);
|
2025-01-06 07:34:16 +01:00
|
|
|
|
free(item->icon_name);
|
2022-06-15 01:49:55 +02:00
|
|
|
|
free(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-07-02 21:17:26 +01:00
|
|
|
|
/*
|
|
|
|
|
|
* 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>
|
|
|
|
|
|
*
|
2023-10-31 21:36:45 +00:00
|
|
|
|
* 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.
|
2023-07-02 21:17:26 +01:00
|
|
|
|
*/
|
|
|
|
|
|
static bool
|
|
|
|
|
|
nodename_supports_cdata(char *nodename)
|
|
|
|
|
|
{
|
2023-10-31 21:36:45 +00:00
|
|
|
|
return !strncmp("command.action.", nodename, 15)
|
|
|
|
|
|
|| !strncmp("execute.action.", nodename, 15);
|
2023-07-02 21:17:26 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2021-02-17 20:38:16 +00:00
|
|
|
|
static void
|
2021-11-02 18:31:19 +00:00
|
|
|
|
entry(xmlNode *node, char *nodename, char *content)
|
2021-02-17 20:38:16 +00:00
|
|
|
|
{
|
2023-07-02 21:17:26 +01: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");
|
2023-10-31 21:36:45 +00:00
|
|
|
|
string_truncate_at_pattern(nodename, ".openbox_pipe_menu");
|
2023-07-02 13:55:23 +01:00
|
|
|
|
if (getenv("LABWC_DEBUG_MENU_NODENAMES")) {
|
2023-07-02 21:17:26 +01:00
|
|
|
|
printf("%s: %s\n", nodename, content ? content : (char *)cdata);
|
2023-07-02 13:55:23 +01:00
|
|
|
|
}
|
2021-02-17 20:38:16 +00:00
|
|
|
|
if (in_item) {
|
2023-07-02 21:17:26 +01:00
|
|
|
|
fill_item(nodename, content ? content : (char *)cdata);
|
2021-02-17 20:38:16 +00:00
|
|
|
|
}
|
2023-07-02 21:17:26 +01:00
|
|
|
|
xmlFree(cdata);
|
2021-02-17 20:38:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
2021-11-02 18:31:19 +00:00
|
|
|
|
process_node(xmlNode *node)
|
2021-02-17 20:38:16 +00:00
|
|
|
|
{
|
|
|
|
|
|
static char buffer[256];
|
|
|
|
|
|
|
2021-11-02 18:31:19 +00:00
|
|
|
|
char *content = (char *)node->content;
|
2021-02-17 20:38:16 +00:00
|
|
|
|
if (xmlIsBlankNode(node)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2021-11-02 18:31:19 +00:00
|
|
|
|
char *name = nodename(node, buffer, sizeof(buffer));
|
|
|
|
|
|
entry(node, name, content);
|
2021-02-17 20:38:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-02 18:31:19 +00:00
|
|
|
|
static void xml_tree_walk(xmlNode *node, struct server *server);
|
2021-02-17 20:38:16 +00:00
|
|
|
|
|
|
|
|
|
|
static void
|
2021-11-02 18:31:19 +00:00
|
|
|
|
traverse(xmlNode *n, struct server *server)
|
2021-02-17 20:38:16 +00:00
|
|
|
|
{
|
2021-09-24 22:14:04 +01:00
|
|
|
|
xmlAttr *attr;
|
|
|
|
|
|
|
2021-11-02 18:31:19 +00:00
|
|
|
|
process_node(n);
|
2021-09-24 22:14:04 +01:00
|
|
|
|
for (attr = n->properties; attr; attr = attr->next) {
|
2021-11-02 18:31:19 +00:00
|
|
|
|
xml_tree_walk(attr->children, server);
|
|
|
|
|
|
}
|
|
|
|
|
|
xml_tree_walk(n->children, server);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-08-11 18:43:51 +01:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
/*
|
|
|
|
|
|
* 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-14 19:46:03 +01:00
|
|
|
|
static bool parse_buf(struct server *server, struct buf *buf);
|
|
|
|
|
|
static int handle_pipemenu_readable(int fd, uint32_t mask, void *_ctx);
|
|
|
|
|
|
static int handle_pipemenu_timeout(void *_ctx);
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
|
parse_root_pipemenu(struct menu *top_level_menu, const char *execute)
|
|
|
|
|
|
{
|
|
|
|
|
|
int pipe_fd = 0;
|
|
|
|
|
|
pid_t pid = spawn_piped(execute, &pipe_fd);
|
|
|
|
|
|
if (pid <= 0) {
|
|
|
|
|
|
wlr_log(WLR_ERROR, "Failed to spawn pipe menu process %s", execute);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct menu_pipe_context *ctx = znew(*ctx);
|
|
|
|
|
|
ctx->server = top_level_menu->server;
|
|
|
|
|
|
ctx->top_level_menu = top_level_menu;
|
|
|
|
|
|
ctx->pid = pid;
|
|
|
|
|
|
ctx->pipe_fd = pipe_fd;
|
|
|
|
|
|
ctx->buf = BUF_INIT;
|
|
|
|
|
|
top_level_menu->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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-02 18:31:19 +00:00
|
|
|
|
/*
|
|
|
|
|
|
* <menu> elements have three different roles:
|
|
|
|
|
|
* * Definition of (sub)menu - has ID, LABEL and CONTENT
|
2023-10-31 21:36:45 +00:00
|
|
|
|
* * Menuitem of pipemenu type - has ID, LABEL and EXECUTE
|
2021-11-02 18:31:19 +00:00
|
|
|
|
* * 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");
|
2025-01-06 07:34:16 +01:00
|
|
|
|
char *icon_name = (char *)xmlGetProp(n, (const xmlChar *)"icon");
|
2021-11-02 18:31:19 +00:00
|
|
|
|
char *execute = (char *)xmlGetProp(n, (const xmlChar *)"execute");
|
|
|
|
|
|
char *id = (char *)xmlGetProp(n, (const xmlChar *)"id");
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
if (execute && label && id) {
|
|
|
|
|
|
wlr_log(WLR_DEBUG, "pipemenu '%s:%s:%s'", id, label, execute);
|
2024-07-15 21:47:10 +01:00
|
|
|
|
if (!current_menu) {
|
|
|
|
|
|
/*
|
2024-10-14 19:46:03 +01:00
|
|
|
|
* Handle pipemenu as the root-menu such this:
|
2024-07-15 21:47:10 +01:00
|
|
|
|
*
|
|
|
|
|
|
* <?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
|
* <openbox_menu>
|
|
|
|
|
|
* <menu id="root-menu" label="foo" execute="bar"/>
|
|
|
|
|
|
* </openbox_menu>
|
|
|
|
|
|
*/
|
2024-10-14 19:46:03 +01:00
|
|
|
|
struct menu *menu = menu_create(server, id, label);
|
|
|
|
|
|
parse_root_pipemenu(menu, execute);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
current_item = item_create(current_menu, label,
|
|
|
|
|
|
/* arrow */ true);
|
2025-01-06 07:34:16 +01:00
|
|
|
|
fill_item("icon", icon_name);
|
2024-10-14 19:46:03 +01:00
|
|
|
|
current_item_action = NULL;
|
|
|
|
|
|
current_item->execute = xstrdup(execute);
|
|
|
|
|
|
current_item->id = xstrdup(id);
|
2024-07-15 21:47:10 +01:00
|
|
|
|
}
|
2023-10-31 21:36:45 +00:00
|
|
|
|
} else if ((label && id) || is_toplevel_static_menu_definition(n, id)) {
|
2022-08-11 18:43:51 +01:00
|
|
|
|
/*
|
|
|
|
|
|
* (label && id) refers to <menu id="" label=""> which is an
|
|
|
|
|
|
* inline menu definition.
|
|
|
|
|
|
*
|
2023-10-31 21:36:45 +00:00
|
|
|
|
* is_toplevel_static_menu_definition() catches:
|
|
|
|
|
|
* <openbox_menu>
|
|
|
|
|
|
* <menu id=""></menu>
|
|
|
|
|
|
* </openbox_menu>
|
2022-08-11 18:43:51 +01:00
|
|
|
|
*
|
|
|
|
|
|
* 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"
|
2022-08-11 18:43:51 +01:00
|
|
|
|
* and "client-menu".
|
|
|
|
|
|
*/
|
2021-11-08 17:20:37 +00:00
|
|
|
|
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);
|
2022-06-12 21:14:48 +02:00
|
|
|
|
if (current_item) {
|
2025-01-06 07:34:16 +01:00
|
|
|
|
fill_item("icon", icon_name);
|
2022-06-12 21:14:48 +02:00
|
|
|
|
submenu = ¤t_item->submenu;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
submenu = NULL;
|
|
|
|
|
|
}
|
2021-11-08 17:20:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
++menu_level;
|
2021-11-02 18:31:19 +00:00
|
|
|
|
current_menu = menu_create(server, id, label);
|
2025-01-06 07:34:16 +01:00
|
|
|
|
if (icon_name) {
|
|
|
|
|
|
current_menu->icon_name = xstrdup(icon_name);
|
|
|
|
|
|
}
|
2021-11-08 17:20:37 +00:00
|
|
|
|
if (submenu) {
|
|
|
|
|
|
*submenu = current_menu;
|
|
|
|
|
|
}
|
|
|
|
|
|
traverse(n, server);
|
|
|
|
|
|
current_menu = current_menu->parent;
|
|
|
|
|
|
--menu_level;
|
2021-11-02 18:31:19 +00:00
|
|
|
|
} else if (id) {
|
2022-08-11 18:43:51 +01:00
|
|
|
|
/*
|
2023-10-31 21:36:45 +00:00
|
|
|
|
* <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.
|
2022-08-11 18:43:51 +01:00
|
|
|
|
*/
|
2023-10-31 21:36:45 +00:00
|
|
|
|
|
|
|
|
|
|
if (current_menu && current_menu->is_pipemenu) {
|
|
|
|
|
|
wlr_log(WLR_ERROR,
|
|
|
|
|
|
"cannot link to static menu from pipemenu");
|
|
|
|
|
|
goto error;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-09 20:59:04 +01:00
|
|
|
|
struct menu *menu = menu_get_by_id(server, id);
|
2024-11-14 00:25:49 +09:00
|
|
|
|
|
|
|
|
|
|
struct menu *iter = current_menu;
|
|
|
|
|
|
while (iter) {
|
|
|
|
|
|
if (iter == menu) {
|
|
|
|
|
|
wlr_log(WLR_ERROR, "menus with the same id '%s' "
|
|
|
|
|
|
"cannot be nested", id);
|
|
|
|
|
|
goto error;
|
|
|
|
|
|
}
|
|
|
|
|
|
iter = iter->parent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-02 18:31:19 +00:00
|
|
|
|
if (menu) {
|
2022-08-02 22:00:24 +01:00
|
|
|
|
current_item = item_create(current_menu, menu->label, true);
|
2022-06-12 21:14:48 +02:00
|
|
|
|
if (current_item) {
|
2025-01-06 07:34:16 +01:00
|
|
|
|
fill_item("icon", menu->icon_name);
|
2022-06-12 21:14:48 +02:00
|
|
|
|
current_item->submenu = menu;
|
|
|
|
|
|
}
|
2021-11-02 18:31:19 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
wlr_log(WLR_ERROR, "no menu with id '%s'", id);
|
|
|
|
|
|
}
|
2021-02-17 20:38:16 +00:00
|
|
|
|
}
|
2023-10-31 21:36:45 +00:00
|
|
|
|
error:
|
2023-10-09 20:46:23 +01:00
|
|
|
|
free(label);
|
2025-01-06 07:34:16 +01:00
|
|
|
|
free(icon_name);
|
2023-10-09 20:46:23 +01:00
|
|
|
|
free(execute);
|
|
|
|
|
|
free(id);
|
2021-02-17 20:38:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-06-22 21:07:25 +01: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);
|
2022-06-22 21:07:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2021-02-17 20:38:16 +00:00
|
|
|
|
static void
|
2021-11-02 18:31:19 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2021-11-02 18:31:19 +00:00
|
|
|
|
if (!strcasecmp((char *)n->name, "menu")) {
|
|
|
|
|
|
handle_menu_element(n, server);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2022-06-22 21:07:25 +01:00
|
|
|
|
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")) {
|
2024-06-13 21:00:44 +01:00
|
|
|
|
if (!current_menu) {
|
|
|
|
|
|
wlr_log(WLR_ERROR,
|
|
|
|
|
|
"ignoring <item> without parent <menu>");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2021-02-17 20:38:16 +00:00
|
|
|
|
in_item = true;
|
2021-11-02 18:31:19 +00:00
|
|
|
|
traverse(n, server);
|
2021-02-17 20:38:16 +00:00
|
|
|
|
in_item = false;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2021-11-02 18:31:19 +00:00
|
|
|
|
traverse(n, server);
|
2021-02-17 20:38:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
static bool
|
|
|
|
|
|
parse_buf(struct server *server, struct buf *buf)
|
|
|
|
|
|
{
|
2024-12-08 17:21:11 +00:00
|
|
|
|
int options = 0;
|
|
|
|
|
|
xmlDoc *d = xmlReadMemory(buf->data, buf->len, NULL, NULL, options);
|
2023-10-31 21:36:45 +00:00
|
|
|
|
if (!d) {
|
|
|
|
|
|
wlr_log(WLR_ERROR, "xmlParseMemory()");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
xml_tree_walk(xmlDocGetRootElement(d), server);
|
|
|
|
|
|
xmlFreeDoc(d);
|
|
|
|
|
|
xmlCleanupParser();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-09 20:43:43 +01:00
|
|
|
|
/*
|
|
|
|
|
|
* @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
|
2023-10-31 21:36:45 +00:00
|
|
|
|
parse_stream(struct server *server, FILE *stream)
|
2021-02-17 20:38:16 +00:00
|
|
|
|
{
|
|
|
|
|
|
char *line = NULL;
|
|
|
|
|
|
size_t len = 0;
|
2024-04-14 14:20:57 -04:00
|
|
|
|
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);
|
2023-10-31 21:36:45 +00:00
|
|
|
|
parse_buf(server, &b);
|
2024-04-14 14:20:57 -04:00
|
|
|
|
buf_reset(&b);
|
2021-02-17 20:38:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-09 20:43:43 +01: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) {
|
2024-07-08 11:04:57 -04:00
|
|
|
|
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);
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2023-10-09 20:43:43 +01:00
|
|
|
|
}
|
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);
|
2023-10-09 20:43:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-26 06:42:08 +09:00
|
|
|
|
/*
|
|
|
|
|
|
* Returns the box of a menuitem next to which its submenu is opened.
|
|
|
|
|
|
* This box can be shrunk or expanded by menu overlaps and borders.
|
2024-06-28 18:27:43 +01:00
|
|
|
|
*/
|
2023-10-31 21:13:41 +00:00
|
|
|
|
static struct wlr_box
|
2024-11-26 06:42:08 +09:00
|
|
|
|
get_item_anchor_rect(struct theme *theme, struct menuitem *item)
|
2023-10-31 21:13:41 +00:00
|
|
|
|
{
|
|
|
|
|
|
struct menu *menu = item->parent;
|
2024-11-26 06:42:08 +09:00
|
|
|
|
int menu_x = menu->scene_tree->node.x;
|
|
|
|
|
|
int menu_y = menu->scene_tree->node.y;
|
|
|
|
|
|
int overlap_x = theme->menu_overlap_x + theme->menu_border_width;
|
|
|
|
|
|
int overlap_y = theme->menu_overlap_y - theme->menu_border_width;
|
|
|
|
|
|
return (struct wlr_box) {
|
|
|
|
|
|
.x = menu_x + overlap_x,
|
|
|
|
|
|
.y = menu_y + item->tree->node.y + overlap_y,
|
|
|
|
|
|
.width = menu->size.width - 2 * overlap_x,
|
|
|
|
|
|
.height = theme->menu_item_height - 2 * overlap_y,
|
|
|
|
|
|
};
|
2023-10-31 21:13:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2021-11-02 18:31:19 +00:00
|
|
|
|
static void
|
2024-11-26 06:42:08 +09:00
|
|
|
|
menu_configure(struct menu *menu, struct wlr_box anchor_rect)
|
2020-10-19 22:14:17 +01:00
|
|
|
|
{
|
2021-11-08 17:36:39 +00:00
|
|
|
|
struct theme *theme = menu->server->theme;
|
|
|
|
|
|
|
2024-11-26 06:42:08 +09:00
|
|
|
|
/* Get output usable area to place the menu within */
|
|
|
|
|
|
struct output *output = output_nearest_to(menu->server,
|
|
|
|
|
|
anchor_rect.x, anchor_rect.y);
|
2023-02-08 23:40:59 -05:00
|
|
|
|
if (!output) {
|
2024-11-26 06:42:08 +09:00
|
|
|
|
wlr_log(WLR_ERROR, "no output found around (%d,%d)",
|
|
|
|
|
|
anchor_rect.x, anchor_rect.y);
|
2022-03-16 05:51:36 +01:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-11-26 06:42:08 +09:00
|
|
|
|
struct wlr_box usable = output_usable_area_in_layout_coords(output);
|
2022-02-19 02:05:38 +01:00
|
|
|
|
|
2024-11-26 06:42:08 +09:00
|
|
|
|
/* Policy for menu placement */
|
|
|
|
|
|
struct wlr_xdg_positioner_rules rules = {0};
|
|
|
|
|
|
rules.size.width = menu->size.width;
|
|
|
|
|
|
rules.size.height = menu->size.height;
|
|
|
|
|
|
/* A rectangle next to which the menu is opened */
|
|
|
|
|
|
rules.anchor_rect = anchor_rect;
|
|
|
|
|
|
/*
|
|
|
|
|
|
* Place menu at left or right side of anchor_rect, with their
|
|
|
|
|
|
* top edges aligned. The alignment is inherited from parent.
|
|
|
|
|
|
*/
|
|
|
|
|
|
if (menu->parent && menu->parent->align_left) {
|
|
|
|
|
|
rules.anchor = XDG_POSITIONER_ANCHOR_TOP_LEFT;
|
|
|
|
|
|
rules.gravity = XDG_POSITIONER_GRAVITY_BOTTOM_LEFT;
|
2022-02-19 02:05:38 +01:00
|
|
|
|
} else {
|
2024-11-26 06:42:08 +09:00
|
|
|
|
rules.anchor = XDG_POSITIONER_ANCHOR_TOP_RIGHT;
|
|
|
|
|
|
rules.gravity = XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT;
|
2022-02-19 02:05:38 +01:00
|
|
|
|
}
|
2024-11-26 06:42:08 +09:00
|
|
|
|
/* Flip or slide the menu when it overflows from the output */
|
|
|
|
|
|
rules.constraint_adjustment =
|
|
|
|
|
|
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X
|
|
|
|
|
|
| XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X
|
|
|
|
|
|
| XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y;
|
|
|
|
|
|
if (!menu->parent) {
|
|
|
|
|
|
/* Allow vertically flipping the root menu */
|
|
|
|
|
|
rules.constraint_adjustment |=
|
|
|
|
|
|
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y;
|
2021-02-17 20:38:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-26 06:42:08 +09:00
|
|
|
|
struct wlr_box box;
|
|
|
|
|
|
wlr_xdg_positioner_rules_get_geometry(&rules, &box);
|
|
|
|
|
|
wlr_xdg_positioner_rules_unconstrain_box(&rules, &usable, &box);
|
|
|
|
|
|
wlr_scene_node_set_position(&menu->scene_tree->node, box.x, box.y);
|
|
|
|
|
|
|
|
|
|
|
|
menu->align_left = (box.x < anchor_rect.x);
|
2023-10-31 21:36:45 +00:00
|
|
|
|
|
2022-02-19 02:05:38 +01:00
|
|
|
|
struct menuitem *item;
|
2023-08-30 02:37:08 +02:00
|
|
|
|
wl_list_for_each(item, &menu->menuitems, link) {
|
2022-02-19 02:05:38 +01:00
|
|
|
|
if (!item->submenu) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2024-11-26 06:42:08 +09:00
|
|
|
|
anchor_rect = get_item_anchor_rect(theme, item);
|
|
|
|
|
|
menu_configure(item->submenu, anchor_rect);
|
2022-02-19 02:05:38 +01:00
|
|
|
|
}
|
2021-11-02 18:31:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-06-15 01:49:55 +02:00
|
|
|
|
static void
|
2023-10-09 20:59:04 +01:00
|
|
|
|
menu_hide_submenu(struct server *server, const char *id)
|
2022-06-15 01:49:55 +02:00
|
|
|
|
{
|
|
|
|
|
|
struct menu *menu, *hide_menu;
|
2023-10-09 20:59:04 +01:00
|
|
|
|
hide_menu = menu_get_by_id(server, id);
|
2022-06-15 01:49:55 +02:00
|
|
|
|
if (!hide_menu) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2023-10-09 20:59:04 +01:00
|
|
|
|
wl_list_for_each(menu, &server->menus, link) {
|
2022-06-22 21:07:25 +01:00
|
|
|
|
struct menuitem *item, *next;
|
2023-08-30 02:37:08 +02:00
|
|
|
|
wl_list_for_each_safe(item, next, &menu->menuitems, link) {
|
2022-06-15 01:49:55 +02:00
|
|
|
|
if (item->submenu == hide_menu) {
|
|
|
|
|
|
item_destroy(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-11-10 18:54:01 +09:00
|
|
|
|
}
|
2022-06-15 01:49:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-18 19:01:28 -05:00
|
|
|
|
static void
|
|
|
|
|
|
init_client_send_to_menu(struct server *server)
|
|
|
|
|
|
{
|
|
|
|
|
|
/* Just create placeholder. Contents will be created when launched */
|
|
|
|
|
|
menu_create(server, "client-send-to-menu", "");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
* This is client-send-to-menu
|
|
|
|
|
|
* an internal menu similar to root-menu and client-menu
|
|
|
|
|
|
*
|
|
|
|
|
|
* This will look at workspaces and produce a menu
|
|
|
|
|
|
* with the workspace names that can be used with
|
|
|
|
|
|
* SendToDesktop, left/right options are included.
|
|
|
|
|
|
*/
|
|
|
|
|
|
void
|
|
|
|
|
|
update_client_send_to_menu(struct server *server)
|
|
|
|
|
|
{
|
|
|
|
|
|
struct menu *menu = menu_get_by_id(server,
|
|
|
|
|
|
"client-send-to-menu");
|
|
|
|
|
|
|
|
|
|
|
|
if (menu) {
|
|
|
|
|
|
struct menuitem *item, *next;
|
|
|
|
|
|
wl_list_for_each_safe(item, next, &menu->menuitems, link) {
|
|
|
|
|
|
item_destroy(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
menu->size.height = 0;
|
|
|
|
|
|
|
|
|
|
|
|
struct workspace *workspace;
|
|
|
|
|
|
|
2024-10-01 21:41:36 +01:00
|
|
|
|
wl_list_for_each(workspace, &server->workspaces.all, link) {
|
|
|
|
|
|
if (workspace == server->workspaces.current) {
|
2024-10-20 17:44:32 +09:00
|
|
|
|
char *label = strdup_printf(">%s<", workspace->name);
|
|
|
|
|
|
current_item = item_create(menu, label,
|
|
|
|
|
|
/*show arrow*/ false);
|
|
|
|
|
|
free(label);
|
2024-09-18 19:01:28 -05:00
|
|
|
|
} else {
|
|
|
|
|
|
current_item = item_create(menu, workspace->name, /*show arrow*/ false);
|
|
|
|
|
|
}
|
|
|
|
|
|
fill_item("name.action", "SendToDesktop");
|
|
|
|
|
|
fill_item("to.action", workspace->name);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-10 18:54:01 +09:00
|
|
|
|
menu_update_scene(menu);
|
2024-09-18 19:01:28 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-17 06:52:10 -05:00
|
|
|
|
static void
|
|
|
|
|
|
init_client_list_combined_menu(struct server *server)
|
|
|
|
|
|
{
|
|
|
|
|
|
/* Just create placeholder. Contents will be created when launched */
|
|
|
|
|
|
menu_create(server, "client-list-combined-menu", "");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
* This is client-list-combined-menu an internal menu similar to root-menu and
|
|
|
|
|
|
* client-menu.
|
|
|
|
|
|
*
|
|
|
|
|
|
* This will look at workspaces and produce a menu with the workspace name as a
|
|
|
|
|
|
* separator label and the titles of the view, if any, below each workspace
|
2025-01-08 23:35:23 -04:00
|
|
|
|
* name. Active view is indicated by "*" preceding title.
|
2024-09-17 06:52:10 -05:00
|
|
|
|
*/
|
|
|
|
|
|
void
|
|
|
|
|
|
update_client_list_combined_menu(struct server *server)
|
|
|
|
|
|
{
|
|
|
|
|
|
struct menu *menu = menu_get_by_id(server, "client-list-combined-menu");
|
|
|
|
|
|
|
|
|
|
|
|
if (!menu) {
|
|
|
|
|
|
/* Menu is created on compositor startup/reconfigure */
|
|
|
|
|
|
wlr_log(WLR_ERROR, "client-list-combined-menu does not exist");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct menuitem *item, *next;
|
|
|
|
|
|
wl_list_for_each_safe(item, next, &menu->menuitems, link) {
|
|
|
|
|
|
item_destroy(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
menu->size.height = 0;
|
|
|
|
|
|
|
|
|
|
|
|
struct workspace *workspace;
|
|
|
|
|
|
struct view *view;
|
|
|
|
|
|
struct buf buffer = BUF_INIT;
|
|
|
|
|
|
|
2024-10-01 21:41:36 +01:00
|
|
|
|
wl_list_for_each(workspace, &server->workspaces.all, link) {
|
|
|
|
|
|
buf_add_fmt(&buffer, workspace == server->workspaces.current ? ">%s<" : "%s",
|
2024-09-17 06:52:10 -05:00
|
|
|
|
workspace->name);
|
|
|
|
|
|
current_item = separator_create(menu, buffer.data);
|
|
|
|
|
|
buf_clear(&buffer);
|
|
|
|
|
|
|
|
|
|
|
|
wl_list_for_each(view, &server->views, link) {
|
|
|
|
|
|
if (view->workspace == workspace) {
|
2024-09-19 14:53:12 -05:00
|
|
|
|
const char *title = view_get_string_prop(view, "title");
|
2024-08-12 20:31:52 +02:00
|
|
|
|
if (!view->foreign_toplevel || string_null_or_empty(title)) {
|
2024-09-19 14:53:12 -05:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-17 06:52:10 -05:00
|
|
|
|
if (view == server->active_view) {
|
|
|
|
|
|
buf_add(&buffer, "*");
|
|
|
|
|
|
}
|
2024-09-19 14:53:12 -05:00
|
|
|
|
buf_add(&buffer, title);
|
2024-09-17 06:52:10 -05:00
|
|
|
|
|
|
|
|
|
|
current_item = item_create(menu, buffer.data, /*show arrow*/ false);
|
|
|
|
|
|
current_item->id = xstrdup(menu->id);
|
|
|
|
|
|
current_item->client_list_view = view;
|
|
|
|
|
|
fill_item("name.action", "Focus");
|
|
|
|
|
|
fill_item("name.action", "Raise");
|
|
|
|
|
|
buf_clear(&buffer);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
current_item = item_create(menu, _("Go there..."), /*show arrow*/ false);
|
|
|
|
|
|
current_item->id = xstrdup(menu->id);
|
|
|
|
|
|
fill_item("name.action", "GoToDesktop");
|
|
|
|
|
|
fill_item("to.action", workspace->name);
|
|
|
|
|
|
}
|
|
|
|
|
|
buf_reset(&buffer);
|
2024-11-10 18:54:01 +09:00
|
|
|
|
menu_update_scene(menu);
|
2024-09-17 06:52:10 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-06 11:54:55 +01:00
|
|
|
|
static void
|
|
|
|
|
|
init_rootmenu(struct server *server)
|
2021-11-02 18:31:19 +00:00
|
|
|
|
{
|
2023-10-09 20:59:04 +01:00
|
|
|
|
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", "");
|
2021-11-02 18:31:19 +00: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, _("Reconfigure"), false);
|
2022-01-05 09:11:24 +01:00
|
|
|
|
fill_item("name.action", "Reconfigure");
|
2022-08-02 22:00:24 +01:00
|
|
|
|
current_item = item_create(menu, _("Exit"), false);
|
2022-01-05 09:11:24 +01:00
|
|
|
|
fill_item("name.action", "Exit");
|
2021-02-17 20:38:16 +00:00
|
|
|
|
}
|
2020-10-19 22:14:17 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-06 11:54:55 +01:00
|
|
|
|
static void
|
|
|
|
|
|
init_windowmenu(struct server *server)
|
2022-01-26 00:07:10 +01:00
|
|
|
|
{
|
2023-10-09 20:59:04 +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");
|
2024-06-14 22:17:03 +01:00
|
|
|
|
current_item = item_create(menu, _("Roll Up/Down"), false);
|
2023-08-08 03:39:35 +02:00
|
|
|
|
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");
|
2023-12-19 21:24:55 +00:00
|
|
|
|
current_item = item_create(menu, _("Always on Top"), false);
|
2022-04-09 01:16:09 +02:00
|
|
|
|
fill_item("name.action", "ToggleAlwaysOnTop");
|
2022-06-15 02:02:15 +02:00
|
|
|
|
|
|
|
|
|
|
/* Workspace sub-menu */
|
|
|
|
|
|
struct menu *workspace_menu = menu_create(server, "workspaces", "");
|
2024-06-14 22:17:03 +01:00
|
|
|
|
current_item = item_create(workspace_menu, _("Move Left"), false);
|
2023-03-26 10:57:53 +01:00
|
|
|
|
/*
|
|
|
|
|
|
* <action name="SendToDesktop"><follow> is true by default so
|
|
|
|
|
|
* GoToDesktop will be called as part of the action.
|
|
|
|
|
|
*/
|
2022-06-15 02:02:15 +02:00
|
|
|
|
fill_item("name.action", "SendToDesktop");
|
|
|
|
|
|
fill_item("to.action", "left");
|
2024-06-14 22:17:03 +01:00
|
|
|
|
current_item = item_create(workspace_menu, _("Move Right"), false);
|
2022-06-15 02:02:15 +02:00
|
|
|
|
fill_item("name.action", "SendToDesktop");
|
|
|
|
|
|
fill_item("to.action", "right");
|
2023-12-19 21:24:55 +00:00
|
|
|
|
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);
|
2022-06-15 02:02:15 +02:00
|
|
|
|
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");
|
|
|
|
|
|
}
|
2022-06-15 01:49:55 +02:00
|
|
|
|
|
|
|
|
|
|
if (wl_list_length(&rc.workspace_config.workspaces) == 1) {
|
2023-10-09 20:59:04 +01:00
|
|
|
|
menu_hide_submenu(server, "workspaces");
|
2022-06-15 01:49:55 +02:00
|
|
|
|
}
|
2022-01-26 00:07:10 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-06 11:54:55 +01:00
|
|
|
|
void
|
|
|
|
|
|
menu_init(struct server *server)
|
|
|
|
|
|
{
|
2023-10-09 20:59:04 +01:00
|
|
|
|
wl_list_init(&server->menus);
|
2022-12-08 20:41:38 +00:00
|
|
|
|
parse_xml("menu.xml", server);
|
2022-12-06 11:54:55 +01:00
|
|
|
|
init_rootmenu(server);
|
|
|
|
|
|
init_windowmenu(server);
|
2024-09-17 06:52:10 -05:00
|
|
|
|
init_client_list_combined_menu(server);
|
2024-09-18 19:01:28 -05:00
|
|
|
|
init_client_send_to_menu(server);
|
2022-12-05 14:38:16 +01:00
|
|
|
|
post_processing(server);
|
2023-05-13 16:27:46 +02:00
|
|
|
|
validate(server);
|
2022-12-06 11:54:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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
|
|
|
|
{
|
2023-10-31 21:36:45 +00:00
|
|
|
|
/* Keep items clean on pipemenu destruction */
|
|
|
|
|
|
nullify_item_pointing_to_this_menu(menu);
|
|
|
|
|
|
|
2024-11-13 23:57:42 +09:00
|
|
|
|
if (menu->server->menu_current == menu) {
|
|
|
|
|
|
menu_close_root(menu->server);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
struct menuitem *item, *next;
|
|
|
|
|
|
wl_list_for_each_safe(item, next, &menu->menuitems, link) {
|
|
|
|
|
|
item_destroy(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-14 19:46:03 +01:00
|
|
|
|
if (menu->pipe_ctx) {
|
|
|
|
|
|
menu->pipe_ctx->top_level_menu = NULL;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
/*
|
|
|
|
|
|
* Destroying the root node will destroy everything,
|
|
|
|
|
|
* including node descriptors and scaled_font_buffers.
|
|
|
|
|
|
*/
|
2024-11-14 18:16:43 +09:00
|
|
|
|
if (menu->scene_tree) {
|
|
|
|
|
|
wlr_scene_node_destroy(&menu->scene_tree->node);
|
|
|
|
|
|
}
|
2023-10-31 21:36:45 +00:00
|
|
|
|
wl_list_remove(&menu->link);
|
2024-07-08 11:09:20 -04:00
|
|
|
|
zfree(menu->id);
|
|
|
|
|
|
zfree(menu->label);
|
2025-01-06 07:34:16 +01:00
|
|
|
|
zfree(menu->icon_name);
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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;
|
2023-10-09 20:59:04 +01:00
|
|
|
|
struct menu *menu, *tmp_menu;
|
|
|
|
|
|
wl_list_for_each_safe(menu, tmp_menu, &server->menus, link) {
|
2023-10-31 21:36:45 +00:00
|
|
|
|
if (menu == from) {
|
|
|
|
|
|
destroying = true;
|
2021-11-02 18:31:19 +00:00
|
|
|
|
}
|
2023-10-31 21:36:45 +00:00
|
|
|
|
if (!destroying) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
menu_free(menu);
|
2021-11-02 18:31:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
void
|
|
|
|
|
|
menu_finish(struct server *server)
|
|
|
|
|
|
{
|
|
|
|
|
|
menu_free_from(server, NULL);
|
2024-05-05 18:41:10 +09:00
|
|
|
|
|
|
|
|
|
|
/* Reset state vars for starting fresh when Reload is triggered */
|
|
|
|
|
|
current_item = NULL;
|
|
|
|
|
|
current_item_action = NULL;
|
|
|
|
|
|
current_menu = NULL;
|
2023-10-31 21:36:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-23 20:47:50 +01:00
|
|
|
|
void
|
|
|
|
|
|
menu_on_view_destroy(struct view *view)
|
|
|
|
|
|
{
|
|
|
|
|
|
struct server *server = view->server;
|
|
|
|
|
|
|
|
|
|
|
|
/* If the view being destroy has an open window menu, then close it */
|
|
|
|
|
|
if (server->menu_current
|
|
|
|
|
|
&& server->menu_current->triggered_by_view == view) {
|
|
|
|
|
|
menu_close_root(server);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
* TODO: Instead of just setting client_list_view to NULL and deleting
|
|
|
|
|
|
* the actions (as below), consider destroying the item and somehow
|
|
|
|
|
|
* updating the menu and its selection state.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
/* Also nullify the destroyed view in client-list-combined-menu */
|
|
|
|
|
|
struct menu *menu = menu_get_by_id(server, "client-list-combined-menu");
|
|
|
|
|
|
if (menu) {
|
|
|
|
|
|
struct menuitem *item;
|
|
|
|
|
|
wl_list_for_each(item, &menu->menuitems, link) {
|
|
|
|
|
|
if (item->client_list_view == view) {
|
|
|
|
|
|
item->client_list_view = NULL;
|
|
|
|
|
|
action_list_free(&item->actions);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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(
|
2024-12-03 15:58:07 +09:00
|
|
|
|
&menu->selection.item->normal_tree->node, true);
|
2022-02-19 02:05:38 +01:00
|
|
|
|
wlr_scene_node_set_enabled(
|
2024-12-03 15:58:07 +09:00
|
|
|
|
&menu->selection.item->selected_tree->node, false);
|
2022-02-19 02:05:38 +01:00
|
|
|
|
}
|
|
|
|
|
|
/* Set new selection */
|
|
|
|
|
|
if (item) {
|
2024-12-03 15:58:07 +09: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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00: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.
|
|
|
|
|
|
*/
|
2022-06-09 17:10:36 +02:00
|
|
|
|
static void
|
2023-10-31 21:36:45 +00:00
|
|
|
|
destroy_pipemenus(struct server *server)
|
2022-06-09 17:10:36 +02:00
|
|
|
|
{
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2022-06-09 17:10:36 +02:00
|
|
|
|
}
|
2023-10-31 21:36:45 +00:00
|
|
|
|
|
|
|
|
|
|
wlr_log(WLR_DEBUG, "number of menus after close=%d",
|
|
|
|
|
|
wl_list_length(&server->menus));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
|
_close(struct menu *menu)
|
|
|
|
|
|
{
|
2022-06-09 17:10:36 +02:00
|
|
|
|
wlr_scene_node_set_enabled(&menu->scene_tree->node, false);
|
|
|
|
|
|
menu_set_selection(menu, NULL);
|
|
|
|
|
|
if (menu->selection.menu) {
|
2023-10-31 21:36:45 +00:00
|
|
|
|
_close(menu->selection.menu);
|
2022-06-09 17:10:36 +02:00
|
|
|
|
menu->selection.menu = NULL;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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
|
2023-10-31 21:08:04 +00:00
|
|
|
|
menu_open_root(struct menu *menu, int x, int y)
|
2020-10-19 22:14:17 +01:00
|
|
|
|
{
|
2025-01-18 13:09:47 +09:00
|
|
|
|
assert(menu);
|
|
|
|
|
|
|
2024-12-28 17:03:26 +09:00
|
|
|
|
if (menu->server->input_mode != LAB_INPUT_STATE_PASSTHROUGH) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-01-18 13:09:47 +09:00
|
|
|
|
assert(!menu->server->menu_current);
|
|
|
|
|
|
|
2024-11-26 06:42:08 +09:00
|
|
|
|
menu_configure(menu, (struct wlr_box){.x = x, .y = y});
|
2022-02-19 02:05:38 +01:00
|
|
|
|
wlr_scene_node_set_enabled(&menu->scene_tree->node, true);
|
|
|
|
|
|
menu->server->menu_current = menu;
|
2023-10-31 21:36:45 +00:00
|
|
|
|
selected_item = NULL;
|
Clear keyboard/pointer focus on Move/Resize, window switcher and menu
The previous revert fixed the problem of stuck modifier keys with
keybinds in Blender, but made Firefox show its menu bar with Alt-*
keybinds. This is fundamentally inevitable due to the limitation of
wayland protocol, but at least for the default Alt-Tab keybind for
window switcher, we can mitigate this problem by clearing the keyboard
focus when the window switcher is activated. This is what KWin does, and
we decided to follow that.
So in this commit, keyboard and pointer focus are temporarily cleared
while Move/Resize, window switcher and menu interactions and restored
after them. We slightly deviate from KWin as KWin doesn't clear the
keyboard focus while Move/Resize, but it solves our existing problem
that Firefox shows its menu bar after dragging it with default Alt-Drag
mousebind, and this is what Mutter does.
We considered other solutions, but they don't work well:
1. Send wl_keyboard.{leave,enter} every time keybinds/mousebinds are
triggered. This solves the Firefox's menu bar problem, but that
sounds like a workaround and sending unnecessary events every time is
not desirable.
2. Send release events for both modifiers and keys even when they are
bound to keybinds. This is what Mutter is doing, but it looks like an
implementation issue and violates wayland protocol.
2024-12-29 00:48:55 +09:00
|
|
|
|
seat_focus_override_begin(&menu->server->seat,
|
|
|
|
|
|
LAB_INPUT_STATE_MENU, LAB_CURSOR_DEFAULT);
|
2023-10-31 21:36:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
2024-08-20 18:09:17 +02:00
|
|
|
|
create_pipe_menu(struct menu_pipe_context *ctx)
|
2023-10-31 21:36:45 +00:00
|
|
|
|
{
|
2024-10-14 19:46:03 +01:00
|
|
|
|
if (ctx->top_level_menu) {
|
|
|
|
|
|
/*
|
|
|
|
|
|
* We execute the scripts for the toplevel pipemenus at startup
|
|
|
|
|
|
* or Reconfigure, but they can be opened before they finish
|
|
|
|
|
|
* execution, usually with their content empty. Make sure they
|
|
|
|
|
|
* are closed and emptied.
|
|
|
|
|
|
*/
|
|
|
|
|
|
if (ctx->server->menu_current == ctx->top_level_menu) {
|
|
|
|
|
|
menu_close_root(ctx->server);
|
|
|
|
|
|
}
|
|
|
|
|
|
struct menuitem *item, *tmp;
|
|
|
|
|
|
wl_list_for_each_safe(item, tmp, &ctx->top_level_menu->menuitems, link) {
|
|
|
|
|
|
item_destroy(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
menu_level++;
|
|
|
|
|
|
current_menu = ctx->top_level_menu;
|
|
|
|
|
|
if (!parse_buf(ctx->server, &ctx->buf)) {
|
|
|
|
|
|
wlr_log(WLR_ERROR, "Failed to parse piped top level menu %s",
|
|
|
|
|
|
ctx->top_level_menu->id);
|
|
|
|
|
|
}
|
|
|
|
|
|
menu_level--;
|
|
|
|
|
|
post_processing(ctx->server);
|
|
|
|
|
|
validate(ctx->server);
|
|
|
|
|
|
menu_update_scene(current_menu);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-20 18:09:17 +02:00
|
|
|
|
assert(ctx->item);
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
struct menu *pipe_parent = ctx->item->parent;
|
|
|
|
|
|
if (!pipe_parent) {
|
2024-08-20 18:09:17 +02:00
|
|
|
|
wlr_log(WLR_INFO, "[pipemenu %ld] invalid parent",
|
2023-10-31 21:36:45 +00:00
|
|
|
|
(long)ctx->pid);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!pipe_parent->scene_tree->node.enabled) {
|
2024-08-20 18:09:17 +02:00
|
|
|
|
wlr_log(WLR_INFO, "[pipemenu %ld] parent menu already closed",
|
2023-10-31 21:36:45 +00:00
|
|
|
|
(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);
|
|
|
|
|
|
|
2024-11-26 06:42:08 +09:00
|
|
|
|
struct wlr_box anchor_rect =
|
|
|
|
|
|
get_item_anchor_rect(ctx->server->theme, ctx->item);
|
|
|
|
|
|
menu_configure(pipe_menu, anchor_rect);
|
2023-10-31 21:36:45 +00:00
|
|
|
|
|
|
|
|
|
|
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
|
2024-08-20 18:09:17 +02:00
|
|
|
|
pipemenu_ctx_destroy(struct menu_pipe_context *ctx)
|
2023-10-31 21:36:45 +00:00
|
|
|
|
{
|
|
|
|
|
|
wl_event_source_remove(ctx->event_read);
|
|
|
|
|
|
wl_event_source_remove(ctx->event_timeout);
|
|
|
|
|
|
spawn_piped_close(ctx->pid, ctx->pipe_fd);
|
2024-04-14 14:20:57 -04:00
|
|
|
|
buf_reset(&ctx->buf);
|
2024-08-20 18:09:17 +02:00
|
|
|
|
if (ctx->item) {
|
|
|
|
|
|
ctx->item->pipe_ctx = NULL;
|
|
|
|
|
|
}
|
2024-10-14 19:46:03 +01:00
|
|
|
|
if (ctx->top_level_menu) {
|
|
|
|
|
|
ctx->top_level_menu->pipe_ctx = NULL;
|
|
|
|
|
|
}
|
2023-10-31 21:36:45 +00:00
|
|
|
|
free(ctx);
|
|
|
|
|
|
waiting_for_pipe_menu = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static int
|
|
|
|
|
|
handle_pipemenu_timeout(void *_ctx)
|
|
|
|
|
|
{
|
2024-08-20 18:09:17 +02:00
|
|
|
|
struct menu_pipe_context *ctx = _ctx;
|
2023-10-31 21:36:45 +00:00
|
|
|
|
wlr_log(WLR_ERROR, "[pipemenu %ld] timeout reached, killing %s",
|
2024-08-20 18:09:17 +02:00
|
|
|
|
(long)ctx->pid, ctx->item ? ctx->item->execute : "n/a");
|
2023-10-31 21:36:45 +00:00
|
|
|
|
kill(ctx->pid, SIGTERM);
|
|
|
|
|
|
pipemenu_ctx_destroy(ctx);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static int
|
|
|
|
|
|
handle_pipemenu_readable(int fd, uint32_t mask, void *_ctx)
|
|
|
|
|
|
{
|
2024-08-20 18:09:17 +02:00
|
|
|
|
struct menu_pipe_context *ctx = _ctx;
|
2023-10-31 21:36:45 +00:00
|
|
|
|
/* two 4k pages + 1 NULL byte */
|
|
|
|
|
|
char data[8193];
|
|
|
|
|
|
ssize_t size;
|
|
|
|
|
|
|
2024-10-14 19:46:03 +01:00
|
|
|
|
if (!ctx->item && !ctx->top_level_menu) {
|
2024-08-20 18:09:17 +02:00
|
|
|
|
/* 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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)",
|
2024-10-14 19:46:03 +01:00
|
|
|
|
(long)ctx->pid, ctx->item ? ctx->item->execute : "n/a");
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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",
|
2024-10-14 19:46:03 +01:00
|
|
|
|
(long)ctx->pid, PIPEMENU_MAX_BUF_SIZE,
|
|
|
|
|
|
ctx->item ? ctx->item->execute : "n/a");
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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 */
|
2024-08-20 17:59:46 +01:00
|
|
|
|
if (!str_starts_with(ctx->buf.data, '<', " \t\r\n")) {
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-20 18:09:17 +02:00
|
|
|
|
if (item->pipe_ctx) {
|
|
|
|
|
|
wlr_log(WLR_ERROR, "item already has a pipe context attached");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
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;
|
2024-08-20 18:09:17 +02:00
|
|
|
|
struct menu_pipe_context *ctx = znew(*ctx);
|
2023-10-31 21:36:45 +00:00
|
|
|
|
ctx->server = item->parent->server;
|
|
|
|
|
|
ctx->item = item;
|
|
|
|
|
|
ctx->pid = pid;
|
|
|
|
|
|
ctx->pipe_fd = pipe_fd;
|
2024-04-14 14:20:57 -04:00
|
|
|
|
ctx->buf = BUF_INIT;
|
2024-08-20 18:09:17 +02:00
|
|
|
|
item->pipe_ctx = ctx;
|
2023-10-31 21:36:45 +00:00
|
|
|
|
|
|
|
|
|
|
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);
|
2021-11-02 18:31:19 +00:00
|
|
|
|
}
|
2020-10-19 22:14:17 +01:00
|
|
|
|
|
2023-08-30 11:20:46 +02:00
|
|
|
|
static void
|
|
|
|
|
|
menu_process_item_selection(struct menuitem *item)
|
2021-11-02 18:31:19 +00:00
|
|
|
|
{
|
2022-06-22 21:07:25 +01:00
|
|
|
|
assert(item);
|
|
|
|
|
|
|
2023-10-31 21:21:20 +00:00
|
|
|
|
/* Do not keep selecting the same item */
|
2023-10-31 21:36:45 +00:00
|
|
|
|
if (item == selected_item) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (waiting_for_pipe_menu) {
|
2023-10-31 21:21:20 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2023-10-31 21:36:45 +00:00
|
|
|
|
selected_item = item;
|
2023-10-31 21:21:20 +00:00
|
|
|
|
|
2022-06-22 21:07:25 +01:00
|
|
|
|
if (!item->selectable) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2022-02-19 02:05:38 +01:00
|
|
|
|
|
2023-10-31 21:36:45 +00: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
|
|
|
|
|
2023-10-31 21:36:45 +00: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) {
|
2022-06-09 17:10:36 +02:00
|
|
|
|
/* Sync the triggering view */
|
|
|
|
|
|
item->submenu->triggered_by_view = item->parent->triggered_by_view;
|
2023-08-30 11:20:46 +02:00
|
|
|
|
/* Ensure the submenu has its parent set correctly */
|
|
|
|
|
|
item->submenu->parent = item->parent;
|
2022-06-09 17:10:36 +02:00
|
|
|
|
/* 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);
|
2021-11-02 18:31:19 +00:00
|
|
|
|
}
|
2023-10-31 21:36:45 +00:00
|
|
|
|
|
2022-03-03 04:33:33 +01:00
|
|
|
|
item->parent->selection.menu = item->submenu;
|
2020-10-19 22:14:17 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-30 11:20:46 +02: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
|
|
|
|
{
|
2023-08-30 11:20:46 +02:00
|
|
|
|
struct menu *menu = server->menu_current;
|
|
|
|
|
|
if (!menu) {
|
|
|
|
|
|
return NULL;
|
|
|
|
|
|
}
|
2022-02-19 02:05:38 +01:00
|
|
|
|
|
2023-08-30 11:20:46 +02: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-12-28 22:44:59 +00:00
|
|
|
|
struct server *server = item->parent->server;
|
2023-10-31 21:36:45 +00:00
|
|
|
|
menu_close(server->menu_current);
|
2025-01-24 09:57:17 +09:00
|
|
|
|
server->menu_current = NULL;
|
Clear keyboard/pointer focus on Move/Resize, window switcher and menu
The previous revert fixed the problem of stuck modifier keys with
keybinds in Blender, but made Firefox show its menu bar with Alt-*
keybinds. This is fundamentally inevitable due to the limitation of
wayland protocol, but at least for the default Alt-Tab keybind for
window switcher, we can mitigate this problem by clearing the keyboard
focus when the window switcher is activated. This is what KWin does, and
we decided to follow that.
So in this commit, keyboard and pointer focus are temporarily cleared
while Move/Resize, window switcher and menu interactions and restored
after them. We slightly deviate from KWin as KWin doesn't clear the
keyboard focus while Move/Resize, but it solves our existing problem
that Firefox shows its menu bar after dragging it with default Alt-Drag
mousebind, and this is what Mutter does.
We considered other solutions, but they don't work well:
1. Send wl_keyboard.{leave,enter} every time keybinds/mousebinds are
triggered. This solves the Firefox's menu bar problem, but that
sounds like a workaround and sending unnecessary events every time is
not desirable.
2. Send release events for both modifiers and keys even when they are
bound to keybinds. This is what Mutter is doing, but it looks like an
implementation issue and violates wayland protocol.
2024-12-29 00:48:55 +09:00
|
|
|
|
seat_focus_override_end(&server->seat);
|
2023-12-28 22:44:59 +00:00
|
|
|
|
|
|
|
|
|
|
/*
|
2023-10-31 21:36:45 +00:00
|
|
|
|
* We call the actions after closing the menu so that virtual keyboard
|
2023-12-28 22:44:59 +00:00
|
|
|
|
* input is sent to the focused_surface instead of being absorbed by the
|
|
|
|
|
|
* menu. Consider for example: `wlrctl keyboard type abc`
|
2023-10-31 21:36:45 +00:00
|
|
|
|
*
|
|
|
|
|
|
* 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.
|
2023-12-28 22:44:59 +00:00
|
|
|
|
*/
|
2024-09-17 06:52:10 -05:00
|
|
|
|
if (item->id && !strcmp(item->id, "client-list-combined-menu")
|
|
|
|
|
|
&& item->client_list_view) {
|
2024-09-21 01:11:27 +09:00
|
|
|
|
actions_run(item->client_list_view, server, &item->actions, NULL);
|
2024-09-17 06:52:10 -05:00
|
|
|
|
} else {
|
|
|
|
|
|
actions_run(item->parent->triggered_by_view, server,
|
2024-09-21 01:11:27 +09:00
|
|
|
|
&item->actions, NULL);
|
2024-09-17 06:52:10 -05:00
|
|
|
|
}
|
2023-12-28 22:44:59 +00:00
|
|
|
|
|
2023-10-31 21:36:45 +00:00
|
|
|
|
destroy_pipemenus(server);
|
2022-03-03 04:33:33 +01:00
|
|
|
|
return true;
|
2020-10-19 22:14:17 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-30 11:20:46 +02: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
|
2022-06-09 17:10:36 +02:00
|
|
|
|
menu_close_root(struct server *server)
|
2020-10-19 22:14:17 +01:00
|
|
|
|
{
|
2022-06-09 17:10:36 +02:00
|
|
|
|
assert(server->input_mode == LAB_INPUT_STATE_MENU);
|
|
|
|
|
|
if (server->menu_current) {
|
|
|
|
|
|
menu_close(server->menu_current);
|
|
|
|
|
|
server->menu_current = NULL;
|
2023-10-31 21:36:45 +00:00
|
|
|
|
destroy_pipemenus(server);
|
2020-10-19 22:14:17 +01:00
|
|
|
|
}
|
Clear keyboard/pointer focus on Move/Resize, window switcher and menu
The previous revert fixed the problem of stuck modifier keys with
keybinds in Blender, but made Firefox show its menu bar with Alt-*
keybinds. This is fundamentally inevitable due to the limitation of
wayland protocol, but at least for the default Alt-Tab keybind for
window switcher, we can mitigate this problem by clearing the keyboard
focus when the window switcher is activated. This is what KWin does, and
we decided to follow that.
So in this commit, keyboard and pointer focus are temporarily cleared
while Move/Resize, window switcher and menu interactions and restored
after them. We slightly deviate from KWin as KWin doesn't clear the
keyboard focus while Move/Resize, but it solves our existing problem
that Firefox shows its menu bar after dragging it with default Alt-Drag
mousebind, and this is what Mutter does.
We considered other solutions, but they don't work well:
1. Send wl_keyboard.{leave,enter} every time keybinds/mousebinds are
triggered. This solves the Firefox's menu bar problem, but that
sounds like a workaround and sending unnecessary events every time is
not desirable.
2. Send release events for both modifiers and keys even when they are
bound to keybinds. This is what Mutter is doing, but it looks like an
implementation issue and violates wayland protocol.
2024-12-29 00:48:55 +09:00
|
|
|
|
seat_focus_override_end(&server->seat);
|
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
|
|
|
|
{
|
2023-10-09 20:59:04 +01:00
|
|
|
|
menu_finish(server);
|
2022-03-16 06:08:08 +01:00
|
|
|
|
server->menu_current = NULL;
|
2022-12-06 11:54:55 +01:00
|
|
|
|
menu_init(server);
|
2021-02-19 23:05:14 +00:00
|
|
|
|
}
|