Add "active" and "primary" options to the <monitor> placement option for per-app...
[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     if (c->group) {
163         for (it = c->group->members; it; it = g_slist_next(it)) {
164             ObClient *itc = it->data;
165             if (itc != c) {
166                 guint m = client_monitor(itc);
167
168                 if (m < screen_num_monitors) {
169                     if (screen_compare_desktops(itc->desktop, c->desktop))
170                         choice[m].flags |= HEAD_GROUP_DESK;
171                     else
172                         choice[m].flags |= HEAD_GROUP;
173                 }
174             }
175         }
176     }
177
178     i = screen_monitor_primary(FALSE);
179     if (i < screen_num_monitors) {
180         choice[i].flags |= HEAD_PRIMARY;
181         if (config_place_monitor == OB_PLACE_MONITOR_PRIMARY)
182             choice[i].flags |= HEAD_PLACED;
183     }
184
185     /* direct parent takes highest precedence */
186     if ((p = client_direct_parent(c))) {
187         i = client_monitor(p);
188         if (i < screen_num_monitors)
189             choice[i].flags |= HEAD_PARENT;
190     }
191
192     qsort(choice, screen_num_monitors, sizeof(ObPlaceHead),
193           foreground ? cmp_foreground : cmp_background);
194
195     /* save the areas of the monitors in order of their being chosen */
196     for (i = 0; i < screen_num_monitors; ++i)
197     {
198         ob_debug("placement choice %d is monitor %d", i, choice[i].monitor);
199         if (choice[i].flags & HEAD_PARENT)
200             ob_debug("  - parent on monitor");
201         if (choice[i].flags & HEAD_PLACED)
202             ob_debug("  - placement choice");
203         if (choice[i].flags & HEAD_PRIMARY)
204             ob_debug("  - primary monitor");
205         if (choice[i].flags & HEAD_GROUP_DESK)
206             ob_debug("  - group on same desktop");
207         if (choice[i].flags & HEAD_GROUP)
208             ob_debug("  - group on other desktop");
209     }
210
211     area = screen_area(c->desktop, choice[0].monitor, NULL);
212
213     g_free(choice);
214
215     /* return the area for the chosen monitor */
216     return area;
217 }
218
219 static gboolean place_random(ObClient *client, Rect *area, gint *x, gint *y)
220 {
221     gint l, r, t, b;
222
223     ob_debug("placing randomly");
224
225     l = area->x;
226     t = area->y;
227     r = area->x + area->width - client->frame->area.width;
228     b = area->y + area->height - client->frame->area.height;
229
230     if (r > l) *x = g_random_int_range(l, r + 1);
231     else       *x = area->x;
232     if (b > t) *y = g_random_int_range(t, b + 1);
233     else       *y = area->y;
234
235     return TRUE;
236 }
237
238 static GSList* area_add(GSList *list, Rect *a)
239 {
240     Rect *r = g_slice_new(Rect);
241     *r = *a;
242     return g_slist_prepend(list, r);
243 }
244
245 static GSList* area_remove(GSList *list, Rect *a)
246 {
247     GSList *sit;
248     GSList *result = NULL;
249
250     for (sit = list; sit; sit = g_slist_next(sit)) {
251         Rect *r = sit->data;
252
253         if (!RECT_INTERSECTS_RECT(*r, *a)) {
254             result = g_slist_prepend(result, r);
255             /* dont free r, it's moved to the result list */
256         } else {
257             Rect isect, extra;
258
259             /* Use an intersection of a and r to determine the space
260                around r that we can use.
261
262                NOTE: the spaces calculated can overlap.
263             */
264
265             RECT_SET_INTERSECTION(isect, *r, *a);
266
267             if (RECT_LEFT(isect) > RECT_LEFT(*r)) {
268                 RECT_SET(extra, r->x, r->y,
269                          RECT_LEFT(isect) - r->x, r->height);
270                 result = area_add(result, &extra);
271             }
272
273             if (RECT_TOP(isect) > RECT_TOP(*r)) {
274                 RECT_SET(extra, r->x, r->y,
275                          r->width, RECT_TOP(isect) - r->y + 1);
276                 result = area_add(result, &extra);
277             }
278
279             if (RECT_RIGHT(isect) < RECT_RIGHT(*r)) {
280                 RECT_SET(extra, RECT_RIGHT(isect) + 1, r->y,
281                          RECT_RIGHT(*r) - RECT_RIGHT(isect), r->height);
282                 result = area_add(result, &extra);
283             }
284
285             if (RECT_BOTTOM(isect) < RECT_BOTTOM(*r)) {
286                 RECT_SET(extra, r->x, RECT_BOTTOM(isect) + 1,
287                          r->width, RECT_BOTTOM(*r) - RECT_BOTTOM(isect));
288                 result = area_add(result, &extra);
289             }
290
291             /* 'r' is not being added to the result list, so free it */
292             g_slice_free(Rect, r);
293         }
294     }
295     g_slist_free(list);
296     return result;
297 }
298
299 enum {
300     IGNORE_FULLSCREEN = 1,
301     IGNORE_MAXIMIZED  = 2,
302     IGNORE_MENUTOOL   = 3,
303     /*IGNORE_SHADED     = 3,*/
304     IGNORE_NONGROUP   = 4,
305     IGNORE_BELOW      = 5,
306     /*IGNORE_NONFOCUS   = 1 << 5,*/
307     IGNORE_DOCK       = 6,
308     IGNORE_END        = 7
309 };
310
311 static gboolean place_nooverlap(ObClient *c, Rect *area, gint *x, gint *y)
312 {
313     gint ignore;
314     gboolean ret;
315     gint maxsize;
316     GSList *spaces = NULL, *sit, *maxit;
317
318     ob_debug("placing nonoverlap");
319
320     ret = FALSE;
321     maxsize = 0;
322     maxit = NULL;
323
324     /* try ignoring different things to find empty space */
325     for (ignore = 0; ignore < IGNORE_END && !ret; ignore++) {
326         GList *it;
327
328         /* add the whole monitor */
329         spaces = area_add(spaces, area);
330
331         /* go thru all the windows */
332         for (it = client_list; it; it = g_list_next(it)) {
333             ObClient *test = it->data;
334
335             /* should we ignore this client? */
336             if (screen_showing_desktop) continue;
337             if (c == test) continue;
338             if (test->iconic) continue;
339             if (c->desktop != DESKTOP_ALL) {
340                 if (test->desktop != c->desktop &&
341                     test->desktop != DESKTOP_ALL) continue;
342             } else {
343                 if (test->desktop != screen_desktop &&
344                     test->desktop != DESKTOP_ALL) continue;
345             }
346             if (test->type == OB_CLIENT_TYPE_SPLASH ||
347                 test->type == OB_CLIENT_TYPE_DESKTOP) continue;
348
349
350             if ((ignore >= IGNORE_FULLSCREEN) &&
351                 test->fullscreen) continue;
352             if ((ignore >= IGNORE_MAXIMIZED) &&
353                 test->max_horz && test->max_vert) continue;
354             if ((ignore >= IGNORE_MENUTOOL) &&
355                 (test->type == OB_CLIENT_TYPE_MENU ||
356                  test->type == OB_CLIENT_TYPE_TOOLBAR) &&
357                 client_has_parent(c)) continue;
358             /*
359               if ((ignore >= IGNORE_SHADED) &&
360               test->shaded) continue;
361             */
362             if ((ignore >= IGNORE_NONGROUP) &&
363                 client_has_group_siblings(c) &&
364                 test->group != c->group) continue;
365             if ((ignore >= IGNORE_BELOW) &&
366                 test->layer < c->layer) continue;
367             /*
368               if ((ignore >= IGNORE_NONFOCUS) &&
369               focus_client != test) continue;
370             */
371             /* don't ignore this window, so remove it from the available
372                area */
373             spaces = area_remove(spaces, &test->frame->area);
374         }
375
376         if (ignore < IGNORE_DOCK) {
377             Rect a;
378             dock_get_area(&a);
379             spaces = area_remove(spaces, &a);
380         }
381
382         for (sit = spaces; sit; sit = g_slist_next(sit)) {
383             Rect *r = sit->data;
384
385             if (r->width >= c->frame->area.width &&
386                 r->height >= c->frame->area.height &&
387                 r->width * r->height > maxsize)
388             {
389                 maxsize = r->width * r->height;
390                 maxit = sit;
391             }
392         }
393
394         if (maxit) {
395             Rect *r = maxit->data;
396
397             /* center it in the area */
398             *x = r->x;
399             *y = r->y;
400             if (config_place_center) {
401                 *x += (r->width - c->frame->area.width) / 2;
402                 *y += (r->height - c->frame->area.height) / 2;
403             }
404             ret = TRUE;
405         }
406
407         while (spaces) {
408             g_slice_free(Rect, spaces->data);
409             spaces = g_slist_delete_link(spaces, spaces);
410         }
411     }
412
413     return ret;
414 }
415
416 static gboolean place_under_mouse(ObClient *client, gint *x, gint *y)
417 {
418     gint l, r, t, b;
419     gint px, py;
420     Rect *area;
421
422     ob_debug("placing under mouse");
423
424     if (!screen_pointer_pos(&px, &py))
425         return FALSE;
426     area = pick_pointer_head(client);
427
428     l = area->x;
429     t = area->y;
430     r = area->x + area->width - client->frame->area.width;
431     b = area->y + area->height - client->frame->area.height;
432
433     *x = px - client->area.width / 2 - client->frame->size.left;
434     *x = MIN(MAX(*x, l), r);
435     *y = py - client->area.height / 2 - client->frame->size.top;
436     *y = MIN(MAX(*y, t), b);
437
438     g_slice_free(Rect, area);
439
440     return TRUE;
441 }
442
443 static gboolean place_per_app_setting(ObClient *client, gint *x, gint *y,
444                                       ObAppSettings *settings)
445 {
446     Rect *screen = NULL;
447
448     if (!settings || (settings && !settings->pos_given))
449         return FALSE;
450
451     ob_debug("placing by per-app settings");
452
453     /* Find which head the pointer is on */
454     if (settings->monitor_type == OB_APP_SETTINGS_MONITOR_PRIMARY) {
455         guint m = screen_monitor_primary(TRUE);
456         screen = screen_area(client->desktop, m, NULL);
457     }
458     else if (settings->monitor_type == OB_APP_SETTINGS_MONITOR_ACTIVE) {
459         guint m = screen_monitor_active();
460         screen = screen_area(client->desktop, m, NULL);
461     }
462     else if (settings->monitor_type == OB_APP_SETTINGS_MONITOR_MOUSE) {
463         screen = pick_pointer_head(client);
464         g_assert(screen);
465     }
466     else {
467         guint m = settings->monitor;
468         if (m < 1 || m > screen_num_monitors)
469             m = screen_monitor_primary(TRUE) + 1;
470         screen = screen_area(client->desktop, m - 1, NULL);
471     }
472
473     if (settings->position.x.center)
474         *x = screen->x + screen->width / 2 - client->area.width / 2;
475     else if (settings->position.x.opposite)
476         *x = screen->x + screen->width - client->frame->area.width -
477             settings->position.x.pos;
478     else
479         *x = screen->x + settings->position.x.pos;
480     if (settings->position.x.denom)
481         *x = (*x * screen->width) / settings->position.x.denom;
482
483     if (settings->position.y.center)
484         *y = screen->y + screen->height / 2 - client->area.height / 2;
485     else if (settings->position.y.opposite)
486         *y = screen->y + screen->height - client->frame->area.height -
487             settings->position.y.pos;
488     else
489         *y = screen->y + settings->position.y.pos;
490     if (settings->position.y.denom)
491         *y = (*y * screen->height) / settings->position.y.denom;
492
493     g_slice_free(Rect, screen);
494     return TRUE;
495 }
496
497 static gboolean place_transient_splash(ObClient *client, Rect *area,
498                                        gint *x, gint *y)
499 {
500     if (client->type == OB_CLIENT_TYPE_DIALOG) {
501         GSList *it;
502         gboolean first = TRUE;
503         gint l, r, t, b;
504
505         ob_debug("placing dialog");
506
507         for (it = client->parents; it; it = g_slist_next(it)) {
508             ObClient *m = it->data;
509             if (!m->iconic) {
510                 if (first) {
511                     l = RECT_LEFT(m->frame->area);
512                     t = RECT_TOP(m->frame->area);
513                     r = RECT_RIGHT(m->frame->area);
514                     b = RECT_BOTTOM(m->frame->area);
515                     first = FALSE;
516                 } else {
517                     l = MIN(l, RECT_LEFT(m->frame->area));
518                     t = MIN(t, RECT_TOP(m->frame->area));
519                     r = MAX(r, RECT_RIGHT(m->frame->area));
520                     b = MAX(b, RECT_BOTTOM(m->frame->area));
521                 }
522             }
523             if (!first) {
524                 *x = ((r + 1 - l) - client->frame->area.width) / 2 + l;
525                 *y = ((b + 1 - t) - client->frame->area.height) / 2 + t;
526                 return TRUE;
527             }
528         }
529     }
530
531     if (client->type == OB_CLIENT_TYPE_DIALOG ||
532         client->type == OB_CLIENT_TYPE_SPLASH)
533     {
534         ob_debug("placing dialog or splash");
535
536         *x = (area->width - client->frame->area.width) / 2 + area->x;
537         *y = (area->height - client->frame->area.height) / 2 + area->y;
538         return TRUE;
539     }
540
541     return FALSE;
542 }
543
544 /*! Return TRUE if openbox chose the position for the window, and FALSE if
545   the application chose it */
546 gboolean place_client(ObClient *client, gboolean foreground, gint *x, gint *y,
547                       ObAppSettings *settings)
548 {
549     Rect *area;
550     gboolean ret;
551
552     /* per-app settings override program specified position
553      * but not user specified, unless pos_force is enabled */
554     if (((client->positioned & USPosition) &&
555          !(settings && settings->pos_given && settings->pos_force)) ||
556         ((client->positioned & PPosition) &&
557          !(settings && settings->pos_given)))
558         return FALSE;
559
560     area = pick_head(client, foreground);
561
562     /* try a number of methods */
563     ret = place_per_app_setting(client, x, y, settings) ||
564         place_transient_splash(client, area, x, y) ||
565         (config_place_policy == OB_PLACE_POLICY_MOUSE &&
566          place_under_mouse(client, x, y)) ||
567         place_nooverlap(client, area, x, y) ||
568         place_random(client, area, x, y);
569     g_assert(ret);
570
571     g_slice_free(Rect, area);
572
573     /* get where the client should be */
574     frame_frame_gravity(client->frame, x, y);
575     return TRUE;
576 }