i3 - improved tiling WM


Send IPC workspace blank event when workspace becomes empty

Patch status: rejected

Patch by Marco Hunsicker

Long description:

This patch adds a new workspace "blank" event that is send after the
last application window in a workspace has been closed and the
workspace becomes empty.

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

b/docs/ipc

20
@@ -624,8 +624,9 @@ strip the highest bit first).
21
 
22
 workspace (0)::
23
 	Sent when the user switches to a different workspace, when a new
24
-	workspace is initialized or when a workspace is removed (because the
25
-	last client vanished).
26
+	workspace is initialized, when a workspace becomes empty (because the last
27
+	application window has been closed) or when a workspace is removed (because
28
+	the last client vanished).
29
 output (1)::
30
 	Sent when RandR issues a change notification (of either screens,
31
 	outputs, CRTCs or output properties).
32
@@ -661,7 +662,7 @@ if ($is_event) {
33
 
34
 This event consists of a single serialized map containing a property
35
 +change (string)+ which indicates the type of the change ("focus", "init",
36
-"empty", "urgent").
37
+"blank", "empty", "urgent").
38
 
39
 Moreover, when the change is "focus", an +old (object)+ and a +current
40
 (object)+ properties will be present with the previous and current
41
@@ -671,6 +672,11 @@ workspace, and the +old+ property will be set to +null+.  Also note
42
 that if the previous is empty it will get destroyed when switching,
43
 but will still be present in the "old" property.
44
 
45
+Please note that for historic reasons, the change "blank" indicates that
46
+a workspace became empty (because the last application window has been
47
+closed), while the change "empty" indicates that a workspace has been
48
+removed (because it was empty).
49
+
50
 *Example:*
51
 ---------------------
