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
This commit is contained in:
Ingo Bürk 2016-01-11 20:53:26 +01:00
parent 9a4dbf366b
commit 328035fb7e
12 changed files with 609 additions and 48 deletions

View File

@ -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,

View File

@ -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);

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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
* dont 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;
}

View File

@ -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]);
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();
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;
}
++idx;
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);
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;
}
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");
floating_enable(con, false);
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);
}
}

View File

@ -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 its 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:

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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(

View File

@ -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 = <<EOT;
# i3 config file (v4)
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
bar {
status_command i3status
}
EOT
$config_mm = <<EOT;
# i3 config file (v4)
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
workspace "0" output "fake-0"
workspace "1" output "fake-0"
workspace "2" output "fake-0"
workspace "10" output "fake-1"
workspace "11" output "fake-1"
workspace "12" output "fake-1"
fake-outputs 1024x768+0+0,1024x768+1024+0
EOT
###############################################################################
# Upon managing a window which does not set _NET_WM_DESKTOP, the property is
# set on the window.
###############################################################################
$pid = launch_with_config($config);
cmd 'workspace 1';
$con = open_window;
is(get_net_wm_desktop($con), 0, '_NET_WM_DESKTOP is set upon managing a window');
kill_windows;
exit_gracefully($pid);
###############################################################################
# Upon managing a window which sets _NET_WM_DESKTOP, the window is moved to
# the specified desktop.
###############################################################################
$pid = launch_with_config($config);
cmd 'workspace 0';
open_window;
cmd 'workspace 1';
open_window;
cmd 'workspace 2';
open_window;
$con = open_window_with_net_wm_desktop(1);
is(get_net_wm_desktop($con), 1, '_NET_WM_DESKTOP still has the correct value');
is_num_children('1', 2, 'The window was moved to workspace 1');
kill_windows;
exit_gracefully($pid);
###############################################################################
# Upon managing a window which sets _NET_WM_DESKTOP to the appropriate value,
# the window is made sticky and floating.
###############################################################################
$pid = launch_with_config($config);
cmd 'workspace 0';
$con = open_window_with_net_wm_desktop(0xFFFFFFFF);
is(get_net_wm_desktop($con), 0xFFFFFFFF, '_NET_WM_DESKTOP still has the correct value');
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 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;