i3 - improved tiling WM


Implementation of 'floating hide|show|toggle_hide' commands

Patch status: needinfo

Patch by Fabián Ezequiel Gallina

Long description:

These allows changing the visibility of floating windows on all
workspaces, but will not affect scratchpad windows.

Visibility state is saved in the floating_hidden global variable, and
it is included in the IPC's TREE reply as a root attribute.

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

b/include/commands.h

29
@@ -134,6 +134,12 @@ void cmd_move_con_to_output(I3_CMD, char *name);
30
 void cmd_floating(I3_CMD, char *floating_mode);
31
 
32
 /**
33
+ * Implementation of floating show|hide|toggle_hide
34
+ *
35
+ */
36
+void cmd_floating_visibility(I3_CMD, char *visibility_mode);
37
+
38
+/**
39
  * Implementation of 'move workspace to [output] <str>'.
40
  *
41
  */

b/include/floating.h

46
@@ -26,6 +26,10 @@ typedef enum { BORDER_LEFT   = (1 << 0),
47
                BORDER_TOP    = (1 << 2),
48
                BORDER_BOTTOM = (1 << 3)} border_t;
49
 
50
+typedef enum { FLOATING_TOGGLE_HIDE,
51
+               FLOATING_SHOW,
52
+               FLOATING_HIDE } floating_visibility_t;
53
+
54
 /**
55
  * Enables floating mode for the given container by detaching it from its
56
  * parent, creating a new container around it and storing this container in the
57
@@ -125,14 +129,8 @@ void floating_focus_direction(xcb_connection_t *conn, Client *currently_focused,
58
 void floating_move(xcb_connection_t *conn, Client *currently_focused,
59
                    direction_t direction);
60
 
61
-/**
62
- * Hides all floating clients (or show them if they are currently hidden) on
63
- * the specified workspace.
64
- *
65
- */
66
-void floating_toggle_hide(xcb_connection_t *conn, Workspace *workspace);
67
-
68
 #endif
69
+
70
 /**
71
  * This function grabs your pointer and lets you drag stuff around (borders).
72
  * Every time you move your mouse, an XCB_MOTION_NOTIFY event will be received
73
@@ -161,4 +159,10 @@ void floating_reposition(Con *con, Rect newrect);
74
  */
75
 void floating_fix_coordinates(Con *con, Rect *old_rect, Rect *new_rect);
76
 
77
+/**
78
+ * Sets visibility of floating clients on all workspaces.
79
+ *
80
+ */
81
+void floating_visibility(floating_visibility_t value);
82
+
83
 #endif

b/include/tree.h

88
@@ -16,6 +16,7 @@ extern Con *croot;
89
 extern Con *focused;
90
 TAILQ_HEAD(all_cons_head, Con);
91
 extern struct all_cons_head all_cons;
92
+extern bool floating_hidden;
93
 
94
 /**
95
  * Initializes the tree by creating the root node, adding all RandR outputs

b/parser-specs/commands.spec

100
@@ -106,7 +106,7 @@ state WORKSPACE:
101
       -> call cmd_workspace_back_and_forth()
102
   'number'
103
       -> WORKSPACE_NUMBER
104
-  workspace = string 
105
+  workspace = string
106
       -> call cmd_workspace_name($workspace)
107
 
108
 state WORKSPACE_NUMBER:
109
@@ -153,8 +153,11 @@ state SPLIT:
110
   direction = 'horizontal', 'vertical', 'v', 'h'
111
       -> call cmd_split($direction)
112
 
113
+# floating hide|show|toggle_hide
114
 # floating enable|disable|toggle
115
 state FLOATING:
116
+  visibility = 'hide', 'show', 'toggle_hide'
117
+      -> call cmd_floating_visibility($visibility)
118
   floating = 'enable', 'disable', 'toggle'
119
       -> call cmd_floating($floating)
120
 

b/src/commands.c

125
@@ -1108,12 +1108,44 @@ void cmd_floating(I3_CMD, char *floating_mode) {
126
         }
127
     }
128
 
129
+    if (floating_hidden && con_is_floating(focused)) {
130
+        Con *con, *workspace = con_get_workspace(focused);
131
+        /* When floating containers are hidden and the current->con
132
+         * turned into a floating one, fix focus. */
133
+        TAILQ_FOREACH(con, &(workspace->focus_head), focused) {
134
+            if (con->type != CT_FLOATING_CON) {
135
+                con_focus(con_descend_focused(con));
136
+                DLOG("moved focus to %p\n", con);
137
+                break;
138
+            }
139
+        }
140
+    }
141
+
142
     cmd_output->needs_tree_render = true;
143
     // XXX: default reply for now, make this a better reply
