Merge pull request #1662 from Airblader/feature-move-to-mark

Allow moving windows to marks
This commit is contained in:
Michael Stapelberg 2015-04-19 21:04:26 +02:00
commit 116294cc41
8 changed files with 541 additions and 74 deletions

View File

@ -1837,6 +1837,26 @@ bindsym $mod+x move workspace to output right
bindsym $mod+x move container to output VGA1 bindsym $mod+x move container to output VGA1
-------------------------------------------------------- --------------------------------------------------------
=== Moving containers/workspaces to marks
To move a container to another container with a specific mark (see <<vim_like_marks>>),
you can use the following command.
The window will be moved right after the marked container in the tree, i.e., it ends up
in the same position as if you had opened a new window when the marked container was
focused. If the mark is on a split container, the window will appear as a new child
after the currently focused child within that container.
*Syntax*:
------------------------------------
move window|container to mark <mark>
------------------------------------
*Example*:
--------------------------------------------------------
for_window [instance="tabme"] move window to mark target
--------------------------------------------------------
[[resizingconfig]] [[resizingconfig]]
=== Resizing containers/windows === Resizing containers/windows

View File

@ -132,6 +132,12 @@ void cmd_mode(I3_CMD, char *mode);
*/ */
void cmd_move_con_to_output(I3_CMD, char *name); void cmd_move_con_to_output(I3_CMD, char *name);
/**
* Implementation of 'move [window|container] [to] mark <str>'.
*
*/
void cmd_move_con_to_mark(I3_CMD, char *mark);
/** /**
* Implementation of 'floating enable|disable|toggle' * Implementation of 'floating enable|disable|toggle'
* *

View File

@ -126,6 +126,13 @@ Con *con_by_window_id(xcb_window_t window);
*/ */
Con *con_by_frame_id(xcb_window_t frame); Con *con_by_frame_id(xcb_window_t frame);
/**
* Returns the container with the given mark or NULL if no such container
* exists.
*
*/
Con *con_by_mark(const char *mark);
/** /**
* Returns the first container below 'con' which wants to swallow this window * Returns the first container below 'con' which wants to swallow this window
* TODO: priority * TODO: priority
@ -203,6 +210,12 @@ void con_disable_fullscreen(Con *con);
*/ */
void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool dont_warp); void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool dont_warp);
/**
* Moves the given container to the given mark.
*
*/
bool con_move_to_mark(Con *con, const char *mark);
/** /**
* Returns the orientation of the given container (for stacked containers, * Returns the orientation of the given container (for stacked containers,
* vertical orientation is used regardless of the actual orientation of the * vertical orientation is used regardless of the actual orientation of the

View File

@ -265,6 +265,7 @@ state RENAME_WORKSPACE_NEW_NAME:
# move <direction> [<pixels> [px]] # move <direction> [<pixels> [px]]
# move [window|container] [to] workspace [<str>|next|prev|next_on_output|prev_on_output|current] # move [window|container] [to] workspace [<str>|next|prev|next_on_output|prev_on_output|current]
# move [window|container] [to] output <str> # move [window|container] [to] output <str>
# move [window|container] [to] mark <str>
# move [window|container] [to] scratchpad # move [window|container] [to] scratchpad
# move workspace to [output] <str> # move workspace to [output] <str>
# move scratchpad # move scratchpad
@ -280,6 +281,8 @@ state MOVE:
-> MOVE_WORKSPACE -> MOVE_WORKSPACE
'output' 'output'
-> MOVE_TO_OUTPUT -> MOVE_TO_OUTPUT
'mark'
-> MOVE_TO_MARK
'scratchpad' 'scratchpad'
-> call cmd_move_scratchpad() -> call cmd_move_scratchpad()
direction = 'left', 'right', 'up', 'down' direction = 'left', 'right', 'up', 'down'
@ -321,6 +324,10 @@ state MOVE_TO_OUTPUT:
output = string output = string
-> call cmd_move_con_to_output($output) -> call cmd_move_con_to_output($output)
state MOVE_TO_MARK:
mark = string
-> call cmd_move_con_to_mark($mark)
state MOVE_WORKSPACE_TO_OUTPUT: state MOVE_WORKSPACE_TO_OUTPUT:
'output' 'output'
-> ->

View File

@ -1100,13 +1100,11 @@ void cmd_unmark(I3_CMD, char *mark) {
} }
DLOG("removed all window marks"); DLOG("removed all window marks");
} else { } else {
Con *con; Con *con = con_by_mark(mark);
TAILQ_FOREACH(con, &all_cons, all_cons) { if (con != NULL) {
if (con->mark && strcmp(con->mark, mark) == 0) {
FREE(con->mark); FREE(con->mark);
con->mark_changed = true; con->mark_changed = true;
} }
}
DLOG("removed window mark %s\n", mark); DLOG("removed window mark %s\n", mark);
} }
@ -1169,6 +1167,26 @@ void cmd_move_con_to_output(I3_CMD, char *name) {
ysuccess(true); ysuccess(true);
} }
/*
* Implementation of 'move [container|window] [to] mark <str>'.
*
*/
void cmd_move_con_to_mark(I3_CMD, char *mark) {
DLOG("moving window to mark \"%s\"\n", mark);
HANDLE_EMPTY_MATCH;
bool result = true;
owindow *current;
TAILQ_FOREACH(current, &owindows, owindows) {
DLOG("moving matched window %p / %s to mark \"%s\"\n", current->con, current->con->name, mark);
result &= con_move_to_mark(current->con, mark);
}
cmd_output->needs_tree_render = true;
ysuccess(result);
}
/* /*
* Implementation of 'floating enable|disable|toggle' * Implementation of 'floating enable|disable|toggle'
* *

196
src/con.c
View File

@ -70,20 +70,10 @@ Con *con_new(Con *parent, i3Window *window) {
return new; return new;
} }
/* static void _con_attach(Con *con, Con *parent, Con *previous, bool ignore_focus) {
* Attaches the given container to the given parent. This happens when moving
* a container or when inserting a new container at a specific place in the
* tree.
*
* ignore_focus is to just insert the Con at the end (useful when creating a
* new split container *around* some containers, that is, detaching and
* attaching them in order without wanting to mess with the focus in between).
*
*/
void con_attach(Con *con, Con *parent, bool ignore_focus) {
con->parent = parent; con->parent = parent;
Con *loop; Con *loop;
Con *current = NULL; Con *current = previous;
struct nodes_head *nodes_head = &(parent->nodes_head); struct nodes_head *nodes_head = &(parent->nodes_head);
struct focus_head *focus_head = &(parent->focus_head); struct focus_head *focus_head = &(parent->focus_head);
@ -155,8 +145,7 @@ void con_attach(Con *con, Con *parent, 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 && parent->type != CT_OUTPUT) {
DLOG("Inserting con = %p after last focused tiling con %p\n", DLOG("Inserting con = %p after con %p\n", con, current);
con, current);
TAILQ_INSERT_AFTER(nodes_head, current, con, nodes); TAILQ_INSERT_AFTER(nodes_head, current, con, nodes);
} else } else
TAILQ_INSERT_TAIL(nodes_head, con, nodes); TAILQ_INSERT_TAIL(nodes_head, con, nodes);
@ -170,6 +159,20 @@ add_to_focus_head:
con_force_split_parents_redraw(con); con_force_split_parents_redraw(con);
} }
/*
* Attaches the given container to the given parent. This happens when moving
* a container or when inserting a new container at a specific place in the
* tree.
*
* ignore_focus is to just insert the Con at the end (useful when creating a
* new split container *around* some containers, that is, detaching and
* attaching them in order without wanting to mess with the focus in between).
*
*/
void con_attach(Con *con, Con *parent, bool ignore_focus) {
_con_attach(con, parent, NULL, ignore_focus);
}
/* /*
* Detaches the given container from its current parent * Detaches the given container from its current parent
* *
@ -460,6 +463,21 @@ Con *con_by_frame_id(xcb_window_t frame) {
return NULL; return NULL;
} }
/*
* Returns the container with the given mark or NULL if no such container
* exists.
*
*/
Con *con_by_mark(const char *mark) {
Con *con;
TAILQ_FOREACH(con, &all_cons, all_cons) {
if (con->mark != NULL && strcmp(con->mark, mark) == 0)
return con;
}
return NULL;
}
/* /*
* Returns the first container below 'con' which wants to swallow this window * Returns the first container below 'con' which wants to swallow this window
* TODO: priority * TODO: priority
@ -682,28 +700,14 @@ 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) {
* Moves the given container to the currently focused container on the given Con *orig_target = target;
* workspace.
*
* The fix_coordinates flag will translate the current coordinates (offset from
* the monitor position basically) to appropriate coordinates on the
* destination workspace.
* Not enabling this behaviour comes in handy when this function gets called by
* floating_maybe_reassign_ws, which will only "move" a floating window when it
* *already* changed its coordinates to a different output.
*
* The dont_warp flag disables pointer warping and will be set when this
* function is called while dragging a floating window.
*
* TODO: is there a better place for this function?
*
*/
void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool dont_warp) {
/* Prevent moving if this would violate the fullscreen focus restrictions. */ /* Prevent moving if this would violate the fullscreen focus restrictions. */
if (!con_fullscreen_permits_focusing(workspace)) { Con *target_ws = con_get_workspace(target);
if (!con_fullscreen_permits_focusing(target_ws)) {
LOG("Cannot move out of a fullscreen container"); LOG("Cannot move out of a fullscreen container");
return; return false;
} }
if (con_is_floating(con)) { if (con_is_floating(con)) {
@ -712,27 +716,23 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool
} }
Con *source_ws = con_get_workspace(con); Con *source_ws = con_get_workspace(con);
if (workspace == source_ws) {
DLOG("Not moving, already there\n");
return;
}
if (con->type == CT_WORKSPACE) { if (con->type == CT_WORKSPACE) {
/* Re-parent all of the old workspace's floating windows. */ /* Re-parent all of the old workspace's floating windows. */
Con *child; Con *child;
while (!TAILQ_EMPTY(&(source_ws->floating_head))) { while (!TAILQ_EMPTY(&(source_ws->floating_head))) {
child = TAILQ_FIRST(&(source_ws->floating_head)); child = TAILQ_FIRST(&(source_ws->floating_head));
con_move_to_workspace(child, workspace, true, true); con_move_to_workspace(child, target_ws, true, true);
} }
/* If there are no non-floating children, ignore the workspace. */ /* If there are no non-floating children, ignore the workspace. */
if (con_is_leaf(con)) if (con_is_leaf(con))
return; return false;
con = workspace_encapsulate(con); con = workspace_encapsulate(con);
if (con == NULL) { if (con == NULL) {
ELOG("Workspace failed to move its contents into a container!\n"); ELOG("Workspace failed to move its contents into a container!\n");
return; return false;
} }
} }
@ -744,34 +744,31 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool
Con *current_ws = con_get_workspace(focused); Con *current_ws = con_get_workspace(focused);
Con *source_output = con_get_output(con), Con *source_output = con_get_output(con),
*dest_output = con_get_output(workspace); *dest_output = con_get_output(target_ws);
/* 1: save the container which is going to be focused after the current /* 1: save the container which is going to be focused after the current
* container is moved away */ * container is moved away */
Con *focus_next = con_next_focused(con); Con *focus_next = con_next_focused(con);
/* 2: get the focused container of this workspace */ /* 2: we go up one level, but only when target is a normal container */
Con *next = con_descend_focused(workspace); if (target->type != CT_WORKSPACE) {
DLOG("target originally = %p / %s / type %d\n", target, target->name, target->type);
/* 3: we go up one level, but only when next is a normal container */ target = target->parent;
if (next->type != CT_WORKSPACE) {
DLOG("next originally = %p / %s / type %d\n", next, next->name, next->type);
next = next->parent;
} }
/* 4: if the target container is floating, we get the workspace instead. /* 3: if the target container is floating, we get the workspace instead.
* Only tiling windows need to get inserted next to the current container. * Only tiling windows need to get inserted next to the current container.
* */ * */
Con *floatingcon = con_inside_floating(next); Con *floatingcon = con_inside_floating(target);
if (floatingcon != NULL) { if (floatingcon != NULL) {
DLOG("floatingcon, going up even further\n"); DLOG("floatingcon, going up even further\n");
next = floatingcon->parent; target = floatingcon->parent;
} }
if (con->type == CT_FLOATING_CON) { if (con->type == CT_FLOATING_CON) {
Con *ws = con_get_workspace(next); Con *ws = con_get_workspace(target);
DLOG("This is a floating window, using workspace %p / %s\n", ws, ws->name); DLOG("This is a floating window, using workspace %p / %s\n", ws, ws->name);
next = ws; target = ws;
} }
if (source_output != dest_output) { if (source_output != dest_output) {
@ -785,8 +782,8 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool
/* If moving to a visible workspace, call show so it can be considered /* If moving to a visible workspace, call show so it can be considered
* focused. Must do before attaching because workspace_show checks to see * focused. Must do before attaching because workspace_show checks to see
* if focused container is in its area. */ * if focused container is in its area. */
if (workspace_is_visible(workspace)) { if (workspace_is_visible(target_ws)) {
workspace_show(workspace); workspace_show(target_ws);
/* Dont warp if told so (when dragging floating windows with the /* Dont warp if told so (when dragging floating windows with the
* mouse for example) */ * mouse for example) */
@ -799,29 +796,29 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool
/* If moving a fullscreen container and the destination already has a /* If moving a fullscreen container and the destination already has a
* fullscreen window on it, un-fullscreen the target's fullscreen con. */ * fullscreen window on it, un-fullscreen the target's fullscreen con. */
Con *fullscreen = con_get_fullscreen_con(workspace, CF_OUTPUT); Con *fullscreen = con_get_fullscreen_con(target_ws, CF_OUTPUT);
if (con->fullscreen_mode != CF_NONE && fullscreen != NULL) { if (con->fullscreen_mode != CF_NONE && fullscreen != NULL) {
con_toggle_fullscreen(fullscreen, CF_OUTPUT); con_toggle_fullscreen(fullscreen, CF_OUTPUT);
fullscreen = NULL; fullscreen = NULL;
} }
DLOG("Re-attaching container to %p / %s\n", next, next->name); DLOG("Re-attaching container to %p / %s\n", target, target->name);
/* 5: re-attach the con to the parent of this focused container */ /* 4: re-attach the con to the parent of this focused container */
Con *parent = con->parent; Con *parent = con->parent;
con_detach(con); con_detach(con);
con_attach(con, next, false); _con_attach(con, target, behind_focused ? NULL : orig_target, !behind_focused);
/* 6: fix the percentages */ /* 5: fix the percentages */
con_fix_percent(parent); con_fix_percent(parent);
con->percent = 0.0; con->percent = 0.0;
con_fix_percent(next); con_fix_percent(target);
/* 7: 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
* invisible. * invisible.
* We dont focus the con for i3 pseudo workspaces like __i3_scratch and * We dont focus the con for i3 pseudo workspaces like __i3_scratch and
* we dont focus when there is a fullscreen con on that workspace. */ * we dont focus when there is a fullscreen con on that workspace. */
if (!con_is_internal(workspace) && !fullscreen) { if (!con_is_internal(target_ws) && !fullscreen) {
/* We need to save the focused workspace on the output in case the /* We need to save the focused workspace on the output in case the
* new workspace is hidden and it's necessary to immediately switch * new workspace is hidden and it's necessary to immediately switch
* back to the originally-focused workspace. */ * back to the originally-focused workspace. */
@ -833,7 +830,7 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool
con_focus(old_focus); con_focus(old_focus);
} }
/* 8: when moving to another workspace, we leave the focus on the current /* 7: when moving to another workspace, we leave the focus on the current
* workspace. (see also #809) */ * workspace. (see also #809) */
/* Descend focus stack in case focus_next is a workspace which can /* Descend focus stack in case focus_next is a workspace which can
@ -846,7 +843,7 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool
if (source_ws == current_ws) if (source_ws == current_ws)
con_focus(con_descend_focused(focus_next)); con_focus(con_descend_focused(focus_next));
/* 9. If anything within the container is associated with a startup sequence, /* 8. If anything within the container is associated with a startup sequence,
* delete it so child windows won't be created on the old workspace. */ * delete it so child windows won't be created on the old workspace. */
struct Startup_Sequence *sequence; struct Startup_Sequence *sequence;
xcb_get_property_cookie_t cookie; xcb_get_property_cookie_t cookie;
@ -880,13 +877,78 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool
CALL(parent, on_remove_child); CALL(parent, on_remove_child);
/* 10. If the container was marked urgent, move the urgency hint. */ /* 9. If the container was marked urgent, move the urgency hint. */
if (urgent) { if (urgent) {
workspace_update_urgent_flag(source_ws); workspace_update_urgent_flag(source_ws);
con_set_urgency(con, true); con_set_urgency(con, true);
} }
ipc_send_window_event("move", con); ipc_send_window_event("move", con);
return true;
}
/*
* Moves the given container to the given mark.
*
*/
bool con_move_to_mark(Con *con, const char *mark) {
Con *target = con_by_mark(mark);
if (target == NULL) {
DLOG("found no container with mark \"%s\"\n", mark);
return false;
}
/* For floating target containers, we just send the window to the same workspace. */
if (con_is_floating(target)) {
DLOG("target container is floating, moving container to target's workspace.\n");
con_move_to_workspace(con, con_get_workspace(target), true, false);
return true;
}
/* For split containers, we use the currently focused container within it.
* This allows setting marks on, e.g., tabbed containers which will move
* con to a new tab behind the focused tab. */
if (con_is_split(target)) {
DLOG("target is a split container, descending to the currently focused child.\n");
target = TAILQ_FIRST(&(target->focus_head));
}
if (con == target) {
DLOG("cannot move the container to itself, aborting.\n");
return false;
}
return _con_move_to_con(con, target, false, true, false);
}
/*
* Moves the given container to the currently focused container on the given
* workspace.
*
* The fix_coordinates flag will translate the current coordinates (offset from
* the monitor position basically) to appropriate coordinates on the
* destination workspace.
* Not enabling this behaviour comes in handy when this function gets called by
* floating_maybe_reassign_ws, which will only "move" a floating window when it
* *already* changed its coordinates to a different output.
*
* The dont_warp flag disables pointer warping and will be set when this
* function is called while dragging a floating window.
*
* TODO: is there a better place for this function?
*
*/
void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool dont_warp) {
assert(workspace->type == CT_WORKSPACE);
Con *source_ws = con_get_workspace(con);
if (workspace == source_ws) {
DLOG("Not moving, already there\n");
return;
}
Con *target = con_descend_focused(workspace);
_con_move_to_con(con, target, true, fix_coordinates, dont_warp);
} }
/* /*

View File

@ -150,7 +150,7 @@ is(parser_calls('unknown_literal'),
'error for unknown literal ok'); 'error for unknown literal ok');
is(parser_calls('move something to somewhere'), is(parser_calls('move something to somewhere'),
"ERROR: Expected one of these tokens: 'window', 'container', 'to', 'workspace', 'output', 'scratchpad', 'left', 'right', 'up', 'down', 'position', 'absolute'\n" . "ERROR: Expected one of these tokens: 'window', 'container', 'to', 'workspace', 'output', 'mark', 'scratchpad', 'left', 'right', 'up', 'down', 'position', 'absolute'\n" .
"ERROR: Your command: move something to somewhere\n" . "ERROR: Your command: move something to somewhere\n" .
"ERROR: ^^^^^^^^^^^^^^^^^^^^^^", "ERROR: ^^^^^^^^^^^^^^^^^^^^^^",
'error for unknown literal ok'); 'error for unknown literal ok');

View File

@ -0,0 +1,341 @@
#!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 for the 'move [window|container] to mark' command
# Ticket: #1643
use i3test;
# In the following tests descriptions, we will always use the following names:
# * 'S' for the source container which is going to be moved,
# * 'M' for the marked target container to which 'S' will be moved.
my ($A, $B, $S, $M, $F, $source_ws, $target_ws, $ws);
my ($nodes, $focus);
my $cmd_result;
my $_NET_WM_STATE_REMOVE = 0;
my $_NET_WM_STATE_ADD = 1;
my $_NET_WM_STATE_TOGGLE = 2;
sub set_urgency {
my ($win, $urgent_flag) = @_;
my $msg = pack "CCSLLLLLL",
X11::XCB::CLIENT_MESSAGE, # response_type
32, # format
0, # sequence
$win->id, # window
$x->atom(name => '_NET_WM_STATE')->id, # message type
($urgent_flag ? $_NET_WM_STATE_ADD : $_NET_WM_STATE_REMOVE), # data32[0]
$x->atom(name => '_NET_WM_STATE_DEMANDS_ATTENTION')->id, # data32[1]
0, # data32[2]
0, # data32[3]
0; # data32[4]
$x->send_event(0, $x->get_root_window(), X11::XCB::EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg);
}
###############################################################################
# Given 'M' and 'S' in a horizontal split, when 'S' is moved to 'M', then
# verify that nothing changed.
###############################################################################
$ws = fresh_workspace;
$M = open_window;
cmd 'mark target';
$S = open_window;
cmd 'move container to mark target';
sync_with_i3;
($nodes, $focus) = get_ws_content($ws);
is(@{$nodes}, 2, 'there are two containers');
is($nodes->[0]->{window}, $M->{id}, 'M is left of S');
is($nodes->[1]->{window}, $S->{id}, 'S is right of M');
###############################################################################
# Given 'S' and 'M' in a horizontal split, when 'S' is moved to 'M', then
# both containers switch places.
###############################################################################
$ws = fresh_workspace;
$S = open_window;
$M = open_window;
cmd 'mark target';
cmd 'focus left';
cmd 'move container to mark target';
sync_with_i3;
($nodes, $focus) = get_ws_content($ws);
is(@{$nodes}, 2, 'there are two containers');
is($nodes->[0]->{window}, $M->{id}, 'M is left of S');
is($nodes->[1]->{window}, $S->{id}, 'S is right of M');
###############################################################################
# Given 'S' and no container 'M' exists, when 'S' is moved to 'M', then
# the command is unsuccessful.
###############################################################################
$ws = fresh_workspace;
$S = open_window;
$cmd_result = cmd 'move container to mark absent';
is($cmd_result->[0]->{success}, 0, 'command was unsuccessful');
###############################################################################
# Given 'S' and 'M' on different workspaces, when 'S' is moved to 'M', then
# 'S' ends up on the same workspace as 'M'.
###############################################################################
$source_ws = fresh_workspace;
$S = open_window;
$target_ws = fresh_workspace;
$M = open_window;
cmd 'mark target';
cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;
($nodes, $focus) = get_ws_content($source_ws);
is(@{$nodes}, 0, 'source workspace is empty');
($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 2, 'both containers are on the target workspace');
is($nodes->[0]->{window}, $M->{id}, 'M is left of S');
is($nodes->[1]->{window}, $S->{id}, 'S is right of M');
###############################################################################
# Given 'S' and 'M' on different workspaces and 'S' is urgent, when 'S' is
# moved to 'M', then the urgency flag is transferred to the target workspace.
###############################################################################
$source_ws = fresh_workspace;
$S = open_window;
$F = open_window;
$target_ws = fresh_workspace;
$M = open_window;
cmd 'mark target';
cmd 'workspace ' . $source_ws;
set_urgency($S, 1);
cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;
$source_ws = get_ws($source_ws);
$target_ws = get_ws($target_ws);
ok(!$source_ws->{urgent}, 'source workspace is no longer urgent');
ok($target_ws->{urgent}, 'target workspace is urgent');
###############################################################################
# Given 'S' and 'M' where 'M' is inside a tabbed container, when 'S' is moved
# to 'M', then 'S' ends up as a new tab.
###############################################################################
$source_ws = fresh_workspace;
$S = open_window;
# open tabbed container ['A' 'M' 'B']
$target_ws = fresh_workspace;
$A = open_window;
cmd 'layout tabbed';
$M = open_window;
cmd 'mark target';
$B = open_window;
cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;
($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'there is a tabbed container');
$nodes = $nodes->[0]->{nodes};
is(@{$nodes}, 4, 'all four containers are on the target workspace');
is($nodes->[0]->{window}, $A->{id}, 'A is the first tab');
is($nodes->[1]->{window}, $M->{id}, 'M is the second tab');
is($nodes->[2]->{window}, $S->{id}, 'S is the third tab');
is($nodes->[3]->{window}, $B->{id}, 'B is the fourth tab');
###############################################################################
# Given 'S' and 'M' where 'M' is a tabbed container where the currently focused
# tab is a nested layout, when 'S' is moved to 'M', then 'S' is a new tab
# within 'M'.
###############################################################################
$source_ws = fresh_workspace;
$S = open_window;
$target_ws = fresh_workspace;
$A = open_window;
cmd 'layout tabbed';
cmd 'focus parent';
cmd 'mark target';
cmd 'focus child';
$B = open_window;
cmd 'split h';
$F = open_window;
cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;
($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'there is a tabbed container');
$nodes = $nodes->[0]->{nodes};
is(@{$nodes}, 3, 'there are three tabs');
is($nodes->[0]->{window}, $A->{id}, 'A is the first tab');
is($nodes->[2]->{window}, $S->{id}, 'S is the third tab');
###############################################################################
# Given 'S' and 'M' where 'M' is inside a split container inside a tabbed
# container, when 'S' is moved to 'M', then 'S' ends up as a container
# within the same tab as 'M'.
###############################################################################
$source_ws = fresh_workspace;
$S = open_window;
# open tabbed container ['A'['B' 'M']]
$target_ws = fresh_workspace;
$A = open_window;
cmd 'layout tabbed';
$B = open_window;
cmd 'split h';
$M = open_window;
cmd 'mark target';
cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;
($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'there is a tabbed container');
$nodes = $nodes->[0]->{nodes};
is(@{$nodes}, 2, 'there are two tabs');
$nodes = $nodes->[1]->{nodes};
is(@{$nodes}, 3, 'the tab with the marked children has three children');
is($nodes->[0]->{window}, $B->{id}, 'B is the first tab');
is($nodes->[1]->{window}, $M->{id}, 'M is the second tab');
is($nodes->[2]->{window}, $S->{id}, 'S is the third tab');
###############################################################################
# Given 'S', 'A' and 'B' where 'A' and 'B' are inside the tabbed container 'M',
# when 'S' is moved to 'M', then 'S' ends up as a new tab in 'M'.
###############################################################################
$source_ws = fresh_workspace;
$S = open_window;
$target_ws = fresh_workspace;
$A = open_window;
cmd 'layout tabbed';
$B = open_window;
cmd 'focus parent';
cmd 'mark target';
cmd 'focus child';
cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;
($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'there is a tabbed container');
$nodes = $nodes->[0]->{nodes};
is(@{$nodes}, 3, 'there are three tabs');
is($nodes->[0]->{window}, $A->{id}, 'A is the first tab');
is($nodes->[1]->{window}, $B->{id}, 'B is the second tab');
is($nodes->[2]->{window}, $S->{id}, 'S is the third tab');
###############################################################################
# Given 'S', 'A', 'F' and 'M', where 'M' is a workspace containing a tabbed
# container, when 'S' is moved to 'M', then 'S' does not end up as a tab, but
# rather as a new window next to the tabbed container.
###############################################################################
$source_ws = fresh_workspace;
$S = open_window;
$target_ws = fresh_workspace;
$A = open_window;
cmd 'layout tabbed';
$F = open_window;
$M = $target_ws;
cmd 'focus parent';
cmd 'focus parent';
cmd 'mark target';
cmd 'focus ' . $source_ws;
cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;
($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 2, 'there is a tabbed container and a window');
is($nodes->[1]->{window}, $S->{id}, 'S is the second window');
###############################################################################
# Given 'S' and 'M' where 'S' is floating and 'M' on a different workspace,
# when 'S' is moved to 'M', then 'S' is a floating container on the same
# workspaces as 'M'.
###############################################################################
$source_ws = fresh_workspace;
$S = open_floating_window;
$target_ws = fresh_workspace;
$M = open_window;
cmd 'mark target';
cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;
is(@{get_ws($target_ws)->{floating_nodes}}, 1, 'target workspace has the container now');
###############################################################################
# Given 'S' and 'M' where 'M' is floating and on a different workspace,
# when 'S' is moved to 'M', then 'S' ends up as a tiling container on the
# same workspace as 'M'.
###############################################################################
$source_ws = fresh_workspace;
$S = open_window;
$target_ws = fresh_workspace;
$M = open_floating_window;
cmd 'mark target';
cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;
($nodes, $focus) = get_ws_content($target_ws);
is(@{$nodes}, 1, 'tiling container moved to the target workspace');
###############################################################################
# Given 'S' and 'M' are the same container, when 'S' is moved to 'M', then
# the command is ignored.
###############################################################################
$ws = fresh_workspace;
$S = open_window;
$M = $S;
cmd 'mark target';
cmd '[id="' . $S->{id} . '"] move container to mark target';
sync_with_i3;
does_i3_live;
###############################################################################
done_testing;