menu: refactor parser

...with the same approach as rcxml.c

- `If` actions now works for menus
- `name` argument no longer have to be the first argument of <action>
- `label` argument no longer have to be the first argument of <item>
This commit is contained in:
tokyo4j 2025-08-04 12:55:13 +09:00 committed by Johan Malm
parent bfaab101af
commit 17d66e5603
2 changed files with 133 additions and 246 deletions

View file

@ -134,8 +134,7 @@ example: *LABWC_DEBUG_FOO=1 labwc*.
Increase logging of paths for config files (for example rc.xml,
autostart, environment and menu.xml) as well as titlebar buttons.
*LABWC_DEBUG_CONFIG_NODENAMES*++
*LABWC_DEBUG_MENU_NODENAMES*
*LABWC_DEBUG_CONFIG_NODENAMES*
Enable logging of all nodenames (for example *policy.placement: Cascade*
for *<placement><policy>Cascade</policy></placement>*) for config and
menu files respectively.

View file

@ -20,12 +20,12 @@
#include "common/lab-scene-rect.h"
#include "common/list.h"
#include "common/mem.h"
#include "common/nodename.h"
#include "common/scaled-font-buffer.h"
#include "common/scaled-icon-buffer.h"
#include "common/scene-helpers.h"
#include "common/spawn.h"
#include "common/string-helpers.h"
#include "common/xml.h"
#include "labwc.h"
#include "output.h"
#include "workspaces.h"
@ -38,15 +38,6 @@
#define ICON_SIZE (rc.theme->menu_item_height - 2 * rc.theme->menu_items_padding_y)
/* state-machine variables for processing <item></item> */
struct menu_parse_context {
struct server *server;
struct menu *menu;
struct menuitem *item;
struct action *action;
bool in_item;
};
static bool waiting_for_pipe_menu;
static struct menuitem *selected_item;
@ -140,7 +131,7 @@ validate(struct server *server)
}
static struct menuitem *
item_create(struct menu *menu, const char *text, bool show_arrow)
item_create(struct menu *menu, const char *text, const char *icon_name, bool show_arrow)
{
assert(menu);
assert(text);
@ -152,6 +143,13 @@ item_create(struct menu *menu, const char *text, bool show_arrow)
menuitem->text = xstrdup(text);
menuitem->arrow = show_arrow ? "" : NULL;
#if HAVE_LIBSFDO
if (rc.menu_show_icons && !string_null_or_empty(icon_name)) {
menuitem->icon_name = xstrdup(icon_name);
menu->has_icons = true;
}
#endif
menuitem->native_width = font_width(&rc.font_menuitem, text);
if (menuitem->arrow) {
menuitem->native_width += font_width(&rc.font_menuitem, menuitem->arrow);
@ -473,34 +471,22 @@ menu_create_scene(struct menu *menu)
* </item>
*/
static void
fill_item(struct menu_parse_context *ctx, const char *nodename,
const char *content)
fill_item(struct menu *menu, xmlNode *node)
{
/* <item label=""> defines the start of a new item */
if (!strcmp(nodename, "label")) {
ctx->item = item_create(ctx->menu, content, false);
ctx->action = NULL;
} else if (!ctx->item) {
wlr_log(WLR_ERROR, "expect <item label=\"\"> element first. "
"nodename: '%s' content: '%s'", nodename, content);
} else if (!strcmp(nodename, "icon")) {
#if HAVE_LIBSFDO
if (rc.menu_show_icons && !string_null_or_empty(content)) {
xstrdup_replace(ctx->item->icon_name, content);
ctx->menu->has_icons = true;
}
#endif
} else if (!strcmp(nodename, "name.action")) {
ctx->action = action_create(content);
if (ctx->action) {
wl_list_append(&ctx->item->actions, &ctx->action->link);
}
} else if (!ctx->action) {
wlr_log(WLR_ERROR, "expect <action name=\"\"> element first. "
"nodename: '%s' content: '%s'", nodename, content);
} else {
action_arg_from_xml_node(ctx->action, nodename, content);
char *label = (char *)xmlGetProp(node, (xmlChar *)"label");
char *icon_name = (char *)xmlGetProp(node, (xmlChar *)"icon");
if (!label) {
wlr_log(WLR_ERROR, "missing label in <item>");
goto out;
}
struct menuitem *item = item_create(menu, (char *)label, icon_name, false);
lab_xml_expand_dotted_attributes(node);
append_parsed_actions(node, &item->actions);
out:
free(label);
free(icon_name);
}
static void
@ -516,103 +502,10 @@ item_destroy(struct menuitem *item)
free(item);
}
/*
* We support XML CDATA for <command> in menu.xml in order to provide backward
* compatibility with obmenu-generator. For example:
*
* <menu id="" label="">
* <item label="">
* <action name="Execute">
* <command><![CDATA[xdg-open .]]></command>
* </action>
* </item>
* </menu>
*
* <execute> is an old, deprecated openbox variety of <command>. We support it
* for backward compatibility with old openbox-menu generators. It has the same
* function and <command>
*
* The following nodenames support CDATA.
* - command.action.item.*menu.openbox_menu
* - execute.action.item.*menu.openbox_menu
* - command.action.item.openbox_pipe_menu
* - execute.action.item.openbox_pipe_menu
* - command.action.item.*menu.openbox_pipe_menu
* - execute.action.item.*menu.openbox_pipe_menu
*
* The *menu allows nested menus with nodenames such as ...menu.menu... or
* ...menu.menu.menu... and so on. We could use match_glob() for all of the
* above but it seems simpler to just check the first three fields.
*/
static bool
nodename_supports_cdata(char *nodename)
{
return !strncmp("command.action.", nodename, 15)
|| !strncmp("execute.action.", nodename, 15);
}
static void
entry(struct menu_parse_context *ctx, xmlNode *node, char *nodename,
char *content)
{
if (!nodename) {
return;
}
xmlChar *cdata = NULL;
if (!content && nodename_supports_cdata(nodename)) {
cdata = xmlNodeGetContent(node);
}
if (!content && !cdata) {
return;
}
string_truncate_at_pattern(nodename, ".openbox_menu");
string_truncate_at_pattern(nodename, ".openbox_pipe_menu");
if (getenv("LABWC_DEBUG_MENU_NODENAMES")) {
printf("%s: %s\n", nodename, content ? content : (char *)cdata);
}
if (ctx->in_item) {
/*
* 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 '.item'
*/
string_truncate_at_pattern(nodename, ".item.menu");
string_truncate_at_pattern(nodename, ".item");
fill_item(ctx, nodename, content ? content : (char *)cdata);
}
xmlFree(cdata);
}
static void
process_node(struct menu_parse_context *ctx, xmlNode *node)
{
static char buffer[256];
char *content = (char *)node->content;
if (xmlIsBlankNode(node)) {
return;
}
char *name = nodename(node, buffer, sizeof(buffer));
entry(ctx, node, name, content);
}
static void xml_tree_walk(struct menu_parse_context *ctx, xmlNode *node);
static void
traverse(struct menu_parse_context *ctx, xmlNode *n)
{
xmlAttr *attr;
process_node(ctx, n);
for (attr = n->properties; attr; attr = attr->next) {
xml_tree_walk(ctx, attr->children);
}
xml_tree_walk(ctx, n->children);
}
static bool parse_buf(struct menu_parse_context *ctx, struct buf *buf);
static bool parse_buf(struct server *server, struct menu *menu, struct buf *buf);
static int handle_pipemenu_readable(int fd, uint32_t mask, void *_ctx);
static int handle_pipemenu_timeout(void *_ctx);
static void fill_menu_children(struct server *server, struct menu *parent, xmlNode *n);
/*
* <menu> elements have three different roles:
@ -621,7 +514,7 @@ static int handle_pipemenu_timeout(void *_ctx);
* * Menuitem of submenu type - has ID only
*/
static void
handle_menu_element(struct menu_parse_context *ctx, xmlNode *n)
fill_menu(struct server *server, struct menu *parent, xmlNode *n)
{
char *label = (char *)xmlGetProp(n, (const xmlChar *)"label");
char *icon_name = (char *)xmlGetProp(n, (const xmlChar *)"icon");
@ -636,10 +529,9 @@ handle_menu_element(struct menu_parse_context *ctx, xmlNode *n)
if (execute && label) {
wlr_log(WLR_DEBUG, "pipemenu '%s:%s:%s'", id, label, execute);
struct menu *pipemenu =
menu_create(ctx->server, ctx->menu, id, label);
struct menu *pipemenu = menu_create(server, parent, id, label);
pipemenu->execute = xstrdup(execute);
if (!ctx->menu) {
if (!parent) {
/*
* A pipemenu may not have its parent like:
*
@ -649,18 +541,16 @@ handle_menu_element(struct menu_parse_context *ctx, xmlNode *n)
* </openbox_menu>
*/
} else {
ctx->item = item_create(ctx->menu, label,
/* arrow */ true);
fill_item(ctx, "icon", icon_name);
ctx->action = NULL;
ctx->item->submenu = pipemenu;
struct menuitem *item = item_create(parent, label,
icon_name, /* arrow */ true);
item->submenu = pipemenu;
}
} else if ((label && ctx->menu) || !ctx->menu) {
} else if ((label && parent) || !parent) {
/*
* (label && ctx->menu) refers to <menu id="" label="">
* (label && parent) refers to <menu id="" label="">
* which is an nested (inline) menu definition.
*
* (!ctx->menu) catches:
* (!parent) catches:
* <openbox_menu>
* <menu id=""></menu>
* </openbox_menu>
@ -676,22 +566,20 @@ handle_menu_element(struct menu_parse_context *ctx, xmlNode *n)
* attribute to make it easier for users to define "root-menu"
* and "client-menu".
*/
struct menu *parent_menu = ctx->menu;
ctx->menu = menu_create(ctx->server, parent_menu, id, label);
struct menu *menu = menu_create(server, parent, id, label);
if (icon_name) {
ctx->menu->icon_name = xstrdup(icon_name);
menu->icon_name = xstrdup(icon_name);
}
if (label && parent_menu) {
if (label && parent) {
/*
* In a nested (inline) menu definition we need to
* create an item pointing to the new submenu
*/
ctx->item = item_create(parent_menu, label, true);
fill_item(ctx, "icon", icon_name);
ctx->item->submenu = ctx->menu;
struct menuitem *item = item_create(parent, label,
icon_name, true);
item->submenu = menu;
}
traverse(ctx, n);
ctx->menu = parent_menu;
fill_menu_children(server, menu, n);
} else {
/*
* <menu id=""> (when inside another <menu> element) creates an
@ -708,13 +596,13 @@ handle_menu_element(struct menu_parse_context *ctx, xmlNode *n)
goto error;
}
struct menu *menu = menu_get_by_id(ctx->server, id);
struct menu *menu = menu_get_by_id(server, id);
if (!menu) {
wlr_log(WLR_ERROR, "no menu with id '%s'", id);
goto error;
}
struct menu *iter = ctx->menu;
struct menu *iter = parent;
while (iter) {
if (iter == menu) {
wlr_log(WLR_ERROR, "menus with the same id '%s' "
@ -724,9 +612,9 @@ handle_menu_element(struct menu_parse_context *ctx, xmlNode *n)
iter = iter->parent;
}
ctx->item = item_create(ctx->menu, menu->label, true);
fill_item(ctx, "icon", menu->icon_name);
ctx->item->submenu = menu;
struct menuitem *item = item_create(parent, menu->label,
parent->icon_name, true);
item->submenu = menu;
}
error:
free(label);
@ -737,50 +625,42 @@ error:
/* This can be one of <separator> and <separator label=""> */
static void
handle_separator_element(struct menu_parse_context *ctx, xmlNode *n)
fill_separator(struct menu *menu, xmlNode *n)
{
char *label = (char *)xmlGetProp(n, (const xmlChar *)"label");
ctx->item = separator_create(ctx->menu, label);
separator_create(menu, label);
free(label);
}
/* parent==NULL when processing toplevel menus in menu.xml */
static void
xml_tree_walk(struct menu_parse_context *ctx, xmlNode *node)
fill_menu_children(struct server *server, struct menu *parent, xmlNode *n)
{
for (xmlNode *n = node; n && n->name; n = n->next) {
if (!strcasecmp((char *)n->name, "comment")) {
continue;
}
if (!strcasecmp((char *)n->name, "menu")) {
handle_menu_element(ctx, n);
continue;
}
if (!strcasecmp((char *)n->name, "separator")) {
if (!ctx->menu) {
xmlNode *child;
char *key, *content;
LAB_XML_FOR_EACH(n, child, key, content) {
if (!strcasecmp(key, "menu")) {
fill_menu(server, parent, child);
} else if (!strcasecmp(key, "separator")) {
if (!parent) {
wlr_log(WLR_ERROR,
"ignoring <separator> without parent <menu>");
continue;
}
handle_separator_element(ctx, n);
continue;
}
if (!strcasecmp((char *)n->name, "item")) {
if (!ctx->menu) {
fill_separator(parent, child);
} else if (!strcasecmp(key, "item")) {
if (!parent) {
wlr_log(WLR_ERROR,
"ignoring <item> without parent <menu>");
continue;
}
ctx->in_item = true;
traverse(ctx, n);
ctx->in_item = false;
continue;
fill_item(parent, child);
}
traverse(ctx, n);
}
}
static bool
parse_buf(struct menu_parse_context *ctx, struct buf *buf)
parse_buf(struct server *server, struct menu *parent, struct buf *buf)
{
int options = 0;
xmlDoc *d = xmlReadMemory(buf->data, buf->len, NULL, NULL, options);
@ -788,7 +668,10 @@ parse_buf(struct menu_parse_context *ctx, struct buf *buf)
wlr_log(WLR_ERROR, "xmlParseMemory()");
return false;
}
xml_tree_walk(ctx, xmlDocGetRootElement(d));
xmlNode *root = xmlDocGetRootElement(d);
fill_menu_children(server, parent, root);
xmlFreeDoc(d);
xmlCleanupParser();
return true;
@ -814,8 +697,7 @@ parse_stream(struct server *server, FILE *stream)
buf_add(&b, line);
}
free(line);
struct menu_parse_context ctx = {.server = server};
parse_buf(&ctx, &b);
parse_buf(server, NULL, &b);
buf_reset(&b);
}
@ -939,6 +821,14 @@ init_client_send_to_menu(struct server *server)
menu_create(server, NULL, "client-send-to-menu", "");
}
static struct action *
item_add_action(struct menuitem *item, const char *action_name)
{
struct action *action = action_create(action_name);
wl_list_append(&item->actions, &action->link);
return action;
}
/*
* This is client-send-to-menu
* an internal menu similar to root-menu and client-menu
@ -955,21 +845,22 @@ update_client_send_to_menu(struct server *server)
reset_menu(menu);
struct menu_parse_context ctx = {.server = server};
struct workspace *workspace;
wl_list_for_each(workspace, &server->workspaces.all, link) {
struct buf buf = BUF_INIT;
if (workspace == server->workspaces.current) {
char *label = strdup_printf(">%s<", workspace->name);
ctx.item = item_create(menu, label,
/*show arrow*/ false);
free(label);
buf_add_fmt(&buf, ">%s<", workspace->name);
} else {
ctx.item = item_create(menu, workspace->name,
/*show arrow*/ false);
buf_add(&buf, workspace->name);
}
fill_item(&ctx, "name.action", "SendToDesktop");
fill_item(&ctx, "to.action", workspace->name);
struct menuitem *item = item_create(menu, buf.data,
NULL, /*show arrow*/ false);
struct action *action = item_add_action(item, "SendToDesktop");
action_arg_add_str(action, "to", "name");
buf_clear(&buf);
}
menu_create_scene(menu);
@ -998,7 +889,7 @@ update_client_list_combined_menu(struct server *server)
reset_menu(menu);
struct menu_parse_context ctx = {.server = server};
struct menuitem *item;
struct workspace *workspace;
struct view *view;
struct buf buffer = BUF_INIT;
@ -1006,7 +897,7 @@ update_client_list_combined_menu(struct server *server)
wl_list_for_each(workspace, &server->workspaces.all, link) {
buf_add_fmt(&buffer, workspace == server->workspaces.current ? ">%s<" : "%s",
workspace->name);
ctx.item = separator_create(menu, buffer.data);
separator_create(menu, buffer.data);
buf_clear(&buffer);
wl_list_for_each(view, &server->views, link) {
@ -1021,19 +912,19 @@ update_client_list_combined_menu(struct server *server)
}
buf_add(&buffer, title);
ctx.item = item_create(menu, buffer.data,
item = item_create(menu, buffer.data, NULL,
/*show arrow*/ false);
ctx.item->client_list_view = view;
fill_item(&ctx, "name.action", "Focus");
fill_item(&ctx, "name.action", "Raise");
item->client_list_view = view;
item_add_action(item, "Focus");
item_add_action(item, "Raise");
buf_clear(&buffer);
menu->has_icons = true;
}
}
ctx.item = item_create(menu, _("Go there..."),
item = item_create(menu, _("Go there..."), NULL,
/*show arrow*/ false);
fill_item(&ctx, "name.action", "GoToDesktop");
fill_item(&ctx, "to.action", workspace->name);
struct action *action = item_add_action(item, "GoToDesktop");
action_arg_add_str(action, "to", workspace->name);
}
buf_reset(&buffer);
menu_create_scene(menu);
@ -1043,22 +934,22 @@ static void
init_rootmenu(struct server *server)
{
struct menu *menu = menu_get_by_id(server, "root-menu");
struct menuitem *item;
/* Default menu if no menu.xml found */
if (!menu) {
struct menu_parse_context ctx = {.server = server};
menu = menu_create(server, NULL, "root-menu", "");
ctx.item = item_create(menu, _("Terminal"), false);
fill_item(&ctx, "name.action", "Execute");
fill_item(&ctx, "command.action", "lab-sensible-terminal");
item = item_create(menu, _("Terminal"), NULL, false);
struct action *action = item_add_action(item, "Execute");
action_arg_add_str(action, "command", "lab-sensible-terminal");
ctx.item = separator_create(menu, NULL);
separator_create(menu, NULL);
ctx.item = item_create(menu, _("Reconfigure"), false);
fill_item(&ctx, "name.action", "Reconfigure");
ctx.item = item_create(menu, _("Exit"), false);
fill_item(&ctx, "name.action", "Exit");
item = item_create(menu, _("Reconfigure"), NULL, false);
item_add_action(item, "Reconfigure");
item = item_create(menu, _("Exit"), NULL, false);
item_add_action(item, "Exit");
}
}
@ -1066,47 +957,48 @@ static void
init_windowmenu(struct server *server)
{
struct menu *menu = menu_get_by_id(server, "client-menu");
struct menuitem *item;
struct action *action;
/* Default menu if no menu.xml found */
if (!menu) {
struct menu_parse_context ctx = {.server = server};
menu = menu_create(server, NULL, "client-menu", "");
ctx.item = item_create(menu, _("Minimize"), false);
fill_item(&ctx, "name.action", "Iconify");
ctx.item = item_create(menu, _("Maximize"), false);
fill_item(&ctx, "name.action", "ToggleMaximize");
ctx.item = item_create(menu, _("Fullscreen"), false);
fill_item(&ctx, "name.action", "ToggleFullscreen");
ctx.item = item_create(menu, _("Roll Up/Down"), false);
fill_item(&ctx, "name.action", "ToggleShade");
ctx.item = item_create(menu, _("Decorations"), false);
fill_item(&ctx, "name.action", "ToggleDecorations");
ctx.item = item_create(menu, _("Always on Top"), false);
fill_item(&ctx, "name.action", "ToggleAlwaysOnTop");
item = item_create(menu, _("Minimize"), NULL, false);
item_add_action(item, "Iconify");
item = item_create(menu, _("Maximize"), NULL, false);
item_add_action(item, "ToggleMaximize");
item = item_create(menu, _("Fullscreen"), NULL, false);
item_add_action(item, "ToggleFullscreen");
item = item_create(menu, _("Roll Up/Down"), NULL, false);
item_add_action(item, "ToggleShade");
item = item_create(menu, _("Decorations"), NULL, false);
item_add_action(item, "ToggleDecorations");
item = item_create(menu, _("Always on Top"), NULL, false);
item_add_action(item, "ToggleAlwaysOnTop");
/* Workspace sub-menu */
struct menu *workspace_menu =
menu_create(server, NULL, "workspaces", "");
ctx.item = item_create(workspace_menu, _("Move Left"), false);
item = item_create(workspace_menu, _("Move Left"), NULL, false);
/*
* <action name="SendToDesktop"><follow> is true by default so
* GoToDesktop will be called as part of the action.
*/
fill_item(&ctx, "name.action", "SendToDesktop");
fill_item(&ctx, "to.action", "left");
ctx.item = item_create(workspace_menu, _("Move Right"), false);
fill_item(&ctx, "name.action", "SendToDesktop");
fill_item(&ctx, "to.action", "right");
ctx.item = separator_create(workspace_menu, "");
ctx.item = item_create(workspace_menu,
_("Always on Visible Workspace"), false);
fill_item(&ctx, "name.action", "ToggleOmnipresent");
action = item_add_action(item, "SendToDesktop");
action_arg_add_str(action, "to", "left");
item = item_create(workspace_menu, _("Move Right"), NULL, false);
action = item_add_action(item, "SendToDesktop");
action_arg_add_str(action, "to", "right");
separator_create(workspace_menu, "");
item = item_create(workspace_menu,
_("Always on Visible Workspace"), NULL, false);
item_add_action(item, "ToggleOmnipresent");
ctx.item = item_create(menu, _("Workspace"), true);
ctx.item->submenu = workspace_menu;
item = item_create(menu, _("Workspace"), NULL, true);
item->submenu = workspace_menu;
ctx.item = item_create(menu, _("Close"), false);
fill_item(&ctx, "name.action", "Close");
item = item_create(menu, _("Close"), NULL, false);
item_add_action(item, "Close");
}
if (wl_list_length(&rc.workspace_config.workspaces) == 1) {
@ -1348,11 +1240,7 @@ static void
create_pipe_menu(struct menu_pipe_context *ctx)
{
struct server *server = ctx->pipemenu->server;
struct menu_parse_context parse_ctx = {
.server = server,
.menu = ctx->pipemenu,
};
if (!parse_buf(&parse_ctx, &ctx->buf)) {
if (!parse_buf(server, ctx->pipemenu, &ctx->buf)) {
return;
}
/* TODO: apply validate() only for generated pipemenus */