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. To ensure
  that the "new" event does not send the same event data as the
  "focus" event, setting focus now happens after the "new" event
  has been sent
* Adds IPC focus event notification to src/x.c. To workaround a problem
  during window close when accessing the window name, a function has
  been added to query whether a window is actually attached to its parent.
  To avoid obsolete focus notification, a new field has been added to
  keep track of the focus without any interference by the click handling
* 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 testcases/t/205-ipc-windows.t to include the "focus" event
  in order to ensure the correct event sequence
* Adds two new unit tests, b/testcases/t/219-ipc-window-focus.t and
  b/testcases/t/220-ipc-window-title.t to ensure proper "focus" and
 "title" events

To apply this patch, use:
curl http://cr.i3wm.org/patch/430/raw.patch | git am

b/docs/ipc

50
@@ -1,7 +1,7 @@
51
 IPC interface (interprocess communication)
52
 ==========================================
53
 Michael Stapelberg <michael@i3wm.org>
54
-October 2012
55
+February 2014
56
 
57
 This document describes how to interface with i3 from a separate process. This
58
 is useful for example to remote-control i3 (to write test cases for example) or
59
@@ -632,7 +632,8 @@ mode (2)::
60
 	Sent whenever i3 changes its binding mode.
61
 window (3)::
62
 	Sent when a client's window is successfully reparented (that is when i3
63
-	has finished fitting it into a container).
64
+	has finished fitting it into a container), when a window received input
65
+	focus or when a window title has been updated.
66
 barconfig_update (4)::
67
     Sent when the hidden_state or mode field in the barconfig of any bar
68
     instance was updated.
69
@@ -712,14 +713,14 @@ mode is simply named default.
70
 === window event
71
 
72
 This event consists of a single serialized map containing a property
73
-+change (string)+ which currently can indicate only that a new window
74
-has been successfully reparented (the value will be "new").
75
++change (string)+ which indicates the type of the change ("focus", "new",
76
+"title").
77
 
78
 Additionally a +container (object)+ field will be present, which consists
79
-of the window's parent container. Be aware that the container will hold
80
-the initial name of the newly reparented window (e.g. if you run urxvt
81
-with a shell that changes the title, you will still at this point get the
82
-window title as "urxvt").
83
+of the window's parent container. Be aware that for the "new" event, the
84
+container will hold the initial name of the newly reparented window (e.g.
85
+if you run urxvt with a shell that changes the title, you will still at
86
+this point get the window title as "urxvt").
87
 
88
 *Example:*
89
 ---------------------------

b/include/ipc.h

94
@@ -87,3 +87,9 @@ void dump_node(yajl_gen gen, Con *con, bool inplace_restart);
95
  * respectively.
96
  */
97
 void ipc_send_workspace_focus_event(Con *current, Con *old);
98
+
99
+/**
100
+ * For the window events we send, along the usual "change" field,
101
+ * also the window container, in "container".
102
+ */
103
+void ipc_send_window_event(const char *property, Con *con);

b/src/handlers.c

108
@@ -528,6 +528,17 @@ static void handle_destroy_notify_event(xcb_destroy_notify_event_t *event) {
109
     handle_unmap_notify_event(&unmap);
110
 }
111
 
