mirror of
https://github.com/swaywm/sway.git
synced 2025-01-12 23:36:42 +01:00
move swap_workspace logic to swap command
Co-authored-by: Arne <ge47com@tum.de>
This commit is contained in:
parent
afc3a54a85
commit
4cad9a1c43
5 changed files with 191 additions and 216 deletions
|
@ -136,7 +136,6 @@ static const struct cmd_handler command_handlers[] = {
|
||||||
{ "splitv", cmd_splitv },
|
{ "splitv", cmd_splitv },
|
||||||
{ "sticky", cmd_sticky },
|
{ "sticky", cmd_sticky },
|
||||||
{ "swap", cmd_swap },
|
{ "swap", cmd_swap },
|
||||||
{ "swap_workspace", cmd_swap_workspace_content },
|
|
||||||
{ "title_format", cmd_title_format },
|
{ "title_format", cmd_title_format },
|
||||||
{ "unmark", cmd_unmark },
|
{ "unmark", cmd_unmark },
|
||||||
{ "urgent", cmd_urgent },
|
{ "urgent", cmd_urgent },
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
|
#include <ctype.h>
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "sway/commands.h"
|
#include "sway/commands.h"
|
||||||
|
#include "sway/ipc-server.h"
|
||||||
#include "sway/output.h"
|
#include "sway/output.h"
|
||||||
#include "sway/tree/arrange.h"
|
#include "sway/tree/arrange.h"
|
||||||
#include "sway/tree/container.h"
|
#include "sway/tree/container.h"
|
||||||
|
@ -11,7 +13,162 @@
|
||||||
#include "stringop.h"
|
#include "stringop.h"
|
||||||
|
|
||||||
static const char expected_syntax[] =
|
static const char expected_syntax[] =
|
||||||
"Expected 'swap container with id|con_id|mark <arg>'";
|
"Expected 'swap (container with id|con_id|mark <arg>) |\n\t(wokspace with [number] <name>)'";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* prepare the containers inside a workspace to be moved to another
|
||||||
|
* workspace. This includes setting a few properties of the containers
|
||||||
|
* as well as ipc events
|
||||||
|
*/
|
||||||
|
|
||||||
|
static void handle_container_after_move(struct sway_container *container,
|
||||||
|
void *data) {
|
||||||
|
node_set_dirty(&container->node);
|
||||||
|
container_update_representation(container);
|
||||||
|
|
||||||
|
struct sway_workspace *old_workspace = container->pending.workspace;
|
||||||
|
struct sway_output *old_output = container->pending.workspace->output;
|
||||||
|
|
||||||
|
struct sway_workspace *destination = data;
|
||||||
|
container->pending.workspace = destination;
|
||||||
|
|
||||||
|
// handle floating containers here by updating their position
|
||||||
|
if (container_is_floating(container)) {
|
||||||
|
if (old_output == destination->output ||
|
||||||
|
container->pending.fullscreen_mode) return;
|
||||||
|
|
||||||
|
struct wlr_box workspace_box, old_workspace_box;
|
||||||
|
|
||||||
|
workspace_get_box(destination, &workspace_box);
|
||||||
|
workspace_get_box(old_workspace, &old_workspace_box);
|
||||||
|
|
||||||
|
floating_fix_coordinates(container,
|
||||||
|
&old_workspace_box, &workspace_box);
|
||||||
|
|
||||||
|
if (!container->scratchpad || !destination->output) return;
|
||||||
|
|
||||||
|
struct wlr_box output_box;
|
||||||
|
output_get_box(destination->output, &output_box);
|
||||||
|
container->transform = workspace_box;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container->view) {
|
||||||
|
ipc_event_window(container, "move");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* swap the properties necessary to preserve the layout
|
||||||
|
* as well as their respective contents
|
||||||
|
*/
|
||||||
|
|
||||||
|
static void swap_workspace_properties(struct sway_workspace *cur_ws,
|
||||||
|
struct sway_workspace *oth_ws) {
|
||||||
|
struct sway_workspace cur_ws_cpy = *cur_ws;
|
||||||
|
|
||||||
|
cur_ws->tiling = oth_ws->tiling;
|
||||||
|
oth_ws->tiling = cur_ws_cpy.tiling;
|
||||||
|
cur_ws->floating = oth_ws->floating;
|
||||||
|
oth_ws->floating = cur_ws_cpy.floating;
|
||||||
|
cur_ws->layout = oth_ws->layout;
|
||||||
|
oth_ws->layout = cur_ws_cpy.layout;
|
||||||
|
cur_ws->prev_split_layout = oth_ws->prev_split_layout;
|
||||||
|
oth_ws->prev_split_layout = cur_ws_cpy.prev_split_layout;
|
||||||
|
cur_ws->current_gaps = oth_ws->current_gaps;
|
||||||
|
oth_ws->current_gaps = cur_ws_cpy.current_gaps;
|
||||||
|
cur_ws->gaps_outer = oth_ws->gaps_outer;
|
||||||
|
oth_ws->gaps_outer = cur_ws_cpy.gaps_outer;
|
||||||
|
cur_ws->gaps_inner = oth_ws->gaps_inner;
|
||||||
|
oth_ws->gaps_inner = cur_ws_cpy.gaps_inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_new_focus(struct sway_workspace *ws, struct sway_seat *seat) {
|
||||||
|
if (ws->tiling->length) {
|
||||||
|
// this needs to be more specific (focus not just every container,
|
||||||
|
// but single windows
|
||||||
|
struct sway_container *container = ws->tiling->items[0];
|
||||||
|
struct sway_container *first_view = container_get_first_view(container);
|
||||||
|
|
||||||
|
if (!first_view) {
|
||||||
|
seat_set_focus(seat, &ws->node);
|
||||||
|
}
|
||||||
|
|
||||||
|
seat_set_focus(seat, &first_view->node);
|
||||||
|
} else if (ws->floating->length) {
|
||||||
|
seat_set_focus(seat, ws->floating->items[0]);
|
||||||
|
} else {
|
||||||
|
seat_set_focus(seat, &ws->node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct cmd_results *swap_workspaces(int argc, char **argv) {
|
||||||
|
char *ws_name = NULL;
|
||||||
|
struct sway_workspace *oth_ws;
|
||||||
|
|
||||||
|
if (strcasecmp(argv[2], "number") == 0) {
|
||||||
|
if (!isdigit(argv[3][0])) {
|
||||||
|
return cmd_results_new(CMD_INVALID,
|
||||||
|
"Invalid workspace number '%s'", argv[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ws_name = join_args(argv + 3, argc - 3);
|
||||||
|
oth_ws = workspace_by_number(ws_name);
|
||||||
|
} else {
|
||||||
|
ws_name = join_args(argv + 2, argc - 2);
|
||||||
|
oth_ws = workspace_by_name(ws_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oth_ws) {
|
||||||
|
oth_ws = workspace_create(NULL, ws_name);
|
||||||
|
|
||||||
|
if (!oth_ws) {
|
||||||
|
return cmd_results_new(CMD_FAILURE,
|
||||||
|
"Unable to create new workspace");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(ws_name);
|
||||||
|
|
||||||
|
// second workspace is the one currently focused
|
||||||
|
struct sway_workspace *cur_ws = config->handler_context.workspace;
|
||||||
|
if (!cur_ws) {
|
||||||
|
return cmd_results_new(CMD_FAILURE, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// exit early if there is nothing to swap
|
||||||
|
if (cur_ws == oth_ws) return cmd_results_new(CMD_SUCCESS, NULL);
|
||||||
|
|
||||||
|
// save seat to set the focus later
|
||||||
|
struct sway_seat *seat = config->handler_context.seat;
|
||||||
|
swap_workspace_properties(cur_ws, oth_ws);
|
||||||
|
|
||||||
|
node_set_dirty(&cur_ws->node);
|
||||||
|
node_set_dirty(&oth_ws->node);
|
||||||
|
workspace_update_representation(cur_ws);
|
||||||
|
workspace_update_representation(oth_ws);
|
||||||
|
|
||||||
|
// before rearranging the workspaces we have to set a few properties
|
||||||
|
// such as dirty
|
||||||
|
workspace_for_each_container(cur_ws, handle_container_after_move, cur_ws);
|
||||||
|
workspace_for_each_container(oth_ws, handle_container_after_move, oth_ws);
|
||||||
|
|
||||||
|
workspace_detect_urgent(cur_ws);
|
||||||
|
workspace_detect_urgent(oth_ws);
|
||||||
|
|
||||||
|
// after swapping we set the focus on the first container in the current
|
||||||
|
// workspace or the workspace itself if there is no container
|
||||||
|
set_new_focus(cur_ws, seat);
|
||||||
|
|
||||||
|
// destroy other workspace in case it is empty
|
||||||
|
workspace_consider_destroy(oth_ws);
|
||||||
|
|
||||||
|
// update both affected workspaces
|
||||||
|
arrange_workspace(cur_ws);
|
||||||
|
arrange_workspace(oth_ws);
|
||||||
|
|
||||||
|
return cmd_results_new(CMD_SUCCESS, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
static bool test_con_id(struct sway_container *container, void *data) {
|
static bool test_con_id(struct sway_container *container, void *data) {
|
||||||
size_t *con_id = data;
|
size_t *con_id = data;
|
||||||
|
@ -34,19 +191,8 @@ static bool test_mark(struct sway_container *container, void *mark) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct cmd_results *cmd_swap(int argc, char **argv) {
|
static struct cmd_results *swap_containers(int argc, char **argv) {
|
||||||
struct cmd_results *error = NULL;
|
struct cmd_results *error = NULL;
|
||||||
if ((error = checkarg(argc, "swap", EXPECTED_AT_LEAST, 4))) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
if (!root->outputs->length) {
|
|
||||||
return cmd_results_new(CMD_INVALID,
|
|
||||||
"Can't run this command while there's no outputs connected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strcasecmp(argv[0], "container") || strcasecmp(argv[1], "with")) {
|
|
||||||
return cmd_results_new(CMD_INVALID, "%s", expected_syntax);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sway_container *current = config->handler_context.container;
|
struct sway_container *current = config->handler_context.container;
|
||||||
struct sway_container *other = NULL;
|
struct sway_container *other = NULL;
|
||||||
|
@ -105,3 +251,22 @@ struct cmd_results *cmd_swap(int argc, char **argv) {
|
||||||
|
|
||||||
return cmd_results_new(CMD_SUCCESS, NULL);
|
return cmd_results_new(CMD_SUCCESS, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct cmd_results *cmd_swap(int argc, char **argv) {
|
||||||
|
struct cmd_results *error = NULL;
|
||||||
|
if ((error = checkarg(argc, "swap", EXPECTED_AT_LEAST, 4))) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
if (!root->outputs->length) {
|
||||||
|
return cmd_results_new(CMD_INVALID,
|
||||||
|
"Can't run this command while there's no outputs connected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcasecmp(argv[0], "container") == 0 && strcasecmp(argv[1], "with") == 0) {
|
||||||
|
return swap_containers(argc, argv);
|
||||||
|
} else if (strcasecmp(argv[0], "workspace") == 0 && strcasecmp(argv[1], "with") == 0) {
|
||||||
|
return swap_workspaces(argc, argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd_results_new(CMD_INVALID, "%s", expected_syntax);
|
||||||
|
}
|
||||||
|
|
|
@ -1,190 +0,0 @@
|
||||||
#include "log.h"
|
|
||||||
#include "stringop.h"
|
|
||||||
#include "sway/output.h"
|
|
||||||
#include "sway/input/input-manager.h"
|
|
||||||
#include "sway/input/seat.h"
|
|
||||||
#include "sway/tree/arrange.h"
|
|
||||||
#include "sway/tree/container.h"
|
|
||||||
#include "sway/tree/view.h"
|
|
||||||
#include "sway/tree/workspace.h"
|
|
||||||
#include "sway/commands.h"
|
|
||||||
#include "sway/ipc-server.h"
|
|
||||||
#include "stringop.h"
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <strings.h>
|
|
||||||
#include <ctype.h>
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* prepare the containers inside a workspace to be moved to another
|
|
||||||
* workspace. This includes setting a few properties of the containers
|
|
||||||
* as well as ipc events
|
|
||||||
*/
|
|
||||||
|
|
||||||
static void handle_container_after_move(struct sway_container *container,
|
|
||||||
void *data) {
|
|
||||||
node_set_dirty(&container->node);
|
|
||||||
container_update_representation(container);
|
|
||||||
|
|
||||||
struct sway_workspace *old_workspace = container->pending.workspace;
|
|
||||||
struct sway_output *old_output = container->pending.workspace->output;
|
|
||||||
|
|
||||||
struct sway_workspace *destination = data;
|
|
||||||
container->pending.workspace = destination;
|
|
||||||
|
|
||||||
// handle floating containers here by updating their position
|
|
||||||
if (container_is_floating(container)) {
|
|
||||||
if (old_output == destination->output ||
|
|
||||||
container->pending.fullscreen_mode) return;
|
|
||||||
|
|
||||||
struct wlr_box workspace_box, old_workspace_box;
|
|
||||||
|
|
||||||
workspace_get_box(destination, &workspace_box);
|
|
||||||
workspace_get_box(old_workspace, &old_workspace_box);
|
|
||||||
|
|
||||||
floating_fix_coordinates(container,
|
|
||||||
&old_workspace_box, &workspace_box);
|
|
||||||
|
|
||||||
if (!container->scratchpad || !destination->output) return;
|
|
||||||
|
|
||||||
struct wlr_box output_box;
|
|
||||||
output_get_box(destination->output, &output_box);
|
|
||||||
container->transform = workspace_box;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (container->view) {
|
|
||||||
ipc_event_window(container, "move");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* swap the properties necessary to preserve the layout
|
|
||||||
* as well as their respective contents
|
|
||||||
*/
|
|
||||||
|
|
||||||
static void swap_workspace_properties(struct sway_workspace *cur_ws,
|
|
||||||
struct sway_workspace *oth_ws) {
|
|
||||||
struct sway_workspace cur_ws_cpy = *cur_ws;
|
|
||||||
|
|
||||||
cur_ws->tiling = oth_ws->tiling;
|
|
||||||
oth_ws->tiling = cur_ws_cpy.tiling;
|
|
||||||
cur_ws->floating = oth_ws->floating;
|
|
||||||
oth_ws->floating = cur_ws_cpy.floating;
|
|
||||||
cur_ws->layout = oth_ws->layout;
|
|
||||||
oth_ws->layout = cur_ws_cpy.layout;
|
|
||||||
cur_ws->prev_split_layout = oth_ws->prev_split_layout;
|
|
||||||
oth_ws->prev_split_layout = cur_ws_cpy.prev_split_layout;
|
|
||||||
cur_ws->current_gaps = oth_ws->current_gaps;
|
|
||||||
oth_ws->current_gaps = cur_ws_cpy.current_gaps;
|
|
||||||
cur_ws->gaps_outer = oth_ws->gaps_outer;
|
|
||||||
oth_ws->gaps_outer = cur_ws_cpy.gaps_outer;
|
|
||||||
cur_ws->gaps_inner = oth_ws->gaps_inner;
|
|
||||||
oth_ws->gaps_inner = cur_ws_cpy.gaps_inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void set_new_focus(struct sway_workspace *ws, struct sway_seat *seat) {
|
|
||||||
if (ws->tiling->length) {
|
|
||||||
// this needs to be more specific (focus not just every container,
|
|
||||||
// but single windows
|
|
||||||
struct sway_container *container = ws->tiling->items[0];
|
|
||||||
struct sway_container *first_view = container_get_first_view(container);
|
|
||||||
|
|
||||||
if (!first_view) {
|
|
||||||
seat_set_focus(seat, &ws->node);
|
|
||||||
}
|
|
||||||
|
|
||||||
seat_set_focus(seat, &first_view->node);
|
|
||||||
} else if (ws->floating->length) {
|
|
||||||
seat_set_focus(seat, ws->floating->items[0]);
|
|
||||||
} else {
|
|
||||||
seat_set_focus(seat, &ws->node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* swap the contents of the currently focused workspace with the content
|
|
||||||
* of the workspace specified in the args
|
|
||||||
*
|
|
||||||
* syntax: swap_workspace with <name|number x>
|
|
||||||
*/
|
|
||||||
|
|
||||||
struct cmd_results *cmd_swap_workspace_content(int argc, char **argv) {
|
|
||||||
// parse arguments
|
|
||||||
if (argc < 2) {
|
|
||||||
return cmd_results_new(CMD_INVALID, "syntax not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strcasecmp(argv[0], "with") != 0) {
|
|
||||||
return cmd_results_new(CMD_FAILURE, "Invalid syntax");
|
|
||||||
}
|
|
||||||
|
|
||||||
char *ws_name = NULL;
|
|
||||||
struct sway_workspace *oth_ws;
|
|
||||||
|
|
||||||
if (strcasecmp(argv[1], "number") == 0) {
|
|
||||||
if (!isdigit(argv[2][0])) {
|
|
||||||
return cmd_results_new(CMD_INVALID,
|
|
||||||
"Invalid workspace number '%s'", argv[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
ws_name = join_args(argv + 2, argc - 2);
|
|
||||||
oth_ws = workspace_by_number(ws_name);
|
|
||||||
} else {
|
|
||||||
ws_name = join_args(argv + 1, argc - 1);
|
|
||||||
oth_ws = workspace_by_name(ws_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oth_ws) {
|
|
||||||
oth_ws = workspace_create(NULL, ws_name);
|
|
||||||
|
|
||||||
if (!oth_ws) {
|
|
||||||
return cmd_results_new(CMD_FAILURE,
|
|
||||||
"Unable to create new workspace");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(ws_name);
|
|
||||||
|
|
||||||
// second workspace is the one currently focused
|
|
||||||
struct sway_workspace *cur_ws = config->handler_context.workspace;
|
|
||||||
if (!cur_ws) {
|
|
||||||
return cmd_results_new(CMD_FAILURE, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// exit early if there is nothing to swap
|
|
||||||
if (cur_ws == oth_ws) return cmd_results_new(CMD_SUCCESS, NULL);
|
|
||||||
|
|
||||||
// save seat to set the focus later
|
|
||||||
struct sway_seat *seat = config->handler_context.seat;
|
|
||||||
swap_workspace_properties(cur_ws, oth_ws);
|
|
||||||
|
|
||||||
node_set_dirty(&cur_ws->node);
|
|
||||||
node_set_dirty(&oth_ws->node);
|
|
||||||
workspace_update_representation(cur_ws);
|
|
||||||
workspace_update_representation(oth_ws);
|
|
||||||
|
|
||||||
// before rearranging the workspaces we have to set a few properties
|
|
||||||
// such as dirty
|
|
||||||
workspace_for_each_container(cur_ws, handle_container_after_move, cur_ws);
|
|
||||||
workspace_for_each_container(oth_ws, handle_container_after_move, oth_ws);
|
|
||||||
|
|
||||||
workspace_detect_urgent(cur_ws);
|
|
||||||
workspace_detect_urgent(oth_ws);
|
|
||||||
|
|
||||||
// after swapping we set the focus on the first container in the current
|
|
||||||
// workspace or the workspace itself if there is no container
|
|
||||||
set_new_focus(cur_ws, seat);
|
|
||||||
|
|
||||||
// destroy other workspace in case it is empty
|
|
||||||
workspace_consider_destroy(oth_ws);
|
|
||||||
|
|
||||||
// update both affected workspaces
|
|
||||||
arrange_workspace(cur_ws);
|
|
||||||
arrange_workspace(oth_ws);
|
|
||||||
|
|
||||||
return cmd_results_new(CMD_SUCCESS, NULL);
|
|
||||||
}
|
|
|
@ -110,7 +110,6 @@ sway_sources = files(
|
||||||
'commands/swaybg_command.c',
|
'commands/swaybg_command.c',
|
||||||
'commands/swaynag_command.c',
|
'commands/swaynag_command.c',
|
||||||
'commands/swap.c',
|
'commands/swap.c',
|
||||||
'commands/swap_workspace.c',
|
|
||||||
'commands/tiling_drag.c',
|
'commands/tiling_drag.c',
|
||||||
'commands/tiling_drag_threshold.c',
|
'commands/tiling_drag_threshold.c',
|
||||||
'commands/title_align.c',
|
'commands/title_align.c',
|
||||||
|
|
|
@ -344,18 +344,20 @@ set|plus|minus|toggle <amount>
|
||||||
"Sticks" a floating window to the current output so that it shows up on all
|
"Sticks" a floating window to the current output so that it shows up on all
|
||||||
workspaces.
|
workspaces.
|
||||||
|
|
||||||
*swap* container with id|con_id|mark <arg>
|
*swap* (container with id|con_id|mark <arg>) |
|
||||||
Swaps the position, geometry, and fullscreen status of two containers. The
|
(workspace with [number] <number>)
|
||||||
first container can be selected either by criteria or focus. The second
|
|
||||||
container can be selected by _id_, _con_id_, or _mark_. _id_ can only be
|
|
||||||
used with xwayland views. If the first container has focus, it will retain
|
|
||||||
focus unless it is moved to a different workspace or the second container
|
|
||||||
becomes fullscreen on the same workspace as the first container. In either
|
|
||||||
of those cases, the second container will gain focus.
|
|
||||||
|
|
||||||
*swap_workspace* with [number] <name>
|
If using the container keyword, swaps the position, geometry, and
|
||||||
Swaps the content of the currently focused workspace with the content
|
fullscreen status of two containers. The first container can be selected
|
||||||
of the workspace specified as an argument.
|
either by criteria or focus. The second container can be selected by
|
||||||
|
_id_, _con_id_, or _mark_. _id_ can only be used with xwayland views.
|
||||||
|
If the first container has focus, it will retain focus unless it is moved
|
||||||
|
to a different workspace or the second container becomes fullscreen on the
|
||||||
|
same workspace as the first container. In either of those cases, the
|
||||||
|
second container will gain focus.
|
||||||
|
|
||||||
|
If using the workspace keyword, swaps the content of the currently focused
|
||||||
|
workspace with the content of the workspace specified as an argument.
|
||||||
|
|
||||||
*title_format* <format>
|
*title_format* <format>
|
||||||
Sets the format of window titles. The following placeholders may be used:
|
Sets the format of window titles. The following placeholders may be used:
|
||||||
|
|
Loading…
Reference in a new issue