Send IPC window events for focus and title changes
Patch status: superseded
Patch by Marco Hunsicker
Long description:
This patch fixes ticket #1168 to extend the window IPC event mechanism
to send IPC events for window focus and title changes. The newly added
window events use the same format as the already established "new"
event.
Specifically this patch:
* Moves the ipc_send_window_event() function from src/manage.c into
src/ipc.c and adds an argument for the change property of the event
* Updates src/manage.c to use the new function signature and moves the
call above tree_render() to ensure that the "new" event is send
before the "focus" event
* Adds IPC focus event notification to src/x.c. To avoid problems
accessing the window name, a function has been added to query
whether a window is actually attached to its parent. To minimize
obsolete focus notification because of the current i3 click
handling, the current input focus is checked (but this is not
enough to avoid all obsolete focus notifications)
* Adds IPC title event notification to src/handlers.c. To avoid
obsolete title notification, a function has been added to determine
whether a window title has actually changed
* Updates the IPC documentation to include the new events
* Updates the testcases/205-ipc-windows.t test case to include the
focus event in order to ensure the correct event sequence
* Adds two new specific test cases for the focus and title event
To apply this patch, use:
curl http://cr.i3wm.org/patch/413/raw.patch | git am
b/docs/ipc
48 |
@@ -1,7 +1,7 @@
|
49 |
IPC interface (interprocess communication)
|
50 |
==========================================
|
51 |
Michael Stapelberg <michael@i3wm.org>
|
52 |
-October 2012
|
53 |
+February 2014
|
54 |
|
55 |
This document describes how to interface with i3 from a separate process. This
|
56 |
is useful for example to remote-control i3 (to write test cases for example) or
|
57 |
@@ -632,7 +632,8 @@ mode (2)::
|
58 |
Sent whenever i3 changes its binding mode.
|
59 |
window (3)::
|
60 |
Sent when a client's window is successfully reparented (that is when i3
|
61 |
- has finished fitting it into a container).
|
62 |
+ has finished fitting it into a container), when a window received input
|
63 |
+ focus or when a window title has been updated.
|
64 |
barconfig_update (4)::
|
65 |
Sent when the hidden_state or mode field in the barconfig of any bar
|
66 |
instance was updated.
|
67 |
@@ -712,14 +713,14 @@ mode is simply named default.
|
68 |
=== window event
|
69 |
|
70 |
This event consists of a single serialized map containing a property
|
71 |
-+change (string)+ which currently can indicate only that a new window
|
72 |
-has been successfully reparented (the value will be "new").
|
73 |
++change (string)+ which indicates the type of the change ("focus", "new",
|
74 |
+"title").
|
75 |
|
76 |
Additionally a +container (object)+ field will be present, which consists
|
77 |
-of the window's parent container. Be aware that the container will hold
|
78 |
-the initial name of the newly reparented window (e.g. if you run urxvt
|
79 |
-with a shell that changes the title, you will still at this point get the
|
80 |
-window title as "urxvt").
|
81 |
+of the window's parent container. Be aware that for the "new" event, the
|
82 |
+container will hold the initial name of the newly reparented window (e.g.
|
83 |
+if you run urxvt with a shell that changes the title, you will still at
|
84 |
+this point get the window title as "urxvt").
|
85 |
|
86 |
*Example:*
|
87 |
---------------------------
|
b/include/ipc.h
92 |
@@ -87,3 +87,9 @@ void dump_node(yajl_gen gen, Con *con, bool inplace_restart);
|
93 |
* respectively.
|
94 |
*/
|
95 |
void ipc_send_workspace_focus_event(Con *current, Con *old);
|
96 |
+
|
97 |
+/**
|
98 |
+ * For the window events we send, along the usual "change" field,
|
99 |
+ * also the window container, in "container".
|
100 |
+ */
|
101 |
+void ipc_send_window_event(const char *property, Con *con);
|
b/src/handlers.c
106 |
@@ -528,6 +528,16 @@ static void handle_destroy_notify_event(xcb_destroy_notify_event_t *event) {
|
107 |
handle_unmap_notify_event(&unmap);
|
108 |
}
|
109 |
|
110 |
+static bool is_windowname_changed(i3Window *window, char *current_name) {
|
111 |
+ if (current_name != NULL) {
|
112 |
+ const char *new_name = window->name != NULL ? i3string_as_utf8(window->name) : "";
|
113 |
+
|
114 |
+ return strcmp(current_name, new_name) != 0;
|
115 |
+ }
|
116 |
+
|
117 |
+ return false;
|
118 |
+}
|
119 |
+
|
120 |
/*
|
121 |
* Called when a window changes its title
|
122 |
*
|
123 |
@@ -538,10 +548,17 @@ static bool handle_windowname_change(void *data, xcb_connection_t *conn, uint8_t
|
124 |
if ((con = con_by_window_id(window)) == NULL || con->window == NULL)
|
125 |
return false;
|
126 |
|
127 |
+ char * current_name = con->window->name != NULL ? sstrdup(i3string_as_utf8(con->window->name)) : NULL;
|
128 |
+
|
129 |
window_update_name(con->window, prop, false);
|
130 |
|
131 |
x_push_changes(croot);
|
132 |
|
133 |
+ if (is_windowname_changed(con->window, current_name))
|
134 |
+ ipc_send_window_event("title", con);
|
135 |
+
|
136 |
+ FREE(current_name);
|
137 |
+
|
138 |
return true;
|
139 |
}
|
140 |
|
141 |
@@ -556,10 +573,17 @@ static bool handle_windowname_change_legacy(void *data, xcb_connection_t *conn,
|
142 |
if ((con = con_by_window_id(window)) == NULL || con->window == NULL)
|
143 |
return false;
|
144 |
|
145 |
+ char * current_name = con->window->name != NULL ? sstrdup(i3string_as_utf8(con->window->name)) : NULL;
|
146 |
+
|
147 |
window_update_name_legacy(con->window, prop, false);
|
148 |
|
149 |
x_push_changes(croot);
|
150 |
|
151 |
+ if (is_windowname_changed(con->window, current_name))
|
152 |
+ ipc_send_window_event("title", con);
|
153 |
+
|
154 |
+ FREE(current_name);
|
155 |
+
|
156 |
return true;
|
157 |
}
|
158 |
|
b/src/ipc.c
163 |
@@ -1056,3 +1056,33 @@ void ipc_send_workspace_focus_event(Con *current, Con *old) {
|
164 |
y(free);
|
165 |
setlocale(LC_NUMERIC, "");
|
166 |
}
|
167 |
+
|
168 |
+/**
|
169 |
+ * For the window events we send, along the usual "change" field,
|
170 |
+ * also the window container, in "container".
|
171 |
+ */
|
172 |
+void ipc_send_window_event(const char *property, Con *con) {
|
173 |
+ DLOG("Issue IPC window %s event for X11 window 0x%08x\n", property, con->window->id);
|
174 |
+
|
175 |
+ setlocale(LC_NUMERIC, "C");
|
176 |
+ yajl_gen gen = ygenalloc();
|
177 |
+
|
178 |
+ y(map_open);
|
179 |
+
|
180 |
+ ystr("change");
|
181 |
+ ystr(property);
|
182 |
+
|
183 |
+ ystr("container");
|
184 |
+ dump_node(gen, con, false);
|
185 |
+
|
186 |
+ y(map_close);
|
187 |
+
|
188 |
+ const unsigned char *payload;
|
189 |
+ ylength length;
|
190 |
+ y(get_buf, &payload, &length);
|
191 |
+
|
192 |
+ ipc_send_event("window", I3_IPC_EVENT_WINDOW, (const char *)payload);
|
193 |
+ y(free);
|
194 |
+ setlocale(LC_NUMERIC, "");
|
195 |
+}
|
196 |
+
|
b/src/manage.c
201 |
@@ -76,35 +76,6 @@ void restore_geometry(void) {
|
202 |
}
|
203 |
|
204 |
/*
|
205 |
- * The following function sends a new window event, which consists
|
206 |
- * of fields "change" and "container", the latter containing a dump
|
207 |
- * of the window's container.
|
208 |
- *
|
209 |
- */
|
210 |
-static void ipc_send_window_new_event(Con *con) {
|
211 |
- setlocale(LC_NUMERIC, "C");
|
212 |
- yajl_gen gen = ygenalloc();
|
213 |
-
|
214 |
- y(map_open);
|
215 |
-
|
216 |
- ystr("change");
|
217 |
- ystr("new");
|
218 |
-
|
219 |
- ystr("container");
|
220 |
- dump_node(gen, con, false);
|
221 |
-
|
222 |
- y(map_close);
|
223 |
-
|
224 |
- const unsigned char *payload;
|
225 |
- ylength length;
|
226 |
- y(get_buf, &payload, &length);
|
227 |
-
|
228 |
- ipc_send_event("window", I3_IPC_EVENT_WINDOW, (const char *)payload);
|
229 |
- y(free);
|
230 |
- setlocale(LC_NUMERIC, "");
|
231 |
-}
|
232 |
-
|
233 |
-/*
|
234 |
* Do some sanity checks and then reparent the window.
|
235 |
*
|
236 |
*/
|
237 |
@@ -497,10 +468,11 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
|
238 |
ws->rect = ws->parent->rect;
|
239 |
render_con(ws, true);
|
240 |
}
|
241 |
- tree_render();
|
242 |
|
243 |
/* Send an event about window creation */
|
244 |
- ipc_send_window_new_event(nc);
|
245 |
+ ipc_send_window_event("new", nc);
|
246 |
+
|
247 |
+ tree_render();
|
248 |
|
249 |
/* Windows might get managed with the urgency hint already set (Pidgin is
|
250 |
* known to do that), so check for that and handle the hint accordingly.
|
b/src/x.c
255 |
@@ -848,6 +848,22 @@ static void x_push_node_unmaps(Con *con) {
|
256 |
x_push_node_unmaps(current);
|
257 |
}
|
258 |
|
259 |
+/**
|
260 |
+ * Returns true if the given container is currently attached to its parent.
|
261 |
+ */
|
262 |
+static bool is_con_attached(Con *con) {
|
263 |
+ if (con->parent == NULL)
|
264 |
+ return false;
|
265 |
+
|
266 |
+ Con *current;
|
267 |
+ TAILQ_FOREACH(current, &(con->parent->nodes_head), nodes) {
|
268 |
+ if (current == con)
|
269 |
+ return true;
|
270 |
+ }
|
271 |
+
|
272 |
+ return false;
|
273 |
+}
|
274 |
+
|
275 |
/*
|
276 |
* Pushes all changes (state of each node, see x_push_node() and the window
|
277 |
* stack) to X11.
|
278 |
@@ -976,6 +992,20 @@ void x_push_changes(Con *con) {
|
279 |
|
280 |
if (set_focus) {
|
281 |
DLOG("Updating focus (focused: %p / %s) to X11 window 0x%08x\n", focused, focused->name, to_focus);
|
282 |
+
|
283 |
+ /* i3 will set focus again with every click in a window, but the
|
284 |
+ * IPC event should only be send when the focus actually changes. */
|
285 |
+ bool already_focused = false;
|
286 |
+ if (focused->window != NULL) {
|
287 |
+ xcb_get_input_focus_cookie_t cookie = xcb_get_input_focus(conn);
|
288 |
+ xcb_get_input_focus_reply_t *reply = xcb_get_input_focus_reply(conn, cookie, NULL);
|
289 |
+
|
290 |
+ if (reply)
|
291 |
+ already_focused = (reply->focus == focused->window->id);
|
292 |
+
|
293 |
+ free(reply);
|
294 |
+ }
|
295 |
+
|
296 |
/* We remove XCB_EVENT_MASK_FOCUS_CHANGE from the event mask to get
|
297 |
* no focus change events for our own focus changes. We only want
|
298 |
* these generated by the clients. */
|
299 |
@@ -990,6 +1020,10 @@ void x_push_changes(Con *con) {
|
300 |
}
|
301 |
|
302 |
ewmh_update_active_window(to_focus);
|
303 |
+
|
304 |
+ if (!already_focused && to_focus != XCB_NONE && focused->window != NULL && is_con_attached(focused)) {
|
305 |
+ ipc_send_window_event("focus", focused);
|
306 |
+ }
|
307 |
}
|
308 |
|
309 |
focused_id = to_focus;
|
b/testcases/t/205-ipc-windows.t
314 |
@@ -30,20 +30,31 @@ $i3->connect()->recv;
|
315 |
# Events
|
316 |
|
317 |
my $new = AnyEvent->condvar;
|
318 |
+my $focus = AnyEvent->condvar;
|
319 |
$i3->subscribe({
|
320 |
window => sub {
|
321 |
my ($event) = @_;
|
322 |
- $new->send($event->{change} eq 'new');
|
323 |
+ if ($event->{change} eq 'new') {
|
324 |
+ $new->send(1);
|
325 |
+ } elsif ($event->{change} eq 'focus') {
|
326 |
+ $focus->send(1);
|
327 |
+ }
|
328 |
}
|
329 |
})->recv;
|
330 |
|
331 |
open_window;
|
332 |
|
333 |
my $t;
|
334 |
-$t = AnyEvent->timer(after => 0.5, cb => sub { $new->send(0); });
|
335 |
+$t = AnyEvent->timer(
|
336 |
+ after => 0.5,
|
337 |
+ cb => sub {
|
338 |
+ $new->send(0);
|
339 |
+ $focus->send(0);
|
340 |
+ }
|
341 |
+);
|
342 |
|
343 |
ok($new->recv, 'Window "new" event received');
|
344 |
-
|
345 |
+ok($focus->recv, 'Window "focus" event received');
|
346 |
}
|
347 |
|
348 |
done_testing;
|
b/testcases/t/219-ipc-window-focus.t
354 |
@@ -0,0 +1,88 @@
|
355 |
+#!perl
|
356 |
+# vim:ts=4:sw=4:expandtab
|
357 |
+#
|
358 |
+# Please read the following documents before working on tests:
|
359 |
+# • http://build.i3wm.org/docs/testsuite.html
|
360 |
+# (or docs/testsuite)
|
361 |
+#
|
362 |
+# • http://build.i3wm.org/docs/lib-i3test.html
|
363 |
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
|
364 |
+#
|
365 |
+# • http://build.i3wm.org/docs/ipc.html
|
366 |
+# (or docs/ipc)
|
367 |
+#
|
368 |
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
|
369 |
+# (unless you are already familiar with Perl)
|
370 |
+
|
371 |
+use i3test;
|
372 |
+
|
373 |
+SKIP: {
|
374 |
+
|
375 |
+ skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
|
376 |
+
|
377 |
+my $i3 = i3(get_socket_path());
|
378 |
+$i3->connect()->recv;
|
379 |
+
|
380 |
+################################
|
381 |
+# Window focus event
|
382 |
+################################
|
383 |
+
|
384 |
+cmd 'split h';
|
385 |
+
|
386 |
+my $win0 = open_window;
|
387 |
+my $win2 = open_window;
|
388 |
+my $win3 = open_window;
|
389 |
+
|
390 |
+is($x->input_focus, $win3->id, "Window 3 focused");
|
391 |
+
|
392 |
+my $action1 = AnyEvent->condvar;
|
393 |
+my $action2 = AnyEvent->condvar;
|
394 |
+my $action3 = AnyEvent->condvar;
|
395 |
+my $action4 = AnyEvent->condvar;
|
396 |
+
|
397 |
+my @actions = ($action1, $action2, $action3, $action4);
|
398 |
+
|
399 |
+# the sequence in which we expect the window focus to change
|
400 |
+my @sequence = ($win2, $win0, $win2, $win3);
|
401 |
+
|
402 |
+my $index = 0;
|
403 |
+
|
404 |
+$i3->subscribe({
|
405 |
+ window => sub {
|
406 |
+ my ($event) = @_;
|
407 |
+ $actions[$index]->send(
|
408 |
+ $event->{change} eq 'focus' and
|
409 |
+ $sequence[$index]->{name} eq $event->{container}->{name}
|
410 |
+ );
|
411 |
+
|
412 |
+ $index++;
|
413 |
+ }
|
414 |
+})->recv;
|
415 |
+
|
416 |
+cmd 'focus left';
|
417 |
+cmd 'focus left';
|
418 |
+cmd 'focus right';
|
419 |
+cmd 'focus right';
|
420 |
+
|
421 |
+# switching to a new workspace should not generate a focus event
|
422 |
+fresh_workspace;
|
423 |
+
|
424 |
+
|
425 |
+my $t;
|
426 |
+$t = AnyEvent->timer(
|
427 |
+ after => 0.5,
|
428 |
+ cb => sub {
|
429 |
+ $action1->send(0);
|
430 |
+ $action2->send(0);
|
431 |
+ $action3->send(0);
|
432 |
+ $action4->send(0);
|
433 |
+ }
|
434 |
+);
|
435 |
+
|
436 |
+ok($action1->recv, 'Window 2 focused');
|
437 |
+ok($action2->recv, 'Window 0 focused');
|
438 |
+ok($action3->recv, 'Window 2 focused');
|
439 |
+ok($action4->recv, 'Window 3 focused');
|
440 |
+}
|
441 |
+
|
442 |
+done_testing;
|
b/testcases/t/220-ipc-window-title.t
448 |
@@ -0,0 +1,59 @@
|
449 |
+#!perl
|
450 |
+# vim:ts=4:sw=4:expandtab
|
451 |
+#
|
452 |
+# Please read the following documents before working on tests:
|
453 |
+# • http://build.i3wm.org/docs/testsuite.html
|
454 |
+# (or docs/testsuite)
|
455 |
+#
|
456 |
+# • http://build.i3wm.org/docs/lib-i3test.html
|
457 |
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
|
458 |
+#
|
459 |
+# • http://build.i3wm.org/docs/ipc.html
|
460 |
+# (or docs/ipc)
|
461 |
+#
|
462 |
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
|
463 |
+# (unless you are already familiar with Perl)
|
464 |
+
|
465 |
+use i3test;
|
466 |
+
|
467 |
+SKIP: {
|
468 |
+
|
469 |
+ skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
|
470 |
+
|
471 |
+my $i3 = i3(get_socket_path());
|
472 |
+$i3->connect()->recv;
|
473 |
+
|
474 |
+################################
|
475 |
+# Window title event
|
476 |
+################################
|
477 |
+
|
478 |
+my $window = open_window;
|
479 |
+
|
480 |
+is($window->name, 'Window 0', 'Window title is Window 0');
|
481 |
+
|
482 |
+my $title = AnyEvent->condvar;
|
483 |
+
|
484 |
+$i3->subscribe({
|
485 |
+ window => sub {
|
486 |
+ my ($event) = @_;
|
487 |
+ $title->send(
|
488 |
+ $event->{change} eq 'title' and
|
489 |
+ $event->{container}->{name} eq 'Test Window'
|
490 |
+ );
|
491 |
+ }
|
492 |
+})->recv;
|
493 |
+
|
494 |
+$window->name('Test Window');
|
495 |
+
|
496 |
+my $t;
|
497 |
+$t = AnyEvent->timer(
|
498 |
+ after => 0.5,
|
499 |
+ cb => sub {
|
500 |
+ $title->send(0);
|
501 |
+ }
|
502 |
+);
|
503 |
+
|
504 |
+ok($title->recv, 'Window title changed');
|
505 |
+}
|
506 |
+
|
507 |
+done_testing;
|