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