]> git.sur5r.net Git - i3/i3/blob - src/manage.c
Obey the client’s border_width setting (Thanks shatter)
[i3/i3] / src / manage.c
1 /*
2  * vim:ts=8:expandtab
3  *
4  * i3 - an improved dynamic tiling window manager
5  *
6  * © 2009 Michael Stapelberg and contributors
7  *
8  * See file LICENSE for license information.
9  *
10  * src/manage.c: Contains all functions for initially managing new windows
11  *               (or existing ones on restart).
12  *
13  */
14 #include <stdlib.h>
15 #include <string.h>
16 #include <assert.h>
17
18 #include <xcb/xcb.h>
19 #include <xcb/xcb_icccm.h>
20
21 #include "xcb.h"
22 #include "data.h"
23 #include "util.h"
24 #include "i3.h"
25 #include "table.h"
26 #include "config.h"
27 #include "handlers.h"
28 #include "layout.h"
29 #include "manage.h"
30 #include "floating.h"
31 #include "client.h"
32 #include "workspace.h"
33
34 /*
35  * Go through all existing windows (if the window manager is restarted) and manage them
36  *
37  */
38 void manage_existing_windows(xcb_connection_t *conn, xcb_property_handlers_t *prophs, xcb_window_t root) {
39         xcb_query_tree_reply_t *reply;
40         int i, len;
41         xcb_window_t *children;
42         xcb_get_window_attributes_cookie_t *cookies;
43
44         /* Get the tree of windows whose parent is the root window (= all) */
45         if ((reply = xcb_query_tree_reply(conn, xcb_query_tree(conn, root), 0)) == NULL)
46                 return;
47
48         len = xcb_query_tree_children_length(reply);
49         cookies = smalloc(len * sizeof(*cookies));
50
51         /* Request the window attributes for every window */
52         children = xcb_query_tree_children(reply);
53         for(i = 0; i < len; ++i)
54                 cookies[i] = xcb_get_window_attributes(conn, children[i]);
55
56         /* Call manage_window with the attributes for every window */
57         for(i = 0; i < len; ++i)
58                 manage_window(prophs, conn, children[i], cookies[i], true);
59
60         free(reply);
61         free(cookies);
62 }
63
64 /*
65  * Do some sanity checks and then reparent the window.
66  *
67  */
68 void manage_window(xcb_property_handlers_t *prophs, xcb_connection_t *conn,
69                    xcb_window_t window, xcb_get_window_attributes_cookie_t cookie,
70                    bool needs_to_be_mapped) {
71         xcb_drawable_t d = { window };
72         xcb_get_geometry_cookie_t geomc;
73         xcb_get_geometry_reply_t *geom;
74         xcb_get_window_attributes_reply_t *attr = 0;
75
76         geomc = xcb_get_geometry(conn, d);
77
78         /* Check if the window is mapped (it could be not mapped when intializing and
79            calling manage_window() for every window) */
80         if ((attr = xcb_get_window_attributes_reply(conn, cookie, 0)) == NULL) {
81                 LOG("Could not get attributes\n");
82                 return;
83         }
84
85         if (needs_to_be_mapped && attr->map_state != XCB_MAP_STATE_VIEWABLE)
86                 goto out;
87
88         /* Don’t manage clients with the override_redirect flag */
89         if (attr->override_redirect)
90                 goto out;
91
92         /* Check if the window is already managed */
93         if (table_get(&by_child, window))
94                 goto out;
95
96         /* Get the initial geometry (position, size, …) */
97         if ((geom = xcb_get_geometry_reply(conn, geomc, 0)) == NULL)
98                 goto out;
99
100         /* Reparent the window and add it to our list of managed windows */
101         reparent_window(conn, window, attr->visual, geom->root, geom->depth,
102                         geom->x, geom->y, geom->width, geom->height,
103                         geom->border_width);
104
105         /* Generate callback events for every property we watch */
106         xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_CLASS);
107         xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_NAME);
108         xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_NORMAL_HINTS);
109         xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_HINTS);
110         xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, WM_TRANSIENT_FOR);
111         xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, atoms[WM_CLIENT_LEADER]);
112         xcb_property_changed(prophs, XCB_PROPERTY_NEW_VALUE, window, atoms[_NET_WM_NAME]);
113
114         free(geom);
115 out:
116         free(attr);
117         return;
118 }
119
120 /*
121  * reparent_window() gets called when a new window was opened and becomes a child of the root
122  * window, or it gets called by us when we manage the already existing windows at startup.
123  *
124  * Essentially, this is the point where we take over control.
125  *
126  */
127 void reparent_window(xcb_connection_t *conn, xcb_window_t child,
128                      xcb_visualid_t visual, xcb_window_t root, uint8_t depth,
129                      int16_t x, int16_t y, uint16_t width, uint16_t height,
130                      uint32_t border_width) {
131
132         xcb_get_property_cookie_t wm_type_cookie, strut_cookie, state_cookie,
133                                   utf8_title_cookie, title_cookie,
134                                   class_cookie, leader_cookie;
135         uint32_t mask = 0;
136         uint32_t values[3];
137         uint16_t original_height = height;
138         bool map_frame = true;
139
140         /* We are interested in property changes */
141         mask = XCB_CW_EVENT_MASK;
142         values[0] = CHILD_EVENT_MASK;
143         xcb_change_window_attributes(conn, child, mask, values);
144
145         /* Place requests for properties ASAP */
146         wm_type_cookie = xcb_get_any_property_unchecked(conn, false, child, atoms[_NET_WM_WINDOW_TYPE], UINT32_MAX);
147         strut_cookie = xcb_get_any_property_unchecked(conn, false, child, atoms[_NET_WM_STRUT_PARTIAL], UINT32_MAX);
148         state_cookie = xcb_get_any_property_unchecked(conn, false, child, atoms[_NET_WM_STATE], UINT32_MAX);
149         utf8_title_cookie = xcb_get_any_property_unchecked(conn, false, child, atoms[_NET_WM_NAME], 128);
150         leader_cookie = xcb_get_any_property_unchecked(conn, false, child, atoms[WM_CLIENT_LEADER], UINT32_MAX);
151         title_cookie = xcb_get_any_property_unchecked(conn, false, child, WM_NAME, 128);
152         class_cookie = xcb_get_any_property_unchecked(conn, false, child, WM_CLASS, 128);
153
154         Client *new = table_get(&by_child, child);
155
156         /* Events for already managed windows should already be filtered in manage_window() */
157         assert(new == NULL);
158
159         LOG("Reparenting window 0x%08x\n", child);
160         LOG("x = %d, y = %d, width = %d, height = %d\n", x, y, width, height);
161         new = calloc(sizeof(Client), 1);
162         new->force_reconfigure = true;
163
164         /* Update the data structures */
165         Client *old_focused = CUR_CELL->currently_focused;
166
167         new->container = CUR_CELL;
168         new->workspace = new->container->workspace;
169
170         /* Minimum useful size for managed windows is 75x50 (primarily affects floating) */
171         width = max(width, 75);
172         height = max(height, 50);
173
174         new->frame = xcb_generate_id(conn);
175         new->child = child;
176         new->rect.width = width;
177         new->rect.height = height;
178         new->width_increment = 1;
179         new->height_increment = 1;
180         new->border_width = border_width;
181         /* Pre-initialize the values for floating */
182         new->floating_rect.x = -1;
183         new->floating_rect.width = width;
184         new->floating_rect.height = height;
185
186         mask = 0;
187
188         /* Don’t generate events for our new window, it should *not* be managed */
189         mask |= XCB_CW_OVERRIDE_REDIRECT;
190         values[0] = 1;
191
192         /* We want to know when… */
193         mask |= XCB_CW_EVENT_MASK;
194         values[1] = FRAME_EVENT_MASK;
195
196         i3Font *font = load_font(conn, config.font);
197         width = min(width, c_ws->rect.x + c_ws->rect.width);
198         height = min(height, c_ws->rect.y + c_ws->rect.height);
199
200         Rect framerect = {x, y,
201                           width + 2 + 2,                  /* 2 px border at each side */
202                           height + 2 + 2 + font->height}; /* 2 px border plus font’s height */
203
204         /* Yo dawg, I heard you like windows, so I create a window around your window… */
205         new->frame = create_window(conn, framerect, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_CURSOR_LEFT_PTR, false, mask, values);
206
207         /* Put the client inside the save set. Upon termination (whether killed or normal exit
208            does not matter) of the window manager, these clients will be correctly reparented
209            to their most closest living ancestor (= cleanup) */
210         xcb_change_save_set(conn, XCB_SET_MODE_INSERT, child);
211
212         /* Generate a graphics context for the titlebar */
213         new->titlegc = xcb_generate_id(conn);
214         xcb_create_gc(conn, new->titlegc, new->frame, 0, 0);
215
216         /* Moves the original window into the new frame we've created for it */
217         new->awaiting_useless_unmap = true;
218         xcb_void_cookie_t cookie = xcb_reparent_window_checked(conn, child, new->frame, 0, font->height);
219         if (xcb_request_check(conn, cookie) != NULL) {
220                 LOG("Could not reparent the window, aborting\n");
221                 xcb_destroy_window(conn, new->frame);
222                 free(new);
223                 return;
224         }
225
226         /* Put our data structure (Client) into the table */
227         table_put(&by_parent, new->frame, new);
228         table_put(&by_child, child, new);
229
230         /* We need to grab the mouse buttons for click to focus */
231         xcb_grab_button(conn, false, child, XCB_EVENT_MASK_BUTTON_PRESS,
232                         XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC, root, XCB_NONE,
233                         1 /* left mouse button */,
234                         XCB_BUTTON_MASK_ANY /* don’t filter for any modifiers */);
235
236         xcb_grab_button(conn, false, child, XCB_EVENT_MASK_BUTTON_PRESS,
237                         XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC, root, XCB_NONE,
238                         3 /* right mouse button */,
239                         XCB_BUTTON_MASK_ANY /* don’t filter for any modifiers */);
240
241         /* Get _NET_WM_WINDOW_TYPE (to see if it’s a dock) */
242         xcb_atom_t *atom;
243         xcb_get_property_reply_t *preply = xcb_get_property_reply(conn, wm_type_cookie, NULL);
244         if (preply != NULL && preply->value_len > 0 && (atom = xcb_get_property_value(preply))) {
245                 for (int i = 0; i < xcb_get_property_value_length(preply); i++)
246                         if (atom[i] == atoms[_NET_WM_WINDOW_TYPE_DOCK]) {
247                                 LOG("Window is a dock.\n");
248                                 new->dock = true;
249                                 new->borderless = true;
250                                 new->titlebar_position = TITLEBAR_OFF;
251                                 new->force_reconfigure = true;
252                                 new->container = NULL;
253                                 SLIST_INSERT_HEAD(&(c_ws->screen->dock_clients), new, dock_clients);
254                                 /* If it’s a dock we can’t make it float, so we break */
255                                 new->floating = FLOATING_AUTO_OFF;
256                                 break;
257                         } else if (atom[i] == atoms[_NET_WM_WINDOW_TYPE_DIALOG] ||
258                                    atom[i] == atoms[_NET_WM_WINDOW_TYPE_UTILITY] ||
259                                    atom[i] == atoms[_NET_WM_WINDOW_TYPE_TOOLBAR] ||
260                                    atom[i] == atoms[_NET_WM_WINDOW_TYPE_SPLASH]) {
261                                 /* Set the dialog window to automatically floating, will be used below */
262                                 new->floating = FLOATING_AUTO_ON;
263                                 LOG("dialog/utility/toolbar/splash window, automatically floating\n");
264                         }
265         }
266
267         /* All clients which have a leader should be floating */
268         if (!new->dock && !client_is_floating(new) && new->leader != 0) {
269                 LOG("Client has WM_CLIENT_LEADER hint set, setting floating\n");
270                 new->floating = FLOATING_AUTO_ON;
271         }
272
273         if (new->workspace->auto_float) {
274                 new->floating = FLOATING_AUTO_ON;
275                 LOG("workspace is in autofloat mode, setting floating\n");
276         }
277
278         if (new->dock) {
279                 /* Get _NET_WM_STRUT_PARTIAL to determine the client’s requested height */
280                 uint32_t *strut;
281                 preply = xcb_get_property_reply(conn, strut_cookie, NULL);
282                 if (preply != NULL && preply->value_len > 0 && (strut = xcb_get_property_value(preply))) {
283                         /* We only use a subset of the provided values, namely the reserved space at the top/bottom
284                            of the screen. This is because the only possibility for bars is at to be at the top/bottom
285                            with maximum horizontal size.
286                            TODO: bars at the top */
287                         new->desired_height = strut[3];
288                         if (new->desired_height == 0) {
289                                 LOG("Client wanted to be 0 pixels high, using the window's height (%d)\n", original_height);
290                                 new->desired_height = original_height;
291                         }
292                         LOG("the client wants to be %d pixels high\n", new->desired_height);
293                 } else {
294                         LOG("The client didn't specify space to reserve at the screen edge, using its height (%d)\n", original_height);
295                         new->desired_height = original_height;
296                 }
297         } else {
298                 /* If it’s not a dock, we can check on which workspace we should put it. */
299
300                 /* Firstly, we need to get the window’s class / title. We asked for the properties at the
301                  * top of this function, get them now and pass them to our callback function for window class / title
302                  * changes. It is important that the client was already inserted into the by_child table,
303                  * because the callbacks won’t work otherwise. */
304                 preply = xcb_get_property_reply(conn, utf8_title_cookie, NULL);
305                 handle_windowname_change(NULL, conn, 0, new->child, atoms[_NET_WM_NAME], preply);
306
307                 preply = xcb_get_property_reply(conn, title_cookie, NULL);
308                 handle_windowname_change_legacy(NULL, conn, 0, new->child, WM_NAME, preply);
309
310                 preply = xcb_get_property_reply(conn, class_cookie, NULL);
311                 handle_windowclass_change(NULL, conn, 0, new->child, WM_CLASS, preply);
312
313                 preply = xcb_get_property_reply(conn, leader_cookie, NULL);
314                 handle_clientleader_change(NULL, conn, 0, new->child, atoms[WM_CLIENT_LEADER], preply);
315
316                 struct Assignment *assign;
317                 TAILQ_FOREACH(assign, &assignments, assignments) {
318                         if (get_matching_client(conn, assign->windowclass_title, new) == NULL)
319                                 continue;
320
321                         if (assign->floating == ASSIGN_FLOATING_ONLY ||
322                             assign->floating == ASSIGN_FLOATING) {
323                                 new->floating = FLOATING_AUTO_ON;
324                                 LOG("Assignment matches, putting client into floating mode\n");
325                                 if (assign->floating == ASSIGN_FLOATING_ONLY)
326                                         break;
327                         }
328
329                         LOG("Assignment \"%s\" matches, so putting it on workspace %d\n",
330                             assign->windowclass_title, assign->workspace);
331
332                         if (c_ws->screen->current_workspace == (assign->workspace-1)) {
333                                 LOG("We are already there, no need to do anything\n");
334                                 break;
335                         }
336
337                         LOG("Changing container/workspace and unmapping the client\n");
338                         Workspace *t_ws = &(workspaces[assign->workspace-1]);
339                         workspace_initialize(t_ws, c_ws->screen);
340
341                         new->container = t_ws->table[t_ws->current_col][t_ws->current_row];
342                         new->workspace = t_ws;
343                         old_focused = new->container->currently_focused;
344
345                         map_frame = workspace_is_visible(t_ws);
346                         break;
347                 }
348         }
349
350         if (CUR_CELL->workspace->fullscreen_client != NULL) {
351                 if (new->container == CUR_CELL) {
352                         /* If we are in fullscreen, we should lower the window to not be annoying */
353                         uint32_t values[] = { XCB_STACK_MODE_BELOW };
354                         xcb_configure_window(conn, new->frame, XCB_CONFIG_WINDOW_STACK_MODE, values);
355                 }
356         }
357
358         /* Insert into the currently active container, if it’s not a dock window */
359         if (!new->dock && !client_is_floating(new)) {
360                 /* Insert after the old active client, if existing. If it does not exist, the
361                    container is empty and it does not matter, where we insert it */
362                 if (old_focused != NULL && !old_focused->dock)
363                         CIRCLEQ_INSERT_AFTER(&(new->container->clients), old_focused, new, clients);
364                 else CIRCLEQ_INSERT_TAIL(&(new->container->clients), new, clients);
365
366                 if (new->container->workspace->fullscreen_client != NULL)
367                         SLIST_INSERT_AFTER(new->container->workspace->fullscreen_client, new, focus_clients);
368                 else SLIST_INSERT_HEAD(&(new->container->workspace->focus_stack), new, focus_clients);
369
370                 client_set_below_floating(conn, new);
371         }
372
373         if (client_is_floating(new)) {
374                 SLIST_INSERT_HEAD(&(new->workspace->focus_stack), new, focus_clients);
375
376                 /* Add the client to the list of floating clients for its workspace */
377                 TAILQ_INSERT_TAIL(&(new->workspace->floating_clients), new, floating_clients);
378
379                 new->container = NULL;
380
381                 new->rect.width = new->floating_rect.width + 2 + 2;
382                 new->rect.height = new->floating_rect.height + (font->height + 2 + 2) + 2;
383
384                 /* Some clients (like GIMP’s color picker window) get mapped
385                  * to (0, 0), so we push them to a reasonable position
386                  * (centered over their leader) */
387                 if (new->leader != 0 && x == 0 && y == 0) {
388                         LOG("Floating client wants to (0x0), moving it over its leader instead\n");
389                         Client *leader = table_get(&by_child, new->leader);
390                         if (leader == NULL) {
391                                 LOG("leader is NULL, centering it over current workspace\n");
392
393                                 x = c_ws->rect.x + (c_ws->rect.width / 2) - (new->rect.width / 2);
394                                 y = c_ws->rect.y + (c_ws->rect.height / 2) - (new->rect.height / 2);
395                         } else {
396                                 x = leader->rect.x + (leader->rect.width / 2) - (new->rect.width / 2);
397                                 y = leader->rect.y + (leader->rect.height / 2) - (new->rect.height / 2);
398                         }
399                 }
400                 new->floating_rect.x = new->rect.x = x;
401                 new->floating_rect.y = new->rect.y = y;
402                 LOG("copying floating_rect from tiling (%d, %d) size (%d, %d)\n",
403                                 new->floating_rect.x, new->floating_rect.y,
404                                 new->floating_rect.width, new->floating_rect.height);
405                 LOG("outer rect (%d, %d) size (%d, %d)\n",
406                                 new->rect.x, new->rect.y, new->rect.width, new->rect.height);
407
408                 /* Make sure it is on top of the other windows */
409                 xcb_raise_window(conn, new->frame);
410                 reposition_client(conn, new);
411                 resize_client(conn, new);
412                 /* redecorate_window flushes */
413                 redecorate_window(conn, new);
414         }
415
416         new->initialized = true;
417
418         /* Check if the window already got the fullscreen hint set */
419         xcb_atom_t *state;
420         if ((preply = xcb_get_property_reply(conn, state_cookie, NULL)) != NULL &&
421             (state = xcb_get_property_value(preply)) != NULL)
422                 /* Check all set _NET_WM_STATEs */
423                 for (int i = 0; i < xcb_get_property_value_length(preply); i++) {
424                         if (state[i] != atoms[_NET_WM_STATE_FULLSCREEN])
425                                 continue;
426                         /* If the window got the fullscreen state, we just toggle fullscreen
427                            and don’t event bother to redraw the layout – that would not change
428                            anything anyways */
429                         client_toggle_fullscreen(conn, new);
430                         goto map;
431                 }
432
433         render_layout(conn);
434
435 map:
436         /* Map the window first to avoid flickering */
437         xcb_map_window(conn, child);
438         if (map_frame)
439                 client_map(conn, new);
440
441         if ((CUR_CELL->workspace->fullscreen_client == NULL || new->fullscreen) && !new->dock) {
442                 /* Focus the new window if we’re not in fullscreen mode and if it is not a dock window */
443                 if ((new->workspace->fullscreen_client == NULL) || new->fullscreen) {
444                         if (!client_is_floating(new))
445                                 new->container->currently_focused = new;
446                         if (new->container == CUR_CELL || client_is_floating(new))
447                                 xcb_set_input_focus(conn, XCB_INPUT_FOCUS_POINTER_ROOT, new->child, XCB_CURRENT_TIME);
448                 }
449         }
450
451         xcb_flush(conn);
452 }