Implement 'swap' command.

This patch introduces a new command 'swap' that swaps two containers so
that they assume each other's position and geometry.

fixes #917
This commit is contained in:
Ingo Bürk 2016-02-26 22:39:44 +01:00
parent 9bc504ebdb
commit 5f9a5e8d7d
13 changed files with 794 additions and 46 deletions

View File

@ -1976,6 +1976,39 @@ bindsym $mod+c move absolute position center
bindsym $mod+m move position mouse bindsym $mod+m move position mouse
------------------------------------------------------- -------------------------------------------------------
=== Swapping containers
Two containers can be swapped (i.e., move to each other's position) by using
the +swap+ command. They will assume the position and geometry of the container
they are swapped with.
The first container to participate in the swapping can be selected through the
normal command criteria process with the focused window being the usual
fallback if no criteria are specified. The second container can be selected
using one of the following methods:
+id+:: The X11 window ID of a client window.
+con_id+:: The i3 container ID of a container.
+mark+:: A container with the specified mark, see <<vim_like_marks>>.
Note that swapping does not work with all containers. Most notably, swapping
floating containers or containers that have a parent-child relationship to one
another does not work.
*Syntax*:
----------------------------------------
swap container with id|con_id|mark <arg>
----------------------------------------
*Examples*:
-----------------------------------------------------------------
# Swaps the focused container with the container marked »swapee«.
swap container with mark swapee
# Swaps container marked »A« and »B«
[con_mark="^A$"] swap container with mark B
-----------------------------------------------------------------
=== Sticky floating windows === Sticky floating windows
If you want a window to stick to the glass, i.e., have it stay on screen even If you want a window to stick to the glass, i.e., have it stay on screen even

View File

@ -290,6 +290,12 @@ void cmd_move_scratchpad(I3_CMD);
*/ */
void cmd_scratchpad_show(I3_CMD); void cmd_scratchpad_show(I3_CMD);
/**
* Implementation of 'swap [container] [with] id|con_id|mark <arg>'.
*
*/
void cmd_swap(I3_CMD, const char *mode, const char *arg);
/** /**
* Implementation of 'title_format <format>' * Implementation of 'title_format <format>'
* *

View File

@ -139,6 +139,12 @@ Con *con_inside_floating(Con *con);
*/ */
bool con_inside_focused(Con *con); bool con_inside_focused(Con *con);
/**
* Checks if the container has the given parent as an actual parent.
*
*/
bool con_has_parent(Con *con, Con *parent);
/** /**
* Returns the container with the given client window ID or NULL if no such * Returns the container with the given client window ID or NULL if no such
* container exists. * container exists.
@ -461,3 +467,9 @@ void con_force_split_parents_redraw(Con *con);
* *
*/ */
i3String *con_parse_title_format(Con *con); i3String *con_parse_title_format(Con *con);
/**
* Swaps the two containers.
*
*/
bool con_swap(Con *first, Con *second);

View File

@ -157,3 +157,10 @@ void start_nagbar(pid_t *nagbar_pid, char *argv[]);
* *
*/ */
void kill_nagbar(pid_t *nagbar_pid, bool wait_for_it); void kill_nagbar(pid_t *nagbar_pid, bool wait_for_it);
/**
* Converts a string into a long using strtol().
* This is a convenience wrapper checking the parsing result. It returns true
* if the number could be parsed.
*/
bool parse_long(const char *str, long *out, int base);

View File

@ -38,6 +38,7 @@ state INITIAL:
'rename' -> RENAME 'rename' -> RENAME
'nop' -> NOP 'nop' -> NOP
'scratchpad' -> SCRATCHPAD 'scratchpad' -> SCRATCHPAD
'swap' -> SWAP
'title_format' -> TITLE_FORMAT 'title_format' -> TITLE_FORMAT
'mode' -> MODE 'mode' -> MODE
'bar' -> BAR 'bar' -> BAR
@ -406,6 +407,21 @@ state SCRATCHPAD:
'show' 'show'
-> call cmd_scratchpad_show() -> call cmd_scratchpad_show()
# swap [container] [with] id <window>
# swap [container] [with] con_id <con_id>
# swap [container] [with] mark <mark>
state SWAP:
'container'
->
'with'
->
mode = 'id', 'con_id', 'mark'
-> SWAP_ARGUMENT
state SWAP_ARGUMENT:
arg = string
-> call cmd_swap($mode, $arg)
state TITLE_FORMAT: state TITLE_FORMAT:
format = string format = string
-> call cmd_title_format($format) -> call cmd_title_format($format)

View File

@ -69,15 +69,15 @@ Binding *configure_binding(const char *bindtype, const char *modifiers, const ch
new_binding->symbol = sstrdup(input_code); new_binding->symbol = sstrdup(input_code);
} else { } else {
char *endptr; long keycode;
long keycode = strtol(input_code, &endptr, 10); if (!parse_long(input_code, &keycode, 10)) {
new_binding->keycode = keycode;
new_binding->input_type = B_KEYBOARD;
if (keycode == LONG_MAX || keycode == LONG_MIN || keycode < 0 || *endptr != '\0' || endptr == input_code) {
ELOG("Could not parse \"%s\" as an input code, ignoring this binding.\n", input_code); ELOG("Could not parse \"%s\" as an input code, ignoring this binding.\n", input_code);
FREE(new_binding); FREE(new_binding);
return NULL; return NULL;
} }
new_binding->keycode = keycode;
new_binding->input_type = B_KEYBOARD;
} }
new_binding->command = sstrdup(command); new_binding->command = sstrdup(command);
new_binding->event_state_mask = event_state_from_str(modifiers); new_binding->event_state_mask = event_state_from_str(modifiers);
@ -459,13 +459,12 @@ void translate_keysyms(void) {
Binding *bind; Binding *bind;
TAILQ_FOREACH(bind, bindings, bindings) { TAILQ_FOREACH(bind, bindings, bindings) {
if (bind->input_type == B_MOUSE) { if (bind->input_type == B_MOUSE) {
char *endptr; long button;
long button = strtol(bind->symbol + (sizeof("button") - 1), &endptr, 10); if (!parse_long(bind->symbol + (sizeof("button") - 1), &button, 10)) {
bind->keycode = button;
if (button == LONG_MAX || button == LONG_MIN || button < 0 || *endptr != '\0' || endptr == bind->symbol)
ELOG("Could not translate string to button: \"%s\"\n", bind->symbol); ELOG("Could not translate string to button: \"%s\"\n", bind->symbol);
}
bind->keycode = button;
continue; continue;
} }
@ -974,9 +973,8 @@ int *bindings_get_buttons_to_grab(void) {
if (bind->input_type != B_MOUSE || !bind->whole_window) if (bind->input_type != B_MOUSE || !bind->whole_window)
continue; continue;
char *endptr; long button;
long button = strtol(bind->symbol + (sizeof("button") - 1), &endptr, 10); if (!parse_long(bind->symbol + (sizeof("button") - 1), &button, 10)) {
if (button == LONG_MAX || button == LONG_MIN || button < 0 || *endptr != '\0' || endptr == bind->symbol) {
ELOG("Could not parse button number, skipping this binding. Please report this bug in i3.\n"); ELOG("Could not parse button number, skipping this binding. Please report this bug in i3.\n");
continue; continue;
} }

View File

@ -1812,6 +1812,65 @@ void cmd_scratchpad_show(I3_CMD) {
ysuccess(true); ysuccess(true);
} }
/*
* Implementation of 'swap [container] [with] id|con_id|mark <arg>'.
*
*/
void cmd_swap(I3_CMD, const char *mode, const char *arg) {
HANDLE_EMPTY_MATCH;
owindow *match = TAILQ_FIRST(&owindows);
if (match == NULL) {
DLOG("No match found for swapping.\n");
return;
}
Con *con;
if (strcmp(mode, "id") == 0) {
long target;
if (!parse_long(arg, &target, 0)) {
yerror("Failed to parse %s into a window id.\n", arg);
return;
}
con = con_by_window_id(target);
} else if (strcmp(mode, "con_id") == 0) {
long target;
if (!parse_long(arg, &target, 0)) {
yerror("Failed to parse %s into a container id.\n", arg);
return;
}
con = (Con *)target;
} else if (strcmp(mode, "mark") == 0) {
con = con_by_mark(arg);
} else {
yerror("Unhandled swap mode \"%s\". This is a bug.\n", mode);
return;
}
if (con == NULL) {
yerror("Could not find container for %s = %s\n", mode, arg);
return;
}
if (match == TAILQ_LAST(&owindows, owindows_head)) {
DLOG("More than one container matched the swap command, only using the first one.");
}
if (match->con == NULL) {
DLOG("Match %p has no container.\n", match);
ysuccess(false);
return;
}
DLOG("Swapping %p with %p.\n", match->con, con);
bool result = con_swap(match->con, con);
cmd_output->needs_tree_render = true;
ysuccess(result);
}
/* /*
* Implementation of 'title_format <format>' * Implementation of 'title_format <format>'
* *

222
src/con.c
View File

@ -22,9 +22,11 @@ static void con_on_remove_child(Con *con);
void con_force_split_parents_redraw(Con *con) { void con_force_split_parents_redraw(Con *con) {
Con *parent = con; Con *parent = con;
while (parent && parent->type != CT_WORKSPACE && parent->type != CT_DOCKAREA) { while (parent != NULL && parent->type != CT_WORKSPACE && parent->type != CT_DOCKAREA) {
if (!con_is_leaf(parent)) if (!con_is_leaf(parent)) {
FREE(parent->deco_render_params); FREE(parent->deco_render_params);
}
parent = parent->parent; parent = parent->parent;
} }
} }
@ -145,7 +147,7 @@ static void _con_attach(Con *con, Con *parent, Con *previous, bool ignore_focus)
/* Insert the container after the tiling container, if found. /* Insert the container after the tiling container, if found.
* When adding to a CT_OUTPUT, just append one after another. */ * When adding to a CT_OUTPUT, just append one after another. */
if (current && parent->type != CT_OUTPUT) { if (current != NULL && parent->type != CT_OUTPUT) {
DLOG("Inserting con = %p after con %p\n", con, current); DLOG("Inserting con = %p after con %p\n", con, current);
TAILQ_INSERT_AFTER(nodes_head, current, con, nodes); TAILQ_INSERT_AFTER(nodes_head, current, con, nodes);
} else } else
@ -525,6 +527,23 @@ bool con_inside_focused(Con *con) {
return con_inside_focused(con->parent); return con_inside_focused(con->parent);
} }
/*
* Checks if the container has the given parent as an actual parent.
*
*/
bool con_has_parent(Con *con, Con *parent) {
Con *current = con->parent;
if (current == NULL) {
return false;
}
if (current == parent) {
return true;
}
return con_has_parent(current, parent);
}
/* /*
* Returns the container with the given client window ID or NULL if no such * Returns the container with the given client window ID or NULL if no such
* container exists. * container exists.
@ -803,23 +822,26 @@ void con_fix_percent(Con *con) {
if (children_with_percent != children) { if (children_with_percent != children) {
TAILQ_FOREACH(child, &(con->nodes_head), nodes) { TAILQ_FOREACH(child, &(con->nodes_head), nodes) {
if (child->percent <= 0.0) { if (child->percent <= 0.0) {
if (children_with_percent == 0) if (children_with_percent == 0) {
total += (child->percent = 1.0); total += (child->percent = 1.0);
else } else {
total += (child->percent = total / children_with_percent); total += (child->percent = total / children_with_percent);
} }
} }
} }
}
// if we got a zero, just distribute the space equally, otherwise // if we got a zero, just distribute the space equally, otherwise
// distribute according to the proportions we got // distribute according to the proportions we got
if (total == 0.0) { if (total == 0.0) {
TAILQ_FOREACH(child, &(con->nodes_head), nodes) TAILQ_FOREACH(child, &(con->nodes_head), nodes) {
child->percent = 1.0 / children; child->percent = 1.0 / children;
}
} else if (total != 1.0) { } else if (total != 1.0) {
TAILQ_FOREACH(child, &(con->nodes_head), nodes) TAILQ_FOREACH(child, &(con->nodes_head), nodes) {
child->percent /= total; child->percent /= total;
} }
}
} }
/* /*
@ -944,7 +966,7 @@ void con_disable_fullscreen(Con *con) {
con_set_fullscreen_mode(con, CF_NONE); con_set_fullscreen_mode(con, CF_NONE);
} }
static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fix_coordinates, bool dont_warp, bool ignore_focus) { static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fix_coordinates, bool dont_warp, bool ignore_focus, bool fix_percentage) {
Con *orig_target = target; Con *orig_target = target;
/* Prevent moving if this would violate the fullscreen focus restrictions. */ /* Prevent moving if this would violate the fullscreen focus restrictions. */
@ -1053,9 +1075,11 @@ static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fi
_con_attach(con, target, behind_focused ? NULL : orig_target, !behind_focused); _con_attach(con, target, behind_focused ? NULL : orig_target, !behind_focused);
/* 5: fix the percentages */ /* 5: fix the percentages */
if (fix_percentage) {
con_fix_percent(parent); con_fix_percent(parent);
con->percent = 0.0; con->percent = 0.0;
con_fix_percent(target); con_fix_percent(target);
}
/* 6: focus the con on the target workspace, but only within that /* 6: focus the con on the target workspace, but only within that
* workspace, that is, dont move focus away if the target workspace is * workspace, that is, dont move focus away if the target workspace is
@ -1127,6 +1151,9 @@ static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fi
con_set_urgency(con, true); con_set_urgency(con, true);
} }
/* Ensure the container will be redrawn. */
FREE(con->deco_render_params);
CALL(parent, on_remove_child); CALL(parent, on_remove_child);
ipc_send_window_event("move", con); ipc_send_window_event("move", con);
@ -1166,12 +1193,12 @@ bool con_move_to_mark(Con *con, const char *mark) {
target = TAILQ_FIRST(&(target->focus_head)); target = TAILQ_FIRST(&(target->focus_head));
} }
if (con == target || con == target->parent) { if (con == target || con_has_parent(target, con)) {
DLOG("cannot move the container to or inside itself, aborting.\n"); DLOG("cannot move the container to or inside itself, aborting.\n");
return false; return false;
} }
return _con_move_to_con(con, target, false, true, false, false); return _con_move_to_con(con, target, false, true, false, false, true);
} }
/* /*
@ -1204,7 +1231,7 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool
} }
Con *target = con_descend_focused(workspace); Con *target = con_descend_focused(workspace);
_con_move_to_con(con, target, true, fix_coordinates, dont_warp, ignore_focus); _con_move_to_con(con, target, true, fix_coordinates, dont_warp, ignore_focus, true);
} }
/* /*
@ -1311,15 +1338,16 @@ Con *con_next_focused(Con *con) {
} else { } else {
/* try to focus the next container on the same level as this one or fall /* try to focus the next container on the same level as this one or fall
* back to its parent */ * back to its parent */
if (!(next = TAILQ_NEXT(con, focused))) if (!(next = TAILQ_NEXT(con, focused))) {
next = con->parent; next = con->parent;
} }
}
/* now go down the focus stack as far as /* now go down the focus stack as far as
* possible, excluding the current container */ * possible, excluding the current container */
while (!TAILQ_EMPTY(&(next->focus_head)) && while (!TAILQ_EMPTY(&(next->focus_head)) && TAILQ_FIRST(&(next->focus_head)) != con) {
TAILQ_FIRST(&(next->focus_head)) != con)
next = TAILQ_FIRST(&(next->focus_head)); next = TAILQ_FIRST(&(next->focus_head));
}
return next; return next;
} }
@ -2159,3 +2187,169 @@ i3String *con_parse_title_format(Con *con) {
return formatted; return formatted;
} }
/*
* Swaps the two containers.
*
*/
bool con_swap(Con *first, Con *second) {
assert(first != NULL);
assert(second != NULL);
DLOG("Swapping containers %p / %p\n", first, second);
if (first->type != CT_CON) {
ELOG("Only regular containers can be swapped, but found con = %p with type = %d.\n", first, first->type);
return false;
}
if (second->type != CT_CON) {
ELOG("Only regular containers can be swapped, but found con = %p with type = %d.\n", second, second->type);
return false;
}
if (con_is_floating(first) || con_is_floating(second)) {
ELOG("Floating windows cannot be swapped.\n");
return false;
}
if (first == second) {
DLOG("Swapping container %p with itself, nothing to do.\n", first);
return false;
}
if (con_has_parent(first, second) || con_has_parent(second, first)) {
ELOG("Cannot swap containers %p and %p because they are in a parent-child relationship.\n", first, second);
return false;
}
Con *old_focus = focused;
Con *first_ws = con_get_workspace(first);
Con *second_ws = con_get_workspace(second);
Con *current_ws = con_get_workspace(old_focus);
const bool focused_within_first = (first == old_focus || con_has_parent(old_focus, first));
const bool focused_within_second = (second == old_focus || con_has_parent(old_focus, second));
if (!con_fullscreen_permits_focusing(first_ws)) {
DLOG("Cannot swap because target workspace \"%s\" is obscured.\n", first_ws->name);
return false;
}
if (!con_fullscreen_permits_focusing(second_ws)) {
DLOG("Cannot swap because target workspace \"%s\" is obscured.\n", second_ws->name);
return false;
}
double first_percent = first->percent;
double second_percent = second->percent;
/* De- and reattaching the containers will insert them at the tail of the
* focus_heads. We will need to fix this. But we need to make sure first
* and second don't get in each other's way if they share the same parent,
* so we select the closest previous focus_head that isn't involved. */
Con *first_prev_focus_head = first;
while (first_prev_focus_head == first || first_prev_focus_head == second) {
first_prev_focus_head = TAILQ_PREV(first_prev_focus_head, focus_head, focused);
}
Con *second_prev_focus_head = second;
while (second_prev_focus_head == second || second_prev_focus_head == first) {
second_prev_focus_head = TAILQ_PREV(second_prev_focus_head, focus_head, focused);
}
/* We use a fake container to mark the spot of where the second container needs to go. */
Con *fake = con_new(NULL, NULL);
fake->layout = L_SPLITH;
_con_attach(fake, first->parent, first, true);
bool result = true;
/* Swap the containers. We set the ignore_focus flag here because after the
* container is attached, the focus order is not yet correct and would
* result in wrong windows being focused. */
/* Move first to second. */
result &= _con_move_to_con(first, second, false, false, false, true, false);
/* If we moved the container holding the focused window to another
* workspace we need to ensure the visible workspace has the focused
* container.
* We don't need to check this for the second container because we've only
* moved the first one at this point.*/
if (first_ws != second_ws && focused_within_first) {
con_focus(con_descend_focused(current_ws));
}
/* Move second to where first has been originally. */
result &= _con_move_to_con(second, fake, false, false, false, true, false);
/* If swapping the containers didn't work we don't need to mess with the focus. */
if (!result) {
goto swap_end;
}
/* Swapping will have inserted the containers at the tail of their parents'
* focus head. We fix this now by putting them in the position of the focus
* head the container they swapped with was in. */
TAILQ_REMOVE(&(first->parent->focus_head), first, focused);
TAILQ_REMOVE(&(second->parent->focus_head), second, focused);
if (second_prev_focus_head == NULL) {
TAILQ_INSERT_HEAD(&(first->parent->focus_head), first, focused);
} else {
TAILQ_INSERT_AFTER(&(first->parent->focus_head), second_prev_focus_head, first, focused);
}
if (first_prev_focus_head == NULL) {
TAILQ_INSERT_HEAD(&(second->parent->focus_head), second, focused);
} else {
TAILQ_INSERT_AFTER(&(second->parent->focus_head), first_prev_focus_head, second, focused);
}
/* If the focus was within any of the swapped containers, do the following:
* - If swapping took place within a workspace, ensure the previously
* focused container stays focused.
* - Otherwise, focus the container that has been swapped in.
*
* To understand why fixing the focus_head previously wasn't enough,
* consider the scenario
* H[ V[ A X ] V[ Y B ] ]
* with B being focused, but X being the focus_head within its parent. If
* we swap A and B now, fixing the focus_head would focus X, but since B
* was the focused container before it should stay focused.
*/
if (focused_within_first) {
if (first_ws == second_ws) {
con_focus(old_focus);
} else {
con_focus(con_descend_focused(second));
}
} else if (focused_within_second) {
if (first_ws == second_ws) {
con_focus(old_focus);
} else {
con_focus(con_descend_focused(first));
}
}
/* We need to copy each other's percentages to ensure that the geometry
* doesn't change during the swap. This needs to happen _before_ we close
* the fake container as closing the tree will recalculate percentages. */
first->percent = second_percent;
second->percent = first_percent;
fake->percent = 0.0;
swap_end:
/* We don't actually need this since percentages-wise we haven't changed
* anything, but we'll better be safe than sorry and just make sure as we'd
* otherwise crash i3. */
con_fix_percent(first->parent);
con_fix_percent(second->parent);
/* We can get rid of the fake container again now. */
con_close(fake, DONT_KILL_WINDOW);
con_force_split_parents_redraw(first);
con_force_split_parents_redraw(second);
return result;
}

