From: Ingo Bürk Date: Mon, 11 Jan 2016 19:53:26 +0000 (+0100) Subject: Handle the EWMH atom _NET_WM_DESKTOP. X-Git-Tag: 4.12~29^2 X-Git-Url: https://git.sur5r.net/?a=commitdiff_plain;h=refs%2Fpull%2F2162%2Fhead;p=i3%2Fi3 Handle the EWMH atom _NET_WM_DESKTOP. We already claim _NET_WM_DESKTOP support in _NET_SUPPORTED since around 2009, but haven't actually done anything with it. However, especially pagers like gnome-panel rely on this property to be updated and many tools, like GTK, want to use the corresponding client messages to make a window sticky, move it around etc. This patch implements full support according to the EWMH spec. This means: * We set the property on all windows when managing it. * We keep the property updated on all windows at all times. * We read and respect the property upon managing a window if it was set before mapping the window. * We react to client messages for it. * We remove the property on withdrawn windows. Note that the special value 0xFFFFFFFF, according to the spec, means that the window shall be shown on all workspaces. We do this by making it sticky and float it. This shows it on all workspaces at least on the output it is on. Furthermore, the spec gives us the freedom to ignore _NET_WM_DESKTOP when managing a window if we have good reason to. In our case, we give window swallowing a higher priority since the user would likely expect that and we want to keep placeholder windows only around for as long as we have to. However, we do prioritize this property over, for example, startup notifications. fixes #2153 fixes #1507 fixes #938 --- diff --git a/include/data.h b/include/data.h index 959068e0..3a059e7b 100644 --- a/include/data.h +++ b/include/data.h @@ -398,6 +398,9 @@ struct Window { /** The _NET_WM_WINDOW_TYPE for this window. */ xcb_atom_t window_type; + /** The _NET_WM_DESKTOP for this window. */ + uint32_t wm_desktop; + /** Whether the window says it is a dock window */ enum { W_NODOCK = 0, W_DOCK_TOP = 1, diff --git a/include/ewmh.h b/include/ewmh.h index 7ed9b544..2a55ab9f 100644 --- a/include/ewmh.h +++ b/include/ewmh.h @@ -36,6 +36,13 @@ void ewmh_update_desktop_names(void); */ void ewmh_update_desktop_viewport(void); +/** + * Updates _NET_WM_DESKTOP for all windows. + * A request will only be made if the cached value differs from the calculated value. + * + */ +void ewmh_update_wm_desktop(void); + /** * Updates _NET_ACTIVE_WINDOW with the currently focused window. * @@ -96,3 +103,21 @@ void ewmh_setup_hints(void); * */ void ewmh_update_workarea(void); + +/** + * Returns the workspace container as enumerated by the EWMH desktop model. + * Returns NULL if no workspace could be found for the index. + * + * This is the reverse of ewmh_get_workspace_index. + * + */ +Con *ewmh_get_workspace_by_index(uint32_t idx); + +/** + * Returns the EWMH desktop index for the workspace the given container is on. + * Returns NET_WM_DESKTOP_NONE if the desktop index cannot be determined. + * + * This is the reverse of ewmh_get_workspace_by_index. + * + */ +uint32_t ewmh_get_workspace_index(Con *con); diff --git a/include/workspace.h b/include/workspace.h index 1bee64e0..0ff5cd30 100644 --- a/include/workspace.h +++ b/include/workspace.h @@ -14,6 +14,14 @@ #include "tree.h" #include "randr.h" +/* We use NET_WM_DESKTOP_NONE for cases where we cannot determine the EWMH + * desktop index for a window. We cannot use a negative value like -1 since we + * need to use uint32_t as we actually need the full range of it. This is + * technically dangerous, but it's safe to assume that we will never have more + * than 4294967279 workspaces open at a time. */ +#define NET_WM_DESKTOP_NONE 0xFFFFFFF0 +#define NET_WM_DESKTOP_ALL 0xFFFFFFFF + /** * Returns a pointer to the workspace with the given number (starting at 0), * creating the workspace if necessary (by allocating the necessary amount of diff --git a/src/commands.c b/src/commands.c index 525a30fc..482560b8 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1542,6 +1542,8 @@ void cmd_sticky(I3_CMD, const char *action) { * sure it gets pushed to the front now. */ output_push_sticky_windows(focused); + ewmh_update_wm_desktop(); + cmd_output->needs_tree_render = true; ysuccess(true); } diff --git a/src/con.c b/src/con.c index a3a2f2e3..cd17f9e5 100644 --- a/src/con.c +++ b/src/con.c @@ -1080,6 +1080,7 @@ static bool _con_move_to_con(Con *con, Con *target, bool behind_focused, bool fi CALL(parent, on_remove_child); ipc_send_window_event("move", con); + ewmh_update_wm_desktop(); return true; } diff --git a/src/ewmh.c b/src/ewmh.c index a5c90175..05f4d3cd 100644 --- a/src/ewmh.c +++ b/src/ewmh.c @@ -21,24 +21,9 @@ xcb_window_t ewmh_window; * */ void ewmh_update_current_desktop(void) { - Con *focused_ws = con_get_workspace(focused); - Con *output; - uint32_t idx = 0; - /* We count to get the index of this workspace because named workspaces - * don’t have the ->num property */ - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - - if (ws == focused_ws) { - xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, - A__NET_CURRENT_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &idx); - return; - } - ++idx; - } + const uint32_t idx = ewmh_get_workspace_index(focused); + if (idx != NET_WM_DESKTOP_NONE) { + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_CURRENT_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &idx); } } @@ -138,6 +123,71 @@ void ewmh_update_desktop_viewport(void) { A__NET_DESKTOP_VIEWPORT, XCB_ATOM_CARDINAL, 32, current_position, &viewports); } +static void ewmh_update_wm_desktop_recursively(Con *con, const uint32_t desktop) { + /* Recursively call this to descend through the entire subtree. */ + Con *child; + TAILQ_FOREACH(child, &(con->nodes_head), nodes) { + ewmh_update_wm_desktop_recursively(child, desktop); + } + /* If con is a workspace, we also need to go through the floating windows on it. */ + if (con->type == CT_WORKSPACE) { + TAILQ_FOREACH(child, &(con->floating_head), floating_windows) { + ewmh_update_wm_desktop_recursively(child, desktop); + } + } + + if (!con_has_managed_window(con)) + return; + + const xcb_window_t window = con->window->id; + + uint32_t wm_desktop = desktop; + /* Sticky windows are only actually sticky when they are floating or inside + * a floating container. This is technically still slightly wrong, since + * sticky windows will only be on all workspaces on this output, but we + * ignore multi-monitor situations for this since the spec isn't too + * precise on this anyway. */ + if (con_is_sticky(con) && con_is_floating(con)) { + wm_desktop = NET_WM_DESKTOP_ALL; + } + + /* If this is the cached value, we don't need to do anything. */ + if (con->window->wm_desktop == wm_desktop) + return; + con->window->wm_desktop = wm_desktop; + + if (wm_desktop != NET_WM_DESKTOP_NONE) { + DLOG("Setting _NET_WM_DESKTOP = %d for window 0x%08x.\n", wm_desktop, window); + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, window, A__NET_WM_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &wm_desktop); + } else { + /* If we can't determine the workspace index, delete the property. We'd + * rather not set it than lie. */ + ELOG("Failed to determine the proper EWMH desktop index for window 0x%08x, deleting _NET_WM_DESKTOP.\n", window); + xcb_delete_property(conn, window, A__NET_WM_DESKTOP); + } +} + +/* + * Updates _NET_WM_DESKTOP for all windows. + * A request will only be made if the cached value differs from the calculated value. + * + */ +void ewmh_update_wm_desktop(void) { + uint32_t desktop = 0; + + Con *output; + TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { + Con *workspace; + TAILQ_FOREACH(workspace, &(output_get_content(output)->nodes_head), nodes) { + if (con_is_internal(workspace)) + continue; + + ewmh_update_wm_desktop_recursively(workspace, desktop); + ++desktop; + } + } +} + /* * Updates _NET_ACTIVE_WINDOW with the currently focused window. * @@ -270,3 +320,61 @@ void ewmh_setup_hints(void) { xcb_map_window(conn, ewmh_window); xcb_configure_window(conn, ewmh_window, XCB_CONFIG_WINDOW_STACK_MODE, (uint32_t[]){XCB_STACK_MODE_BELOW}); } + +/* + * Returns the workspace container as enumerated by the EWMH desktop model. + * Returns NULL if no workspace could be found for the index. + * + * This is the reverse of ewmh_get_workspace_index. + * + */ +Con *ewmh_get_workspace_by_index(uint32_t idx) { + if (idx == NET_WM_DESKTOP_NONE) + return NULL; + + uint32_t current_index = 0; + + Con *output; + TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { + Con *workspace; + TAILQ_FOREACH(workspace, &(output_get_content(output)->nodes_head), nodes) { + if (con_is_internal(workspace)) + continue; + + if (current_index == idx) + return workspace; + + ++current_index; + } + } + + return NULL; +} + +/* + * Returns the EWMH desktop index for the workspace the given container is on. + * Returns NET_WM_DESKTOP_NONE if the desktop index cannot be determined. + * + * This is the reverse of ewmh_get_workspace_by_index. + * + */ +uint32_t ewmh_get_workspace_index(Con *con) { + uint32_t index = 0; + + Con *workspace = con_get_workspace(con); + Con *output; + TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { + Con *current; + TAILQ_FOREACH(current, &(output_get_content(output)->nodes_head), nodes) { + if (con_is_internal(current)) + continue; + + if (current == workspace) + return index; + + ++index; + } + } + + return NET_WM_DESKTOP_NONE; +} diff --git a/src/handlers.c b/src/handlers.c index 6cbc54f2..50e218c8 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -503,6 +503,9 @@ static void handle_unmap_notify_event(xcb_unmap_notify_event_t *event) { goto ignore_end; } + /* Since we close the container, we need to unset _NET_WM_DESKTOP according to the spec. */ + xcb_delete_property(conn, event->window, A__NET_WM_DESKTOP); + tree_close_internal(con, DONT_KILL_WINDOW, false, false); tree_render(); @@ -735,7 +738,9 @@ 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); + ewmh_update_sticky(con->window->id, con->sticky); output_push_sticky_windows(focused); + ewmh_update_wm_desktop(); } tree_render(); @@ -839,32 +844,48 @@ static void handle_client_message(xcb_client_message_event_t *event) { * a request to focus the given workspace. See * http://standards.freedesktop.org/wm-spec/latest/ar01s03.html#idm140251368135008 * */ - Con *output; - uint32_t idx = 0; DLOG("Request to change current desktop to index %d\n", event->data.data32[0]); + Con *ws = ewmh_get_workspace_by_index(event->data.data32[0]); + if (ws == NULL) { + ELOG("Could not determine workspace for this index, ignoring request.\n"); + return; + } - TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { - Con *ws; - TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { - if (STARTS_WITH(ws->name, "__")) - continue; - - if (idx == event->data.data32[0]) { - /* data32[1] is a timestamp used to prevent focus race conditions */ - if (event->data.data32[1]) - last_timestamp = event->data.data32[1]; + DLOG("Handling request to focus workspace %s\n", ws->name); + workspace_show(ws); + tree_render(); + } else if (event->type == A__NET_WM_DESKTOP) { + uint32_t index = event->data.data32[0]; + DLOG("Request to move window %d to EWMH desktop index %d\n", event->window, index); - DLOG("Handling request to focus workspace %s\n", ws->name); + Con *con = con_by_window_id(event->window); + if (con == NULL) { + DLOG("Couldn't find con for window %d, ignoring the request.\n", event->window); + return; + } - workspace_show(ws); - tree_render(); + if (index == NET_WM_DESKTOP_ALL) { + /* The window is requesting to be visible on all workspaces, so + * let's float it and make it sticky. */ + DLOG("The window was requested to be visible on all workspaces, making it sticky and floating.\n"); - return; - } + floating_enable(con, false); - ++idx; + con->sticky = true; + ewmh_update_sticky(con->window->id, true); + output_push_sticky_windows(focused); + } else { + Con *ws = ewmh_get_workspace_by_index(index); + if (ws == NULL) { + ELOG("Could not determine workspace for this index, ignoring request.\n"); + return; } + + con_move_to_workspace(con, ws, false, false, true); } + + tree_render(); + ewmh_update_wm_desktop(); } else if (event->type == A__NET_CLOSE_WINDOW) { /* * Pagers wanting to close a window MUST send a _NET_CLOSE_WINDOW @@ -915,8 +936,7 @@ static void handle_client_message(xcb_client_message_event_t *event) { break; } } else { - DLOG("unhandled clientmessage\n"); - return; + DLOG("Skipping client message for unhandled type %d\n", event->type); } } diff --git a/src/manage.c b/src/manage.c index 2bcb47f3..93272f1b 100644 --- a/src/manage.c +++ b/src/manage.c @@ -90,7 +90,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki utf8_title_cookie, title_cookie, class_cookie, leader_cookie, transient_cookie, role_cookie, startup_id_cookie, wm_hints_cookie, - wm_normal_hints_cookie, motif_wm_hints_cookie, wm_user_time_cookie; + wm_normal_hints_cookie, motif_wm_hints_cookie, wm_user_time_cookie, wm_desktop_cookie; geomc = xcb_get_geometry(conn, d); @@ -162,6 +162,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki wm_normal_hints_cookie = xcb_icccm_get_wm_normal_hints(conn, window); motif_wm_hints_cookie = GET_PROPERTY(A__MOTIF_WM_HINTS, 5 * sizeof(uint64_t)); wm_user_time_cookie = GET_PROPERTY(A__NET_WM_USER_TIME, UINT32_MAX); + wm_desktop_cookie = GET_PROPERTY(A__NET_WM_DESKTOP, UINT32_MAX); DLOG("Managing window 0x%08x\n", window); @@ -194,6 +195,16 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki char *startup_ws = startup_workspace_for_window(cwindow, startup_id_reply); DLOG("startup workspace = %s\n", startup_ws); + /* Get _NET_WM_DESKTOP if it was set. */ + xcb_get_property_reply_t *wm_desktop_reply; + wm_desktop_reply = xcb_get_property_reply(conn, wm_desktop_cookie, NULL); + cwindow->wm_desktop = NET_WM_DESKTOP_NONE; + if (wm_desktop_reply != NULL && xcb_get_property_value_length(wm_desktop_reply) != 0) { + uint32_t *wm_desktops = xcb_get_property_value(wm_desktop_reply); + cwindow->wm_desktop = (int32_t)wm_desktops[0]; + } + FREE(wm_desktop_reply); + /* check if the window needs WM_TAKE_FOCUS */ cwindow->needs_take_focus = window_supports_protocol(cwindow->id, A_WM_TAKE_FOCUS); @@ -244,6 +255,8 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki nc = con_for_window(search_at, cwindow, &match); const bool match_from_restart_mode = (match && match->restart_mode); if (nc == NULL) { + Con *wm_desktop_ws = NULL; + /* If not, check if it is assigned to a specific workspace */ if ((assignment = assignment_for(cwindow, A_TO_WORKSPACE))) { DLOG("Assignment matches (%p)\n", match); @@ -258,9 +271,23 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki /* set the urgency hint on the window if the workspace is not visible */ if (!workspace_is_visible(assigned_ws)) urgency_hint = true; + } else if (cwindow->wm_desktop != NET_WM_DESKTOP_NONE && + cwindow->wm_desktop != NET_WM_DESKTOP_ALL && + (wm_desktop_ws = ewmh_get_workspace_by_index(cwindow->wm_desktop)) != NULL) { + /* If _NET_WM_DESKTOP is set to a specific desktop, we open it + * there. Note that we ignore the special value 0xFFFFFFFF here + * since such a window will be made sticky anyway. */ + + DLOG("Using workspace %p / %s because _NET_WM_DESKTOP = %d.\n", + wm_desktop_ws, wm_desktop_ws->name, cwindow->wm_desktop); + + nc = con_descend_tiling_focused(wm_desktop_ws); + if (nc->type == CT_WORKSPACE) + nc = tree_open_con(nc, cwindow); + else + nc = tree_open_con(nc->parent, cwindow); } else if (startup_ws) { - /* If it’s not assigned, but was started on a specific workspace, - * we want to open it there */ + /* If it was started on a specific workspace, we want to open it there. */ DLOG("Using workspace on which this application was started (%s)\n", startup_ws); nc = con_descend_tiling_focused(workspace_get(startup_ws, NULL)); DLOG("focused on ws %s: %p / %s\n", startup_ws, nc, nc->name); @@ -393,6 +420,12 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki if (xcb_reply_contains_atom(state_reply, A__NET_WM_STATE_STICKY)) nc->sticky = true; + if (cwindow->wm_desktop == NET_WM_DESKTOP_ALL) { + DLOG("This window has _NET_WM_DESKTOP = 0xFFFFFFFF. Will float it and make it sticky.\n"); + nc->sticky = true; + want_floating = true; + } + FREE(state_reply); FREE(type_reply); @@ -574,6 +607,13 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki * needs to be on the final workspace first. */ con_set_urgency(nc, urgency_hint); + /* Update _NET_WM_DESKTOP. We invalidate the cached value first to force an update. */ + cwindow->wm_desktop = NET_WM_DESKTOP_NONE; + ewmh_update_wm_desktop(); + + /* If a sticky window was mapped onto another workspace, make sure to pop it to the front. */ + output_push_sticky_windows(focused); + geom_out: free(geom); out: diff --git a/src/move.c b/src/move.c index bd228a1c..87f78ee3 100644 --- a/src/move.c +++ b/src/move.c @@ -206,6 +206,7 @@ void tree_move(Con *con, int direction) { DLOG("Swapped.\n"); ipc_send_window_event("move", con); + ewmh_update_wm_desktop(); return; } @@ -214,6 +215,7 @@ void tree_move(Con *con, int direction) { * try to move it to a workspace on a different output */ move_to_output_directed(con, direction); ipc_send_window_event("move", con); + ewmh_update_wm_desktop(); return; } @@ -274,4 +276,5 @@ end: tree_flatten(croot); ipc_send_window_event("move", con); + ewmh_update_wm_desktop(); } diff --git a/src/workspace.c b/src/workspace.c index 923bfc83..ba19cb5f 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -101,6 +101,7 @@ Con *workspace_get(const char *num, bool *created) { ewmh_update_number_of_desktops(); ewmh_update_desktop_names(); ewmh_update_desktop_viewport(); + ewmh_update_wm_desktop(); if (created != NULL) *created = true; } else if (created != NULL) { @@ -463,6 +464,7 @@ static void _workspace_show(Con *workspace) { ewmh_update_number_of_desktops(); ewmh_update_desktop_names(); ewmh_update_desktop_viewport(); + ewmh_update_wm_desktop(); } } diff --git a/testcases/t/253-multiple-net-wm-state-atoms.t b/testcases/t/253-multiple-net-wm-state-atoms.t index 0ce7f9fd..bbd6c521 100644 --- a/testcases/t/253-multiple-net-wm-state-atoms.t +++ b/testcases/t/253-multiple-net-wm-state-atoms.t @@ -20,7 +20,6 @@ use X11::XCB qw(:all); sub get_wm_state { sync_with_i3; - my $atom = $x->atom(name => '_NET_WM_STATE_HIDDEN'); my ($con) = @_; my $cookie = $x->get_property( diff --git a/testcases/t/529-net-wm-desktop.t b/testcases/t/529-net-wm-desktop.t new file mode 100644 index 00000000..f6a3b218 --- /dev/null +++ b/testcases/t/529-net-wm-desktop.t @@ -0,0 +1,350 @@ +#!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 _NET_WM_DESKTOP. +# Ticket: #2153 +use i3test i3_autostart => 0; +use X11::XCB qw(:all); + +############################################################################### + +sub get_net_wm_desktop { + sync_with_i3; + + my ($con) = @_; + my $cookie = $x->get_property( + 0, + $con->{id}, + $x->atom(name => '_NET_WM_DESKTOP')->id, + $x->atom(name => 'CARDINAL')->id, + 0, + 1 + ); + + my $reply = $x->get_property_reply($cookie->{sequence}); + return undef if $reply->{length} != 1; + + return unpack("L", $reply->{value}); +} + +sub send_net_wm_desktop { + my ($con, $idx) = @_; + my $msg = pack "CCSLLLLLL", + X11::XCB::CLIENT_MESSAGE, 32, 0, + $con->{id}, + $x->atom(name => '_NET_WM_DESKTOP')->id, + $idx, 0, 0, 0, 0; + + $x->send_event(0, $x->get_root_window(), X11::XCB::EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg); + sync_with_i3; +} + +sub open_window_with_net_wm_desktop { + my $idx = shift; + my $window = open_window( + before_map => sub { + my ($window) = @_; + $x->change_property( + PROP_MODE_REPLACE, + $window->id, + $x->atom(name => '_NET_WM_DESKTOP')->id, + $x->atom(name => 'CARDINAL')->id, + 32, 1, + pack('L', $idx), + ); + }, + ); + + return $window; +} + +# We need to kill all windows in between tests since they survive the i3 restart +# and will interfere with the following tests. +sub kill_windows { + sync_with_i3; + cmd '[title="Window.*"] kill'; +} + +############################################################################### + +my ($config, $config_mm, $pid, $con); + +$config = <{floating_nodes}}, 1, 'The window is floating'); +ok(get_ws('0')->{floating_nodes}->[0]->{nodes}->[0]->{sticky}, 'The window is sticky'); + +kill_windows; +exit_gracefully($pid); + +############################################################################### +# _NET_WM_DESKTOP is updated when the window is moved to another workspace +# on the same output. +############################################################################### + +$pid = launch_with_config($config); + +cmd 'workspace 0'; +open_window; +cmd 'workspace 1'; +open_window; +cmd 'workspace 0'; +$con = open_window; + +cmd 'move window to workspace 1'; + +is(get_net_wm_desktop($con), 1, '_NET_WM_DESKTOP is updated when moving the window'); + +kill_windows; +exit_gracefully($pid); + +############################################################################### +# _NET_WM_DESKTOP is updated when the floating window is moved to another +# workspace on the same output. +############################################################################### + +$pid = launch_with_config($config); + +cmd 'workspace 0'; +open_window; +cmd 'workspace 1'; +open_window; +cmd 'workspace 0'; +$con = open_window; +cmd 'floating enable'; + +cmd 'move window to workspace 1'; + +is(get_net_wm_desktop($con), 1, '_NET_WM_DESKTOP is updated when moving the window'); + +kill_windows; +exit_gracefully($pid); + +############################################################################### +# _NET_WM_DESKTOP is updated when the window is moved to another workspace +# on another output. +############################################################################### + +$pid = launch_with_config($config_mm); + +cmd 'workspace 0'; +open_window; +cmd 'workspace 10'; +open_window; +cmd 'workspace 0'; +$con = open_window; + +cmd 'move window to workspace 10'; + +is(get_net_wm_desktop($con), 1, '_NET_WM_DESKTOP is updated when moving the window'); + +kill_windows; +exit_gracefully($pid); + +############################################################################### +# _NET_WM_DESKTOP is removed when the window is withdrawn. +############################################################################### + +$pid = launch_with_config($config); + +$con = open_window; +is(get_net_wm_desktop($con), 0, '_NET_WM_DESKTOP is set (sanity check)'); + +$con->unmap; +wait_for_unmap($con); + +is(get_net_wm_desktop($con), undef, '_NET_WM_DESKTOP is removed'); + +kill_windows; +exit_gracefully($pid); + +############################################################################### +# A _NET_WM_DESKTOP client message sent to the root window moves a window +# to the correct workspace. +############################################################################### + +$pid = launch_with_config($config); + +cmd 'workspace 0'; +open_window; +cmd 'workspace 1'; +open_window; +cmd 'workspace 0'; + +$con = open_window; +is_num_children('0', 2, 'The window is on workspace 0'); + +send_net_wm_desktop($con, 1); + +is_num_children('0', 1, 'The window is no longer on workspace 0'); +is_num_children('1', 2, 'The window is now on workspace 1'); +is(get_net_wm_desktop($con), 1, '_NET_WM_DESKTOP is updated'); + +kill_windows; +exit_gracefully($pid); + +############################################################################### +# A _NET_WM_DESKTOP client message sent to the root window can make a window +# sticky. +############################################################################### + +$pid = launch_with_config($config); + +cmd 'workspace 0'; +$con = open_window; + +send_net_wm_desktop($con, 0xFFFFFFFF); + +is(get_net_wm_desktop($con), 0xFFFFFFFF, '_NET_WM_DESKTOP is updated'); +is(@{get_ws('0')->{floating_nodes}}, 1, 'The window is floating'); +ok(get_ws('0')->{floating_nodes}->[0]->{nodes}->[0]->{sticky}, 'The window is sticky'); + +kill_windows; +exit_gracefully($pid); + +############################################################################### +# _NET_WM_DESKTOP is updated when a new workspace with a lower number is +# opened and closed. +############################################################################### + +$pid = launch_with_config($config); + +cmd 'workspace 1'; +$con = open_window; +is(get_net_wm_desktop($con), 0, '_NET_WM_DESKTOP is set sanity check)'); + +cmd 'workspace 0'; +is(get_net_wm_desktop($con), 1, '_NET_WM_DESKTOP is updated'); + +kill_windows; +exit_gracefully($pid); + +############################################################################### +# _NET_WM_DESKTOP is updated when a window is made sticky by command. +############################################################################### + +$pid = launch_with_config($config); + +cmd 'workspace 0'; +$con = open_window; +cmd 'floating enable'; +is(get_net_wm_desktop($con), 0, '_NET_WM_DESKTOP is set sanity check)'); + +cmd 'sticky enable'; +is(get_net_wm_desktop($con), 0xFFFFFFFF, '_NET_WM_DESKTOP is updated'); + +kill_windows; +exit_gracefully($pid); + +############################################################################### +# _NET_WM_DESKTOP is updated when a window is made sticky by client message. +############################################################################### + +$pid = launch_with_config($config); + +cmd 'workspace 0'; +$con = open_window; +cmd 'floating enable'; +is(get_net_wm_desktop($con), 0, '_NET_WM_DESKTOP is set sanity check)'); + +my $msg = pack "CCSLLLLLL", + X11::XCB::CLIENT_MESSAGE, 32, 0, + $con->{id}, + $x->atom(name => '_NET_WM_STATE')->id, + 1, + $x->atom(name => '_NET_WM_STATE_STICKY')->id, + 0, 0, 0; + +$x->send_event(0, $x->get_root_window(), X11::XCB::EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg); +sync_with_i3; + +is(get_net_wm_desktop($con), 0xFFFFFFFF, '_NET_WM_DESKTOP is updated'); + +kill_windows; +exit_gracefully($pid); + +############################################################################### + +done_testing;