112
+static bool window_name_changed(i3Window *window, char *old_name) {
113
+    if ((old_name == NULL) && (window->name == NULL))
114
+        return false;
115
+
116
+    /* Either the old or the new one is NULL, but not both. */
117
+    if ((old_name == NULL) ^ (window->name == NULL))
118
+        return true;
119
+
120
+    return (strcmp(old_name, i3string_as_utf8(window->name)) != 0);
121
+}
122
+
123
 /*
124
  * Called when a window changes its title
125
  *
126
@@ -538,10 +549,17 @@ static bool handle_windowname_change(void *data, xcb_connection_t *conn, uint8_t
127
     if ((con = con_by_window_id(window)) == NULL || con->window == NULL)
128
         return false;
129
 
130
+    char *old_name = (con->window->name != NULL ? sstrdup(i3string_as_utf8(con->window->name)) : NULL);
131
+
132
     window_update_name(con->window, prop, false);
133
 
134
     x_push_changes(croot);
135
 
136
+    if (window_name_changed(con->window, old_name))
137
+        ipc_send_window_event("title", con);
138
+
139
+    FREE(old_name);
140
+
141
     return true;
142
 }
143
 
144
@@ -556,10 +574,17 @@ static bool handle_windowname_change_legacy(void *data, xcb_connection_t *conn,
145
     if ((con = con_by_window_id(window)) == NULL || con->window == NULL)
146
         return false;
147
 
148
+    char *old_name = (con->window->name != NULL ? sstrdup(i3string_as_utf8(con->window->name)) : NULL);
149
+
150
     window_update_name_legacy(con->window, prop, false);
151
 
152
     x_push_changes(croot);
153
 
154
+    if (window_name_changed(con->window, old_name))
155
+        ipc_send_window_event("title", con);
156
+
157
+    FREE(old_name);
158
+
159
     return true;
160
 }
161
 

b/src/ipc.c

166
@@ -1056,3 +1056,33 @@ void ipc_send_workspace_focus_event(Con *current, Con *old) {
167
     y(free);
168
     setlocale(LC_NUMERIC, "");
169
 }
170
+
171
+/**
172
+ * For the window events we send, along the usual "change" field,
173
+ * also the window container, in "container".
174
+ */
175
+void ipc_send_window_event(const char *property, Con *con) {
176
+    DLOG("Issue IPC window %s event for X11 window 0x%08x\n", property, con->window->id);
177
+
178
+    setlocale(LC_NUMERIC, "C");
179
+    yajl_gen gen = ygenalloc();
180
+
181
+    y(map_open);
182
+
183
+    ystr("change");
184
+    ystr(property);
185
+
186
+    ystr("container");
187
+    dump_node(gen, con, false);
188
+
189
+    y(map_close);
190
+
191
+    const unsigned char *payload;
192
+    ylength length;
193
+    y(get_buf, &payload, &length);
194
+
195
+    ipc_send_event("window", I3_IPC_EVENT_WINDOW, (const char *)payload);
196
+    y(free);
197
+    setlocale(LC_NUMERIC, "");
198
+}
199
+

b/src/manage.c

204
@@ -76,35 +76,6 @@ void restore_geometry(void) {
205
 }
206
 
207
 /*
208
- * The following function sends a new window event, which consists
209
- * of fields "change" and "container", the latter containing a dump
210
- * of the window's container.
211
- *
212
- */
213
-static void ipc_send_window_new_event(Con *con) {
214
-    setlocale(LC_NUMERIC, "C");
215
-    yajl_gen gen = ygenalloc();
216
-
217
-    y(map_open);
218
-
219
-    ystr("change");
220
-    ystr("new");
221
-
222
-    ystr("container");
223
-    dump_node(gen, con, false);
224
-
225
-    y(map_close);
226
-
227
-    const unsigned char *payload;
228
-    ylength length;
229
-    y(get_buf, &payload, &length);
230
-
231
-    ipc_send_event("window", I3_IPC_EVENT_WINDOW, (const char *)payload);
232
-    y(free);
233
-    setlocale(LC_NUMERIC, "");
234
-}
235
-
236
-/*
237
  * Do some sanity checks and then reparent the window.
238
  *
239
  */
240
@@ -360,6 +331,8 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
241
 
242
     FREE(state_reply);
243
 
