From 2c338b6ae25b5b9aeb51341b26adc82dfb49e913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20B=C3=BCrk?= Date: Sun, 23 Aug 2015 13:36:12 +0200 Subject: [PATCH 1/4] Handle _NET_WM_STATE_STICKY, but only for floating containers. If this atom is set, the floating window will always be automatically moved to the currently active workspace of the output that it is on. This is the equivalent of a sticky note stuck to the monitor. We will respect this atom upon managing a window as well as when we receive a request that changes the sticky state. fixes #1455 --- include/atoms.xmacro | 1 + include/con.h | 6 ++++++ include/data.h | 6 ++++++ src/con.c | 17 ++++++++++++++++ src/ewmh.c | 2 +- src/handlers.c | 13 +++++++++++- src/ipc.c | 3 +++ src/load_layout.c | 3 +++ src/manage.c | 3 +++ src/workspace.c | 39 ++++++++++++++++++++++++++++++++++++ testcases/t/116-nestedcons.t | 1 + 11 files changed, 92 insertions(+), 2 deletions(-) diff --git a/include/atoms.xmacro b/include/atoms.xmacro index 80e3bbf0..f856559c 100644 --- a/include/atoms.xmacro +++ b/include/atoms.xmacro @@ -3,6 +3,7 @@ xmacro(_NET_SUPPORTING_WM_CHECK) xmacro(_NET_WM_NAME) xmacro(_NET_WM_VISIBLE_NAME) xmacro(_NET_WM_MOVERESIZE) +xmacro(_NET_WM_STATE_STICKY) xmacro(_NET_WM_STATE_FULLSCREEN) xmacro(_NET_WM_STATE_DEMANDS_ATTENTION) xmacro(_NET_WM_STATE_MODAL) diff --git a/include/con.h b/include/con.h index 4813b776..c0a3a46b 100644 --- a/include/con.h +++ b/include/con.h @@ -55,6 +55,12 @@ bool con_is_split(Con *con); */ bool con_is_hidden(Con *con); +/** + * Returns whether the container or any of its children is sticky. + * + */ +bool con_is_sticky(Con *con); + /** * Returns true if this node has regular or floating children. * diff --git a/include/data.h b/include/data.h index d75622ec..58e4a00d 100644 --- a/include/data.h +++ b/include/data.h @@ -603,6 +603,12 @@ struct Con { TAILQ_HEAD(swallow_head, Match) swallow_head; fullscreen_mode_t fullscreen_mode; + + /* Whether this window should stick to the glass. This corresponds to + * the _NET_WM_STATE_STICKY atom and will only be respected if the + * window is floating. */ + bool sticky; + /* layout is the layout of this container: one of split[v|h], stacked or * tabbed. Special containers in the tree (above workspaces) have special * layouts like dockarea or output. diff --git a/src/con.c b/src/con.c index 9a5d36c1..24d563a5 100644 --- a/src/con.c +++ b/src/con.c @@ -284,6 +284,23 @@ bool con_is_hidden(Con *con) { return false; } +/* + * Returns whether the container or any of its children is sticky. + * + */ +bool con_is_sticky(Con *con) { + if (con->sticky) + return true; + + Con *child; + TAILQ_FOREACH(child, &(con->nodes_head), nodes) { + if (con_is_sticky(child)) + return true; + } + + return false; +} + /* * Returns true if this node accepts a window (if the node swallows windows, * it might already have swallowed enough and cannot hold any more). diff --git a/src/ewmh.c b/src/ewmh.c index d60bbb50..36c6a160 100644 --- a/src/ewmh.c +++ b/src/ewmh.c @@ -250,7 +250,7 @@ void ewmh_setup_hints(void) { xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_WM_NAME, A_UTF8_STRING, 8, strlen("i3"), "i3"); /* only send the first 31 atoms (last one is _NET_CLOSE_WINDOW) increment that number when adding supported atoms */ - xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_SUPPORTED, XCB_ATOM_ATOM, 32, /* number of atoms */ 31, supported_atoms); + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_SUPPORTED, XCB_ATOM_ATOM, 32, /* number of atoms */ 32, supported_atoms); /* We need to map this window to be able to set the input focus to it if no other window is available to be focused. */ xcb_map_window(conn, ewmh_window); diff --git a/src/handlers.c b/src/handlers.c index f3c2350e..4b01cb5e 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -695,7 +695,8 @@ static void handle_client_message(xcb_client_message_event_t *event) { if (event->type == A__NET_WM_STATE) { if (event->format != 32 || (event->data.data32[1] != A__NET_WM_STATE_FULLSCREEN && - event->data.data32[1] != A__NET_WM_STATE_DEMANDS_ATTENTION)) { + event->data.data32[1] != A__NET_WM_STATE_DEMANDS_ATTENTION && + event->data.data32[1] != A__NET_WM_STATE_STICKY)) { DLOG("Unknown atom in clientmessage of type %d\n", event->data.data32[1]); return; } @@ -725,6 +726,16 @@ static void handle_client_message(xcb_client_message_event_t *event) { con_set_urgency(con, false); else if (event->data.data32[0] == _NET_WM_STATE_TOGGLE) con_set_urgency(con, !con->urgent); + } else if (event->data.data32[1] == A__NET_WM_STATE_STICKY) { + DLOG("Received a client message to modify _NET_WM_STATE_STICKY.\n"); + if (event->data.data32[0] == _NET_WM_STATE_ADD) + con->sticky = true; + else if (event->data.data32[0] == _NET_WM_STATE_REMOVE) + con->sticky = false; + else if (event->data.data32[0] == _NET_WM_STATE_TOGGLE) + con->sticky = !con->sticky; + + DLOG("New sticky status for con = %p is %i.\n", con, con->sticky); } tree_render(); diff --git a/src/ipc.c b/src/ipc.c index ad7ef1cb..1a8d28ed 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -436,6 +436,9 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { ystr("fullscreen_mode"); y(integer, con->fullscreen_mode); + ystr("sticky"); + y(bool, con->sticky); + ystr("floating"); switch (con->floating) { case FLOATING_AUTO_OFF: diff --git a/src/load_layout.c b/src/load_layout.c index 5a139bf2..e0dc4fa0 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -435,6 +435,9 @@ static int json_bool(void *ctx, int val) { to_focus = json_node; } + if (strcasecmp(last_key, "sticky") == 0) + json_node->sticky = val; + if (parsing_swallows) { if (strcasecmp(last_key, "restart_mode") == 0) current_swallow->restart_mode = val; diff --git a/src/manage.c b/src/manage.c index 08e11b57..e3769670 100644 --- a/src/manage.c +++ b/src/manage.c @@ -384,6 +384,9 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki want_floating = true; } + if (xcb_reply_contains_atom(state_reply, A__NET_WM_STATE_STICKY)) + nc->sticky = true; + FREE(state_reply); FREE(type_reply); diff --git a/src/workspace.c b/src/workspace.c index 70022151..2b2c3cbf 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -450,6 +450,45 @@ static void _workspace_show(Con *workspace) { /* Update the EWMH hints */ ewmh_update_current_desktop(); + + /* Floating containers which are sticky need to be moved to the new workspace, + * but only on the same output. */ + if (current != NULL && old_output == new_output) { + Con *prev = focused; + Con *child; + + /* We can't simply iterate over the floating containers since moving a + * sticky container to the target workspace will modify that list. + * Instead, we first count the number of sticky containers, then memorize + * all of them and finally loop over that list to move them to the new + * workspace. */ + int num_sticky = 0; + TAILQ_FOREACH(child, &(current->floating_head), floating_windows) { + if (con_is_sticky(child)) + num_sticky++; + } + + Con *sticky_cons[num_sticky]; + int ctr = 0; + TAILQ_FOREACH(child, &(current->floating_head), floating_windows) { + if (con_is_sticky(child)) + sticky_cons[ctr++] = child; + } + + for (int i = 0; i < num_sticky; i++) { + con_move_to_workspace(sticky_cons[i], workspace, true, false); + + /* We want sticky containers to be at the end of the focus head. */ + TAILQ_REMOVE(&(workspace->focus_head), sticky_cons[i], focused); + TAILQ_INSERT_TAIL(&(workspace->focus_head), sticky_cons[i], focused); + } + + /* Focus the correct container since moving the sticky containers + * changed the focus. However, if no container was focused before, + * we can leave the focus at the sticky container. */ + if (prev != croot) + con_focus(prev); + } } /* diff --git a/testcases/t/116-nestedcons.t b/testcases/t/116-nestedcons.t index d9ff1c39..5e3110ad 100644 --- a/testcases/t/116-nestedcons.t +++ b/testcases/t/116-nestedcons.t @@ -47,6 +47,7 @@ my $ignore = \""; my $expected = { fullscreen_mode => 0, + sticky => $ignore, nodes => $ignore, window => undef, name => 'root', From 9866b0080226504b23eace04947cc44e13ca6697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20B=C3=BCrk?= Date: Sat, 22 Aug 2015 23:37:41 +0200 Subject: [PATCH 2/4] Implement new 'sticky' command to manually set, remove or toggle the sticky state on a window. --- docs/userguide | 21 +++++++++++++++++++++ include/commands.h | 6 ++++++ include/ewmh.h | 6 ++++++ parser-specs/commands.spec | 6 ++++++ src/commands.c | 31 +++++++++++++++++++++++++++++++ src/ewmh.c | 14 ++++++++++++++ testcases/t/187-commands-parser.t | 2 +- 7 files changed, 85 insertions(+), 1 deletion(-) diff --git a/docs/userguide b/docs/userguide index 5e148ff5..d1660c49 100644 --- a/docs/userguide +++ b/docs/userguide @@ -1834,6 +1834,27 @@ bindsym $mod+c move absolute position center bindsym $mod+m move position mouse ------------------------------------------------------- +=== Sticky floating windows + +If you want a window to stick to the glass, i.e., have it stay on screen even +if you switch to another workspace, you can use the +sticky+ command. For +example, this can be useful for notepads, a media player or a video chat +window. + +Note that while any window can be made sticky through this command, it will +only take effect if the window is floating. + +*Syntax*: +---------------------------- +sticky enable|disable|toggle +---------------------------- + +*Examples*: +------------------------------------------------------ +# make a terminal sticky that was started as a notepad +for_window [instance=notepad] sticky enable +------------------------------------------------------ + === Changing (named) workspaces/moving to workspaces To change to a specific workspace, use the +workspace+ command, followed by the diff --git a/include/commands.h b/include/commands.h index b243b9e0..80b0efb0 100644 --- a/include/commands.h +++ b/include/commands.h @@ -204,6 +204,12 @@ void cmd_focus(I3_CMD); */ void cmd_fullscreen(I3_CMD, char *action, char *fullscreen_mode); +/** + * Implementation of 'sticky enable|disable|toggle'. + * + */ +void cmd_sticky(I3_CMD, char *action); + /** * Implementation of 'move [ [px]]'. * diff --git a/include/ewmh.h b/include/ewmh.h index d94b7f3a..7ed9b544 100644 --- a/include/ewmh.h +++ b/include/ewmh.h @@ -68,6 +68,12 @@ void ewmh_update_client_list(xcb_window_t *list, int num_windows); */ void ewmh_update_client_list_stacking(xcb_window_t *stack, int num_windows); +/** + * Set or remove _NET_WM_STATE_STICKY on the window. + * + */ +void ewmh_update_sticky(xcb_window_t window, bool sticky); + /** * Set up the EWMH hints on the root window. * diff --git a/parser-specs/commands.spec b/parser-specs/commands.spec index f5fb9884..5e2bfd8f 100644 --- a/parser-specs/commands.spec +++ b/parser-specs/commands.spec @@ -29,6 +29,7 @@ state INITIAL: 'kill' -> KILL 'open' -> call cmd_open() 'fullscreen' -> FULLSCREEN + 'sticky' -> STICKY 'split' -> SPLIT 'floating' -> FLOATING 'mark' -> MARK @@ -183,6 +184,11 @@ state FULLSCREEN_COMPAT: end -> call cmd_fullscreen("toggle", "output") +# sticky enable|disable|toggle +state STICKY: + action = 'enable', 'disable', 'toggle' + -> call cmd_sticky($action) + # split v|h|vertical|horizontal state SPLIT: direction = 'horizontal', 'vertical', 'v', 'h' diff --git a/src/commands.c b/src/commands.c index 443edf4a..c8164da6 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1569,6 +1569,37 @@ void cmd_fullscreen(I3_CMD, char *action, char *fullscreen_mode) { ysuccess(true); } +/* + * Implementation of 'sticky enable|disable|toggle'. + * + */ +void cmd_sticky(I3_CMD, char *action) { + DLOG("%s sticky on window\n", action); + HANDLE_EMPTY_MATCH; + + owindow *current; + TAILQ_FOREACH(current, &owindows, owindows) { + if (current->con->window == NULL) { + ELOG("only containers holding a window can be made sticky, skipping con = %p\n", current->con); + continue; + } + DLOG("setting sticky for container = %p / %s\n", current->con, current->con->name); + + bool sticky = false; + if (strcmp(action, "enable") == 0) + sticky = true; + else if (strcmp(action, "disable") == 0) + sticky = false; + else if (strcmp(action, "toggle") == 0) + sticky = !current->con->sticky; + + current->con->sticky = sticky; + ewmh_update_sticky(current->con->window->id, sticky); + } + + ysuccess(true); +} + /* * Implementation of 'move [ [px]]'. * diff --git a/src/ewmh.c b/src/ewmh.c index 36c6a160..eb6a6ea6 100644 --- a/src/ewmh.c +++ b/src/ewmh.c @@ -213,6 +213,20 @@ void ewmh_update_client_list_stacking(xcb_window_t *stack, int num_windows) { stack); } +/* + * Set or remove _NET_WM_STATE_STICKY on the window. + * + */ +void ewmh_update_sticky(xcb_window_t window, bool sticky) { + uint32_t values[1]; + unsigned int num = 0; + + if (sticky) + values[num++] = A__NET_WM_STATE_STICKY; + + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, window, A__NET_WM_STATE, XCB_ATOM_ATOM, 32, num, values); +} + /* * Set up the EWMH hints on the root window. * diff --git a/testcases/t/187-commands-parser.t b/testcases/t/187-commands-parser.t index fc7fa882..8aff1f6d 100644 --- a/testcases/t/187-commands-parser.t +++ b/testcases/t/187-commands-parser.t @@ -144,7 +144,7 @@ is(parser_calls("\nworkspace test"), ################################################################################ is(parser_calls('unknown_literal'), - "ERROR: Expected one of these tokens: , '[', 'move', 'exec', 'exit', 'restart', 'reload', 'shmlog', 'debuglog', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'split', 'floating', 'mark', 'unmark', 'resize', 'rename', 'nop', 'scratchpad', 'title_format', 'mode', 'bar'\n" . + "ERROR: Expected one of these tokens: , '[', 'move', 'exec', 'exit', 'restart', 'reload', 'shmlog', 'debuglog', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'sticky', 'split', 'floating', 'mark', 'unmark', 'resize', 'rename', 'nop', 'scratchpad', 'title_format', 'mode', 'bar'\n" . "ERROR: Your command: unknown_literal\n" . "ERROR: ^^^^^^^^^^^^^^^", 'error for unknown literal ok'); From 23a1dadaae73260b951a3c6449b071f0530d3efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20B=C3=BCrk?= Date: Sun, 23 Aug 2015 22:19:20 +0200 Subject: [PATCH 3/4] Added tests for sticky windows. --- testcases/t/251-sticky.t | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 testcases/t/251-sticky.t diff --git a/testcases/t/251-sticky.t b/testcases/t/251-sticky.t new file mode 100644 index 00000000..ada8c9ac --- /dev/null +++ b/testcases/t/251-sticky.t @@ -0,0 +1,82 @@ +#!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 sticky windows. +# Ticket: #1455 +use i3test; + +my ($ws, $focused); + +############################################################################### +# 1: Given a sticky tiling container, when the workspace is switched, then +# nothing happens. +############################################################################### +fresh_workspace; +open_window(wm_class => 'findme'); +cmd 'sticky enable'; +$ws = fresh_workspace; + +is(@{get_ws($ws)->{nodes}}, 0, 'tiling sticky container did not move'); +is(@{get_ws($ws)->{floating_nodes}}, 0, 'tiling sticky container did not move'); +cmd '[class="findme"] kill'; + +############################################################################### +# 2: Given a sticky floating container, when the workspace is switched, then +# the container moves to the new workspace. +############################################################################### +$ws = fresh_workspace; +open_floating_window(wm_class => 'findme'); +$focused = get_focused($ws); +cmd 'sticky enable'; +$ws = fresh_workspace; + +is(@{get_ws($ws)->{floating_nodes}}, 1, 'floating sticky container moved to new workspace'); +is(get_focused($ws), $focused, 'sticky container has focus'); +cmd '[class="findme"] kill'; + +############################################################################### +# 3: Given two sticky floating containers, when the workspace is switched, +# then both containers move to the new workspace. +############################################################################### +fresh_workspace; +open_floating_window(wm_class => 'findme'); +cmd 'sticky enable'; +open_floating_window(wm_class => 'findme'); +cmd 'sticky enable'; +$ws = fresh_workspace; + +is(@{get_ws($ws)->{floating_nodes}}, 2, 'multiple sticky windows can be used at the same time'); +cmd '[class="findme"] kill'; + +############################################################################### +# 4: Given a sticky floating container and a tiling container on the target +# workspace, when the workspace is switched, then the tiling container is +# focused. +############################################################################### +$ws = fresh_workspace; +open_window; +$focused = get_focused($ws); +fresh_workspace; +open_floating_window(wm_class => 'findme'); +cmd 'sticky enable'; +cmd 'workspace ' . $ws; + +is(get_focused($ws), $focused, 'the tiling container has focus'); +cmd '[class="findme"] kill'; + +############################################################################### + +done_testing; From 1c4c3f06fa6ae8f0c4a445bc6f940e43081d40f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20B=C3=BCrk?= Date: Wed, 26 Aug 2015 21:06:53 +0200 Subject: [PATCH 4/4] Make sure sticky windows pop to the front if they get sticky while not being on a visible workspace. This commit also reworks the way focusing sticky windows is prevented by not focusing them temporarily at all, but preventing the focus in the first place. --- include/con.h | 6 +++++- include/output.h | 7 +++++++ src/commands.c | 15 ++++++++++----- src/con.c | 27 ++++++++++++++++----------- src/floating.c | 2 +- src/handlers.c | 1 + src/output.c | 35 +++++++++++++++++++++++++++++++++++ src/scratchpad.c | 6 +++--- src/workspace.c | 40 ++-------------------------------------- testcases/t/251-sticky.t | 14 ++++++++++++++ 10 files changed, 94 insertions(+), 59 deletions(-) diff --git a/include/con.h b/include/con.h index c0a3a46b..cf55978d 100644 --- a/include/con.h +++ b/include/con.h @@ -218,10 +218,14 @@ void con_disable_fullscreen(Con *con); * The dont_warp flag disables pointer warping and will be set when this * function is called while dragging a floating window. * + * If ignore_focus is set, the container will be moved without modifying focus + * at all. + * * TODO: is there a better place for this function? * */ -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, bool ignore_focus); /** * Moves the given container to the given mark. diff --git a/include/output.h b/include/output.h index e0125c06..38e2689a 100644 --- a/include/output.h +++ b/include/output.h @@ -21,3 +21,10 @@ Con *output_get_content(Con *output); * */ Output *get_output_from_string(Output *current_output, const char *output_str); + +/** + * Iterates over all outputs and pushes sticky windows to the currently visible + * workspace on that output. + * + */ +void output_push_sticky_windows(void); diff --git a/src/commands.c b/src/commands.c index c8164da6..81896047 100644 --- a/src/commands.c +++ b/src/commands.c @@ -461,7 +461,7 @@ void cmd_move_con_to_workspace(I3_CMD, char *which) { TAILQ_FOREACH(current, &owindows, owindows) { DLOG("matching: %p / %s\n", current->con, current->con->name); - con_move_to_workspace(current->con, ws, true, false); + con_move_to_workspace(current->con, ws, true, false, false); } cmd_output->needs_tree_render = true; @@ -488,7 +488,7 @@ void cmd_move_con_to_workspace_back_and_forth(I3_CMD) { TAILQ_FOREACH(current, &owindows, owindows) { DLOG("matching: %p / %s\n", current->con, current->con->name); - con_move_to_workspace(current->con, ws, true, false); + con_move_to_workspace(current->con, ws, true, false, false); } cmd_output->needs_tree_render = true; @@ -532,7 +532,7 @@ void cmd_move_con_to_workspace_name(I3_CMD, char *name) { TAILQ_FOREACH(current, &owindows, owindows) { DLOG("matching: %p / %s\n", current->con, current->con->name); - con_move_to_workspace(current->con, ws, true, false); + con_move_to_workspace(current->con, ws, true, false, false); } cmd_output->needs_tree_render = true; @@ -583,7 +583,7 @@ void cmd_move_con_to_workspace_number(I3_CMD, char *which) { TAILQ_FOREACH(current, &owindows, owindows) { DLOG("matching: %p / %s\n", current->con, current->con->name); - con_move_to_workspace(current->con, workspace, true, false); + con_move_to_workspace(current->con, workspace, true, false, false); } cmd_output->needs_tree_render = true; @@ -1223,7 +1223,7 @@ void cmd_move_con_to_output(I3_CMD, char *name) { TAILQ_FOREACH(current, &owindows, owindows) { DLOG("matching: %p / %s\n", current->con, current->con->name); - con_move_to_workspace(current->con, ws, true, false); + con_move_to_workspace(current->con, ws, true, false, false); } cmd_output->needs_tree_render = true; @@ -1597,6 +1597,11 @@ void cmd_sticky(I3_CMD, char *action) { ewmh_update_sticky(current->con->window->id, sticky); } + /* A window we made sticky might not be on a visible workspace right now, so we need to make + * sure it gets pushed to the front now. */ + output_push_sticky_windows(); + + cmd_output->needs_tree_render = true; ysuccess(true); } diff --git a/src/con.c b/src/con.c index 24d563a5..7a8ccd58 100644 --- a/src/con.c +++ b/src/con.c @@ -741,7 +741,7 @@ void con_disable_fullscreen(Con *con) { 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) { +static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fix_coordinates, bool dont_warp, bool ignore_focus) { Con *orig_target = target; /* Prevent moving if this would violate the fullscreen focus restrictions. */ @@ -763,7 +763,7 @@ static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fi Con *child; while (!TAILQ_EMPTY(&(source_ws->floating_head))) { child = TAILQ_FIRST(&(source_ws->floating_head)); - con_move_to_workspace(child, target_ws, true, true); + con_move_to_workspace(child, target_ws, true, true, false); } /* If there are no non-floating children, ignore the workspace. */ @@ -823,7 +823,7 @@ static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fi /* If moving to a visible workspace, call show so it can be considered * focused. Must do before attaching because workspace_show checks to see * if focused container is in its area. */ - if (workspace_is_visible(target_ws)) { + if (!ignore_focus && workspace_is_visible(target_ws)) { workspace_show(target_ws); /* Don’t warp if told so (when dragging floating windows with the @@ -858,8 +858,9 @@ static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fi * workspace, that is, don’t move focus away if the target workspace is * invisible. * We don’t focus the con for i3 pseudo workspaces like __i3_scratch and - * we don’t focus when there is a fullscreen con on that workspace. */ - if (!con_is_internal(target_ws) && !fullscreen) { + * we don’t focus when there is a fullscreen con on that workspace. We + * also don't do it if the caller requested to ignore focus. */ + if (!ignore_focus && !con_is_internal(target_ws) && !fullscreen) { /* We need to save the focused workspace on the output in case the * new workspace is hidden and it's necessary to immediately switch * back to the originally-focused workspace. */ @@ -877,11 +878,12 @@ static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fi /* Descend focus stack in case focus_next is a workspace which can * occur if we move to the same workspace. Also show current workspace * to ensure it is focused. */ - workspace_show(current_ws); + if (!ignore_focus) + workspace_show(current_ws); /* Set focus only if con was on current workspace before moving. * Otherwise we would give focus to some window on different workspace. */ - if (source_ws == current_ws) + if (!ignore_focus && source_ws == current_ws) con_focus(con_descend_focused(focus_next)); /* 8. If anything within the container is associated with a startup sequence, @@ -942,7 +944,7 @@ bool con_move_to_mark(Con *con, const char *mark) { /* 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); + con_move_to_workspace(con, con_get_workspace(target), true, false, false); return true; } @@ -959,7 +961,7 @@ bool con_move_to_mark(Con *con, const char *mark) { return false; } - return _con_move_to_con(con, target, false, true, false); + return _con_move_to_con(con, target, false, true, false, false); } /* @@ -976,10 +978,13 @@ bool con_move_to_mark(Con *con, const char *mark) { * The dont_warp flag disables pointer warping and will be set when this * function is called while dragging a floating window. * + * If ignore_focus is set, the container will be moved without modifying focus + * at all. + * * TODO: is there a better place for this function? * */ -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, bool ignore_focus) { assert(workspace->type == CT_WORKSPACE); Con *source_ws = con_get_workspace(con); @@ -989,7 +994,7 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool } Con *target = con_descend_focused(workspace); - _con_move_to_con(con, target, true, fix_coordinates, dont_warp); + _con_move_to_con(con, target, true, fix_coordinates, dont_warp, ignore_focus); } /* diff --git a/src/floating.c b/src/floating.c index 0a510f67..a9da2c70 100644 --- a/src/floating.c +++ b/src/floating.c @@ -412,7 +412,7 @@ bool floating_maybe_reassign_ws(Con *con) { Con *content = output_get_content(output->con); Con *ws = TAILQ_FIRST(&(content->focus_head)); DLOG("Moving con %p / %s to workspace %p / %s\n", con, con->name, ws, ws->name); - con_move_to_workspace(con, ws, false, true); + con_move_to_workspace(con, ws, false, true, false); con_focus(con_descend_focused(con)); return true; } diff --git a/src/handlers.c b/src/handlers.c index 4b01cb5e..d0b66374 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -736,6 +736,7 @@ static void handle_client_message(xcb_client_message_event_t *event) { con->sticky = !con->sticky; DLOG("New sticky status for con = %p is %i.\n", con, con->sticky); + output_push_sticky_windows(); } tree_render(); diff --git a/src/output.c b/src/output.c index ec5d5f47..e29ab746 100644 --- a/src/output.c +++ b/src/output.c @@ -46,3 +46,38 @@ Output *get_output_from_string(Output *current_output, const char *output_str) { return output; } + +/* + * Iterates over all outputs and pushes sticky windows to the currently visible + * workspace on that output. + * + */ +void output_push_sticky_windows(void) { + Con *output; + TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { + Con *workspace, *visible_ws = NULL; + GREP_FIRST(visible_ws, output_get_content(output), workspace_is_visible(child)); + + /* We use this loop instead of TAILQ_FOREACH to avoid problems if the + * sticky window was the last window on that workspace as moving it in + * this case will close the workspace. */ + for (workspace = TAILQ_FIRST(&(output_get_content(output)->nodes_head)); + workspace != TAILQ_END(&(output_get_content(output)->nodes_head));) { + Con *current_ws = workspace; + workspace = TAILQ_NEXT(workspace, nodes); + + /* Since moving the windows actually removes them from the list of + * floating windows on this workspace, here too we need to use + * another loop than TAILQ_FOREACH. */ + Con *child; + for (child = TAILQ_FIRST(&(current_ws->floating_head)); + child != TAILQ_END(&(current_ws->floating_head));) { + Con *current = child; + child = TAILQ_NEXT(child, floating_windows); + + if (con_is_sticky(current)) + con_move_to_workspace(current, visible_ws, true, false, true); + } + } + } +} diff --git a/src/scratchpad.c b/src/scratchpad.c index 06a7cc73..6d83a558 100644 --- a/src/scratchpad.c +++ b/src/scratchpad.c @@ -60,7 +60,7 @@ void scratchpad_move(Con *con) { /* 2: Send the window to the __i3_scratch workspace, mainting its * coordinates and not warping the pointer. */ - con_move_to_workspace(con, __i3_scratch, true, true); + con_move_to_workspace(con, __i3_scratch, true, true, false); /* 3: If this is the first time this window is used as a scratchpad, we set * the scratchpad_state to SCRATCHPAD_FRESH. The window will then be @@ -142,7 +142,7 @@ void scratchpad_show(Con *con) { floating->scratchpad_state != SCRATCHPAD_NONE) { DLOG("Found a visible scratchpad window on another workspace,\n"); DLOG("moving it to this workspace: con = %p\n", walk_con); - con_move_to_workspace(walk_con, focused_ws, true, false); + con_move_to_workspace(walk_con, focused_ws, true, false, false); return; } } @@ -189,7 +189,7 @@ void scratchpad_show(Con *con) { } /* 1: Move the window from __i3_scratch to the current workspace. */ - con_move_to_workspace(con, active, true, false); + con_move_to_workspace(con, active, true, false, false); /* 2: Adjust the size if this window was not adjusted yet. */ if (con->scratchpad_state == SCRATCHPAD_FRESH) { diff --git a/src/workspace.c b/src/workspace.c index 2b2c3cbf..04053e90 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -451,44 +451,8 @@ static void _workspace_show(Con *workspace) { /* Update the EWMH hints */ ewmh_update_current_desktop(); - /* Floating containers which are sticky need to be moved to the new workspace, - * but only on the same output. */ - if (current != NULL && old_output == new_output) { - Con *prev = focused; - Con *child; - - /* We can't simply iterate over the floating containers since moving a - * sticky container to the target workspace will modify that list. - * Instead, we first count the number of sticky containers, then memorize - * all of them and finally loop over that list to move them to the new - * workspace. */ - int num_sticky = 0; - TAILQ_FOREACH(child, &(current->floating_head), floating_windows) { - if (con_is_sticky(child)) - num_sticky++; - } - - Con *sticky_cons[num_sticky]; - int ctr = 0; - TAILQ_FOREACH(child, &(current->floating_head), floating_windows) { - if (con_is_sticky(child)) - sticky_cons[ctr++] = child; - } - - for (int i = 0; i < num_sticky; i++) { - con_move_to_workspace(sticky_cons[i], workspace, true, false); - - /* We want sticky containers to be at the end of the focus head. */ - TAILQ_REMOVE(&(workspace->focus_head), sticky_cons[i], focused); - TAILQ_INSERT_TAIL(&(workspace->focus_head), sticky_cons[i], focused); - } - - /* Focus the correct container since moving the sticky containers - * changed the focus. However, if no container was focused before, - * we can leave the focus at the sticky container. */ - if (prev != croot) - con_focus(prev); - } + /* Push any sticky windows to the now visible workspace. */ + output_push_sticky_windows(); } /* diff --git a/testcases/t/251-sticky.t b/testcases/t/251-sticky.t index ada8c9ac..6729556d 100644 --- a/testcases/t/251-sticky.t +++ b/testcases/t/251-sticky.t @@ -77,6 +77,20 @@ cmd 'workspace ' . $ws; is(get_focused($ws), $focused, 'the tiling container has focus'); cmd '[class="findme"] kill'; +############################################################################### +# 5: Given a floating container on a non-visible workspace, when the window +# is made sticky, then the window immediately jumps to the currently +# visible workspace. +############################################################################### +fresh_workspace; +open_floating_window(wm_class => 'findme'); +cmd 'mark sticky'; +$ws = fresh_workspace; +cmd '[con_mark=sticky] sticky enable'; + +is(@{get_ws($ws)->{floating_nodes}}, 1, 'the sticky window jumps to the front'); +cmd '[class="findme"] kill'; + ############################################################################### done_testing;