labwc/src/action.c
Consolatis 43db75f43c Restore SnapToEdge and MoveToEdge default keybinds
When adding the fallback keybinds we add them as string but expect them being an int.
This commit fixes that by using the same parsing routines that are used when parsing
user supplied configuration.

Fixes 1ee8715d57
actions: use enum for _ToEdge action
2023-08-06 10:37:36 +02:00

761 lines
18 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
#define _POSIX_C_SOURCE 200809L
#include <assert.h>
#include <signal.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <wlr/util/log.h>
#include "action.h"
#include "common/list.h"
#include "common/mem.h"
#include "common/parse-bool.h"
#include "common/spawn.h"
#include "common/string-helpers.h"
#include "debug.h"
#include "labwc.h"
#include "menu/menu.h"
#include "regions.h"
#include "ssd.h"
#include "view.h"
#include "workspaces.h"
enum action_arg_type {
LAB_ACTION_ARG_STR = 0,
LAB_ACTION_ARG_BOOL,
LAB_ACTION_ARG_INT,
};
struct action_arg {
struct wl_list link; /* struct action.args */
char *key; /* May be NULL if there is just one arg */
enum action_arg_type type;
};
struct action_arg_str {
struct action_arg base;
char *value;
};
struct action_arg_bool {
struct action_arg base;
bool value;
};
struct action_arg_int {
struct action_arg base;
int value;
};
enum action_type {
ACTION_TYPE_INVALID = 0,
ACTION_TYPE_NONE,
ACTION_TYPE_CLOSE,
ACTION_TYPE_KILL,
ACTION_TYPE_DEBUG,
ACTION_TYPE_EXECUTE,
ACTION_TYPE_EXIT,
ACTION_TYPE_MOVE_TO_EDGE,
ACTION_TYPE_SNAP_TO_EDGE,
ACTION_TYPE_NEXT_WINDOW,
ACTION_TYPE_PREVIOUS_WINDOW,
ACTION_TYPE_RECONFIGURE,
ACTION_TYPE_SHOW_MENU,
ACTION_TYPE_TOGGLE_MAXIMIZE,
ACTION_TYPE_MAXIMIZE,
ACTION_TYPE_TOGGLE_FULLSCREEN,
ACTION_TYPE_TOGGLE_DECORATIONS,
ACTION_TYPE_TOGGLE_ALWAYS_ON_TOP,
ACTION_TYPE_TOGGLE_ALWAYS_ON_BOTTOM,
ACTION_TYPE_FOCUS,
ACTION_TYPE_ICONIFY,
ACTION_TYPE_MOVE,
ACTION_TYPE_RAISE,
ACTION_TYPE_LOWER,
ACTION_TYPE_RESIZE,
ACTION_TYPE_RESIZE_RELATIVE,
ACTION_TYPE_MOVETO,
ACTION_TYPE_MOVE_RELATIVE,
ACTION_TYPE_SEND_TO_DESKTOP,
ACTION_TYPE_GO_TO_DESKTOP,
ACTION_TYPE_SNAP_TO_REGION,
ACTION_TYPE_TOGGLE_KEYBINDS,
ACTION_TYPE_FOCUS_OUTPUT,
};
const char *action_names[] = {
"INVALID",
"None",
"Close",
"Kill",
"Debug",
"Execute",
"Exit",
"MoveToEdge",
"SnapToEdge",
"NextWindow",
"PreviousWindow",
"Reconfigure",
"ShowMenu",
"ToggleMaximize",
"Maximize",
"ToggleFullscreen",
"ToggleDecorations",
"ToggleAlwaysOnTop",
"ToggleAlwaysOnBottom",
"Focus",
"Iconify",
"Move",
"Raise",
"Lower",
"Resize",
"ResizeRelative",
"MoveTo",
"MoveRelative",
"SendToDesktop",
"GoToDesktop",
"SnapToRegion",
"ToggleKeybinds",
"FocusOutput",
NULL
};
void
action_arg_add_str(struct action *action, const char *key, const char *value)
{
assert(action);
assert(key);
assert(value && "Tried to add NULL action string argument");
struct action_arg_str *arg = znew(*arg);
arg->base.type = LAB_ACTION_ARG_STR;
arg->base.key = xstrdup(key);
arg->value = xstrdup(value);
wl_list_append(&action->args, &arg->base.link);
}
static void
action_arg_add_bool(struct action *action, const char *key, bool value)
{
assert(action);
assert(key);
struct action_arg_bool *arg = znew(*arg);
arg->base.type = LAB_ACTION_ARG_BOOL;
arg->base.key = xstrdup(key);
arg->value = value;
wl_list_append(&action->args, &arg->base.link);
}
static void
action_arg_add_int(struct action *action, const char *key, int value)
{
assert(action);
assert(key);
struct action_arg_int *arg = znew(*arg);
arg->base.type = LAB_ACTION_ARG_INT;
arg->base.key = xstrdup(key);
arg->value = value;
wl_list_append(&action->args, &arg->base.link);
}
void
action_arg_from_xml_node(struct action *action, const char *nodename, const char *content)
{
assert(action);
char *argument = xstrdup(nodename);
string_truncate_at_pattern(argument, ".action");
switch (action->type) {
case ACTION_TYPE_EXECUTE:
/*
* <action name="Execute"> with an <execute> child is
* deprecated, but we support it anyway for backward
* compatibility with old openbox-menu generators
*/
if (!strcmp(argument, "command") || !strcmp(argument, "execute")) {
action_arg_add_str(action, "command", content);
goto cleanup;
}
break;
case ACTION_TYPE_MOVE_TO_EDGE:
case ACTION_TYPE_SNAP_TO_EDGE:
if (!strcmp(argument, "direction")) {
enum view_edge edge = view_edge_parse(content);
if ((edge == VIEW_EDGE_CENTER && action->type != ACTION_TYPE_SNAP_TO_EDGE)
|| edge == VIEW_EDGE_INVALID) {
wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)",
action_names[action->type], argument, content);
} else {
action_arg_add_int(action, argument, edge);
}
goto cleanup;
}
break;
case ACTION_TYPE_SHOW_MENU:
if (!strcmp(argument, "menu")) {
action_arg_add_str(action, argument, content);
goto cleanup;
}
break;
case ACTION_TYPE_RESIZE_RELATIVE:
if (!strcmp(argument, "left") || !strcmp(argument, "right") ||
!strcmp(argument, "top") || !strcmp(argument, "bottom")) {
action_arg_add_int(action, argument, atoi(content));
goto cleanup;
}
break;
case ACTION_TYPE_MOVETO:
case ACTION_TYPE_MOVE_RELATIVE:
if (!strcmp(argument, "x") || !strcmp(argument, "y")) {
action_arg_add_int(action, argument, atoi(content));
goto cleanup;
}
break;
case ACTION_TYPE_SEND_TO_DESKTOP:
if (!strcmp(argument, "follow")) {
action_arg_add_bool(action, argument, parse_bool(content, true));
goto cleanup;
}
/* Falls through to GoToDesktop */
case ACTION_TYPE_GO_TO_DESKTOP:
if (!strcmp(argument, "to")) {
action_arg_add_str(action, argument, content);
goto cleanup;
}
if (!strcmp(argument, "wrap")) {
action_arg_add_bool(action, argument, parse_bool(content, true));
goto cleanup;
}
break;
case ACTION_TYPE_SNAP_TO_REGION:
if (!strcmp(argument, "region")) {
action_arg_add_str(action, argument, content);
goto cleanup;
}
break;
case ACTION_TYPE_FOCUS_OUTPUT:
if (!strcmp(argument, "output")) {
action_arg_add_str(action, argument, content);
goto cleanup;
}
break;
}
wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s'",
action_names[action->type], argument);
cleanup:
free(argument);
}
static const char *
action_str_from_arg(struct action_arg *arg)
{
assert(arg->type == LAB_ACTION_ARG_STR);
return ((struct action_arg_str *)arg)->value;
}
static bool
arg_value_exists(struct action *action, const char *key, enum action_arg_type type)
{
assert(action);
assert(key);
struct action_arg *arg;
wl_list_for_each(arg, &action->args, link) {
if (!strcasecmp(key, arg->key) && arg->type == type) {
return true;
}
}
return false;
}
static const char *
get_arg_value_str(struct action *action, const char *key, const char *default_value)
{
assert(action);
assert(key);
struct action_arg *arg;
wl_list_for_each(arg, &action->args, link) {
if (!strcasecmp(key, arg->key)) {
return action_str_from_arg(arg);
}
}
return default_value;
}
static bool
get_arg_value_bool(struct action *action, const char *key, bool default_value)
{
assert(action);
assert(key);
struct action_arg *arg;
wl_list_for_each(arg, &action->args, link) {
if (!strcasecmp(key, arg->key)) {
assert(arg->type == LAB_ACTION_ARG_BOOL);
return ((struct action_arg_bool *)arg)->value;
}
}
return default_value;
}
static int
get_arg_value_int(struct action *action, const char *key, int default_value)
{
assert(action);
assert(key);
struct action_arg *arg;
wl_list_for_each(arg, &action->args, link) {
if (!strcasecmp(key, arg->key)) {
assert(arg->type == LAB_ACTION_ARG_INT);
return ((struct action_arg_int *)arg)->value;
}
}
return default_value;
}
static struct action_arg *
action_get_first_arg(struct action *action)
{
struct action_arg *arg;
struct wl_list *item = action->args.next;
if (item == &action->args) {
return NULL;
}
return wl_container_of(item, arg, link);
}
static enum action_type
action_type_from_str(const char *action_name)
{
for (size_t i = 1; action_names[i]; i++) {
if (!strcasecmp(action_name, action_names[i])) {
return i;
}
}
wlr_log(WLR_ERROR, "Invalid action: %s", action_name);
return ACTION_TYPE_INVALID;
}
struct action *
action_create(const char *action_name)
{
if (!action_name) {
wlr_log(WLR_ERROR, "action name not specified");
return NULL;
}
enum action_type action_type = action_type_from_str(action_name);
if (action_type == ACTION_TYPE_NONE) {
return NULL;
}
struct action *action = znew(*action);
action->type = action_type;
wl_list_init(&action->args);
return action;
}
bool
actions_contain_toggle_keybinds(struct wl_list *action_list)
{
struct action *action;
wl_list_for_each(action, action_list, link) {
if (action->type == ACTION_TYPE_TOGGLE_KEYBINDS) {
return true;
}
}
return false;
}
/* Checks for *required* arguments */
bool
action_is_valid(struct action *action)
{
const char *arg_name = NULL;
enum action_arg_type arg_type = LAB_ACTION_ARG_STR;
switch (action->type) {
case ACTION_TYPE_EXECUTE:
arg_name = "command";
break;
case ACTION_TYPE_MOVE_TO_EDGE:
case ACTION_TYPE_SNAP_TO_EDGE:
arg_name = "direction";
arg_type = LAB_ACTION_ARG_INT;
break;
case ACTION_TYPE_SHOW_MENU:
arg_name = "menu";
break;
case ACTION_TYPE_GO_TO_DESKTOP:
case ACTION_TYPE_SEND_TO_DESKTOP:
arg_name = "to";
break;
case ACTION_TYPE_SNAP_TO_REGION:
arg_name = "region";
break;
case ACTION_TYPE_FOCUS_OUTPUT:
arg_name = "output";
break;
default:
/* No arguments required */
return true;
}
if (arg_value_exists(action, arg_name, arg_type)) {
return true;
}
wlr_log(WLR_ERROR, "Missing required argument for %s: %s",
action_names[action->type], arg_name);
return false;
}
void
action_free(struct action *action)
{
/* Free args */
struct action_arg *arg, *arg_tmp;
wl_list_for_each_safe(arg, arg_tmp, &action->args, link) {
wl_list_remove(&arg->link);
zfree(arg->key);
if (arg->type == LAB_ACTION_ARG_STR) {
struct action_arg_str *str_arg = (struct action_arg_str *)arg;
zfree(str_arg->value);
}
zfree(arg);
}
zfree(action);
}
void
action_list_free(struct wl_list *action_list)
{
struct action *action, *action_tmp;
wl_list_for_each_safe(action, action_tmp, action_list, link) {
wl_list_remove(&action->link);
action_free(action);
}
}
static void
show_menu(struct server *server, struct view *view, const char *menu_name)
{
if (server->input_mode != LAB_INPUT_STATE_PASSTHROUGH
&& server->input_mode != LAB_INPUT_STATE_MENU) {
/* Prevent opening a menu while resizing / moving a view */
return;
}
bool force_menu_top_left = false;
struct menu *menu = menu_get_by_id(menu_name);
if (!menu) {
return;
}
if (!strcasecmp(menu_name, "client-menu")) {
if (!view) {
return;
}
enum ssd_part_type type = ssd_at(view->ssd, server->scene,
server->seat.cursor->x, server->seat.cursor->y);
if (type == LAB_SSD_BUTTON_WINDOW_MENU) {
force_menu_top_left = true;
} else if (ssd_part_contains(LAB_SSD_PART_TITLEBAR, type)) {
force_menu_top_left = false;
} else {
force_menu_top_left = true;
}
}
int x, y;
if (force_menu_top_left) {
x = view->current.x;
y = view->current.y;
} else {
x = server->seat.cursor->x;
y = server->seat.cursor->y;
}
/* Replaced by next show_menu() or cleaned on view_destroy() */
menu->triggered_by_view = view;
menu_open(menu, x, y);
}
static struct view *
view_for_action(struct view *activator, struct server *server,
struct action *action, uint32_t *resize_edges)
{
/* View is explicitly specified for mousebinds */
if (activator) {
return activator;
}
/* Select view based on action type for keybinds */
switch (action->type) {
case ACTION_TYPE_FOCUS:
case ACTION_TYPE_MOVE:
case ACTION_TYPE_RESIZE: {
struct cursor_context ctx = get_cursor_context(server);
if (action->type == ACTION_TYPE_RESIZE) {
/* Select resize edges for the keybind case */
*resize_edges = cursor_get_resize_edges(
server->seat.cursor, &ctx);
}
return ctx.view;
}
default:
return desktop_focused_view(server);
}
}
void
actions_run(struct view *activator, struct server *server,
struct wl_list *actions, uint32_t resize_edges)
{
if (!actions) {
wlr_log(WLR_ERROR, "empty actions");
return;
}
struct view *view;
struct action *action;
struct action_arg *arg;
wl_list_for_each(action, actions, link) {
/* Get arg now so we don't have to repeat every time we only need one */
arg = action_get_first_arg(action);
if (arg && arg->type == LAB_ACTION_ARG_STR) {
wlr_log(WLR_DEBUG, "Handling action %u: %s %s",
action->type, action_names[action->type],
action_str_from_arg(arg));
} else {
wlr_log(WLR_DEBUG, "Handling action %u: %s",
action->type, action_names[action->type]);
}
/*
* Refetch view because it may have been changed due to the
* previous action
*/
view = view_for_action(activator, server, action,
&resize_edges);
switch (action->type) {
case ACTION_TYPE_CLOSE:
if (view) {
view_close(view);
}
break;
case ACTION_TYPE_KILL:
if (view && view->surface) {
/* Send SIGTERM to the process associated with the surface */
pid_t pid = -1;
struct wl_client *client = view->surface->resource->client;
wl_client_get_credentials(client, &pid, NULL, NULL);
if (pid != -1) {
kill(pid, SIGTERM);
}
}
break;
case ACTION_TYPE_DEBUG:
debug_dump_scene(server);
break;
case ACTION_TYPE_EXECUTE:
{
struct buf cmd;
buf_init(&cmd);
buf_add(&cmd, action_str_from_arg(arg));
buf_expand_shell_variables(&cmd);
spawn_async_no_shell(cmd.buf);
free(cmd.buf);
}
break;
case ACTION_TYPE_EXIT:
wl_display_terminate(server->wl_display);
break;
case ACTION_TYPE_MOVE_TO_EDGE:
if (view) {
/* Config parsing makes sure that direction is a valid direction */
enum view_edge edge = get_arg_value_int(action, "direction", 0);
view_move_to_edge(view, edge);
}
break;
case ACTION_TYPE_SNAP_TO_EDGE:
if (view) {
/* Config parsing makes sure that direction is a valid direction */
enum view_edge edge = get_arg_value_int(action, "direction", 0);
view_snap_to_edge(view, edge, /*store_natural_geometry*/ true);
}
break;
case ACTION_TYPE_NEXT_WINDOW:
server->osd_state.cycle_view = desktop_cycle_view(server,
server->osd_state.cycle_view, LAB_CYCLE_DIR_FORWARD);
osd_update(server);
break;
case ACTION_TYPE_PREVIOUS_WINDOW:
server->osd_state.cycle_view = desktop_cycle_view(server,
server->osd_state.cycle_view, LAB_CYCLE_DIR_BACKWARD);
osd_update(server);
break;
case ACTION_TYPE_RECONFIGURE:
kill(getpid(), SIGHUP);
break;
case ACTION_TYPE_SHOW_MENU:
show_menu(server, view, action_str_from_arg(arg));
break;
case ACTION_TYPE_TOGGLE_MAXIMIZE:
if (view) {
view_toggle_maximize(view);
}
break;
case ACTION_TYPE_MAXIMIZE:
if (view) {
view_maximize(view, true, /*store_natural_geometry*/ true);
}
break;
case ACTION_TYPE_TOGGLE_FULLSCREEN:
if (view) {
view_toggle_fullscreen(view);
}
break;
case ACTION_TYPE_TOGGLE_DECORATIONS:
if (view) {
view_toggle_decorations(view);
}
break;
case ACTION_TYPE_TOGGLE_ALWAYS_ON_TOP:
if (view) {
view_toggle_always_on_top(view);
}
break;
case ACTION_TYPE_TOGGLE_ALWAYS_ON_BOTTOM:
if (view) {
view_toggle_always_on_bottom(view);
}
break;
case ACTION_TYPE_FOCUS:
if (view) {
desktop_focus_and_activate_view(&server->seat, view);
}
break;
case ACTION_TYPE_ICONIFY:
if (view) {
view_minimize(view, true);
}
break;
case ACTION_TYPE_MOVE:
if (view) {
interactive_begin(view, LAB_INPUT_STATE_MOVE, 0);
}
break;
case ACTION_TYPE_RAISE:
if (view) {
view_move_to_front(view);
}
break;
case ACTION_TYPE_LOWER:
if (view) {
view_move_to_back(view);
}
break;
case ACTION_TYPE_RESIZE:
if (view) {
interactive_begin(view, LAB_INPUT_STATE_RESIZE,
resize_edges);
}
break;
case ACTION_TYPE_RESIZE_RELATIVE:
if (view) {
int left = get_arg_value_int(action, "left", 0);
int right = get_arg_value_int(action, "right", 0);
int top = get_arg_value_int(action, "top", 0);
int bottom = get_arg_value_int(action, "bottom", 0);
view_resize_relative(view, left, right, top, bottom);
}
break;
case ACTION_TYPE_MOVETO:
if (view) {
int x = get_arg_value_int(action, "x", 0);
int y = get_arg_value_int(action, "y", 0);
view_move(view, x, y);
}
break;
case ACTION_TYPE_MOVE_RELATIVE:
if (view) {
int x = get_arg_value_int(action, "x", 0);
int y = get_arg_value_int(action, "y", 0);
view_move_relative(view, x, y);
}
break;
case ACTION_TYPE_SEND_TO_DESKTOP:
if (!view) {
break;
}
/* Falls through to GoToDesktop */
case ACTION_TYPE_GO_TO_DESKTOP:
{
bool follow = true;
bool wrap = get_arg_value_bool(action, "wrap", true);
const char *to = get_arg_value_str(action, "to", NULL);
/*
* `to` is always != NULL here because otherwise we would have
* removed the action during the initial parsing step as it is
* a required argument for both SendToDesktop and GoToDesktop.
*/
struct workspace *target = workspaces_find(
server->workspace_current, to, wrap);
if (!target) {
break;
}
if (action->type == ACTION_TYPE_SEND_TO_DESKTOP) {
view_move_to_workspace(view, target);
follow = get_arg_value_bool(action, "follow", true);
}
if (follow) {
workspaces_switch_to(target);
}
}
break;
case ACTION_TYPE_SNAP_TO_REGION:
if (!view) {
break;
}
struct output *output = view->output;
if (!output) {
break;
}
const char *region_name = action_str_from_arg(arg);
struct region *region = regions_from_name(region_name, output);
if (region) {
view_snap_to_region(view, region,
/*store_natural_geometry*/ true);
} else {
wlr_log(WLR_ERROR, "Invalid SnapToRegion id: '%s'", region_name);
}
break;
case ACTION_TYPE_TOGGLE_KEYBINDS:
if (view) {
view_toggle_keybinds(view);
}
break;
case ACTION_TYPE_FOCUS_OUTPUT:
{
const char *output_name = action_str_from_arg(arg);
desktop_focus_output(output_from_name(server, output_name));
}
break;
case ACTION_TYPE_INVALID:
wlr_log(WLR_ERROR, "Not executing unknown action");
break;
default:
/*
* If we get here it must be a BUG caused most likely by
* action_names and action_type being out of sync or by
* adding a new action without installing a handler here.
*/
wlr_log(WLR_ERROR,
"Not executing invalid action (%u)"
" This is a BUG. Please report.", action->type);
}
}
}