Feature: EWMH desktop properties
Patch status: needinfo
Patch by Tony Crisci
Long description:
Implement the following EWMH desktop properties: * _NET_WM_DESKTOP * _NET_DESKTOP_VIEWPORT * _NET_DESKTOP_NAMES * _NET_NUMBER_OF_DESKTOPS And one client message: * _NET_CURRENT_DESKTOP For more information see the EWMH spec at: http://standards.freedesktop.org/wm-spec/latest This should enable most of the features of most taskbars and pagers such as candybar, xfce4-panel, tint2, and others. fixes #1241
To apply this patch, use:
curl http://cr.i3wm.org/patch/543/raw.patch | git am
b/include/atoms.xmacro
41 |
@@ -16,6 +16,9 @@ xmacro(_NET_WM_STRUT_PARTIAL) |
42 |
xmacro(_NET_CLIENT_LIST) |
43 |
xmacro(_NET_CLIENT_LIST_STACKING) |
44 |
xmacro(_NET_CURRENT_DESKTOP) |
45 |
+xmacro(_NET_NUMBER_OF_DESKTOPS) |
46 |
+xmacro(_NET_DESKTOP_NAMES) |
47 |
+xmacro(_NET_DESKTOP_VIEWPORT) |
48 |
xmacro(_NET_ACTIVE_WINDOW) |
49 |
xmacro(_NET_STARTUP_ID) |
50 |
xmacro(_NET_WORKAREA) |
b/include/data.h
55 |
@@ -501,6 +501,10 @@ struct Con { |
56 |
* workspace is not a named workspace (for named workspaces, num == -1) */ |
57 |
int num; |
58 |
|
59 |
+ /** if this is a workspace, this is the index of the desktop in the context |
60 |
+ * of ewmh standards compliance. */ |
61 |
+ uint32_t ewmh_desktop_index; |
62 |
+ |
63 |
struct Con *parent; |
64 |
|
65 |
struct Rect rect; |
b/include/ewmh.h
70 |
@@ -10,10 +10,9 @@ |
71 |
#pragma once |
72 |
|
73 |
/** |
74 |
- * Updates _NET_CURRENT_DESKTOP with the current desktop number. |
75 |
- * |
76 |
- * EWMH: The index of the current desktop. This is always an integer between 0 |
77 |
- * and _NET_NUMBER_OF_DESKTOPS - 1. |
78 |
+ * Updates EWMH properties of the root and managed windows related to |
79 |
+ * _NET_CURRENT_DESKTOP so that other clients such as taskbars and pagers can |
80 |
+ * find information about the state of the managed windows. |
81 |
* |
82 |
*/ |
83 |
void ewmh_update_current_desktop(void); |
84 |
@@ -44,6 +43,16 @@ void ewmh_update_client_list(xcb_window_t *list, int num_windows); |
85 |
*/ |
86 |
void ewmh_update_client_list_stacking(xcb_window_t *stack, int num_windows); |
87 |
|
88 |
+/* |
89 |
+ * Updates the _NET_WM_DESKTOP hint. |
90 |
+ * |
91 |
+ * Cardinal to determine the desktop the window is in (or wants to |
92 |
+ * be) starting with 0 for the first desktop. |
93 |
+ * http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm140251368061040 |
94 |
+ * |
95 |
+ */ |
96 |
+void ewmh_update_wm_desktop(xcb_window_t window, uint32_t idx); |
97 |
+ |
98 |
/** |
99 |
* Set up the EWMH hints on the root window. |
100 |
* |
b/src/ewmh.c
105 |
@@ -12,32 +12,63 @@ |
106 |
#include "all.h" |
107 |
|
108 |
/* |
109 |
- * Updates _NET_CURRENT_DESKTOP with the current desktop number. |
110 |
- * |
111 |
- * EWMH: The index of the current desktop. This is always an integer between 0 |
112 |
- * and _NET_NUMBER_OF_DESKTOPS - 1. |
113 |
+ * Updates EWMH properties of the root and managed windows related to |
114 |
+ * _NET_CURRENT_DESKTOP so that other clients such as taskbars and pagers can |
115 |
+ * find information about the state of the managed windows. |
116 |
* |
117 |
*/ |
118 |
void ewmh_update_current_desktop(void) { |
119 |
Con *focused_ws = con_get_workspace(focused); |
120 |
Con *output; |
121 |
uint32_t idx = 0; |
122 |
- /* We count to get the index of this workspace because named workspaces |
123 |
- * don’t have the ->num property */ |
124 |
+ |
125 |
TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { |
126 |
Con *ws; |
127 |
TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { |
128 |
if (STARTS_WITH(ws->name, "__")) |
129 |
continue; |
130 |
|
131 |
+ /* this index can be used in the context of client requests |
132 |
+ * specified by ewmh for simple window manager commands */ |
133 |
+ ws->ewmh_desktop_index = idx; |
134 |
+ |
135 |
+ /* _NET_CURRENT_DESKTOP |
136 |
+ * The index of the current desktop. This is always an integer |
137 |
+ * between 0 and _NET_NUMBER_OF_DESKTOPS - 1. |
138 |
+ */ |
139 |
if (ws == focused_ws) { |
140 |
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, |
141 |
A__NET_CURRENT_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, &idx); |
142 |
- return; |
143 |
} |
144 |
+ |
145 |
+ /* _NET_DESKTOP_VIEWPORT |
146 |
+ * Array of pairs of cardinals that define the top left corner of each desktop's viewport. |
147 |
+ */ |
148 |
+ uint32_t viewport[] = { |
149 |
+ output->rect.x, |
150 |
+ output->rect.y, |
151 |
+ }; |
152 |
+ |
153 |
+ xcb_change_property(conn, |
154 |
+ (idx == 0 ? XCB_PROP_MODE_REPLACE : XCB_PROP_MODE_APPEND), |
155 |
+ root, A__NET_DESKTOP_VIEWPORT, XCB_ATOM_CARDINAL, 32, 2, viewport); |
156 |
+ |
157 |
+ /* _NET_DESKTOP_NAMES |
158 |
+ * The names of all virtual desktops |
159 |
+ */ |
160 |
+ xcb_change_property(conn, |
161 |
+ (idx == 0 ? XCB_PROP_MODE_REPLACE : XCB_PROP_MODE_APPEND), |
162 |
+ root, A__NET_DESKTOP_NAMES, A_UTF8_STRING, 8, |
163 |
+ strlen(ws->name) + 1, ws->name); |
164 |
+ |
165 |
++idx; |
166 |
} |
167 |
} |
168 |
+ |
169 |
+ /* _NET_NUMBER_OF_DESKTOPS |
170 |
+ */ |
171 |
+ xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, |
172 |
+ A__NET_NUMBER_OF_DESKTOPS, XCB_ATOM_CARDINAL, 32, 1, &idx); |
173 |
} |
174 |
|
175 |
/* |
176 |
@@ -104,6 +135,24 @@ void ewmh_update_client_list_stacking(xcb_window_t *stack, int num_windows) { |
177 |
} |
178 |
|
179 |
/* |
180 |
+ * Updates the _NET_WM_DESKTOP hint. |
181 |
+ * |
182 |
+ * Cardinal to determine the desktop the window is in (or wants to |
183 |
+ * be) starting with 0 for the first desktop. |
184 |
+ * |
185 |
+ */ |
186 |
+void ewmh_update_wm_desktop(xcb_window_t window, uint32_t idx) { |
187 |
+ xcb_change_property(conn, |
188 |
+ XCB_PROP_MODE_REPLACE, |
189 |
+ window, |
190 |
+ A__NET_WM_DESKTOP, |
191 |
+ XCB_ATOM_CARDINAL, |
192 |
+ 32, |
193 |
+ 1, |
194 |
+ &idx); |
195 |
+} |
196 |
+ |
197 |
+/* |
198 |
* Set up the EWMH hints on the root window. |
199 |
* |
200 |
*/ |
201 |
@@ -138,5 +187,5 @@ void ewmh_setup_hints(void) { |
202 |
/* I’m not entirely sure if we need to keep _NET_WM_NAME on root. */ |
203 |
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_WM_NAME, A_UTF8_STRING, 8, strlen("i3"), "i3"); |
204 |
|
205 |
- xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_SUPPORTED, XCB_ATOM_ATOM, 32, 19, supported_atoms); |
206 |
+ xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_SUPPORTED, XCB_ATOM_ATOM, 35, 19, supported_atoms); |
207 |
} |
b/src/handlers.c
212 |
@@ -786,6 +786,29 @@ static void handle_client_message(xcb_client_message_event_t *event) { |
213 |
XCB_ATOM_CARDINAL, 32, 4, |
214 |
&r); |
215 |
xcb_flush(conn); |
216 |
+ } else if (event->type == A__NET_CURRENT_DESKTOP) { |
217 |
+ if (event->format != 32) |
218 |
+ return; |
219 |
+ |
220 |
+ DLOG("_NET_CURRENT_DESKTOP for ewmh desktop index %d\n", event->data.data32[0]); |
221 |
+ |
222 |
+ Con *output; |
223 |
+ TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { |
224 |
+ Con *ws; |
225 |
+ TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { |
226 |
+ if (STARTS_WITH(ws->name, "__")) |
227 |
+ continue; |
228 |
+ |
229 |
+ if (event->data.data32[0] == ws->ewmh_desktop_index) { |
230 |
+ DLOG("Client message requests \"%s\" become the current workspace (con = %p). \n", |
231 |
+ ws->name, ws); |
232 |
+ |
233 |
+ workspace_show(ws); |
234 |
+ tree_render(); |
235 |
+ return; |
236 |
+ } |
237 |
+ } |
238 |
+ } |
239 |
} else { |
240 |
DLOG("unhandled clientmessage\n"); |
241 |
return; |
b/src/ipc.c
246 |
@@ -88,6 +88,9 @@ void ipc_send_event(const char *event, uint32_t message_type, const char *payloa |
247 |
|
248 |
ipc_send_message(current->fd, strlen(payload), message_type, (const uint8_t*)payload); |
249 |
} |
250 |
+ |
251 |
+ if (message_type == I3_IPC_EVENT_WORKSPACE) |
252 |
+ ewmh_update_current_desktop(); |
253 |
} |
254 |
|
255 |
/* |
256 |
@@ -1067,6 +1070,8 @@ void ipc_send_workspace_focus_event(Con *current, Con *old) { |
257 |
ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, (const char *)payload); |
258 |
y(free); |
259 |
setlocale(LC_NUMERIC, ""); |
260 |
+ |
261 |
+ ewmh_update_current_desktop(); |
262 |
} |
263 |
|
264 |
/** |
b/src/workspace.c
269 |
@@ -436,9 +436,6 @@ static void _workspace_show(Con *workspace) { |
270 |
if (old_output != new_output) { |
271 |
x_set_warp_to(&next->rect); |
272 |
} |
273 |
- |
274 |
- /* Update the EWMH hints */ |
275 |
- ewmh_update_current_desktop(); |
276 |
} |
277 |
|
278 |
/* |
b/src/x.c
283 |
@@ -954,10 +954,15 @@ void x_push_changes(Con *con) { |
284 |
|
285 |
walk = client_list_windows; |
286 |
|
287 |
- /* reorder by initial mapping */ |
288 |
TAILQ_FOREACH(state, &initial_mapping_head, initial_mapping_order) { |
289 |
- if (con_has_managed_window(state->con)) |
290 |
+ if (con_has_managed_window(state->con)) { |
291 |
+ /* update _NET_WM_DESKTOP */ |
292 |
+ ewmh_update_wm_desktop(state->con->window->id, |
293 |
+ con_get_workspace(state->con)->ewmh_desktop_index); |
294 |
+ |
295 |
+ /* reorder by initial mapping */ |
296 |
*walk++ = state->con->window->id; |
297 |
+ } |
298 |
} |
299 |
|
300 |
ewmh_update_client_list(client_list_windows, client_list_count); |
b/testcases/t/518-ewmh-desktops.t
306 |
@@ -0,0 +1,236 @@ |
307 |
+#!perl |
308 |
+# vim:ts=4:sw=4:expandtab |
309 |
+# |
310 |
+# Please read the following documents before working on tests: |
311 |
+# • http://build.i3wm.org/docs/testsuite.html |
312 |
+# (or docs/testsuite) |
313 |
+# |
314 |
+# • http://build.i3wm.org/docs/lib-i3test.html |
315 |
+# (alternatively: perldoc ./testcases/lib/i3test.pm) |
316 |
+# |
317 |
+# • http://build.i3wm.org/docs/ipc.html |
318 |
+# (or docs/ipc) |
319 |
+# |
320 |
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf |
321 |
+# (unless you are already familiar with Perl) |
322 |
+# |
323 |
+# Tests that EWMH desktop hints and client messages work well enough for a |
324 |
+# pager or taskbar like candybar, xfce4-panel, tint2, etc. |
325 |
+# Ticket: #1241 |
326 |
+# Bug still in: 4.7.2-149-g708996b |
327 |
+use i3test i3_autostart => 0; |
328 |
+ |
329 |
+my $config = <<EOT; |
330 |
+# i3 config file (v4) |
331 |
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 |
332 |
+ |
333 |
+workspace "1:L" output fake-0 |
334 |
+workspace "2:R" output fake-1 |
335 |
+workspace "3:L" output fake-0 |
336 |
+workspace "4:R" output fake-1 |
337 |
+ |
338 |
+fake-outputs 1000x500+1+2,1000x500+1000+500 |
339 |
+EOT |
340 |
+ |
341 |
+my $pid = launch_with_config($config); |
342 |
+ |
343 |
+# constants for readability |
344 |
+my $OUTPUT_L_X = 1; |
345 |
+my $OUTPUT_L_Y = 2; |
346 |
+my $OUTPUT_R_X = 1000; |
347 |
+my $OUTPUT_R_Y = 500; |
348 |
+ |
349 |
+# boilerplate for property access |
350 |
+sub x_get_property { |
351 |
+ my ($prop, $prop_type, $win_id) = @_; |
352 |
+ |
353 |
+ $win_id = $x->get_root_window() unless $win_id; |
354 |
+ |
355 |
+ my $cookie = $x->get_property( |
356 |
+ 0, |
357 |
+ $win_id, |
358 |
+ $x->atom(name => $prop)->id, |
359 |
+ $x->atom(name => $prop_type)->id, |
360 |
+ 0, |
361 |
+ 4096, |
362 |
+ ); |
363 |
+ return $x->get_property_reply($cookie->{sequence}); |
364 |
+} |
365 |
+ |
366 |
+# _NET_NUMBER_OF_DESKTOPS |
367 |
+sub get_number_of_desktops { |
368 |
+ my $reply = x_get_property('_NET_NUMBER_OF_DESKTOPS', 'CARDINAL'); |
369 |
+ my $len = $reply->{length}; |
370 |
+ |
371 |
+ return -1 if $len == 0; |
372 |
+ |
373 |
+ return unpack("L", $reply->{value}); |
374 |
+} |
375 |
+ |
376 |
+# _NET_DESKTOP_NAMES |
377 |
+# The names of all virtual desktops |
378 |
+sub get_desktop_names { |
379 |
+ my $reply = x_get_property('_NET_DESKTOP_NAMES', 'UTF8_STRING'); |
380 |
+ my $len = $reply->{value_len} - 1; |
381 |
+ |
382 |
+ return () if $len < 1; |
383 |
+ |
384 |
+ return split(/\0/, unpack("a$len", $reply->{value})); |
385 |
+} |
386 |
+ |
387 |
+# _NET_CURRENT_DESKTOP |
388 |
+# The index of the current desktop. This is always an integer |
389 |
+# between 0 and _NET_NUMBER_OF_DESKTOPS - 1. |
390 |
+sub get_current_desktop { |
391 |
+ my $reply = x_get_property('_NET_CURRENT_DESKTOP', 'CARDINAL'); |
392 |
+ |
393 |
+ my $len = $reply->{length}; |
394 |
+ return -1 if $len == 0; |
395 |
+ |
396 |
+ return unpack("L", $reply->{value}); |
397 |
+} |
398 |
+ |
399 |
+# _NET_DESKTOP_VIEWPORT |
400 |
+# Array of pairs of cardinals that define the top left corner of each desktop's viewport. |
401 |
+sub get_desktop_viewports { |
402 |
+ my $reply = x_get_property('_NET_DESKTOP_VIEWPORT', 'CARDINAL'); |
403 |
+ |
404 |
+ my $len = $reply->{length}; |
405 |
+ return () if $len == 0; |
406 |
+ |
407 |
+ my @value = unpack("L$len", $reply->{value}); |
408 |
+ my @viewports = (); |
409 |
+ |
410 |
+ while (@value) { |
411 |
+ my %vp = ( |
412 |
+ 'x' => shift @value, |
413 |
+ 'y' => shift @value, |
414 |
+ ); |
415 |
+ push @viewports, \%vp; |
416 |
+ } |
417 |
+ |
418 |
+ return @viewports; |
419 |
+} |
420 |
+ |
421 |
+# _NET_WM_DESKTOP |
422 |
+# Cardinal to determine the desktop the window is in (or wants to |
423 |
+# be) starting with 0 for the first desktop. |
424 |
+sub get_desktop_for_window { |
425 |
+ my ($win) = @_; |
426 |
+ my $reply = x_get_property('_NET_WM_DESKTOP', 'CARDINAL', $win); |
427 |
+ |
428 |
+ my $len = $reply->{length}; |
429 |
+ return -1 if $len == 0; |
430 |
+ |
431 |
+ return unpack("L", $reply->{value}); |
432 |
+} |
433 |
+ |
434 |
+sub send_current_desktop_message { |
435 |
+ my ($desktop_index) = @_; |
436 |
+ |
437 |
+ my $msg = pack "CCSLLLLLLL", |
438 |
+ X11::XCB::CLIENT_MESSAGE, # response_type |
439 |
+ 32, # format |
440 |
+ 0, |
441 |
+ 0, |
442 |
+ $x->atom(name => '_NET_CURRENT_DESKTOP')->id, |
443 |
+ $desktop_index, |
444 |
+ 0, |
445 |
+ 0, |
446 |
+ 0, |
447 |
+ 0; |
448 |
+ |
449 |
+ $x->send_event(0, $x->get_root_window(), X11::XCB::EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg); |
450 |
+} |
451 |
+ |
452 |
+# The point of these hints is so any application such as a taskbar or pager can |
453 |
+# look at the properties of the root window and have enough to meaningfully |
454 |
+# display information for the user and send us some basic commands based on the |
455 |
+# user input they receive, so this objective will guide the tests. |
456 |
+sub compare_desktops_to_workspaces { |
457 |
+ my $output = shift @_; |
458 |
+ |
459 |
+ my $note = '-- comparing ewmh desktop properties to i3wm workspace properties'; |
460 |
+ |
461 |
+ my $output_x; |
462 |
+ my $output_y; |
463 |
+ |
464 |
+ if ($output =~ '^L') { |
465 |
+ $note .= ' on the left output'; |
466 |
+ $output_x = $OUTPUT_L_X; |
467 |
+ $output_y = $OUTPUT_L_Y; |
468 |
+ } else { |
469 |
+ $note .= ' on the right output'; |
470 |
+ $output_x = $OUTPUT_R_X; |
471 |
+ $output_y = $OUTPUT_R_Y; |
472 |
+ } |
473 |
+ |
474 |
+ note $note, ' for workspace ', focused_ws(); |
475 |
+ |
476 |
+ is(get_number_of_desktops(), @{get_workspace_names()}, 'the number of desktops should match the number of workspaces'); |
477 |
+ |
478 |
+ # The current desktop is an index that is not related to the i3 concept of a |
479 |
+ # "workspace number" |
480 |
+ my $current_desktop = get_current_desktop(); |
481 |
+ |
482 |
+ is($current_desktop, get_desktop_for_window(@{get_ws_content(focused_ws)}[0]->{window}), |
483 |
+ 'a window on a workspace should have the correct desktop index'); |
484 |
+ |
485 |
+ my @desktop_names = get_desktop_names(); |
486 |
+ is_deeply(\@desktop_names, get_workspace_names(), |
487 |
+ 'the names of the desktops should match the names of the workspaces'); |
488 |
+ |
489 |
+ is($desktop_names[$current_desktop], focused_ws(), |
490 |
+ 'the value at the index of a workspace in the desktop names list should match the workspace name'); |
491 |
+ |
492 |
+ my @desktop_viewports = get_desktop_viewports(); |
493 |
+ is($desktop_viewports[$current_desktop]{x}, $output_x, |
494 |
+ 'the desktop viewport should match the workspace output x'); |
495 |
+ is($desktop_viewports[$current_desktop]{y}, $output_y, |
496 |
+ 'the desktop viewport should match the workspace output y'); |
497 |
+} |
498 |
+ |
499 |
+##################################################################### |
500 |
+# Get basic layout information with ewmh hints, such as the names of the |
501 |
+# workspaces, where they are on the screen, which one is active, and so on. |
502 |
+##################################################################### |
503 |
+ |
504 |
+# Open a window on each of our two outputs |
505 |
+cmd 'workspace "2:R"'; |
506 |
+my $win_ws2 = open_window; |
507 |
+ |
508 |
+cmd 'workspace "1:L"'; |
509 |
+my $win_ws1 = open_window; |
510 |
+ |
511 |
+note 'When a workspace is focused, the ewmh desktop hints should be updated correctly'; |
512 |
+compare_desktops_to_workspaces('Left'); |
513 |
+ |
514 |
+##################################################################### |
515 |
+# Send basic commands, like switching to a particular workspace |
516 |
+##################################################################### |
517 |
+ |
518 |
+send_current_desktop_message(get_desktop_for_window($win_ws2->{id})); |
519 |
+sync_with_i3; |
520 |
+ |
521 |
+note 'Sending a current desktop client message should switch to the workspace with the given ewmh desktop index'; |
522 |
+compare_desktops_to_workspaces('Right'); |
523 |
+ |
524 |
+cmd 'workspace "3:L"'; |
525 |
+my $win_ws3 = open_window; |
526 |
+ |
527 |
+cmd 'workspace "4:R"'; |
528 |
+my $win_ws4 = open_window; |
529 |
+ |
530 |
+note 'The desktop properties should stay up to date for any number of workspaces'; |
531 |
+send_current_desktop_message(get_desktop_for_window($win_ws3->{id})); |
532 |
+sync_with_i3; |
533 |
+compare_desktops_to_workspaces('Left'); |
534 |
+ |
535 |
+note 'When a workspace is renamed, the ewmh desktop name property should be updated immediately to reflect the change.'; |
536 |
+cmd 'workspace 4:R'; |
537 |
+cmd 'rename workspace "4:R" to "8:web"'; |
538 |
+compare_desktops_to_workspaces('Right'); |
539 |
+ |
540 |
+exit_gracefully($pid); |
541 |
+ |
542 |
+done_testing; |