i3 - improved tiling WM


Implement sticky windows

Patch status: rejected

Patch by Tony Crisci

Long description:

Sticky windows will always be on a visible workspace.

fixes #11

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

b/include/commands.h

24
@@ -139,6 +139,12 @@ void cmd_move_con_to_output(I3_CMD, char *name);
25
 void cmd_floating(I3_CMD, char *floating_mode);
26
 
27
 /**
28
+ * Implementation of 'sticky enable|disable|toggle'
29
+ *
30
+ */
31
+void cmd_sticky(I3_CMD, char *sticky_mode);
32
+
33
+/**
34
  * Implementation of 'move workspace to [output] <str>'.
35
  *
36
  */

b/include/data.h

41
@@ -538,6 +538,9 @@ struct Con {
42
 
43
     struct Window *window;
44
 
45
+    /* whether or not this is a sticky window */
46
+    bool is_sticky;
47
+
48
     /* timer used for disabling urgency */
49
     struct ev_timer *urgency_timer;
50
 

b/include/util.h

55
@@ -33,6 +33,10 @@
56
     for (Con *child = (Con*)-1; (child == (Con*)-1) && ((child = 0), true);) \
57
         TAILQ_FOREACH_REVERSE(child, &((head)->nodes_head), nodes_head, nodes)
58
 
59
+#define FLOATING_FOREACH(head) \
60
+    for (Con *child = (Con*)-1; (child == (Con*)-1) && ((child = 0), true);) \
61
+        TAILQ_FOREACH(child, &((head)->floating_head), nodes)
62
+
63
 /* greps the ->nodes of the given head and returns the first node that matches the given condition */
64
 #define GREP_FIRST(dest, head, condition) \
65
     NODES_FOREACH(head) { \

b/parser-specs/commands.spec

70
@@ -31,6 +31,7 @@ state INITIAL:
71
   'fullscreen' -> FULLSCREEN
72
   'split' -> SPLIT
73
   'floating' -> FLOATING
74
+  'sticky' -> STICKY
75
   'mark' -> MARK
76
   'unmark' -> UNMARK
77
   'resize' -> RESIZE
78
@@ -173,6 +174,11 @@ state FLOATING:
79
   floating = 'enable', 'disable', 'toggle'
80
       -> call cmd_floating($floating)
81
 
82
+# sticky enable|disable|toggle
83
+state STICKY:
84
+  sticky = 'enable', 'disable', 'toggle'
85
+      -> call cmd_sticky($sticky)
86
+
87
 # mark <mark>
88
 state MARK:
89
   mark = string

b/src/commands.c

94
@@ -1168,6 +1168,37 @@ void cmd_floating(I3_CMD, char *floating_mode) {
95
 }
96
 
97
 /*
98
+ * Implementation of 'sticky enable|disable|toggle'
99
+ *
100
+ */
101
+void cmd_sticky(I3_CMD, char *sticky_mode) {
102
+    owindow *current;
103
+
104
+    DLOG("sticky_mode=%s\n", sticky_mode);
105
+
106
+    HANDLE_EMPTY_MATCH;
107
+
108
+    TAILQ_FOREACH(current, &owindows, owindows) {
109
+        DLOG("matching: %p / %s\n", current->con, current->con->name);
110
+        if (strcmp(sticky_mode, "toggle") == 0) {
111
+            DLOG("should toggle mode\n");
112
+            current->con->is_sticky = !current->con->is_sticky;
113
+        } else {
114
+            DLOG("should switch mode to %s\n", sticky_mode);
115
+            if (strcmp(sticky_mode, "enable") == 0) {
116
+                current->con->is_sticky = true;
117
+            } else {
118
+                current->con->is_sticky = false;
119
+            }
120
+        }
121
+    }
122
+
123
+    cmd_output->needs_tree_render = true;
124
+    // XXX: default reply for now, make this a better reply
125
+    ysuccess(true);
126
+}
127
+
128
+/*
129
  * Implementation of 'move workspace to [output] <str>'.
130
  *
131
  */

b/src/workspace.c

136
@@ -336,6 +336,60 @@ static void workspace_defer_update_urgent_hint_cb(EV_P_ ev_timer *w, int revents
137
     FREE(con->urgency_timer);
138
 }
139
 
140
+static bool collect_sticky_recursive(Con *con, Con *ws);
141
+
142
+/*
143
+ * Collects all the sticky containers within the tree and puts them on the
144
+ * given workspace.
145
+ */
146
+static bool workspace_collect_sticky(Con *ws) {
147
+    Con *output;
148
+    TAILQ_FOREACH(output, &(croot->nodes_head), nodes) {
149
+        /* iterate through all the workspaces on the outputs */
150
+        Con *current;
151
+        TAILQ_FOREACH(current, &(output_get_content(output)->nodes_head), nodes) {
152
+            /* iterate through all the containers on the workspaces */
153
+            NODES_FOREACH(current) {
154
+                /* The recursive func returns true if a sticky window was
155
+                 * indeed collected. */
156
+                if (collect_sticky_recursive(child, ws)) {
157
+                    /* If a container was moved from one workspace to another,
158
+                     * our lists are invalidated and we start from the
159
+                     * beginning of this function */
160
+                    workspace_collect_sticky(ws);
161
+                    return true;
162
+                }
163
+            }
164
+            FLOATING_FOREACH(current) {
165
+                if (collect_sticky_recursive(child, ws)) {
166
+                    workspace_collect_sticky(ws);
167
+                    return true;
168
+                }
169
+            }
170
+        }
171
+    }
172
+
173
+    return false;
174
+}
175
+
176
+static bool collect_sticky_recursive(Con *con, Con *ws) {
177
+    if (con->is_sticky) {
178
+        Con *current = con_get_workspace(con);
179
+
180
+        if (current != ws && !workspace_is_visible(current)) {
181
+            con_move_to_workspace(con, ws, true, false);
182
+            return true;
183
+        }
184
+    }
185
+
186
+    NODES_FOREACH(con) {
187
+        if (collect_sticky_recursive(child, ws))
188
+            return true;
189
+    }
190
+
191
+    return false;
192
+}
193
+
194
 static void _workspace_show(Con *workspace) {
195
     Con *current, *old = NULL;
196
 
197
@@ -437,6 +491,14 @@ static void _workspace_show(Con *workspace) {
198
         x_set_warp_to(&next->rect);
199
     }
200
 
201
+    Con *focused_before = focused;
202
+    /* move all the sticky containers within the tree to this workspace */
203
+    if (workspace_collect_sticky(workspace)) {
204
+        /* set focus back to where it was, given that there now could be some new
205
+         * containers */
206
+        con_focus(con_descend_focused(focused_before));
207
+    }
208
+
209
     /* Update the EWMH hints */
210
     ewmh_update_current_desktop();
211
 }

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

216
@@ -144,7 +144,7 @@ is(parser_calls("\nworkspace test"),
217
 ################################################################################
218
 
219
 is(parser_calls('unknown_literal'),
220
-   "ERROR: Expected one of these tokens: <end>, '[', 'move', 'exec', 'exit', 'restart', 'reload', 'shmlog', 'debuglog', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'split', 'floating', 'mark', 'unmark', 'resize', 'rename', 'nop', 'scratchpad', 'mode', 'bar'\n" .
221
+   "ERROR: Expected one of these tokens: <end>, '[', 'move', 'exec', 'exit', 'restart', 'reload', 'shmlog', 'debuglog', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'split', 'floating', 'sticky', 'mark', 'unmark', 'resize', 'rename', 'nop', 'scratchpad', 'mode', 'bar'\n" .
222
    "ERROR: Your command: unknown_literal\n" .
223
    "ERROR:               ^^^^^^^^^^^^^^^",
224
    'error for unknown literal ok');

b/testcases/t/518-sticky-windows.t

230
@@ -0,0 +1,106 @@
231
+#!perl
232
+# vim:ts=4:sw=4:expandtab
233
+#
234
+# Please read the following documents before working on tests:
235
+# • http://build.i3wm.org/docs/testsuite.html
236
+#   (or docs/testsuite)
237
+#
238
+# • http://build.i3wm.org/docs/lib-i3test.html
239
+#   (alternatively: perldoc ./testcases/lib/i3test.pm)
240
+#
241
+# • http://build.i3wm.org/docs/ipc.html
242
+#   (or docs/ipc)
243
+#
244
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
245
+#   (unless you are already familiar with Perl)
246
+#
247
+# TODO: Description of this file.
248
+# Ticket: #999
249
+# Bug still in: 4.7.2-163-g7dedc66
250
+use i3test i3_autostart => 0;
251
+use X11::XCB qw(PROP_MODE_REPLACE);
252
+
253
+my $config = <<EOT;
254
+# i3 config file (v4)
255
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
256
+
257
+workspace 1:L output fake-0
258
+workspace 2:R output fake-1
259
+workspace 3:L output fake-0
260
+workspace 4:R output fake-1
261
+
262
+fake-outputs 1024x768+0+0,1024x768+1024+0
263
+EOT
264
+
265
+my $pid = launch_with_config($config);
266
+
267
+# The sticky window
268
+my $sticky1;
269
+my $sticky2;
270
+
271
+sub cmd_win {
272
+    my ($win, $cmd) = @_;
273
+    cmd "[id=" . $win->{id} . "] $cmd";
274
+}
275
+
276
+# Start on workspace 1 on the Left output
277
+cmd 'workspace 1:L';
278
+
279
+$sticky1 = open_window;
280
+cmd_win($sticky1, 'sticky enable');
281
+
282
+# Move to a workspace on the same output (the Left output), which would cause
283
+# the sticky window to be hidden but for its stickiness
284
+cmd 'workspace 3:L';
285
+
286
+is(@{get_ws_content('3:L')}, 1,
287
+    'sticky windows should stick to the focused workspace when they would become hidden');
288
+
289
+# Zero to many sticky windows should each work as sticky windows in concert
290
+$sticky2 = open_window;
291
+cmd_win($sticky2, 'sticky enable');
292
+
293
+# Move to a workspace on the same output as the sticky windows
294
+cmd 'workspace 1:L';
295
+
296
+is(@{get_ws_content('1:L')}, 2, 'sticky windows should stick together');
297
+
298
+# Move to a workspace on other output (the Right output) and no stickiness
299
+# should happen, because the windows were never going to be hidden.
300
+cmd 'workspace 2:R';
301
+is(@{get_ws_content('1:L')}, 2, 'sticky windows should only restick when they would be hidden');
302
+
303
+# Test that the 'sticky toggle' command works to disable sticky mode
304
+cmd_win($sticky2, 'sticky toggle, move to workspace 2:R');
305
+
306
+# Moving to the other workspace on the Right output should not trigger any stickiness
307
+cmd 'workspace 4:R';
308
+is(@{get_ws_content('4:R')}, 0,
309
+    'stickiness should be disabled when "sticky toggle" is commanded of a sticky window');
310
+
311
+cmd_win($sticky2, 'kill');
312
+
313
+# Test that 'sticky disable' command works to disable sticky mode
314
+cmd_win($sticky1, 'sticky disable');
315
+cmd 'workspace 3:L';
316
+is(@{get_ws_content('3:L')}, 0,
317
+    'stickiness should be disabled when "sticky disable" is commanded of a sticky window');
318
+
319
+cmd_win($sticky1, 'kill');
320
+
321
+# Opening on 3:L
322
+my $window = open_window;
323
+$sticky1 = open_floating_window;
324
+cmd_win($sticky1, 'sticky enable');
325
+
326
+# Moving from 3:L to 1:L
327
+cmd 'workspace 1:L';
328
+is(@{get_ws('1:L')->{floating_nodes}}, 1, 'floating windows should have the ability to become sticky');
329
+
330
+cmd 'workspace 3:L';
331
+is(@{get_ws_content('3:L')}[0]->{id}, get_focused('3:L'),
332
+    'sticky windows that move because of stickiness should not take focus');
333
+
334
+exit_gracefully($pid);
335
+
336
+done_testing;