52
 {

b/src/tree.c

57
@@ -229,6 +229,9 @@ bool tree_close(Con *con, kill_window_t kill_window, bool dont_kill_parent, bool
58
         return false;
59
     }
60
 
61
+    Con *ws = con_get_workspace(con);
62
+    bool ws_was_empty = !ws || (TAILQ_EMPTY(&(ws->nodes_head)) && TAILQ_EMPTY(&(ws->floating_head)));
63
+
64
     if (con->window != NULL) {
65
         if (kill_window != DONT_KILL_WINDOW) {
66
             x_window_kill(con->window->id, kill_window);
67
@@ -264,8 +267,6 @@ bool tree_close(Con *con, kill_window_t kill_window, bool dont_kill_parent, bool
68
         FREE(con->window);
69
     }
70
 
71
-    Con *ws = con_get_workspace(con);
72
-
73
     /* Figure out which container to focus next before detaching 'con'. */
74
     if (con_is_floating(con)) {
75
         if (con == focused) {
76
@@ -321,6 +322,12 @@ bool tree_close(Con *con, kill_window_t kill_window, bool dont_kill_parent, bool
77
     TAILQ_REMOVE(&all_cons, con, all_cons);
78
     free(con);
79
 
80
+    /* if the workspace became empty, notify any interested parties. */
81
+    if (!ws_was_empty && TAILQ_EMPTY(&(ws->nodes_head)) && TAILQ_EMPTY(&(ws->floating_head))) {
82
+        DLOG("Issue IPC workspace blank event for workspace %s\n", ws->name);
83
+        ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, "{\"change\":\"blank\"}");
84
+    }
85
+
86
     /* in the case of floating windows, we already focused another container
87
      * when closing the parent, so we can exit now. */
88
     if (!next) {

b/src/workspace.c

93
@@ -424,6 +424,12 @@ static void _workspace_show(Con *workspace) {
94
         }
95
     }
96
 
97
+    /* if the new workspace is empty, notify any interested parties. */
98
+    if (TAILQ_EMPTY(&(workspace->nodes_head)) && TAILQ_EMPTY(&(workspace->floating_head))) {
99
+        DLOG("Issue IPC workspace blank event for workspace %s\n", workspace->name);
100
+        ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, "{\"change\":\"blank\"}");
101
+    }
102
+
103
     workspace->fullscreen_mode = CF_OUTPUT;
104
     LOG("focused now = %p / %s\n", focused, focused->name);
105
 

b/testcases/t/223-ipc-workspace-blank.t

111
@@ -0,0 +1,158 @@
112
+#!perl
113
+# vim:ts=4:sw=4:expandtab
114
+#
115
+# Please read the following documents before working on tests:
116
+# • http://build.i3wm.org/docs/testsuite.html
117
+#   (or docs/testsuite)
118
+#
119
+# • http://build.i3wm.org/docs/lib-i3test.html
120
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
121
+#
122
+# • http://build.i3wm.org/docs/ipc.html
123
+#   (or docs/ipc)
124
+#
125
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
126
+#   (unless you are already familiar with Perl)
127
+#
128
+# Checks the workspace "blank" event semantics.
129
+#
130
+use i3test;
131
+
132
+SKIP: {
133
+
134
+    skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
135
+
136
+################################################################################
137
+# check that the workspace blank event is send when the last window was closed
138
+################################################################################
139
+subtest 'Workspace blank event', sub {
140
+    fresh_workspace;
141
+
142
+    my $cond = AnyEvent->condvar;
143
+    my $client = i3(get_socket_path(0));
144
+    $client->connect()->recv;
145
+    $client->subscribe({
146
+        workspace => sub {
147
+            my ($event) = @_;
148
+            $cond->send($event);
149
+        }
150
+    })->recv;
151
+
152
+    my $w1 = open_window();
153
+
154
+    cmd '[id="' . $w1->id . '"] kill';
155
+    sync_with_i3;
156
+
157
+    my $event = $cond->recv;
158
+    is($event->{change}, 'blank', '"Blank" event received after last window close');
159
+};
160
+
161
+################################################################################
162
+# check that no blank workspace event is send when the workspace is not empty
163
+# when a window was closed
164
+################################################################################
165
+subtest 'No workspace blank event', sub {
166
+    my $ws1 = fresh_workspace;
167
+    my $w1 = open_window();
168
+    my $w2 = open_window();
169
+    my $ws2 = fresh_workspace;
170
+    my $w3 = open_window();
171
+
172
+    is(focused_ws(), $ws2, 'Focused workspace is ws2');
173
+
174
+    my @events;
175
+    my $cond = AnyEvent->condvar;
176
+    my $client = i3(get_socket_path(0));
177
+    $client->connect()->recv;
178
+    $client->subscribe({
179
+        workspace => sub {
180
+            my ($event) = @_;
181
+            push @events, $event;
182
+        }
183
+    })->recv;
184
+
185
+    # Wait for the workspace event on a new connection. Events will be delivered
186
+    # to older connections earlier, so by the time it arrives here, it should be
187
+    # in @events already.
188
+    my $ws_event_block_conn = i3(get_socket_path(0));
189
+    $ws_event_block_conn->connect()->recv;
190
+    $ws_event_block_conn->subscribe({ workspace => sub { $cond->send(1) }});
191
+
192
+    cmd "workspace $ws1";
193
+    cmd 'open';
194
+
195
+    is(focused_ws(), $ws1, 'Focused workspace is ws1');
196
+
197
+    cmd '[id="' . $w1->id . '"] kill';
198
+    sync_with_i3;
199
+
200
+    my @expected_events = grep { $_->{change} eq 'focus' } @events;
201
+    my @blank_events = grep { $_->{change} eq 'blank' } @events;
202
+    is(@expected_events, 1, '"Focus" event received');
203
+    is(@blank_events, 0, 'No "blank" events received');
204
+};
205
+
206
+################################################################################
207
+# check that blank workspace event is send when switching to an empty workspace
208
+################################################################################
209
+subtest 'Workspace blank event when switching', sub {
210
+    my $ws2 = fresh_workspace;
211
+    my $ws1 = fresh_workspace;
212
+    my $w1 = open_window();
213
+
214
+    is(focused_ws(), $ws1, 'Focused workspace is ws1');
215
+
216
+    my $cond = AnyEvent->condvar;
217
+    my $client = i3(get_socket_path());
218
+    $client->connect()->recv;
219
+    $client->subscribe({
220
+        workspace => sub {
221
+            my ($event) = @_;
222
+            $cond->send($event);
223
+        }
224
+    })->recv;
225
+
226
+    cmd "workspace $ws2";
227
+    cmd 'open';
228
+
229
+    is(focused_ws(), $ws2, 'Focused workspace is ws2');
230
+
231
+    my $event = $cond->recv;
232
+    is($event->{change}, 'blank', '"Blank" event received upon workspace switch');
233
+};
234
+
235
+################################################################################
236
+# check that no workspace blank event is send when switching from an empty
237
+# workspace
238
+################################################################################
239
+subtest 'No workspace blank event when switching', sub {
240
+    my $ws1 = fresh_workspace;
241
+    my $window1 = open_window();
242
+    my $ws2 = fresh_workspace;
243
+
244
+    is(focused_ws(), $ws2, 'Focused workspace is ws2');
245
+
246
+    my @events;
247
+    my $client = i3(get_socket_path());
248
+    $client->connect()->recv;
249
+    $client->subscribe({
250
+        workspace => sub {
251
+            my ($event) = @_;
252
+            push @events, $event;
253
+        }
254
+    })->recv;
255
+
256
+    cmd "workspace $ws1";
257
+    cmd 'open';
258
+
259
+    is(focused_ws(), $ws1, 'Focused workspace is ws1');
260
+
261
+    my @focus_events = grep { $_->{change} eq 'focus' } @events;
262
+    my @blank_events = grep { $_->{change} eq 'blank' } @events;
263
+    is(@focus_events, 1, '"Focus" event received');
264
+    is(@blank_events, 0, 'No "blank" events received');
265
+};
266
+
267
+}
268
+
269
+done_testing;