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.

fixes #807

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

b/include/commands.h

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

b/include/floating.h

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

b/include/tree.h

91
@@ -16,6 +16,7 @@ extern Con *croot;
92
 extern Con *focused;
93
 TAILQ_HEAD(all_cons_head, Con);
94
 extern struct all_cons_head all_cons;
95
+extern bool floating_hidden;
96
 
97
 /**
98
  * Initializes the tree by creating the root node, adding all RandR outputs
99
@@ -51,6 +52,12 @@ bool level_up(void);
100
 bool level_down(void);
101
 
102
 /**
103
+ * Marks Con and its children as mapped/unmapped.
104
+ *
105
+ */
106
+void mark_mapped(Con *con, bool mapped);
107
+
108
+/**
109
  * Renders the tree, that is rendering all outputs using render_con() and
110
  * pushing the changes to X11 using x_push_changes().
111
  *

b/parser-specs/commands.spec

116
@@ -106,7 +106,7 @@ state WORKSPACE:
117
       -> call cmd_workspace_back_and_forth()
118
   'number'
119
       -> WORKSPACE_NUMBER
120
-  workspace = string 
121
+  workspace = string
122
       -> call cmd_workspace_name($workspace)
123
 
124
 state WORKSPACE_NUMBER:
125
@@ -153,8 +153,11 @@ state SPLIT:
126
   direction = 'horizontal', 'vertical', 'v', 'h'
127
       -> call cmd_split($direction)
128
 
129
+# floating hide|show|toggle_hide
130
 # floating enable|disable|toggle
131
 state FLOATING:
132
+  visibility = 'hide', 'show', 'toggle_hide'
133
+      -> call cmd_floating_visibility($visibility)
134
   floating = 'enable', 'disable', 'toggle'
135
       -> call cmd_floating($floating)
136
 

b/src/commands.c

141
@@ -1108,12 +1108,44 @@ void cmd_floating(I3_CMD, char *floating_mode) {
142
         }
143
     }
144
 
145
+    if (floating_hidden && con_is_floating(focused)) {
146
+        Con *con, *workspace = con_get_workspace(focused);
147
+        /* When floating containers are hidden and the current->con
148
+         * turned into a floating one, fix focus. */
149
+        TAILQ_FOREACH(con, &(workspace->focus_head), focused) {
150
+            if (con->type != CT_FLOATING_CON) {
151
+                con_focus(con_descend_focused(con));
152
+                DLOG("moved focus to %p\n", con);
153
+                break;
154
+            }
155
+        }
156
+    }
157
+
158
     cmd_output->needs_tree_render = true;
159
     // XXX: default reply for now, make this a better reply
160
     ysuccess(true);
161
 }
162
 
163
 /*
164
+ * Implementation of floating show|hide|toggle_hide
165
+ *
166
+ */
167
+void cmd_floating_visibility(I3_CMD, char *visibility_mode) {
168
+
169
+    if (strcmp(visibility_mode, "toggle_hide") == 0) {
170
+        floating_visibility(FLOATING_TOGGLE_HIDE);
171
+    } else {
172
+        if (strcmp(visibility_mode, "show") == 0) {
173
+            floating_visibility(FLOATING_SHOW);
174
+        } else {
175
+            floating_visibility(FLOATING_HIDE);
176
+        }
177
+    }
178
+
179
+    ysuccess(true);
180
+}
181
+
182
+/*
183
  * Implementation of 'move workspace to [output] <str>'.
184
  *
185
  */

b/src/floating.c

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

b/src/ipc.c

303
@@ -165,6 +165,11 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) {
304
         else ystr("vertical");
305
     }
306
 
307
+    if (con == croot) {
308
+      ystr("floating_hidden");
309
+      y(bool, floating_hidden);
310
+    }
311
+
312
     ystr("scratchpad_state");