144
     ysuccess(true);
145
 }
146
 
147
 /*
148
+ * Implementation of floating show|hide|toggle_hide
149
+ *
150
+ */
151
+void cmd_floating_visibility(I3_CMD, char *visibility_mode) {
152
+
153
+    if (strcmp(visibility_mode, "toggle_hide") == 0) {
154
+        floating_visibility(FLOATING_TOGGLE_HIDE);
155
+    } else {
156
+        if (strcmp(visibility_mode, "show") == 0) {
157
+            floating_visibility(FLOATING_SHOW);
158
+        } else {
159
+            floating_visibility(FLOATING_HIDE);
160
+        }
161
+    }
162
+
163
+    ysuccess(true);
164
+}
165
+
166
+/*
167
  * Implementation of 'move workspace to [output] <str>'.
168
  *
169
  */

b/src/floating.c

174
@@ -754,33 +754,84 @@ void floating_move(xcb_connection_t *conn, Client *currently_focused, direction_
175
         fake_absolute_configure_notify(conn, currently_focused);
176
         /* fake_absolute_configure_notify flushes */
177
 }
178
+#endif
179
 
180
 /*
181
- * Hides all floating clients (or show them if they are currently hidden) on
182
- * the specified workspace.
183
+ * Sets visibility of floating clients on all workspaces.
184
  *
185
  */
