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