313
     switch (con->scratchpad_state) {
314
         case SCRATCHPAD_NONE:

b/src/main.c

319
@@ -92,6 +92,10 @@ bool xkb_supported = true;
320
  * the config, for example. */
321
 bool only_check_config = false;
322
 
323
+/* Tells if floating windows hidden. This is here for global access
324
+ * because at the time render.c and floating.c need it. */
325
+bool floating_hidden = false;
326
+
327
 /*
328
  * This callback is only a dummy, see xcb_prepare_cb and xcb_check_cb.
329
  * See also man libev(3): "ev_prepare" and "ev_check" - customise your event loop

b/src/render.c

334
@@ -268,6 +268,12 @@ void render_con(Con *con, bool render_fullscreen) {
335
             Con *fullscreen = con_get_fullscreen_con(workspace, CF_OUTPUT);
336
             Con *child;
337
             TAILQ_FOREACH(child, &(workspace->floating_head), floating_windows) {
338
+                /** Don't show floating windows when floating_hidden
339
+                 * is set but also don't mess with scratchpad windows
340
+                 * in the process */
341
+                if (floating_hidden &&
342
+                    child->scratchpad_state == SCRATCHPAD_NONE)
343
+                        continue;
344
                 /* Don’t render floating windows when there is a fullscreen window
345
                  * on that workspace. Necessary to make floating fullscreen work
346
                  * correctly (ticket #564). */

b/src/tree.c

351
@@ -474,17 +474,21 @@ bool level_down(void) {
352
     return true;
353
 }
354
 
355
-static void mark_unmapped(Con *con) {
356
+/*
357
+ * Marks Con and its children as mapped/unmapped.
358
+ *
359
+ */
360
+void mark_mapped(Con *con, bool mapped) {
361
     Con *current;
362
 
363
-    con->mapped = false;
364
+    con->mapped = mapped;
365
     TAILQ_FOREACH(current, &(con->nodes_head), nodes)
366
-        mark_unmapped(current);
367
+            mark_mapped(current, mapped);
368
     if (con->type == CT_WORKSPACE) {
369
-        /* We need to call mark_unmapped on floating nodes aswell since we can
370
+        /* We need to call mark_mapped on floating nodes aswell since we can
371
          * make containers floating. */
372
         TAILQ_FOREACH(current, &(con->floating_head), floating_windows)
373
-            mark_unmapped(current);
374
+                mark_mapped(current, mapped);
375
     }
376
 }
377
 
378
@@ -500,7 +504,7 @@ void tree_render(void) {
379
     DLOG("-- BEGIN RENDERING --\n");
380
     /* Reset map state for all nodes in tree */
381
     /* TODO: a nicer method to walk all nodes would be good, maybe? */
382
-    mark_unmapped(croot);
383
+    mark_mapped(croot, false);
384
     croot->mapped = true;
385
 
386
     render_con(croot, false);

b/testcases/t/116-nestedcons.t

391
@@ -60,6 +60,7 @@ my $expected = {
392
     percent => undef,
393
     layout => 'splith',
394
     floating => 'auto_off',
395
+    floating_hidden => JSON::XS::false,
396
     last_split_layout => 'splith',
397
     scratchpad_state => 'none',
398
     focus => $ignore,

b/testcases/t/206-floating_toggle_hide.t

404
@@ -0,0 +1,169 @@
405
+#!perl
406
+# vim:ts=4:sw=4:expandtab
407
+#
408
+# Please read the following documents before working on tests:
409
+# • http://build.i3wm.org/docs/testsuite.html
410
+#   (or docs/testsuite)
411
+#
412
+# • http://build.i3wm.org/docs/lib-i3test.html
413
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
414
+#
415
+# • http://build.i3wm.org/docs/ipc.html
416
+#   (or docs/ipc)
417
+#
418
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
419
+#   (unless you are already familiar with Perl)
420
+#
421
+# Test floating_toggle_hide command implementation.
422
+# Ticket: #807
423
+# Bug still in: 4.4-145-g8327f83
424
+use i3test;
425
+
426
+my $i3 = i3(get_socket_path());
427
+
428
+sub get_floating_hidden () {
429
+    my $sock = i3(get_socket_path())->get_tree;
430
+    sync_with_i3;
431
+    return $sock->recv->{floating_hidden};
432
+}
433
+
434
+sub clear_scratchpad () {
435
+    # Clear scratchpad
436
+    while (scalar @{get_ws('__i3_scratch')->{floating_nodes}}) {
437
+        cmd 'scratchpad show';
438
+        cmd 'kill';
439
+    }
440
+
441
+}
442
+
443
+###################################################################
444
+# Floating visibility toggle works globally
445
+###################################################################
446
+
447
+my $ws1 = fresh_workspace;
448
+my $ws2 = fresh_workspace;
449
+
450
+my $ws1_con = get_ws($ws1);
451
+ok(!$ws1_con->{floating_hidden}, 'ws1 floating cons are not hidden by default');
452
+
453
+cmd 'floating toggle_hide';
454
+$ws1_con = get_ws($ws1);
455
+ok(get_floating_hidden(), 'ws1 floating cons are now hidden');
456
+
457
+my $ws2_con = get_ws($ws2);
458
+ok(get_floating_hidden(), 'ws2 should have the same floating hidden status');
459
+
460
+# Show floating again
461
+cmd 'floating toggle_hide';
462
+
463
+###################################################################
464
+# Floating windows mapped property should match !floating_hidden
465
+###################################################################
466
+
467
+$ws1 = fresh_workspace;
468
+$ws1_con = get_ws($ws1);
469
+my $ws1_window = open_floating_window;
470
+$ws2 = fresh_workspace;
471
+my $ws2_window = open_floating_window;
472
+
473
+cmd "workspace $ws1";
474
+ok($ws1_window->mapped, 'ws1 window is mapped');
475
+
476
+cmd 'floating hide';
477
+ok(!$ws1_window->mapped, 'ws1 window is not mapped');
478
+
479
+cmd "workspace $ws2";
480
+ok(!$ws2_window->mapped, 'ws2 window is not mapped');
481
+
482
+cmd 'floating show';
483
+ok($ws2_window->mapped, 'ws2 window is mapped');
484
+
485
+cmd "workspace $ws1";
486
+ok($ws1_window->mapped, 'ws1 window is mapped');
487
+
488
+###################################################################
489
+# Focus must be relocated if it was on a hidden floating window
490
+###################################################################
491
+
492
+$ws1 = fresh_workspace;
493
+$ws1_window = open_window;
494
+cmd 'focus tiling';
495
+my $ws1_focused = get_focused($ws1);
496
+my $ws1_floating_window = open_floating_window;
497
+cmd 'focus floating';
498
+my $ws1_floating_focused = get_focused($ws1);
499
+
500
+is(get_focused($ws1), $ws1_floating_focused, 'the floating window is now focused');
501
+
502
+cmd 'floating hide';
503
+is(get_focused($ws1), $ws1_focused, 'the focus changed to the tiling window');
504
+
505
+cmd 'floating show';
506
+
507
+###################################################################
508
+# Don't mess with focus when its on a tiling window
509
+###################################################################
510
+
511
+$ws1 = fresh_workspace;
512
+open_floating_window;
513
+my $ws1_win = open_window;
514
+cmd 'focus tiling';
515
+my $tiling_win_focused = get_focused($ws1);
516
+
517
+cmd 'floating hide';
518
+is(get_focused($ws1), $tiling_win_focused, 'the focus remained on the tiling window');
519
+
520
+cmd 'floating show';
521
+
522
+###################################################################
523
+# Floating enable must focus tiling if floating cons are hidden
524
+###################################################################
525
+
526
+$ws1 = fresh_workspace;
527
+my $ws1_win1 = open_window;
528
+my $ws1_win1_focused = get_focused($ws1);
529
+my $ws1_win2 = open_window;
530
+my $ws1_win2_focused = get_focused($ws1);
531
+
532
+cmd 'floating hide';
533
+cmd 'floating enable';
534
+is(get_focused($ws1), $ws1_win1_focused, 'the focus changed to the tiling window');
535
+
536
+cmd 'floating show';
537
+
538
+###################################################################
539
+# Don't mess with focus when its on a scratchpad window
540
+###################################################################
541
+
542
+$ws1 = fresh_workspace;
543
+open_floating_window;
544
+open_floating_window;
545
+open_window;
546
+open_window;
547
+
548
+clear_scratchpad();
549
+
550
+my $scratch_win = open_window;
551
+cmd 'focus tiling';
552
+cmd 'scratchpad move';
553
+cmd 'scratchpad show';
554
+my $scratch_win_focused = get_focused($ws1);
555
+
556
+cmd 'floating hide';
557
+is(get_focused($ws1), $scratch_win_focused, 'the focus remained on the scratchpad window');
558
+
559
+###################################################################
560
+# Don't mess with scratchpad window mapping
561
+###################################################################
562
+
563
+ok($scratch_win->mapped, 'scratchpad window remained mapped after hidding floating ones');
564
+
565
+###################################################################
566
+# Scratchpad must remain on top of re-displayed floating cons
567
+###################################################################
568
+
569
+cmd 'floating show';
570
+(my $nodes) = get_ws_content($ws1);
571
+is($scratch_win->id, $nodes->[-1]->{window}, 'scratchpad is on top');
572
+
573
+done_testing;