186
-void floating_toggle_hide(xcb_connection_t *conn, Workspace *workspace) {
187
-        Client *client;
188
-
189
-        workspace->floating_hidden = !workspace->floating_hidden;
190
-        DLOG("floating_hidden is now: %d\n", workspace->floating_hidden);
191
-        TAILQ_FOREACH(client, &(workspace->floating_clients), floating_clients) {
192
-                if (workspace->floating_hidden)
193
-                        client_unmap(conn, client);
194
-                else client_map(conn, client);
195
-        }
196
+void floating_visibility(floating_visibility_t value) {
197
+    Con *output, *con, *scratchpad_con = NULL, *workspace;
198
+    bool old_floating_hidden = floating_hidden;
199
+
200
+    if (value == FLOATING_TOGGLE_HIDE) {
201
+        floating_hidden = !floating_hidden;
202
+        DLOG("toggling floating_hidden: %d -> %d\n",
203
+             !floating_hidden, floating_hidden);
204
+    } else if (value == FLOATING_SHOW) {
205
+        floating_hidden = false;
206
+        DLOG("setting floating_hidden to false\n");
207
+    } else {
208
+        floating_hidden = true;
209
+        DLOG("setting floating_hidden to true\n");
210
+    }
211
+
212
+    /* Flag didn't changed, do nothing */
213
+    if (floating_hidden == old_floating_hidden)
214
+        return;
215
 
216
-        /* If we just unmapped all floating windows we should ensure that the focus
217
-         * is set correctly, that ist, to the first non-floating client in stack */
218
-        if (workspace->floating_hidden)
219
-                SLIST_FOREACH(client, &(workspace->focus_stack), focus_clients) {
220
-                        if (client_is_floating(client))
221
-                                continue;
222
-                        set_focus(conn, client, true);
223
-                        return;
224
+    TAILQ_FOREACH(output, &(croot->nodes_head), nodes) {
225
+        if (con_is_internal(output))
226
+            continue;
227
+        Con *content = output_get_content(output);
228
+        if (!content || TAILQ_EMPTY(&(content->focus_head))) {
229
+            continue;
230
+        }
231
+        workspace = TAILQ_FIRST(&(content->focus_head));
232
+        DLOG("applying floating display changes on %p\n", workspace);
233
+        TAILQ_FOREACH(con, &(workspace->floating_head), floating_windows) {
234
+            /* Don't mess with scratchpad windows. */
235
+            if (con->scratchpad_state != SCRATCHPAD_NONE) {
236
+                /* but in the case there's one showing, save its
237
+                 * reference so it can be raised over all re-displayed
238
+                 * floating windows. */
239
+                if (!floating_hidden) {
240
+                    scratchpad_con = con;
241
                 }
242
+                DLOG("ignore scratchpad window %p with state %d\n",
243
+                     con, con->scratchpad_state);
244
+                continue;
245
+            }
246
+            con->mapped = !floating_hidden;
247
+            DLOG("mapped for con (and children) %p is now %d\n",
248
+                 con, !floating_hidden);
249
+        }
250
+        /* Put the scratchpad window in front of all re-displayed
251
+         * floating windows */
252
+        if (scratchpad_con != NULL) {
253
+            x_raise_con(scratchpad_con);
254
+            DLOG("re-raised scratchpad window %p\n", scratchpad_con);
255
+        }
256
+     }
257
+
258
+    Con *current = TAILQ_FIRST(&workspace->focus_head);
259
+    bool floating_focused = (current != NULL &&
260
+                             current->type == CT_FLOATING_CON &&
261
+                             current->scratchpad_state == SCRATCHPAD_NONE);
262
+
263
+    /* If all floating windows are hidden and focus was in a floating
264
+     * container (not a scratchpad window), set focus to the first
265
+     * non-floating window available. */
266
+    if (floating_hidden && floating_focused) {
267
+        workspace = con_get_workspace(focused);
268
+        TAILQ_FOREACH(con, &(workspace->focus_head), focused) {
269
+            if (con->type != CT_FLOATING_CON) {
270
+                con_focus(con);
271
+                DLOG("moved focus to %p\n", con);
272
+                break;
273
+            }
274
+        }
275
+    }
276
 
277
-        xcb_flush(conn);
278
+    tree_render();
279
 }
280
-#endif

b/src/ipc.c

285
@@ -165,6 +165,11 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) {
286
         else ystr("vertical");
287
     }
288
 
289
+    if (con == croot) {
290
+        ystr("floating_hidden");
291
+        y(bool, floating_hidden);
292
+    }
293
+
294
     ystr("scratchpad_state");
295
     switch (con->scratchpad_state) {
296
         case SCRATCHPAD_NONE:

b/src/main.c

301
@@ -92,6 +92,10 @@ bool xkb_supported = true;
302
  * the config, for example. */
303
 bool only_check_config = false;
304
 
305
+/* Tells if floating windows hidden. This is here for global access
306
+ * because at the time render.c and floating.c need it. */
307
+bool floating_hidden = false;
308
+
309
 /*
310
  * This callback is only a dummy, see xcb_prepare_cb and xcb_check_cb.
311
  * See also man libev(3): "ev_prepare" and "ev_check" - customise your event loop

b/src/render.c

316
@@ -268,6 +268,12 @@ void render_con(Con *con, bool render_fullscreen) {
317
             Con *fullscreen = con_get_fullscreen_con(workspace, CF_OUTPUT);
318
             Con *child;
319
             TAILQ_FOREACH(child, &(workspace->floating_head), floating_windows) {
320
+                /* Don't show floating windows when floating_hidden is
321
+                 * set but also don't mess with scratchpad windows in
322
+                 * the process */
323
+                if (floating_hidden &&
324
+                    child->scratchpad_state == SCRATCHPAD_NONE)
325
+                        continue;
326
                 /* Don’t render floating windows when there is a fullscreen window
327
                  * on that workspace. Necessary to make floating fullscreen work
328
                  * correctly (ticket #564). */

b/testcases/t/116-nestedcons.t

333
@@ -60,6 +60,7 @@ my $expected = {
334
     percent => undef,
335
     layout => 'splith',
336
     floating => 'auto_off',
337
+    floating_hidden => JSON::XS::false,
338
     last_split_layout => 'splith',
339
     scratchpad_state => 'none',
340
     focus => $ignore,

b/testcases/t/206-floating_toggle_hide.t

346
@@ -0,0 +1,169 @@
347
+#!perl
348
+# vim:ts=4:sw=4:expandtab
349
+#
350
+# Please read the following documents before working on tests:
351
+# • http://build.i3wm.org/docs/testsuite.html
352
+#   (or docs/testsuite)
353
+#
354
+# • http://build.i3wm.org/docs/lib-i3test.html
355
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
356
+#
357
+# • http://build.i3wm.org/docs/ipc.html
358
+#   (or docs/ipc)
359
+#
360
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
361
+#   (unless you are already familiar with Perl)
362
+#
363
+# Test floating_toggle_hide command implementation.
364
+# Ticket: #807
365
+# Bug still in: 4.4-145-g8327f83
366
+use i3test;
367
+
368
+my $i3 = i3(get_socket_path());
369
+
370
+sub get_floating_hidden () {
371
+    my $sock = i3(get_socket_path())->get_tree;
372
+    sync_with_i3;
373
+    return $sock->recv->{floating_hidden};
374
+}
375
+
376
+sub clear_scratchpad () {
377
+    # Clear scratchpad
378
+    while (scalar @{get_ws('__i3_scratch')->{floating_nodes}}) {
379
+        cmd 'scratchpad show';
380
+        cmd 'kill';
381
+    }
382
+
383
+}
384
+
385
+###################################################################
386
+# Floating visibility toggle works globally
387
+###################################################################
388
+
389
+my $ws1 = fresh_workspace;
390
+my $ws2 = fresh_workspace;
391
+
392
+my $ws1_con = get_ws($ws1);
393
+ok(!$ws1_con->{floating_hidden}, 'ws1 floating cons are not hidden by default');
394
+
395
+cmd 'floating toggle_hide';
396
+$ws1_con = get_ws($ws1);
397
+ok(get_floating_hidden(), 'ws1 floating cons are now hidden');
398
+
399
+my $ws2_con = get_ws($ws2);
400
+ok(get_floating_hidden(), 'ws2 should have the same floating hidden status');
401
+
402
+# Show floating again
403
+cmd 'floating toggle_hide';
404
+
405
+###################################################################
406
+# Floating windows mapped property should match !floating_hidden
407
+###################################################################
408
+
409
+$ws1 = fresh_workspace;
410
+$ws1_con = get_ws($ws1);
411
+my $ws1_window = open_floating_window;
412
+$ws2 = fresh_workspace;
413
+my $ws2_window = open_floating_window;
414
+
415
+cmd "workspace $ws1";
416
+ok($ws1_window->mapped, 'ws1 window is mapped');
417
+
418
+cmd 'floating hide';
419
+ok(!$ws1_window->mapped, 'ws1 window is not mapped');
420
+
421
+cmd "workspace $ws2";
422
+ok(!$ws2_window->mapped, 'ws2 window is not mapped');
423
+
424
+cmd 'floating show';
425
+ok($ws2_window->mapped, 'ws2 window is mapped');
426
+
427
+cmd "workspace $ws1";
428
+ok($ws1_window->mapped, 'ws1 window is mapped');
429
+
430
+###################################################################
431
+# Focus must be relocated if it was on a hidden floating window
432
+###################################################################
433
+
434
+$ws1 = fresh_workspace;
435
+$ws1_window = open_window;
436
+cmd 'focus tiling';
437
+my $ws1_focused = get_focused($ws1);
438
+my $ws1_floating_window = open_floating_window;
439
+cmd 'focus floating';
440
+my $ws1_floating_focused = get_focused($ws1);
441
+
442
+is(get_focused($ws1), $ws1_floating_focused, 'the floating window is now focused');
443
+
444
+cmd 'floating hide';
445
+is(get_focused($ws1), $ws1_focused, 'the focus changed to the tiling window');
446
+
447
+cmd 'floating show';
448
+
449
+###################################################################
450
+# Don't mess with focus when its on a tiling window
451
+###################################################################
452
+
453
+$ws1 = fresh_workspace;
454
+open_floating_window;
455
+my $ws1_win = open_window;
456
+cmd 'focus tiling';
457
+my $tiling_win_focused = get_focused($ws1);
458
+
459
+cmd 'floating hide';
460
+is(get_focused($ws1), $tiling_win_focused, 'the focus remained on the tiling window');
461
+
462
+cmd 'floating show';
463
+
464
+###################################################################
465
+# Floating enable must focus tiling if floating cons are hidden
466
+###################################################################
467
+
468
+$ws1 = fresh_workspace;
469
+my $ws1_win1 = open_window;
470
+my $ws1_win1_focused = get_focused($ws1);
471
+my $ws1_win2 = open_window;
472
+my $ws1_win2_focused = get_focused($ws1);
473
+
474
+cmd 'floating hide';
475
+cmd 'floating enable';
476
+is(get_focused($ws1), $ws1_win1_focused, 'the focus changed to the tiling window');
477
+
478
+cmd 'floating show';
479
+
480
+###################################################################
481
+# Don't mess with focus when its on a scratchpad window
482
+###################################################################
483
+
484
+$ws1 = fresh_workspace;
485
+open_floating_window;
486
+open_floating_window;
487
+open_window;
488
+open_window;
489
+
490
+clear_scratchpad();
491
+
492
+my $scratch_win = open_window;
493
+cmd 'focus tiling';
494
+cmd 'scratchpad move';
495
+cmd 'scratchpad show';
496
+my $scratch_win_focused = get_focused($ws1);
497
+
498
+cmd 'floating hide';
499
+is(get_focused($ws1), $scratch_win_focused, 'the focus remained on the scratchpad window');
500
+
501
+###################################################################
502
+# Don't mess with scratchpad window mapping
503
+###################################################################
504
+
505
+ok($scratch_win->mapped, 'scratchpad window remained mapped after hidding floating ones');
506
+
507
+###################################################################
508
+# Scratchpad must remain on top of re-displayed floating cons
509
+###################################################################
510
+
511
+cmd 'floating show';
512
+(my $nodes) = get_ws_content($ws1);
513
+is($scratch_win->id, $nodes->[-1]->{window}, 'scratchpad is on top');
514
+
515
+done_testing;