i3 - improved tiling WM


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.

Patch status: needinfo

Patch by Marco Hunsicker

Long description:

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

b/src/ipc.c

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

b/src/manage.c

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

b/src/x.c

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

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

347
@@ -30,19 +30,31 @@ $i3->connect()->recv;
348
 # Events
349
 
350
 my $new = AnyEvent->condvar;
351
+my $focus = AnyEvent->condvar;
352
 $i3->subscribe({
353
     window => sub {
354
         my ($event) = @_;
355
-        $new->send($event->{change} eq 'new');
356
+        if ($event->{change} eq 'new') {
357
+            $new->send($event);
358
+        } elsif ($event->{change} eq 'focus') {
359
+            $focus->send($event);
360
+        }
361
     }
362
 })->recv;
363
 
364
 open_window;
365
 
366
 my $t;
367
-$t = AnyEvent->timer(after => 0.5, cb => sub { $new->send(0); });
368
+$t = AnyEvent->timer(
369
+    after => 0.5,
370
+    cb => sub {
371
+        $new->send(0);
372
+        $focus->send(0);
373
+    }
374
+);
375
 
376
-ok($new->recv, 'Window "new" event received');
377
+is($new->recv->{container}->{focused}, 0, 'Window "new" event received');
378
+is($focus->recv->{container}->{focused}, 1, 'Window "focus" event received');
379
 
380
 }
381
 

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

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

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

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