]> git.sur5r.net Git - i3/i3/commitdiff
Handle the EWMH atom _NET_WM_DESKTOP. 2162/head
authorIngo Bürk <ingo.buerk@tngtech.com>
Mon, 11 Jan 2016 19:53:26 +0000 (20:53 +0100)
committerIngo Bürk <ingo.buerk@tngtech.com>
Mon, 18 Jan 2016 11:13:36 +0000 (12:13 +0100)
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

12 files changed:
include/data.h
include/ewmh.h
include/workspace.h
src/commands.c
src/con.c
src/ewmh.c
src/handlers.c
src/manage.c
src/move.c
src/workspace.c
testcases/t/253-multiple-net-wm-state-atoms.t
testcases/t/529-net-wm-desktop.t [new file with mode: 0644]

index 959068e0f78da0800db35cd5148de3e1380afed4..3a059e7bf7d3898ebb6613f18687065823ab43f9 100644 (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,
index 7ed9b544a7b5321296cbe97d1dc9daa90e974a07..2a55ab9fb4ba738b179e14a9b4414f836547b0a8 100644 (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);
index 1bee64e08d5cf81f08612e90a76fb1bc8bc8cb6a..0ff5cd303abba2f264b437fdb8d814fc788d7e5f 100644 (file)
 #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
index 525a30fceef2e7e681887f51f4940fc14d92bf66..482560b8f974e885f0da349a25e5c1124b51c304 100644 (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);
 }
index a3a2f2e332d322f54a4f1f68d15e6d93749a4cdb..cd17f9e51e0c035754b3b1580a8b4b276e0c071b 100644 (file)
--- 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;
 }
 
index a5c90175495942b7460169afc645575ba2867fb1..05f4d3cd0229e891af3515fcac569bfe7a16fb86 100644 (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
-     * 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;
+}
index 6cbc54f230d0dfc2864be17ed6fcf35e45e73461..50e218c80edc3734c6808058a4015072ce24d4a7 100644 (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]);
+        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);
     }
 }
 
index 2bcb47f3684b2683a6dead59fc8fce3b6037a586..93272f1b7e049a5ead27139372a83ae4345efa26 100644 (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 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:
index bd228a1cc19a19692f48e3f3dd7fb9b6c920f39a..87f78ee3f370abbb883731c76e6dc3d3f1d78b06 100644 (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();
 }
index 923bfc83a4d289c061fd5dc49e5f376a09c1114b..ba19cb5f38bc68f2b8ef616f8bbbebb3216f737c 100644 (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();
         }
     }
 
index 0ce7f9fd5dd900e996c135332b55f47891f03107..bbd6c5214c5889ae2e2f9996a6652b868b229328 100644 (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(
diff --git a/testcases/t/529-net-wm-desktop.t b/testcases/t/529-net-wm-desktop.t
new file mode 100644 (file)
index 0000000..f6a3b21
--- /dev/null
@@ -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;