i3 - improved tiling WM


Send IPC window events for focus and title changes

Patch status: needinfo

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 avoid problems
  accessing the window name, a function has been added to query
  whether a window is actually attached to its parent
* 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/205-ipc-windows.t to include the "focus" event
  in order to ensure the correct event sequence
* Adds two new specific test cases for the "focus" and "title" events

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

b/docs/ipc

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

b/include/ipc.h

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

b/src/handlers.c

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

b/src/ipc.c

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

b/src/manage.c

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

b/src/x.c

281
@@ -848,6 +848,22 @@ static void x_push_node_unmaps(Con *con) {
282
         x_push_node_unmaps(current);
283
 }
284
 
285
+/**
286
+ * Returns true if the given container is currently attached to its parent.
287
+ */
288
+static bool is_con_attached(Con *con) {
289
+    if (con->parent == NULL)
290
+        return false;
291
+
292
+    Con *current;
293
+    TAILQ_FOREACH(current, &(con->parent->nodes_head), nodes) {
294
+        if (current == con)
295
+            return true;
296
+    }
297
+
298
+    return false;
299
+}
300
+
301
 /*
302
  * Pushes all changes (state of each node, see x_push_node() and the window
303
  * stack) to X11.
304
@@ -990,6 +1006,10 @@ void x_push_changes(Con *con) {
305
                 }
306
 
307
                 ewmh_update_active_window(to_focus);
308
+
309
+                if (to_focus != XCB_NONE && focused->window != NULL && is_con_attached(focused)) {
310
+                   ipc_send_window_event("focus", focused);
311
+                }
312
             }
313
 
314
             focused_id = to_focus;

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

319
@@ -30,19 +30,31 @@ $i3->connect()->recv;
320
 # Events
321
 
322
 my $new = AnyEvent->condvar;
323
+my $focus = AnyEvent->condvar;
324
 $i3->subscribe({
325
     window => sub {
326
         my ($event) = @_;
327
-        $new->send($event->{change} eq 'new');
328
+        if ($event->{change} eq 'new') {
329
+            $new->send($event);
330
+        } elsif ($event->{change} eq 'focus') {
331
+            $focus->send($event);
332
+        }
333
     }
334
 })->recv;
335
 
336
 open_window;
337
 
338
 my $t;
339
-$t = AnyEvent->timer(after => 0.5, cb => sub { $new->send(0); });
340
+$t = AnyEvent->timer(
341
+    after => 0.5,
342
+    cb => sub {
343
+        $new->send(0);
344
+        $focus->send(0);
345
+    }
346
+);
347
 
348
-ok($new->recv, 'Window "new" event received');
349
+is($new->recv->{container}->{focused}, 0, 'Window "new" event received');
350
+is($focus->recv->{container}->{focused}, 1, 'Window "focus" event received');
351
 
352
 }
353
 

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

359
@@ -0,0 +1,82 @@
360
+#!perl
361
+# vim:ts=4:sw=4:expandtab
362
+#
363
+# Please read the following documents before working on tests:
364
+# • http://build.i3wm.org/docs/testsuite.html
365
+#   (or docs/testsuite)
366
+#
367
+# • http://build.i3wm.org/docs/lib-i3test.html
368
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
369
+#
370
+# • http://build.i3wm.org/docs/ipc.html
371
+#   (or docs/ipc)
372
+#
373
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
374
+#   (unless you are already familiar with Perl)
375
+
376
+use i3test;
377
+
378
+SKIP: {
379
+
380
+    skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
381
+
382
+my $i3 = i3(get_socket_path());
383
+$i3->connect()->recv;
384
+
385
+################################
386
+# Window focus event
387
+################################
388
+
389
+cmd 'split h';
390
+
391
+my $win0 = open_window;
392
+my $win1 = open_window;
393
+my $win2 = open_window;
394
+
395
+my $focus = AnyEvent->condvar;
396
+
397
+$i3->subscribe({
398
+    window => sub {
399
+        my ($event) = @_;
400
+        $focus->send($event);
401
+    }
402
+})->recv;
403
+
404
+my $t;
405
+$t = AnyEvent->timer(
406
+    after => 0.5,
407
+    cb => sub {
408
+        $focus->send(0);
409
+    }
410
+);
411
+
412
+# ensure the rightmost window contains input focus
413
+$i3->command('[id="' . $win2->id . '"] focus')->recv;
414
+is($x->input_focus, $win2->id, "Window 2 focused");
415
+
416
+cmd 'focus left';
417
+is($focus->recv->{container}->{name}, 'Window 1', 'Window 1 focused');
418
+
419
+$focus = AnyEvent->condvar;
420
+cmd 'focus left';
421
+is($focus->recv->{container}->{name}, 'Window 0', 'Window 0 focused');
422
+
423
+$focus = AnyEvent->condvar;
424
+cmd 'focus right';
425
+is($focus->recv->{container}->{name}, 'Window 1', 'Window 1 focused');
426
+
427
+$focus = AnyEvent->condvar;
428
+cmd 'focus right';
429
+is($focus->recv->{container}->{name}, 'Window 2', 'Window 2 focused');
430
+
431
+$focus = AnyEvent->condvar;
432
+cmd 'focus right';
433
+is($focus->recv->{container}->{name}, 'Window 0', 'Window 0 focused');
434
+
435
+$focus = AnyEvent->condvar;
436
+cmd 'focus left';
437
+is($focus->recv->{container}->{name}, 'Window 2', 'Window 2 focused');
438
+
439
+}
440
+
441
+done_testing;

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

447
@@ -0,0 +1,57 @@
448
+#!perl
449
+# vim:ts=4:sw=4:expandtab
450
+#
451
+# Please read the following documents before working on tests:
452
+# • http://build.i3wm.org/docs/testsuite.html
453
+#   (or docs/testsuite)
454
+#
455
+# • http://build.i3wm.org/docs/lib-i3test.html
456
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
457
+#
458
+# • http://build.i3wm.org/docs/ipc.html
459
+#   (or docs/ipc)
460
+#
461
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
462
+#   (unless you are already familiar with Perl)
463
+
464
+use i3test;
465
+
466
+SKIP: {
467
+
468
+    skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
469
+
470
+my $i3 = i3(get_socket_path());
471
+$i3->connect()->recv;
472
+
473
+################################
474
+# Window title event
475
+################################
476
+
477
+my $window = open_window(name => 'Window 0');
478
+
479
+my $title = AnyEvent->condvar;
480
+
481
+$i3->subscribe({
482
+    window => sub {
483
+        my ($event) = @_;
484
+        $title->send($event);
485
+    }
486
+})->recv;
487
+
488
+$window->name('New Window Title');
489
+
490
+my $t;
491
+$t = AnyEvent->timer(
492
+    after => 0.5,
493
+    cb => sub {
494
+        $title->send(0);
495
+    }
496
+);
497
+
498
+my $event = $title->recv;
499
+is($event->{change}, 'title', 'Window title change event received');
500
+is($event->{container}->{name}, 'New Window Title', 'Window title changed');
501
+
502
+}
503
+
504
+done_testing;