]> icculus.org git repositories - dana/openbox.git/blob - openbox/place.c
Big changes to placement across multiple monitors.
[dana/openbox.git] / openbox / place.c
1 /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-
2
3    place.c for the Openbox window manager
4    Copyright (c) 2006        Mikael Magnusson
5    Copyright (c) 2003-2007   Dana Jansens
6
7    This program is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 2 of the License, or
10    (at your option) any later version.
11
12    This program is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16
17    See the COPYING file for a copy of the GNU General Public License.
18 */
19
20 #include "client.h"
21 #include "group.h"
22 #include "screen.h"
23 #include "frame.h"
24 #include "focus.h"
25 #include "config.h"
26 #include "dock.h"
27 #include "debug.h"
28
29 extern ObDock *dock;
30
31 static Rect *pick_pointer_head(ObClient *c)
32 {
33     return screen_area(c->desktop, screen_monitor_pointer(), NULL);
34 }
35
36 /* use the following priority lists for pick_head()
37
38    When a window is being placed in the FOREGROUND, use a monitor chosen in
39    the following order:
40    1. same monitor as parent
41    2. primary monitor if placement=PRIMARY
42       active monitor if placement=ACTIVE
43       pointer monitor if placement=MOUSE
44    3. primary monitor
45    4. other monitors where the window has group members on the same desktop
46    5. other monitors where the window has group members on other desktops
47    6. other monitors
48
49    When a window is being placed in the BACKGROUND, use a monitor chosen in the
50    following order:
51    1. same monitor as parent
52    2. other monitors where the window has group members on the same desktop
53     2a. primary monitor in this set
54     2b. other monitors in this set
55    3. other monitors where the window has group members on other desktops
56     3a. primary monitor in this set
57     3b. other monitors in this set
58    4. other monitors
59     4a. primary monitor in this set
60     4b. other monitors in this set
61 */
62
63 /*! One for each possible head, used to sort them in order of precedence. */
64 typedef struct {
65     guint monitor;
66     guint flags;
67 } ObPlaceHead;
68
69 /*! Flags for ObPlaceHead */
70 enum {
71     HEAD_PARENT = 1 << 0, /* parent's monitor */
72     HEAD_PLACED = 1 << 1, /* chosen monitor by placement */
73     HEAD_PRIMARY = 1 << 2, /* primary monitor */
74     HEAD_GROUP_DESK = 1 << 3, /* has a group member on the same desktop */
75     HEAD_GROUP = 1 << 4, /* has a group member on another desktop */
76 };
77
78 gint cmp_foreground(const void *a, const void *b)
79 {
80     const ObPlaceHead *h1 = a;
81     const ObPlaceHead *h2 = b;
82     gint i = 0;
83
84     if (h1->monitor == h2->monitor) return 0;
85
86     if (h1->flags & HEAD_PARENT) --i;
87     if (h2->flags & HEAD_PARENT) ++i;
88     if (i) return i;
89
90     if (h1->flags & HEAD_PLACED) --i;
91     if (h2->flags & HEAD_PLACED) ++i;
92     if (i) return i;
93
94     if (h1->flags & HEAD_PRIMARY) --i;
95     if (h2->flags & HEAD_PRIMARY) ++i;
96     if (i) return i;
97
98     if (h1->flags & HEAD_GROUP_DESK) --i;
99     if (h2->flags & HEAD_GROUP_DESK) ++i;
100     if (i) return i;
101
102     if (h1->flags & HEAD_GROUP) --i;
103     if (h2->flags & HEAD_GROUP) ++i;
104     if (i) return i;
105
106     return h1->monitor - h2->monitor;
107 }
108
109 gint cmp_background(const void *a, const void *b)
110 {
111     const ObPlaceHead *h1 = a;
112     const ObPlaceHead *h2 = b;
113     gint i = 0;
114
115     if (h1->monitor == h2->monitor) return 0;
116
117     if (h1->flags & HEAD_PARENT) --i;
118     if (h2->flags & HEAD_PARENT) ++i;
119     if (i) return i;
120
121     if (h1->flags & HEAD_GROUP_DESK || h2->flags & HEAD_GROUP_DESK) {
122         if (h1->flags & HEAD_GROUP_DESK) --i;
123         if (h2->flags & HEAD_GROUP_DESK) ++i;
124         if (i) return i;
125         if (h1->flags & HEAD_PRIMARY) --i;
126         if (h2->flags & HEAD_PRIMARY) ++i;
127         if (i) return i;
128     }
129
130     if (h1->flags & HEAD_GROUP || h2->flags & HEAD_GROUP) {
131         if (h1->flags & HEAD_GROUP) --i;
132         if (h2->flags & HEAD_GROUP) ++i;
133         if (i) return i;
134         if (h1->flags & HEAD_PRIMARY) --i;
135         if (h2->flags & HEAD_PRIMARY) ++i;
136         if (i) return i;
137     }
138
139     if (h1->flags & HEAD_PRIMARY) --i;
140     if (h2->flags & HEAD_PRIMARY) ++i;
141     if (i) return i;
142
143     return h1->monitor - h2->monitor;
144 }
145
146 /*! Pick a monitor to place a window on. */
147 static Rect *pick_head(ObClient *c, gboolean foreground)
148 {
149     Rect *area;
150     ObPlaceHead *choice;
151     guint i;
152     ObClient *p;
153     GSList *it;
154
155     choice = g_new(ObPlaceHead, screen_num_monitors);
156     for (i = 0; i < screen_num_monitors; ++i) {
157         choice[i].monitor = i;
158         choice[i].flags = 0;
159     }
160
161     /* find monitors with group members */
162     for (it = c->group->members; it; it = g_slist_next(it)) {
163         ObClient *itc = it->data;
164         if (itc != c) {
165             guint m = client_monitor(itc);
166
167             if (m < screen_num_monitors) {
168                 if (screen_compare_desktops(itc->desktop, c->desktop))
169                     choice[m].flags |= HEAD_GROUP_DESK;
170                 else
171                     choice[m].flags |= HEAD_GROUP;
172             }
173         }
174     }
175
176     i = screen_monitor_primary(FALSE);
177     if (i < screen_num_monitors) {
178         choice[i].flags |= HEAD_PRIMARY;
179         if (config_place_monitor == OB_PLACE_MONITOR_PRIMARY)
180             choice[i].flags |= HEAD_PLACED;
181     }
182
183     /* direct parent takes highest precedence */
184     if ((p = client_direct_parent(c))) {
185         i = client_monitor(p);
186         if (i < screen_num_monitors)
187             choice[i].flags |= HEAD_PARENT;
188     }
189
190     qsort(choice, screen_num_monitors, sizeof(ObPlaceHead),
191           foreground ? cmp_foreground : cmp_background);
192
193     /* save the areas of the monitors in order of their being chosen */
194     for (i = 0; i < screen_num_monitors; ++i)
195     {
196         ob_debug("placement choice %d is monitor %d", i, choice[i].monitor);
197         if (choice[i].flags & HEAD_PARENT)
198             ob_debug("  - parent on monitor");
199         if (choice[i].flags & HEAD_PLACED)
200             ob_debug("  - placement choice");
201         if (choice[i].flags & HEAD_PRIMARY)
202             ob_debug("  - primary monitor");
203         if (choice[i].flags & HEAD_GROUP_DESK)
204             ob_debug("  - group on same desktop");
205         if (choice[i].flags & HEAD_GROUP)
206             ob_debug("  - group on other desktop");
207     }
208
209     area = screen_area(c->desktop, choice[0].monitor, NULL);
210
211     g_free(choice);
212
213     /* return the area for the chosen monitor */
214     return area;
215 }
216
217 static gboolean place_random(ObClient *client, Rect *area, gint *x, gint *y)
218 {
219     gint l, r, t, b;
220
221     ob_debug("placing randomly");
222
223     l = area->x;
224     t = area->y;
225     r = area->x + area->width - client->frame->area.width;
226     b = area->y + area->height - client->frame->area.height;
227
228     if (r > l) *x = g_random_int_range(l, r + 1);
229     else       *x = area->x;
230     if (b > t) *y = g_random_int_range(t, b + 1);
231     else       *y = area->y;
232
233     return TRUE;
234 }
235
236 static GSList* area_add(GSList *list, Rect *a)
237 {
238     Rect *r = g_slice_new(Rect);
239     *r = *a;
240     return g_slist_prepend(list, r);
241 }
242
243 static GSList* area_remove(GSList *list, Rect *a)
244 {
245     GSList *sit;
246     GSList *result = NULL;
247
248     for (sit = list; sit; sit = g_slist_next(sit)) {
249         Rect *r = sit->data;
250
251         if (!RECT_INTERSECTS_RECT(*r, *a)) {
252             result = g_slist_prepend(result, r);
253             /* dont free r, it's moved to the result list */
254         } else {
255             Rect isect, extra;
256
257             /* Use an intersection of a and r to determine the space
258                around r that we can use.
259
260                NOTE: the spaces calculated can overlap.
261             */
262
263             RECT_SET_INTERSECTION(isect, *r, *a);
264
265             if (RECT_LEFT(isect) > RECT_LEFT(*r)) {
266                 RECT_SET(extra, r->x, r->y,
267                          RECT_LEFT(isect) - r->x, r->height);
268                 result = area_add(result, &extra);
269             }
270
271             if (RECT_TOP(isect) > RECT_TOP(*r)) {
272                 RECT_SET(extra, r->x, r->y,
273                          r->width, RECT_TOP(isect) - r->y + 1);
274                 result = area_add(result, &extra);
275             }
276
277             if (RECT_RIGHT(isect) < RECT_RIGHT(*r)) {
278                 RECT_SET(extra, RECT_RIGHT(isect) + 1, r->y,
279                          RECT_RIGHT(*r) - RECT_RIGHT(isect), r->height);
280                 result = area_add(result, &extra);
281             }
282
283             if (RECT_BOTTOM(isect) < RECT_BOTTOM(*r)) {
284                 RECT_SET(extra, r->x, RECT_BOTTOM(isect) + 1,
285                          r->width, RECT_BOTTOM(*r) - RECT_BOTTOM(isect));
286                 result = area_add(result, &extra);
287             }
288
289             /* 'r' is not being added to the result list, so free it */
290             g_slice_free(Rect, r);
291         }
292     }
293     g_slist_free(list);
294     return result;
295 }
296
297 enum {
298     IGNORE_FULLSCREEN = 1,
299     IGNORE_MAXIMIZED  = 2,
300     IGNORE_MENUTOOL   = 3,
301     /*IGNORE_SHADED     = 3,*/
302     IGNORE_NONGROUP   = 4,
303     IGNORE_BELOW      = 5,
304     /*IGNORE_NONFOCUS   = 1 << 5,*/
305     IGNORE_DOCK       = 6,
306     IGNORE_END        = 7
307 };
308
309 static gboolean place_nooverlap(ObClient *c, Rect *area, gint *x, gint *y)
310 {
311     gint ignore;
312     gboolean ret;
313     gint maxsize;
314     GSList *spaces = NULL, *sit, *maxit;
315
316     ob_debug("placing nonoverlap");
317
318     ret = FALSE;
319     maxsize = 0;
320     maxit = NULL;
321
322     /* try ignoring different things to find empty space */
323     for (ignore = 0; ignore < IGNORE_END && !ret; ignore++) {
324         GList *it;
325
326         /* add the whole monitor */
327         spaces = area_add(spaces, area);
328
329         /* go thru all the windows */
330         for (it = client_list; it; it = g_list_next(it)) {
331             ObClient *test = it->data;
332
333             /* should we ignore this client? */
334             if (screen_showing_desktop) continue;
335             if (c == test) continue;
336             if (test->iconic) continue;
337             if (c->desktop != DESKTOP_ALL) {
338                 if (test->desktop != c->desktop &&
339                     test->desktop != DESKTOP_ALL) continue;
340             } else {
341                 if (test->desktop != screen_desktop &&
342                     test->desktop != DESKTOP_ALL) continue;
343             }
344             if (test->type == OB_CLIENT_TYPE_SPLASH ||
345                 test->type == OB_CLIENT_TYPE_DESKTOP) continue;
346
347
348             if ((ignore >= IGNORE_FULLSCREEN) &&
349                 test->fullscreen) continue;
350             if ((ignore >= IGNORE_MAXIMIZED) &&
351                 test->max_horz && test->max_vert) continue;
352             if ((ignore >= IGNORE_MENUTOOL) &&
353                 (test->type == OB_CLIENT_TYPE_MENU ||
354                  test->type == OB_CLIENT_TYPE_TOOLBAR) &&
355                 client_has_parent(c)) continue;
356             /*
357               if ((ignore >= IGNORE_SHADED) &&
358               test->shaded) continue;
359             */
360             if ((ignore >= IGNORE_NONGROUP) &&
361                 client_has_group_siblings(c) &&
362                 test->group != c->group) continue;
363             if ((ignore >= IGNORE_BELOW) &&
364                 test->layer < c->layer) continue;
365             /*
366               if ((ignore >= IGNORE_NONFOCUS) &&
367               focus_client != test) continue;
368             */
369             /* don't ignore this window, so remove it from the available
370                area */
371             spaces = area_remove(spaces, &test->frame->area);
372         }
373
374         if (ignore < IGNORE_DOCK) {
375             Rect a;
376             dock_get_area(&a);
377             spaces = area_remove(spaces, &a);
378         }
379
380         for (sit = spaces; sit; sit = g_slist_next(sit)) {
381             Rect *r = sit->data;
382
383             if (r->width >= c->frame->area.width &&
384                 r->height >= c->frame->area.height &&
385                 r->width * r->height > maxsize)
386             {
387                 maxsize = r->width * r->height;
388                 maxit = sit;
389             }
390         }
391
392         if (maxit) {
393             Rect *r = maxit->data;
394
395             /* center it in the area */
396             *x = r->x;
397             *y = r->y;
398             if (config_place_center) {
399                 *x += (r->width - c->frame->area.width) / 2;
400                 *y += (r->height - c->frame->area.height) / 2;
401             }
402             ret = TRUE;
403         }
404
405         while (spaces) {
406             g_slice_free(Rect, spaces->data);
407             spaces = g_slist_delete_link(spaces, spaces);
408         }
409     }
410
411     return ret;
412 }
413
414 static gboolean place_under_mouse(ObClient *client, gint *x, gint *y)
415 {
416     gint l, r, t, b;
417     gint px, py;
418     Rect *area;
419
420     ob_debug("placing under mouse");
421
422     if (!screen_pointer_pos(&px, &py))
423         return FALSE;
424     area = pick_pointer_head(client);
425
426     l = area->x;
427     t = area->y;
428     r = area->x + area->width - client->frame->area.width;
429     b = area->y + area->height - client->frame->area.height;
430
431     *x = px - client->area.width / 2 - client->frame->size.left;
432     *x = MIN(MAX(*x, l), r);
433     *y = py - client->area.height / 2 - client->frame->size.top;
434     *y = MIN(MAX(*y, t), b);
435
436     g_slice_free(Rect, area);
437
438     return TRUE;
439 }
440
441 static gboolean place_per_app_setting(ObClient *client, gint *x, gint *y,
442                                       ObAppSettings *settings)
443 {
444     Rect *screen = NULL;
445
446     if (!settings || (settings && !settings->pos_given))
447         return FALSE;
448
449     ob_debug("placing by per-app settings");
450
451     /* Find which head the pointer is on */
452     if (settings->monitor == 0)
453         /* this can return NULL */
454         screen = pick_pointer_head(client);
455     else {
456         guint m = settings->monitor;
457         if (m < 1 || m > screen_num_monitors)
458             m = screen_monitor_primary(TRUE) + 1;
459         screen = screen_area(client->desktop, m - 1, NULL);
460     }
461
462     if (settings->position.x.center)
463         *x = screen->x + screen->width / 2 - client->area.width / 2;
464     else if (settings->position.x.opposite)
465         *x = screen->x + screen->width - client->frame->area.width -
466             settings->position.x.pos;
467     else
468         *x = screen->x + settings->position.x.pos;
469     if (settings->position.x.denom)
470         *x = (*x * screen->width) / settings->position.x.denom;
471
472     if (settings->position.y.center)
473         *y = screen->y + screen->height / 2 - client->area.height / 2;
474     else if (settings->position.y.opposite)
475         *y = screen->y + screen->height - client->frame->area.height -
476             settings->position.y.pos;
477     else
478         *y = screen->y + settings->position.y.pos;
479     if (settings->position.y.denom)
480         *y = (*y * screen->height) / settings->position.y.denom;
481
482     g_slice_free(Rect, screen);
483     return TRUE;
484 }
485
486 static gboolean place_transient_splash(ObClient *client, Rect *area,
487                                        gint *x, gint *y)
488 {
489     if (client->type == OB_CLIENT_TYPE_DIALOG) {
490         GSList *it;
491         gboolean first = TRUE;
492         gint l, r, t, b;
493
494         ob_debug("placing dialog");
495
496         for (it = client->parents; it; it = g_slist_next(it)) {
497             ObClient *m = it->data;
498             if (!m->iconic) {
499                 if (first) {
500                     l = RECT_LEFT(m->frame->area);
501                     t = RECT_TOP(m->frame->area);
502                     r = RECT_RIGHT(m->frame->area);
503                     b = RECT_BOTTOM(m->frame->area);
504                     first = FALSE;
505                 } else {
506                     l = MIN(l, RECT_LEFT(m->frame->area));
507                     t = MIN(t, RECT_TOP(m->frame->area));
508                     r = MAX(r, RECT_RIGHT(m->frame->area));
509                     b = MAX(b, RECT_BOTTOM(m->frame->area));
510                 }
511             }
512             if (!first) {
513                 *x = ((r + 1 - l) - client->frame->area.width) / 2 + l;
514                 *y = ((b + 1 - t) - client->frame->area.height) / 2 + t;
515                 return TRUE;
516             }
517         }
518     }
519
520     if (client->type == OB_CLIENT_TYPE_DIALOG ||
521         client->type == OB_CLIENT_TYPE_SPLASH)
522     {
523         ob_debug("placing dialog or splash");
524
525         *x = (area->width - client->frame->area.width) / 2 + area->x;
526         *y = (area->height - client->frame->area.height) / 2 + area->y;
527         return TRUE;
528     }
529
530     return FALSE;
531 }
532
533 /*! Return TRUE if openbox chose the position for the window, and FALSE if
534   the application chose it */
535 gboolean place_client(ObClient *client, gboolean foreground, gint *x, gint *y,
536                       ObAppSettings *settings)
537 {
538     Rect *area;
539     gboolean ret;
540
541     /* per-app settings override program specified position
542      * but not user specified, unless pos_force is enabled */
543     if (((client->positioned & USPosition) &&
544          !(settings && settings->pos_given && settings->pos_force)) ||
545         ((client->positioned & PPosition) &&
546          !(settings && settings->pos_given)))
547         return FALSE;
548
549     area = pick_head(client, foreground);
550
551     /* try a number of methods */
552     ret = place_per_app_setting(client, x, y, settings) ||
553         place_transient_splash(client, area, x, y) ||
554         (config_place_policy == OB_PLACE_POLICY_MOUSE &&
555          place_under_mouse(client, x, y)) ||
556         place_nooverlap(client, area, x, y) ||
557         place_random(client, area, x, y);
558     g_assert(ret);
559
560     g_slice_free(Rect, area);
561
562     /* get where the client should be */
563     frame_frame_gravity(client->frame, x, y);
564     return TRUE;
565 }