View File

@ -307,12 +307,8 @@ void match_parse_property(Match *match, const char *ctype, const char *cvalue) {
return; return;
} }
char *end; long parsed;
long parsed = strtol(cvalue, &end, 0); if (!parse_long(cvalue, &parsed, 0)) {
if (parsed == LONG_MIN ||
parsed == LONG_MAX ||
parsed < 0 ||
(end && *end != '\0')) {
ELOG("Could not parse con id \"%s\"\n", cvalue); ELOG("Could not parse con id \"%s\"\n", cvalue);
match->error = sstrdup("invalid con_id"); match->error = sstrdup("invalid con_id");
} else { } else {
@ -323,12 +319,8 @@ void match_parse_property(Match *match, const char *ctype, const char *cvalue) {
} }
if (strcmp(ctype, "id") == 0) { if (strcmp(ctype, "id") == 0) {
char *end; long parsed;
long parsed = strtol(cvalue, &end, 0); if (!parse_long(cvalue, &parsed, 0)) {
if (parsed == LONG_MIN ||
parsed == LONG_MAX ||
parsed < 0 ||
(end && *end != '\0')) {
ELOG("Could not parse window id \"%s\"\n", cvalue); ELOG("Could not parse window id \"%s\"\n", cvalue);
match->error = sstrdup("invalid id"); match->error = sstrdup("invalid id");
} else { } else {

View File

@ -458,3 +458,20 @@ void kill_nagbar(pid_t *nagbar_pid, bool wait_for_it) {
* waitpid() here. */ * waitpid() here. */
waitpid(*nagbar_pid, NULL, 0); waitpid(*nagbar_pid, NULL, 0);
} }
/*
* Converts a string into a long using strtol().
* This is a convenience wrapper checking the parsing result. It returns true
* if the number could be parsed.
*/
bool parse_long(const char *str, long *out, int base) {
char *end;
long result = strtol(str, &end, base);
if (result == LONG_MIN || result == LONG_MAX || result < 0 || (end != NULL && *end != '\0')) {
*out = result;
return false;
}
*out = result;
return true;
}

View File

@ -904,8 +904,9 @@ void x_push_node(Con *con) {
/* Handle all children and floating windows of this node. We recurse /* Handle all children and floating windows of this node. We recurse
* in focus order to display the focused client in a stack first when * in focus order to display the focused client in a stack first when
* switching workspaces (reduces flickering). */ * switching workspaces (reduces flickering). */
TAILQ_FOREACH(current, &(con->focus_head), focused) TAILQ_FOREACH(current, &(con->focus_head), focused) {
x_push_node(current); x_push_node(current);
}
} }
/* /*

View File

@ -169,6 +169,7 @@ is(parser_calls('unknown_literal'),
rename rename
nop nop
scratchpad scratchpad
swap
title_format title_format
mode mode
bar bar

412
testcases/t/265-swap.t Normal file
View File

@ -0,0 +1,412 @@
#!perl
# vim:ts=4:sw=4:expandtab
#
# Please read the following documents before working on tests:
# • http://build.i3wm.org/docs/testsuite.html
# (or docs/testsuite)
#
# • http://build.i3wm.org/docs/lib-i3test.html
# (alternatively: perldoc ./testcases/lib/i3test.pm)
#
# • http://build.i3wm.org/docs/ipc.html
# (or docs/ipc)
#
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
# (unless you are already familiar with Perl)
#
# Tests the swap command.
# Ticket: #917
use i3test i3_autostart => 0;
my $config = <<EOT;
# i3 config file (v4)
font font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
for_window[class="mark_A"] mark A
for_window[class="mark_B"] mark B
EOT
my ($pid);
my ($ws, $ws1, $ws2, $ws3);
my ($nodes, $expected_focus, $A, $B, $F);
my ($result);
my @urgent;
###############################################################################
# Swap two containers next to each other.
# Focus should stay on B because both windows are on the focused workspace.
# The focused container is B.
#
# +---+---+ Layout: H1[ A B ]
# | A | B | Focus Stacks:
# +---+---+ H1: B, A
###############################################################################
$pid = launch_with_config($config);
$ws = fresh_workspace;
$A = open_window(wm_class => 'mark_A');
$B = open_window(wm_class => 'mark_B');
$expected_focus = get_focused($ws);
cmd '[con_mark=B] swap container with mark A';
$nodes = get_ws_content($ws);
is($nodes->[0]->{window}, $B->{id}, 'B is on the left');
is($nodes->[1]->{window}, $A->{id}, 'A is on the right');
is(get_focused($ws), $expected_focus, 'B is still focused');
exit_gracefully($pid);
###############################################################################
# Swap two containers with different parents.
# In this test, the focus head of the left v-split container is A.
# The focused container is B.
#
# +---+---+ Layout: H1[ V1[ A Y ] V2[ X B ] ]
# | A | X | Focus Stacks:
# +---+---+ H1: V2, V1
# | Y | B | V1: A, Y
# +---+---+ V2: B, X
###############################################################################
$pid = launch_with_config($config);
$ws = fresh_workspace;
$A = open_window(wm_class => 'mark_A');
$B = open_window(wm_class => 'mark_B');
cmd 'split v';
open_window;
cmd 'move up, focus left';
cmd 'split v';
open_window;
cmd 'focus up, focus right, focus down';
$expected_focus = get_focused($ws);
cmd '[con_mark=B] swap container with mark A';
$nodes = get_ws_content($ws);
is($nodes->[0]->{nodes}->[0]->{window}, $B->{id}, 'B is on the top left');
is($nodes->[1]->{nodes}->[1]->{window}, $A->{id}, 'A is on the bottom right');
is(get_focused($ws), $expected_focus, 'B is still focused');
exit_gracefully($pid);
###############################################################################
# Swap two containers with different parents.
# In this test, the focus head of the left v-split container is _not_ A.
# The focused container is B.
#
# +---+---+ Layout: H1[ V1[ A Y ] V2[ X B ] ]
# | A | X | Focus Stacks:
# +---+---+ H1: V2, V1
# | Y | B | V1: Y, A
# +---+---+ V2: B, X
###############################################################################
$pid = launch_with_config($config);
$ws = fresh_workspace;
$A = open_window(wm_class => 'mark_A');
$B = open_window(wm_class => 'mark_B');
cmd 'split v';
open_window;
cmd 'move up, focus left';
cmd 'split v';
open_window;
cmd 'focus right, focus down';
$expected_focus = get_focused($ws);
cmd '[con_mark=B] swap container with mark A';
$nodes = get_ws_content($ws);
is($nodes->[0]->{nodes}->[0]->{window}, $B->{id}, 'B is on the top left');
is($nodes->[1]->{nodes}->[1]->{window}, $A->{id}, 'A is on the bottom right');
is(get_focused($ws), $expected_focus, 'B is still focused');
exit_gracefully($pid);
###############################################################################
# Swap two containers with one being on a different workspace.
# The focused container is B.
#
# Layout: O1[ W1[ H1 ] W2[ H2 ] ]
# Focus Stacks:
# O1: W2, W1
#
# +---+---+ Layout: H1[ A X ]
# | A | X | Focus Stacks:
# +---+---+ H1: A, X
#
# +---+---+ Layout: H2[ Y, B ]
# | Y | B | Focus Stacks:
# +---+---+ H2: B, Y
###############################################################################
$pid = launch_with_config($config);
$ws1 = fresh_workspace;
$A = open_window(wm_class => 'mark_A');
$expected_focus = get_focused($ws1);
open_window;
cmd 'focus left';
$ws2 = fresh_workspace;
open_window;
$B = open_window(wm_class => 'mark_B');
cmd '[con_mark=B] swap container with mark A';
$nodes = get_ws_content($ws1);
is($nodes->[0]->{window}, $B->{id}, 'B is on ws1:left');
$nodes = get_ws_content($ws2);
is($nodes->[1]->{window}, $A->{id}, 'A is on ws1:right');
is(get_focused($ws2), $expected_focus, 'A is focused');
exit_gracefully($pid);
###############################################################################
# Swap two non-focused containers within the same workspace.
#
# +---+---+ Layout: H1[ V1[ A X ] V2[ F B ] ]
# | A | F | Focus Stacks:
# +---+---+ H1: V2, V1
# | X | B | V1: A, X
# +---+---+ V2: F, B
###############################################################################
$pid = launch_with_config($config);
$ws = fresh_workspace;
$A = open_window(wm_class => 'mark_A');
$B = open_window(wm_class => 'mark_B');
cmd 'split v';
open_window;
cmd 'move up, focus left';
cmd 'split v';
open_window;
cmd 'focus up, focus right';
$expected_focus = get_focused($ws);
cmd '[con_mark=B] swap container with mark A';
$nodes = get_ws_content($ws);
is($nodes->[0]->{nodes}->[0]->{window}, $B->{id}, 'B is on the top left');
is($nodes->[1]->{nodes}->[1]->{window}, $A->{id}, 'A is on the bottom right');
is(get_focused($ws), $expected_focus, 'F is still focused');
exit_gracefully($pid);
###############################################################################
# Swap two non-focused containers which are both on different workspaces.
#
# Layout: O1[ W1[ A ] W2[ B ] W3[ F ] ]
# Focus Stacks:
# O1: W3, W2, W1
#
# +---+
# | A |
# +---+
#
# +---+
# | B |
# +---+
#
# +---+
# | F |
# +---+
###############################################################################
$pid = launch_with_config($config);
$ws1 = fresh_workspace;
$A = open_window(wm_class => 'mark_A');
$ws2 = fresh_workspace;
$B = open_window(wm_class => 'mark_B');
$ws3 = fresh_workspace;
open_window;
$expected_focus = get_focused($ws3);
cmd '[con_mark=B] swap container with mark A';
$nodes = get_ws_content($ws1);
is($nodes->[0]->{window}, $B->{id}, 'B is on the first workspace');
$nodes = get_ws_content($ws2);
is($nodes->[0]->{window}, $A->{id}, 'A is on the second workspace');
is(get_focused($ws3), $expected_focus, 'F is still focused');
exit_gracefully($pid);
###############################################################################
# Swap two non-focused containers with one being on a different workspace.
#
# Layout: O1[ W1[ A ] W2[ H2 ] ]
# Focus Stacks:
# O1: W2, W1
#
# +---+
# | A |
# +---+
#
# +---+---+ Layout: H2[ B, F ]
# | B | F | Focus Stacks:
# +---+---+ H2: F, B
###############################################################################
$pid = launch_with_config($config);
$ws1 = fresh_workspace;
$A = open_window(wm_class => 'mark_A');
$ws2 = fresh_workspace;
$B = open_window(wm_class => 'mark_B');
open_window;
$expected_focus = get_focused($ws2);
cmd '[con_mark=B] swap container with mark A';
$nodes = get_ws_content($ws1);
is($nodes->[0]->{window}, $B->{id}, 'B is on the first workspace');
$nodes = get_ws_content($ws2);
is($nodes->[0]->{window}, $A->{id}, 'A is on the left of the second workspace');
is(get_focused($ws2), $expected_focus, 'F is still focused');
exit_gracefully($pid);
###############################################################################
# 1. A container cannot be swapped with its parent.
# 2. A container cannot be swapped with one of its children.
#
# ↓A↓
# +---+---+ Layout: H1[ X V1[ Y B ] ]
# | | Y | (with A := V1)
# | X +---+
# | | B |
# +---+---+
###############################################################################
$pid = launch_with_config($config);
$ws = fresh_workspace;
open_window;
open_window;
cmd 'split v';
$B = open_window(wm_class => 'mark_B');
cmd 'focus parent, mark A, focus child';
$result = cmd '[con_mark=B] swap container with mark A';
is($result->[0]->{success}, 0, 'B cannot be swappd with its parent');
$result = cmd '[con_mark=A] swap container with mark B';
is($result->[0]->{success}, 0, 'A cannot be swappd with one of its children');
exit_gracefully($pid);
###############################################################################
# Swapping two containers preserves the geometry of the container they are
# being swapped with.
#
# Before:
# +---+-------+
# | A | B |
# +---+-------+
#
# After:
# +---+-------+
# | B | A |
# +---+-------+
###############################################################################
$pid = launch_with_config($config);
$ws = fresh_workspace;
$A = open_window(wm_class => 'mark_A');
$B = open_window(wm_class => 'mark_B');
cmd 'resize grow width 0 or 25 ppt';
# sanity checks
$nodes = get_ws_content($ws);
cmp_float($nodes->[0]->{percent}, 0.25, 'A has 25% width');
cmp_float($nodes->[1]->{percent}, 0.75, 'B has 75% width');
cmd '[con_mark=B] swap container with mark A';
$nodes = get_ws_content($ws);
cmp_float($nodes->[0]->{percent}, 0.25, 'B has 25% width');
cmp_float($nodes->[1]->{percent}, 0.75, 'A has 75% width');
exit_gracefully($pid);
###############################################################################
# Swapping containers not sharing the same parent preserves the geometry of
# the container they are swapped with.
#
# Before:
# +---+-----+
# | A | |
# +---+ B |
# | | |
# | Y +-----+
# | | X |
# +---+-----+
#
# After:
# +---+-----+
# | B | |
# +---+ A |
# | | |
# | Y +-----+
# | | X |
# +---+-----+
###############################################################################
$pid = launch_with_config($config);
$ws = fresh_workspace;
$A = open_window(wm_class => 'mark_A');
$B = open_window(wm_class => 'mark_B');
cmd 'split v';
open_window;
cmd 'focus up, resize grow height 0 or 25 ppt';
cmd 'focus left, split v';
open_window;
cmd 'resize grow height 0 or 25 ppt';
# sanity checks
$nodes = get_ws_content($ws);
cmp_float($nodes->[0]->{nodes}->[0]->{percent}, 0.25, 'A has 25% height');
cmp_float($nodes->[1]->{nodes}->[0]->{percent}, 0.75, 'B has 75% height');
cmd '[con_mark=B] swap container with mark A';
$nodes = get_ws_content($ws);
cmp_float($nodes->[0]->{nodes}->[0]->{percent}, 0.25, 'B has 25% height');
cmp_float($nodes->[1]->{nodes}->[0]->{percent}, 0.75, 'A has 75% height');
exit_gracefully($pid);
###############################################################################
# Swapping containers moves the urgency hint correctly.
###############################################################################
$pid = launch_with_config($config);
$ws1 = fresh_workspace;
$A = open_window(wm_class => 'mark_A');
$ws2 = fresh_workspace;
$B = open_window(wm_class => 'mark_B');
open_window;
$B->add_hint('urgency');
sync_with_i3;
cmd '[con_mark=B] swap container with mark A';
@urgent = grep { $_->{urgent} } @{get_ws_content($ws1)};
is(@urgent, 1, 'B is marked urgent');
is(get_ws($ws1)->{urgent}, 1, 'the first workspace is marked urgent');
@urgent = grep { $_->{urgent} } @{get_ws_content($ws2)};
is(@urgent, 0, 'A is not marked urgent');
is(get_ws($ws2)->{urgent}, 0, 'the second workspace is not marked urgent');
exit_gracefully($pid);
###############################################################################
done_testing;