i3 - improved tiling WM


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;