244
+    bool set_focus = false;
245
+
246
     if (fs == NULL) {
247
         DLOG("Not in fullscreen mode, focusing\n");
248
         if (!cwindow->dock) {
249
@@ -371,7 +344,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
250
 
251
             if (workspace_is_visible(ws) && current_output == target_output) {
252
                 if (!match || !match->restart_mode) {
253
-                    con_focus(nc);
254
+                    set_focus = true;
255
                 } else DLOG("not focusing, matched with restart_mode == true\n");
256
             } else DLOG("workspace not visible, not focusing\n");
257
         } else DLOG("dock, not focusing\n");
258
@@ -421,7 +394,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
259
                    transient_win->transient_for != XCB_NONE) {
260
                 if (transient_win->transient_for == fs->window->id) {
261
                     LOG("This floating window belongs to the fullscreen window (popup_during_fullscreen == smart)\n");
262
-                    con_focus(nc);
263
+                    set_focus = true;
264
                     break;
265
                 }
266
                 Con *next_transient = con_by_window_id(transient_win->transient_for);
267
@@ -500,7 +473,14 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
268
     tree_render();
269
 
270
     /* Send an event about window creation */
271
-    ipc_send_window_new_event(nc);
272
+    ipc_send_window_event("new", nc);
273
+
274
+    /* Defer setting focus after the 'new' event has been sent to ensure the
275
+     * proper window event sequence. */
276
+    if (set_focus) {
277
+        con_focus(nc);
278
+        tree_render();
279
+    }
280
 
281
     /* Windows might get managed with the urgency hint already set (Pidgin is
282
      * known to do that), so check for that and handle the hint accordingly.

b/src/x.c

287
@@ -15,6 +15,11 @@
288
 /* Stores the X11 window ID of the currently focused window */
289
 xcb_window_t focused_id = XCB_NONE;
290
 
291
+/* Because 'focused_id' might be reset to force input focus (after click to
292
+ * raise), we separately keep track of the X11 window ID to be able to always
293
+ * tell whether the focused window actually changed. */
294
+static xcb_window_t last_focused = XCB_NONE;
295
+
296
 /* The bottom-to-top window stack of all windows which are managed by i3.
297
  * Used for x_get_window_stack(). */
298
 static xcb_window_t *btt_stack;
299
@@ -232,7 +237,7 @@ void x_con_kill(Con *con) {
300
     free(state);
301
 
302
     /* Invalidate focused_id to correctly focus new windows with the same ID */
303
-    focused_id = XCB_NONE;
304
+    focused_id = last_focused = XCB_NONE;
305
 }
306
 
307
 /*
308
@@ -849,6 +854,24 @@ static void x_push_node_unmaps(Con *con) {
309
 }
310
 
311
 /*
312
+ * Returns true if the given container is currently attached to its parent.
313
+ *
314
+ * TODO: Remove once #1185 has been fixed
315
+ */
316
+static bool is_con_attached(Con *con) {
317
+    if (con->parent == NULL)
318
+        return false;
319
+
320
+    Con *current;
321
+    TAILQ_FOREACH(current, &(con->parent->nodes_head), nodes) {
322
+        if (current == con)
323
+            return true;
324
+    }
325
+
326
+    return false;
327
+}
328
+
329
+/*
330
  * Pushes all changes (state of each node, see x_push_node() and the window
331
  * stack) to X11.
332
  *
333
@@ -972,6 +995,9 @@ void x_push_changes(Con *con) {
334
                 send_take_focus(to_focus);
335
                 set_focus = !focused->window->doesnt_accept_focus;
336
                 DLOG("set_focus = %d\n", set_focus);
337
+
338
+                if (!set_focus && to_focus != last_focused)
339
+                   ipc_send_window_event("focus", focused);
340
             }
341
 
342
             if (set_focus) {
343
@@ -990,9 +1016,12 @@ void x_push_changes(Con *con) {
344
                 }
345
 
346
                 ewmh_update_active_window(to_focus);
347
+
348
+                if (to_focus != XCB_NONE && to_focus != last_focused && focused->window != NULL && is_con_attached(focused))
349
+                   ipc_send_window_event("focus", focused);
350
             }
351
 
352
-            focused_id = to_focus;
353
+            focused_id = last_focused = to_focus;
354
         }
355
     }
356
 

b/testcases/t/205-ipc-windows.t

361
@@ -30,19 +30,31 @@ $i3->connect()->recv;
362
 # Events
363
 
364
 my $new = AnyEvent->condvar;
365
+my $focus = AnyEvent->condvar;
366
 $i3->subscribe({
367
     window => sub {
368
         my ($event) = @_;
369
-        $new->send($event->{change} eq 'new');
370
+        if ($event->{change} eq 'new') {
371
+            $new->send($event);
372
+        } elsif ($event->{change} eq 'focus') {
373
+            $focus->send($event);
374
+        }
375
     }
376
 })->recv;
377
 
378
 open_window;
379
 
380
 my $t;
381
-$t = AnyEvent->timer(after => 0.5, cb => sub { $new->send(0); });
382
+$t = AnyEvent->timer(
383
+    after => 0.5,
384
+    cb => sub {
385
+        $new->send(0);
386
+        $focus->send(0);
387
+    }
388
+);
389
 
390
-ok($new->recv, 'Window "new" event received');
391
+is($new->recv->{container}->{focused}, 0, 'Window "new" event received');
392
+is($focus->recv->{container}->{focused}, 1, 'Window "focus" event received');
393
 
394
 }
395
 

b/testcases/t/219-ipc-window-focus.t

401
@@ -0,0 +1,94 @@
402
+#!perl
403
+# vim:ts=4:sw=4:expandtab
404
+#
405
+# Please read the following documents before working on tests:
406
+# • http://build.i3wm.org/docs/testsuite.html
407
+#   (or docs/testsuite)
408
+#
409
+# • http://build.i3wm.org/docs/lib-i3test.html
410
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
411
+#
412
+# • http://build.i3wm.org/docs/ipc.html
413
+#   (or docs/ipc)
414
+#
415
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
416
+#   (unless you are already familiar with Perl)
417
+
418
+use i3test;
419
+
420
+SKIP: {
421
+
422
+    skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
423
+
424
+my $i3 = i3(get_socket_path());
425
+$i3->connect()->recv;
426
+
427
+################################
428
+# Window focus event
429
+################################
430
+
431
+cmd 'split h';
432
+
433
+my $win0 = open_window;
434
+my $win1 = open_window;
435
+my $win2 = open_window;
436
+
437
+my $focus = AnyEvent->condvar;
438
+
439
+$i3->subscribe({
440
+    window => sub {
441
+        my ($event) = @_;
442
+        $focus->send($event);
443
+    }
444
+})->recv;
445
+
446
+my $t;
447
+$t = AnyEvent->timer(
448
+    after => 0.5,
449
+    cb => sub {
450
+        $focus->send(0);
451
+    }
452
+);
453
+
454
+# ensure the rightmost window contains input focus
455
+$i3->command('[id="' . $win2->id . '"] focus')->recv;
456
+is($x->input_focus, $win2->id, "Window 2 focused");
457
+
458
+cmd 'focus left';
459
+my $event = $focus->recv;
460
+is($event->{change}, 'focus', 'Focus event received');
461
+is($focus->recv->{container}->{name}, 'Window 1', 'Window 1 focused');
462
+
463
+$focus = AnyEvent->condvar;
464
+cmd 'focus left';
465
+$event = $focus->recv;
466
+is($event->{change}, 'focus', 'Focus event received');
467
+is($event->{container}->{name}, 'Window 0', 'Window 0 focused');
468
+
469
+$focus = AnyEvent->condvar;
470
+cmd 'focus right';
471
+$event = $focus->recv;
472
+is($event->{change}, 'focus', 'Focus event received');
473
+is($event->{container}->{name}, 'Window 1', 'Window 1 focused');
474
+
475
+$focus = AnyEvent->condvar;
476
+cmd 'focus right';
477
+$event = $focus->recv;
478
+is($event->{change}, 'focus', 'Focus event received');
479
+is($event->{container}->{name}, 'Window 2', 'Window 2 focused');
480
+
481
+$focus = AnyEvent->condvar;
482
+cmd 'focus right';
483
+$event = $focus->recv;
484
+is($event->{change}, 'focus', 'Focus event received');
485
+is($event->{container}->{name}, 'Window 0', 'Window 0 focused');
486
+
487
+$focus = AnyEvent->condvar;
488
+cmd 'focus left';
489
+$event = $focus->recv;
490
+is($event->{change}, 'focus', 'Focus event received');
491
+is($event->{container}->{name}, 'Window 2', 'Window 2 focused');
492
+
493
+}
494
+
495
+done_testing;

b/testcases/t/220-ipc-window-title.t

501
@@ -0,0 +1,57 @@
502
+#!perl
503
+# vim:ts=4:sw=4:expandtab
504
+#
505
+# Please read the following documents before working on tests:
506
+# • http://build.i3wm.org/docs/testsuite.html
507
+#   (or docs/testsuite)
508
+#
509
+# • http://build.i3wm.org/docs/lib-i3test.html
510
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
511
+#
512
+# • http://build.i3wm.org/docs/ipc.html
513
+#   (or docs/ipc)
514
+#
515
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
516
+#   (unless you are already familiar with Perl)
517
+
518
+use i3test;
519
+
520
+SKIP: {
521
+
522
+    skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
523
+
524
+my $i3 = i3(get_socket_path());
525
+$i3->connect()->recv;
526
+
527
+################################
528
+# Window title event
529
+################################
530
+
531
+my $window = open_window(name => 'Window 0');
532
+
533
+my $title = AnyEvent->condvar;
534
+
535
+$i3->subscribe({
536
+    window => sub {
537
+        my ($event) = @_;
538
+        $title->send($event);
539
+    }
540
+})->recv;
541
+
542
+$window->name('New Window Title');
543
+
544
+my $t;
545
+$t = AnyEvent->timer(
546
+    after => 0.5,
547
+    cb => sub {
548
+        $title->send(0);
549
+    }
550
+);
551
+
552
+my $event = $title->recv;
553
+is($event->{change}, 'title', 'Window title change event received');
554
+is($event->{container}->{name}, 'New Window Title', 'Window title changed');
555
+
556
+}
557
+
558
+done_testing;