i3 - improved tiling WM


Send IPC workspace empty event after last window closed

Patch status: needinfo

Patch by Marco Hunsicker

Long description:

This patch sends the workspace empty event after the last container in
a workspace has been closed and the workspace becomes empty again.

Additionally this patch adjusts the prior behavior that the workspace
empty event was send when switching *from* an empty workspace. Now it
is send when switching *to* an empty workspace (as clients have
already been notified about the old empty workspace).

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

b/src/tree.c

23
@@ -229,6 +229,9 @@ bool tree_close(Con *con, kill_window_t kill_window, bool dont_kill_parent, bool
24
         return false;
25
     }
26
 
27
+    Con *ws = con_get_workspace(con);
28
+    bool ws_was_empty = !ws || (TAILQ_EMPTY(&(ws->nodes_head)) && TAILQ_EMPTY(&(ws->floating_head)));
29
+
30
     if (con->window != NULL) {
31
         if (kill_window != DONT_KILL_WINDOW) {
32
             x_window_kill(con->window->id, kill_window);
33
@@ -264,8 +267,6 @@ bool tree_close(Con *con, kill_window_t kill_window, bool dont_kill_parent, bool
34
         FREE(con->window);
35
     }
36
 
37
-    Con *ws = con_get_workspace(con);
38
-
39
     /* Figure out which container to focus next before detaching 'con'. */
40
     if (con_is_floating(con)) {
41
         if (con == focused) {
42
@@ -321,6 +322,12 @@ bool tree_close(Con *con, kill_window_t kill_window, bool dont_kill_parent, bool
43
     TAILQ_REMOVE(&all_cons, con, all_cons);
44
     free(con);
45
 
46
+    /* if the workspace became empty, notify any interested parties. */
47
+    if (ws && !ws_was_empty && TAILQ_EMPTY(&(ws->nodes_head)) && TAILQ_EMPTY(&(ws->floating_head))) {
48
+        DLOG("Issue IPC workspace empty event for workspace %s\n", ws->name);
49
+        ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, "{\"change\":\"empty\"}");
50
+    }
51
+
52
     /* in the case of floating windows, we already focused another container
53
      * when closing the parent, so we can exit now. */
54
     if (!next) {

b/src/workspace.c

59
@@ -420,10 +420,15 @@ static void _workspace_show(Con *workspace) {
60
         if (!workspace_is_visible(old)) {
61
             LOG("Closing old workspace (%p / %s), it is empty\n", old, old->name);
62
             tree_close(old, DONT_KILL_WINDOW, false, false);
63
-            ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, "{\"change\":\"empty\"}");
64
         }
65
     }
66
 
67
+    /* if the new workspace is empty, notify any interested parties. */
68
+    if (TAILQ_EMPTY(&(workspace->nodes_head)) && TAILQ_EMPTY(&(workspace->floating_head))) {
69
+        DLOG("Issue IPC workspace empty event for workspace %s\n", workspace->name);
70
+        ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, "{\"change\":\"empty\"}");
71
+    }
72
+
73
     workspace->fullscreen_mode = CF_OUTPUT;
74
     LOG("focused now = %p / %s\n", focused, focused->name);
75
 

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

81
@@ -0,0 +1,158 @@
82
+#!perl
83
+# vim:ts=4:sw=4:expandtab
84
+#
85
+# Please read the following documents before working on tests:
86
+# • http://build.i3wm.org/docs/testsuite.html
87
+#   (or docs/testsuite)
88
+#
89
+# • http://build.i3wm.org/docs/lib-i3test.html
90
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
91
+#
92
+# • http://build.i3wm.org/docs/ipc.html
93
+#   (or docs/ipc)
94
+#
95
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
96
+#   (unless you are already familiar with Perl)
97
+#
98
+# Check if the workspace empty event is correctly send after window close.
99
+#
100
+use i3test;
101
+
102
+SKIP: {
103
+
104
+    skip "AnyEvent::I3 too old (need >= 0.15)", 1 if $AnyEvent::I3::VERSION < 0.15;
105
+
106
+my $i3 = i3(get_socket_path());
107
+$i3->connect()->recv;
108
+
109
+##############################################################
110
+# check that the workspace empty event is send when last
111
+# window was closed
112
+##############################################################
113
+
114
+my $tmp = fresh_workspace;
115
+
116
+my $cond = AnyEvent->condvar;
117
+
118
+$i3->subscribe({
119
+    workspace => sub {
120
+        my ($event) = @_;
121
+        $cond->send($event);
122
+    }
123
+})->recv;
124
+
125
+my $w1 = open_window();
126
+
127
+cmd '[id="' . $w1->id . '"] kill';
128
+sync_with_i3;
129
+
130
+my $event = $cond->recv;
131
+is($event->{change}, 'empty', '"Empty" event received after last window close');
132
+
133
+
134
+##############################################################
135
+# check that no empty workspace event is send when workspace
136
+# is not empty after a window was closed
137
+##############################################################
138
+
139
+my $ws2 = fresh_workspace;
140
+my $w3 = open_window(name => 'Window 3');
141
+my $ws1 = fresh_workspace;
142
+$w1 = open_window(name => 'Window 1');
143
+my $w2 = open_window(name => 'Window 2');
144
+
145
+is(focused_ws(), $ws1, 'Focused workspace is ws1');
146
+
147
+my @events;
148
+my $ws_event_conn = i3(get_socket_path());
149
+$ws_event_conn->connect()->recv;
150
+$ws_event_conn->subscribe({
151
+    workspace => sub {
152
+        my ($event) = @_;
153
+        push @events, $event;
154
+    }
155
+});
156
+
157
+cmd "workspace $ws2";
158
+cmd 'open';
159
+
160
+is(focused_ws(), $ws2, 'Focused workspace is ws2');
161
+
162
+# Wait for the workspace event on a new connection. Events will be delivered
163
+# to older connections earlier, so by the time it arrives here, it should be
164
+# in @events already.
165
+my $ws_event_block_conn = i3(get_socket_path());
166
+$ws_event_block_conn->connect()->recv;
167
+$ws_event_block_conn->subscribe({ workspace => sub { $cond->send(1) }});
168
+$cond->recv;
169
+
170
+my @expected_events = grep { $_->{change} eq 'focus' } @events;
171
+my @empty_events = grep { $_->{change} eq 'empty' } @events;
172
+is(@expected_events, 1, '"Focus" event received');
173
+is(@empty_events, 0, 'No "empty" events received');
174
+
175
+
176
+##############################################################
177
+# check that empty workspace event is send when switching to
178
+# an empty workspace
179
+##############################################################
180
+
181
+$ws2 = fresh_workspace;
182
+$ws1 = fresh_workspace;
183
+$w1 = open_window(name => 'Window 1');
184
+
185
+is(focused_ws(), $ws1, 'Focused workspace is ws1');
186
+
187
+$i3 = i3(get_socket_path());
188
+$i3->connect()->recv;
189
+$i3->subscribe({
190
+    workspace => sub {
191
+        my ($event) = @_;
192
+        $cond->send($event);
193
+    }
194
+})->recv;
195
+
196
+cmd "workspace $ws2";
197
+cmd 'open';
198
+
199
+is(focused_ws(), $ws2, 'Focused workspace is ws2');
200
+
201
+$event = $cond->recv;
202
+is($event->{change}, 'empty', '"Empty" event received upon workspace switch');
203
+
204
+
205
+##############################################################
206
+# check that no workspace empty event is send when switching
207
+# from an empty workspace
208
+##############################################################
209
+
210
+$ws1 = fresh_workspace;
211
+$w1 = open_window(name => 'Window 1');
212
+$ws2 = fresh_workspace;
213
+
214
+is(focused_ws(), $ws2, 'Focused workspace is ws2');
215
+
216
+my @events_;
217
+
218
+$i3 = i3(get_socket_path());
219
+$i3->connect()->recv;
220
+$i3->subscribe({
221
+    workspace => sub {
222
+        my ($event) = @_;
223
+        push @events_, $event;
224
+    }
225
+})->recv;
226
+
227
+cmd "workspace $ws1";
228
+cmd 'open';
229
+
230
+is(focused_ws(), $ws1, 'Focused workspace is ws1');
231
+
232
+my @focus_events = grep { $_->{change} eq 'focus' } @events_;
233
+my @empty_events_ = grep { $_->{change} eq 'empty' } @events_;
234
+is(@focus_events, 1, '"Focus" event received');
235
+is(@empty_events_, 0, 'No "empty" events received');
236
+
237
+}
238
+
239
+done_testing;