i3 - improved tiling WM


Add floating_toggle_hide command to show/hide floating windows.

Patch status: needinfo

Patch by Fabián Ezequiel Gallina

Long description:

The toggle is controlled by a new floating_hidden attribute in the Con
struct. This new attribute is used only for workspaces and its
existence is also reflected in IPC calls.

Issuing the floating_toggle_hide command does not affect scratchpad
windows and its effect is always local to a single workspace.

fixes #807

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

b/include/commands.h

34
@@ -134,6 +134,12 @@ void cmd_move_con_to_output(I3_CMD, char *name);
35
 void cmd_floating(I3_CMD, char *floating_mode);
36
 
37
 /**
38
+ * Implementation of floating_toggle_hide;
39
+ *
40
+ */
41
+void cmd_floating_toggle_hide(I3_CMD);
42
+
43
+/**
44
  * Implementation of 'move workspace to [output] <str>'.
45
  *
46
  */

b/include/data.h

51
@@ -463,6 +463,9 @@ struct Con {
52
      * workspace is not a named workspace (for named workspaces, num == -1) */
53
     int num;
54
 
55
+    /* tells if a workspace has all its floating windows hidden. */
56
+    bool floating_hidden;
57
+
58
     /* a sticky-group is an identifier which bundles several containers to a
59
      * group. The contents are shared between all of them, that is they are
60
      * displayed on whichever of the containers is currently visible */

b/include/floating.h

65
@@ -125,14 +125,15 @@ void floating_focus_direction(xcb_connection_t *conn, Client *currently_focused,
66
 void floating_move(xcb_connection_t *conn, Client *currently_focused,
67
                    direction_t direction);
68
 
69
+#endif
70
+
71
 /**
72
  * Hides all floating clients (or show them if they are currently hidden) on
73
  * the specified workspace.
74
  *
75
  */
76
-void floating_toggle_hide(xcb_connection_t *conn, Workspace *workspace);
77
+void floating_toggle_hide(Con *ws);
78
 
79
-#endif
80
 /**
81
  * This function grabs your pointer and lets you drag stuff around (borders).
82
  * Every time you move your mouse, an XCB_MOTION_NOTIFY event will be received

b/include/tree.h

87
@@ -51,6 +51,12 @@ bool level_up(void);
88
 bool level_down(void);
89
 
90
 /**
91
+ * Marks Con and its children as mapped/unmapped.
92
+ *
93
+ */
94
+void mark_mapped(Con *con, bool mapped);
95
+
96
+/**
97
  * Renders the tree, that is rendering all outputs using render_con() and
98
  * pushing the changes to X11 using x_push_changes().
99
  *

b/parser-specs/commands.spec

104
@@ -28,6 +28,7 @@ state INITIAL:
105
   'open' -> call cmd_open()
106
   'fullscreen' -> FULLSCREEN
107
   'split' -> SPLIT
108
+  'floating_toggle_hide' -> call cmd_floating_toggle_hide()
109
   'floating' -> FLOATING
110
   'mark' -> MARK
111
   'resize' -> RESIZE
112
@@ -106,7 +107,7 @@ state WORKSPACE:
113
       -> call cmd_workspace_back_and_forth()
114
   'number'
115
       -> WORKSPACE_NUMBER
116
-  workspace = string 
117
+  workspace = string
118
       -> call cmd_workspace_name($workspace)
119
 
120
 state WORKSPACE_NUMBER:

b/src/commands.c

125
@@ -1114,6 +1114,37 @@ void cmd_floating(I3_CMD, char *floating_mode) {
126
 }
127
 
128
 /*
129
+ * Implementation of floating_toggle_hide
130
+ *
131
+ */
132
+void cmd_floating_toggle_hide(I3_CMD) {
133
+    Con *ws = con_get_workspace(focused);
134
+
135
+    floating_toggle_hide(ws);
136
+
137
+    Con *current = TAILQ_FIRST(&(ws->focus_head));
138
+    bool floating_focused = (current != NULL &&
139
+                             current->type == CT_FLOATING_CON &&
140
+                             current->scratchpad_state == SCRATCHPAD_NONE);
141
+
142
+    /** If all floating windows are hidden and focus was in a floating
143
+     * container (not a scratchpad window), set focus to the first
144
+     * non-floating window available. */
145
+    if (ws->floating_hidden && floating_focused) {
146
+        TAILQ_FOREACH(current, &(ws->focus_head), focused) {
147
+            if (current->type != CT_FLOATING_CON) {
148
+                con_focus(current);
149
+                DLOG("moved focus to %p\n", current);
150
+                cmd_output->needs_tree_render = true;
151
+                break;
152
+            }
153
+        }
154
+    }
155
+
156
+    ysuccess(true);
157
+}
158
+
159
+/*
160
  * Implementation of 'move workspace to [output] <str>'.
161
  *
162
  */

b/src/floating.c

167
@@ -752,33 +752,50 @@ void floating_move(xcb_connection_t *conn, Client *currently_focused, direction_
168
         fake_absolute_configure_notify(conn, currently_focused);
169
         /* fake_absolute_configure_notify flushes */
170
 }
171
+#endif
172
 
173
 /*
174
  * Hides all floating clients (or show them if they are currently hidden) on
175
  * the specified workspace.
176
  *
177
  */
178
-void floating_toggle_hide(xcb_connection_t *conn, Workspace *workspace) {
179
-        Client *client;
180
-
181
-        workspace->floating_hidden = !workspace->floating_hidden;
182
-        DLOG("floating_hidden is now: %d\n", workspace->floating_hidden);
183
-        TAILQ_FOREACH(client, &(workspace->floating_clients), floating_clients) {
184
-                if (workspace->floating_hidden)
185
-                        client_unmap(conn, client);
186
-                else client_map(conn, client);
187
+void floating_toggle_hide(Con *ws) {
188
+    Con *con, *scratchpad_con = NULL;
189
+
190
+    ws->floating_hidden = !ws->floating_hidden;
191
+    DLOG("floating_hidden for workspace %p is now %d\n",
192
+         ws, ws->floating_hidden);
193
+
194
+    TAILQ_FOREACH(con, &(ws->floating_head), floating_windows) {
195
+        /** Don't mess with scratchpad windows. */
196
+        if (con->scratchpad_state != SCRATCHPAD_NONE) {
197
+            /** but in the case there's one showing, save
198
+             * its reference so it can be raised over all
199
+             * re-displayed floating windows. */
200
+            if (!ws->floating_hidden) {
201
+                scratchpad_con = con;
202
+            }
203
+            DLOG("ignore scratchpad window %p with state %d\n",
204
+                 con, con->scratchpad_state);
205
+            continue;
206
+        }
207
+        mark_mapped(con, !ws->floating_hidden);
208
+        DLOG("mapped for con (and children) %p is now %d\n",
209
+             con, !ws->floating_hidden);
210
+        if (!ws->floating_hidden) {
211
+            x_raise_con(con);
212
+            render_con(con, false);
213
+            DLOG("rendered floating con %p\n", con);
214
         }
215
+        x_push_changes(con);
216
+    }
217
 
218
-        /* If we just unmapped all floating windows we should ensure that the focus
219
-         * is set correctly, that ist, to the first non-floating client in stack */
220
-        if (workspace->floating_hidden)
221
-                SLIST_FOREACH(client, &(workspace->focus_stack), focus_clients) {
222
-                        if (client_is_floating(client))
223
-                                continue;
224
-                        set_focus(conn, client, true);
225
-                        return;
226
-                }
227
-
228
-        xcb_flush(conn);
229
+    /** Put the scratchpad window in front of all re-displayed
230
+     * floating windows */
231
+    if (scratchpad_con != NULL) {
232
+        x_raise_con(scratchpad_con);
233
+        render_con(scratchpad_con, false);
234
+        x_push_changes(scratchpad_con);
235
+        DLOG("re-raised scratchpad window %p\n", scratchpad_con);
236
+    }
237
 }
238
-#endif

b/src/ipc.c

243
@@ -276,6 +276,8 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) {
244
     if (con->type == CT_WORKSPACE) {
245
         ystr("num");
246
         y(integer, con->num);
247
+        ystr("floating_hidden");
248
+        y(bool, con->floating_hidden);
249
     }
250
 
251
     ystr("window");
252
@@ -397,6 +399,9 @@ IPC_HANDLER(get_workspaces) {
253
                 y(null);
254
             else y(integer, ws->num);
255
 
256
+            ystr("floating_hidden");
257
+            y(bool, ws->floating_hidden);
258
+
259
             ystr("name");
260
             ystr(ws->name);
261
 

b/src/render.c

266
@@ -268,6 +268,12 @@ void render_con(Con *con, bool render_fullscreen) {
267
             Con *fullscreen = con_get_fullscreen_con(workspace, CF_OUTPUT);
268
             Con *child;
269
             TAILQ_FOREACH(child, &(workspace->floating_head), floating_windows) {
270
+                /** Don't show floating windows when floating_hidden
271
+                 * is set but also don't mess with scratchpad windows
272
+                 * in the process */
273
+                if (workspace->floating_hidden &&
274
+                    child->scratchpad_state == SCRATCHPAD_NONE)
275
+                        continue;
276
                 /* Don’t render floating windows when there is a fullscreen window
277
                  * on that workspace. Necessary to make floating fullscreen work
278
                  * correctly (ticket #564). */

b/src/tree.c

283
@@ -474,17 +474,21 @@ bool level_down(void) {
284
     return true;
285
 }
286
 
287
-static void mark_unmapped(Con *con) {
288
+/*
289
+ * Marks Con and its children as mapped/unmapped.
290
+ *
291
+ */
292
+void mark_mapped(Con *con, bool mapped) {
293
     Con *current;
294
 
295
-    con->mapped = false;
296
+    con->mapped = mapped;
297
     TAILQ_FOREACH(current, &(con->nodes_head), nodes)
298
-        mark_unmapped(current);
299
+            mark_mapped(current, mapped);
300
     if (con->type == CT_WORKSPACE) {
301
-        /* We need to call mark_unmapped on floating nodes aswell since we can
302
+        /* We need to call mark_mapped on floating nodes aswell since we can
303
          * make containers floating. */
304
         TAILQ_FOREACH(current, &(con->floating_head), floating_windows)
305
-            mark_unmapped(current);
306
+                mark_mapped(current, mapped);
307
     }
308
 }
309
 
310
@@ -500,7 +504,7 @@ void tree_render(void) {
311
     DLOG("-- BEGIN RENDERING --\n");
312
     /* Reset map state for all nodes in tree */
313
     /* TODO: a nicer method to walk all nodes would be good, maybe? */
314
-    mark_unmapped(croot);
315
+    mark_mapped(croot, false);
316
     croot->mapped = true;
317
 
318
     render_con(croot, false);

b/testcases/t/187-commands-parser.t

323
@@ -144,7 +144,7 @@ is(parser_calls("\nworkspace test"),
324
 ################################################################################
325
 
326
 is(parser_calls('unknown_literal'),
327
-   "ERROR: Expected one of these tokens: <end>, '[', 'move', 'exec', 'exit', 'restart', 'reload', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'split', 'floating', 'mark', 'resize', 'rename', 'nop', 'scratchpad', 'mode'\n" .
328
+   "ERROR: Expected one of these tokens: <end>, '[', 'move', 'exec', 'exit', 'restart', 'reload', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'split', 'floating_toggle_hide', 'floating', 'mark', 'resize', 'rename', 'nop', 'scratchpad', 'mode'\n" .
329
    "ERROR: Your command: unknown_literal\n" .
330
    "ERROR:               ^^^^^^^^^^^^^^^",
331
    'error for unknown literal ok');

b/testcases/t/206-floating_toggle_hide.t

337
@@ -0,0 +1,148 @@
338
+#!perl
339
+# vim:ts=4:sw=4:expandtab
340
+#
341
+# Please read the following documents before working on tests:
342
+# • http://build.i3wm.org/docs/testsuite.html
343
+#   (or docs/testsuite)
344
+#
345
+# • http://build.i3wm.org/docs/lib-i3test.html
346
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
347
+#
348
+# • http://build.i3wm.org/docs/ipc.html
349
+#   (or docs/ipc)
350
+#
351
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
352
+#   (unless you are already familiar with Perl)
353
+#
354
+# Test floating_toggle_hide command implementation.
355
+# Ticket: #807
356
+# Bug still in: 4.4-145-g8327f83
357
+use i3test;
358
+
359
+my $i3 = i3(get_socket_path());
360
+
361
+###################################################################
362
+# 1. floating visibility toggle is local to a single workspace
363
+###################################################################
364
+
365
+my $ws1 = fresh_workspace;
366
+
367
+my $ws1_con = get_ws($ws1);
368
+ok(!$ws1_con->{floating_hidden}, 'ws1 floating cons are not hidden by default');
369
+
370
+cmd 'floating_toggle_hide';
371
+$ws1_con = get_ws($ws1);
372
+ok($ws1_con->{floating_hidden}, 'ws1 floating cons are now hidden');
373
+
374
+cmd 'floating_toggle_hide';
375
+$ws1_con = get_ws($ws1);
376
+ok(!$ws1_con->{floating_hidden}, 'ws1 floating cons are NOT hidden again');
377
+
378
+my $ws2 = fresh_workspace;
379
+my $ws2_con = get_ws($ws2);
380
+ok(!$ws2_con->{floating_hidden}, 'ws2 floating cons are not hidden by default');
381
+
382
+cmd 'floating_toggle_hide';
383
+$ws2_con = get_ws($ws2);
384
+ok($ws2_con->{floating_hidden}, 'ws2 floating cons are now hidden');
385
+
386
+$ws1_con = get_ws($ws1);
387
+ok(!$ws1_con->{floating_hidden}, 'ws1 floating cons remain not hidden');
388
+
389
+###################################################################
390
+# 2. floating windows mapped property should be changed on toggle
391
+###################################################################
392
+
393
+$ws1 = fresh_workspace;
394
+$ws1_con = get_ws($ws1);
395
+
396
+my $ws1_window = open_floating_window;
397
+ok($ws1_window->mapped, 'ws1 window is mapped');
398
+
399
+cmd 'floating_toggle_hide';
400
+ok(!$ws1_window->mapped, 'ws1 window is not mapped');
401
+
402
+cmd 'floating_toggle_hide';
403
+ok($ws1_window->mapped, 'ws1 window is mapped again');
404
+
405
+$ws2 = fresh_workspace;
406
+my $ws2_window = open_floating_window;
407
+ok($ws2_window->mapped, 'floating window are mapped for fresh ws2');
408
+
409
+cmd 'floating_toggle_hide';
410
+ok(!$ws2_window->mapped, 'ws2 window is not mapped');
411
+
412
+cmd "workspace $ws1";
413
+ok($ws1_window->mapped, 'ws1 window remains mapped');
414
+
415
+###################################################################
416
+# 3. Focus must be relocated if it was on a hidden floating window
417
+###################################################################
418
+
419
+$ws1 = fresh_workspace;
420
+$ws1_window = open_window;
421
+cmd 'focus tiling';
422
+my $ws1_focused = get_focused($ws1);
423
+my $ws1_floating_window = open_floating_window;
424
+cmd 'focus floating';
425
+my $ws1_floating_focused = get_focused($ws1);
426
+
427
+is(get_focused($ws1), $ws1_floating_focused, 'the floating window is now focused');
428
+
429
+cmd 'floating_toggle_hide';
430
+
431
+is(get_focused($ws1), $ws1_focused, 'the focus changed to the tiling window');
432
+
433
+###################################################################
434
+# 4. Don't mess with focus when its on a tiling window
435
+###################################################################
436
+
437
+$ws1 = fresh_workspace;
438
+open_floating_window;
439
+my $tiling_win = open_window;
440
+cmd 'focus tiling';
441
+my $tiling_win_focused = get_focused($ws1);
442
+
443
+cmd 'floating_toggle_hide';
444
+is(get_focused($ws1), $tiling_win_focused, 'the focus remained on the tiling window');
445
+
446
+###################################################################
447
+# 5. Don't mess with focus when its on a scratchpad window
448
+###################################################################
449
+
450
+$ws1 = fresh_workspace;
451
+open_floating_window;
452
+open_floating_window;
453
+open_window;
454
+open_window;
455
+
456
+# Clear scratchpad.
457
+while (scalar @{get_ws('__i3_scratch')->{floating_nodes}}) {
458
+    cmd 'scratchpad show';
459
+    cmd 'kill';
460
+}
461
+
462
+my $scratch_win = open_window;
463
+cmd 'focus tiling';
464
+cmd 'scratchpad move';
465
+cmd 'scratchpad show';
466
+my $scratch_win_focused = get_focused($ws1);
467
+
468
+cmd 'floating_toggle_hide';
469
+is(get_focused($ws1), $scratch_win_focused, 'the focus remained on the scratchpad window');
470
+
471
+###################################################################
472
+# 6. Don't mess with scratchpad window mapping
473
+###################################################################
474
+
475
+ok($scratch_win->mapped, 'scratchpad window remained mapped after hidding floating ones');
476
+
477
+###################################################################
478
+# 7. scratchpad window remain on top of re-displayed floating ones
479
+###################################################################
480
+
481
+cmd 'floating_toggle_hide';
482
+(my $nodes) = get_ws_content($ws1);
483
+is($scratch_win->id, $nodes->[-1]->{window}, 'scratchpad is on top');
484
+
485
+done_testing;