Let menus place themselves on monitors where the mouse is not present
[dana/openbox.git] / openbox / menuframe.c
1 /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-
2
3    menuframe.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 "menuframe.h"
21 #include "client.h"
22 #include "menu.h"
23 #include "screen.h"
24 #include "prop.h"
25 #include "actions.h"
26 #include "grab.h"
27 #include "openbox.h"
28 #include "mainloop.h"
29 #include "config.h"
30 #include "render/theme.h"
31
32 #define PADDING 2
33 #define MAX_MENU_WIDTH 400
34
35 #define ITEM_HEIGHT (ob_rr_theme->menu_font_height + 2*PADDING)
36
37 #define FRAME_EVENTMASK (ButtonPressMask |ButtonMotionMask | EnterWindowMask |\
38                          LeaveWindowMask)
39 #define ENTRY_EVENTMASK (EnterWindowMask | LeaveWindowMask | \
40                          ButtonPressMask | ButtonReleaseMask)
41
42 GList *menu_frame_visible;
43 GHashTable *menu_frame_map;
44
45 static RrAppearance *a_sep;
46
47 static ObMenuEntryFrame* menu_entry_frame_new(ObMenuEntry *entry,
48                                               ObMenuFrame *frame);
49 static void menu_entry_frame_free(ObMenuEntryFrame *self);
50 static void menu_frame_update(ObMenuFrame *self);
51 static gboolean menu_entry_frame_submenu_timeout(gpointer data);
52 static void menu_frame_hide(ObMenuFrame *self);
53
54 static Window createWindow(Window parent, gulong mask,
55                            XSetWindowAttributes *attrib)
56 {
57     return XCreateWindow(ob_display, parent, 0, 0, 1, 1, 0,
58                          RrDepth(ob_rr_inst), InputOutput,
59                          RrVisual(ob_rr_inst), mask, attrib);
60 }
61
62 void menu_frame_startup(gboolean reconfig)
63 {
64     gint i;
65
66     a_sep = RrAppearanceCopy(ob_rr_theme->a_clear);
67     RrAppearanceAddTextures(a_sep, ob_rr_theme->menu_sep_width);
68     for (i = 0; i < ob_rr_theme->menu_sep_width; ++i) {
69         a_sep->texture[i].type = RR_TEXTURE_LINE_ART;
70         a_sep->texture[i].data.lineart.color =
71             ob_rr_theme->menu_sep_color;
72     }
73
74     if (reconfig) return;
75
76     menu_frame_map = g_hash_table_new(g_int_hash, g_int_equal);
77 }
78
79 void menu_frame_shutdown(gboolean reconfig)
80 {
81     RrAppearanceFree(a_sep);
82
83     if (reconfig) return;
84
85     g_hash_table_destroy(menu_frame_map);
86 }
87
88 ObMenuFrame* menu_frame_new(ObMenu *menu, guint show_from, ObClient *client)
89 {
90     ObMenuFrame *self;
91     XSetWindowAttributes attr;
92
93     self = g_new0(ObMenuFrame, 1);
94     self->type = Window_Menu;
95     self->menu = menu;
96     self->selected = NULL;
97     self->client = client;
98     self->direction_right = TRUE;
99     self->show_from = show_from;
100
101     attr.event_mask = FRAME_EVENTMASK;
102     self->window = createWindow(RootWindow(ob_display, ob_screen),
103                                 CWEventMask, &attr);
104
105     /* make it a popup menu type window */
106     PROP_SET32(self->window, net_wm_window_type, atom,
107                prop_atoms.net_wm_window_type_popup_menu);
108
109     XSetWindowBorderWidth(ob_display, self->window, ob_rr_theme->mbwidth);
110     XSetWindowBorder(ob_display, self->window,
111                      RrColorPixel(ob_rr_theme->menu_border_color));
112
113     self->a_items = RrAppearanceCopy(ob_rr_theme->a_menu);
114
115     stacking_add(MENU_AS_WINDOW(self));
116
117     return self;
118 }
119
120 void menu_frame_free(ObMenuFrame *self)
121 {
122     if (self) {
123         while (self->entries) {
124             menu_entry_frame_free(self->entries->data);
125             self->entries = g_list_delete_link(self->entries, self->entries);
126         }
127
128         stacking_remove(MENU_AS_WINDOW(self));
129
130         RrAppearanceFree(self->a_items);
131
132         XDestroyWindow(ob_display, self->window);
133
134         g_free(self);
135     }
136 }
137
138 static ObMenuEntryFrame* menu_entry_frame_new(ObMenuEntry *entry,
139                                               ObMenuFrame *frame)
140 {
141     ObMenuEntryFrame *self;
142     XSetWindowAttributes attr;
143
144     self = g_new0(ObMenuEntryFrame, 1);
145     self->entry = entry;
146     self->frame = frame;
147
148     menu_entry_ref(entry);
149
150     attr.event_mask = ENTRY_EVENTMASK;
151     self->window = createWindow(self->frame->window, CWEventMask, &attr);
152     self->text = createWindow(self->window, 0, NULL);
153     g_hash_table_insert(menu_frame_map, &self->window, self);
154     g_hash_table_insert(menu_frame_map, &self->text, self);
155     if (entry->type == OB_MENU_ENTRY_TYPE_NORMAL) {
156         self->icon = createWindow(self->window, 0, NULL);
157         g_hash_table_insert(menu_frame_map, &self->icon, self);
158     }
159     if (entry->type == OB_MENU_ENTRY_TYPE_SUBMENU) {
160         self->bullet = createWindow(self->window, 0, NULL);
161         g_hash_table_insert(menu_frame_map, &self->bullet, self);
162     }
163
164     XMapWindow(ob_display, self->window);
165     XMapWindow(ob_display, self->text);
166
167     return self;
168 }
169
170 static void menu_entry_frame_free(ObMenuEntryFrame *self)
171 {
172     if (self) {
173         menu_entry_unref(self->entry);
174
175         XDestroyWindow(ob_display, self->text);
176         XDestroyWindow(ob_display, self->window);
177         g_hash_table_remove(menu_frame_map, &self->text);
178         g_hash_table_remove(menu_frame_map, &self->window);
179         if (self->entry->type == OB_MENU_ENTRY_TYPE_NORMAL) {
180             XDestroyWindow(ob_display, self->icon);
181             g_hash_table_remove(menu_frame_map, &self->icon);
182         }
183         if (self->entry->type == OB_MENU_ENTRY_TYPE_SUBMENU) {
184             XDestroyWindow(ob_display, self->bullet);
185             g_hash_table_remove(menu_frame_map, &self->bullet);
186         }
187
188         g_free(self);
189     }
190 }
191
192 void menu_frame_move(ObMenuFrame *self, gint x, gint y)
193 {
194     RECT_SET_POINT(self->area, x, y);
195     self->monitor = screen_find_monitor_point(x, y);
196     XMoveWindow(ob_display, self->window, self->area.x, self->area.y);
197 }
198
199 static void menu_frame_place_topmenu(ObMenuFrame *self, gint *x, gint *y)
200 {
201     gint dx, dy;
202
203     if (config_menu_middle) {
204         gint myx;
205
206         myx = *x;
207         *y -= self->area.height / 2;
208
209         /* try to the right of the cursor */
210         menu_frame_move_on_screen(self, myx, *y, &dx, &dy);
211         self->direction_right = TRUE;
212         if (dx != 0) {
213             /* try to the left of the cursor */
214             myx = *x - self->area.width;
215             menu_frame_move_on_screen(self, myx, *y, &dx, &dy);
216             self->direction_right = FALSE;
217         }
218         if (dx != 0) {
219             /* if didnt fit on either side so just use what it says */
220             myx = *x;
221             menu_frame_move_on_screen(self, myx, *y, &dx, &dy);
222             self->direction_right = TRUE;
223         }
224         *x = myx + dx;
225         *y += dy;
226     } else {
227         gint myx, myy;
228
229         myx = *x;
230         myy = *y;
231
232         /* try to the bottom right of the cursor */
233         menu_frame_move_on_screen(self, myx, myy, &dx, &dy);
234         self->direction_right = TRUE;
235         if (dx != 0 || dy != 0) {
236             /* try to the bottom left of the cursor */
237             myx = *x - self->area.width;
238             myy = *y;
239             menu_frame_move_on_screen(self, myx, myy, &dx, &dy);
240             self->direction_right = FALSE;
241         }
242         if (dx != 0 || dy != 0) {
243             /* try to the top right of the cursor */
244             myx = *x;
245             myy = *y - self->area.height;
246             menu_frame_move_on_screen(self, myx, myy, &dx, &dy);
247             self->direction_right = TRUE;
248         }
249         if (dx != 0 || dy != 0) {
250             /* try to the top left of the cursor */
251             myx = *x - self->area.width;
252             myy = *y - self->area.height;
253             menu_frame_move_on_screen(self, myx, myy, &dx, &dy);
254             self->direction_right = FALSE;
255         }
256         if (dx != 0 || dy != 0) {
257             /* if didnt fit on either side so just use what it says */
258             myx = *x;
259             myy = *y;
260             menu_frame_move_on_screen(self, myx, myy, &dx, &dy);
261             self->direction_right = TRUE;
262         }
263         *x = myx + dx;
264         *y = myy + dy;
265     }
266 }
267
268 static void menu_frame_place_submenu(ObMenuFrame *self, gint *x, gint *y)
269 {
270     gint overlapx, overlapy;
271     gint bwidth;
272
273     overlapx = ob_rr_theme->menu_overlap_x;
274     overlapy = ob_rr_theme->menu_overlap_y;
275     bwidth = ob_rr_theme->mbwidth;
276
277     if (self->direction_right)
278         *x = self->parent->area.x + self->parent->area.width -
279             overlapx - bwidth;
280     else
281         *x = self->parent->area.x - self->area.width + overlapx + bwidth;
282
283     *y = self->parent->area.y + self->parent_entry->area.y;
284     if (config_menu_middle)
285         *y -= (self->area.height - (bwidth * 2) - ITEM_HEIGHT) / 2;
286     else
287         *y += overlapy;
288 }
289
290 void menu_frame_move_on_screen(ObMenuFrame *self, gint x, gint y,
291                                gint *dx, gint *dy)
292 {
293     Rect *a = NULL;
294     gint pos, half;
295
296     *dx = *dy = 0;
297
298     a = screen_physical_area_monitor(screen_find_monitor_point(x, y));
299
300     half = g_list_length(self->entries) / 2;
301     pos = g_list_index(self->entries, self->selected);
302
303     /* if in the bottom half then check this stuff first, will keep the bottom
304        edge of the menu visible */
305     if (pos > half) {
306         *dx = MAX(*dx, a->x - x);
307         *dy = MAX(*dy, a->y - y);
308     }
309     *dx = MIN(*dx, (a->x + a->width) - (x + self->area.width));
310     *dy = MIN(*dy, (a->y + a->height) - (y + self->area.height));
311     /* if in the top half then check this stuff last, will keep the top
312        edge of the menu visible */
313     if (pos <= half) {
314         *dx = MAX(*dx, a->x - x);
315         *dy = MAX(*dy, a->y - y);
316     }
317
318     g_free(a);
319 }
320
321 static void menu_entry_frame_render(ObMenuEntryFrame *self)
322 {
323     RrAppearance *item_a, *text_a;
324     gint th; /* temp */
325     ObMenu *sub;
326     ObMenuFrame *frame = self->frame;
327
328     switch (self->entry->type) {
329     case OB_MENU_ENTRY_TYPE_NORMAL:
330     case OB_MENU_ENTRY_TYPE_SUBMENU:
331         item_a = (self->entry->type == OB_MENU_ENTRY_TYPE_NORMAL &&
332                   !self->entry->data.normal.enabled ?
333                   /* disabled */
334                   (self == self->frame->selected ?
335                    ob_rr_theme->a_menu_disabled_selected :
336                    ob_rr_theme->a_menu_disabled) :
337                   /* enabled */
338                   (self == self->frame->selected ?
339                    ob_rr_theme->a_menu_selected :
340                    ob_rr_theme->a_menu_normal));
341         th = ITEM_HEIGHT;
342         break;
343     case OB_MENU_ENTRY_TYPE_SEPARATOR:
344         if (self->entry->data.separator.label) {
345             item_a = ob_rr_theme->a_menu_title;
346             th = ob_rr_theme->menu_title_height;
347         } else {
348             item_a = ob_rr_theme->a_menu_normal;
349             th = ob_rr_theme->menu_sep_width +
350                 2*ob_rr_theme->menu_sep_paddingy;
351         }
352         break;
353     default:
354         g_assert_not_reached();
355     }
356
357     RECT_SET_SIZE(self->area, self->frame->inner_w, th);
358     XResizeWindow(ob_display, self->window,
359                   self->area.width, self->area.height);
360     item_a->surface.parent = self->frame->a_items;
361     item_a->surface.parentx = self->area.x;
362     item_a->surface.parenty = self->area.y;
363     RrPaint(item_a, self->window, self->area.width, self->area.height);
364
365     switch (self->entry->type) {
366     case OB_MENU_ENTRY_TYPE_NORMAL:
367         text_a = (self->entry->type == OB_MENU_ENTRY_TYPE_NORMAL &&
368                   !self->entry->data.normal.enabled ?
369                   /* disabled */
370                   (self == self->frame->selected ?
371                    ob_rr_theme->a_menu_text_disabled_selected :
372                    ob_rr_theme->a_menu_text_disabled) :
373                   /* enabled */
374                   (self == self->frame->selected ?
375                    ob_rr_theme->a_menu_text_selected :
376                    ob_rr_theme->a_menu_text_normal));
377         text_a->texture[0].data.text.string = self->entry->data.normal.label;
378         if (self->entry->data.normal.shortcut &&
379             (self->frame->menu->show_all_shortcuts ||
380              self->entry->data.normal.shortcut_always_show ||
381              self->entry->data.normal.shortcut_position > 0))
382         {
383             text_a->texture[0].data.text.shortcut = TRUE;
384             text_a->texture[0].data.text.shortcut_pos =
385                 self->entry->data.normal.shortcut_position;
386         } else
387             text_a->texture[0].data.text.shortcut = FALSE;
388         break;
389     case OB_MENU_ENTRY_TYPE_SUBMENU:
390         text_a = (self == self->frame->selected ?
391                   ob_rr_theme->a_menu_text_selected :
392                   ob_rr_theme->a_menu_text_normal);
393         sub = self->entry->data.submenu.submenu;
394         text_a->texture[0].data.text.string = sub ? sub->title : "";
395         if (sub->shortcut && (self->frame->menu->show_all_shortcuts ||
396                               sub->shortcut_always_show ||
397                               sub->shortcut_position > 0))
398         {
399             text_a->texture[0].data.text.shortcut = TRUE;
400             text_a->texture[0].data.text.shortcut_pos = sub->shortcut_position;
401         } else
402             text_a->texture[0].data.text.shortcut = FALSE;
403         break;
404     case OB_MENU_ENTRY_TYPE_SEPARATOR:
405         if (self->entry->data.separator.label != NULL) {
406             text_a = ob_rr_theme->a_menu_text_title;
407             text_a->texture[0].data.text.string =
408                 self->entry->data.separator.label;
409         }
410         else
411             text_a = ob_rr_theme->a_menu_text_normal;
412         break;
413     }
414
415     switch (self->entry->type) {
416     case OB_MENU_ENTRY_TYPE_NORMAL:
417         XMoveResizeWindow(ob_display, self->text,
418                           self->frame->text_x, PADDING,
419                           self->frame->text_w,
420                           ITEM_HEIGHT - 2*PADDING);
421         text_a->surface.parent = item_a;
422         text_a->surface.parentx = self->frame->text_x;
423         text_a->surface.parenty = PADDING;
424         RrPaint(text_a, self->text, self->frame->text_w,
425                 ITEM_HEIGHT - 2*PADDING);
426         break;
427     case OB_MENU_ENTRY_TYPE_SUBMENU:
428         XMoveResizeWindow(ob_display, self->text,
429                           self->frame->text_x, PADDING,
430                           self->frame->text_w - ITEM_HEIGHT,
431                           ITEM_HEIGHT - 2*PADDING);
432         text_a->surface.parent = item_a;
433         text_a->surface.parentx = self->frame->text_x;
434         text_a->surface.parenty = PADDING;
435         RrPaint(text_a, self->text, self->frame->text_w - ITEM_HEIGHT,
436                 ITEM_HEIGHT - 2*PADDING);
437         break;
438     case OB_MENU_ENTRY_TYPE_SEPARATOR:
439         if (self->entry->data.separator.label != NULL) {
440             /* labeled separator */
441             XMoveResizeWindow(ob_display, self->text,
442                               ob_rr_theme->paddingx, ob_rr_theme->paddingy,
443                               self->area.width - 2*ob_rr_theme->paddingx,
444                               ob_rr_theme->menu_title_height -
445                               2*ob_rr_theme->paddingy);
446             text_a->surface.parent = item_a;
447             text_a->surface.parentx = ob_rr_theme->paddingx;
448             text_a->surface.parenty = ob_rr_theme->paddingy;
449             RrPaint(text_a, self->text,
450                     self->area.width - 2*ob_rr_theme->paddingx,
451                     ob_rr_theme->menu_title_height -
452                     2*ob_rr_theme->paddingy);
453         } else {
454             gint i;
455
456             /* unlabeled separator */
457             XMoveResizeWindow(ob_display, self->text, 0, 0,
458                               self->area.width,
459                               ob_rr_theme->menu_sep_width +
460                               2*ob_rr_theme->menu_sep_paddingy);
461
462             a_sep->surface.parent = item_a;
463             a_sep->surface.parentx = 0;
464             a_sep->surface.parenty = 0;
465             for (i = 0; i < ob_rr_theme->menu_sep_width; ++i) {
466                 a_sep->texture[i].data.lineart.x1 =
467                     ob_rr_theme->menu_sep_paddingx;
468                 a_sep->texture[i].data.lineart.y1 =
469                     ob_rr_theme->menu_sep_paddingy + i;
470                 a_sep->texture[i].data.lineart.x2 =
471                     self->area.width - ob_rr_theme->menu_sep_paddingx - 1;
472                 a_sep->texture[i].data.lineart.y2 =
473                     ob_rr_theme->menu_sep_paddingy + i;
474             }
475
476             RrPaint(a_sep, self->text, self->area.width,
477                     ob_rr_theme->menu_sep_width +
478                     2*ob_rr_theme->menu_sep_paddingy);
479         }
480         break;
481     }
482
483     if (self->entry->type == OB_MENU_ENTRY_TYPE_NORMAL &&
484         self->entry->data.normal.icon)
485     {
486         RrAppearance *clear;
487
488         XMoveResizeWindow(ob_display, self->icon,
489                           PADDING, frame->item_margin.top,
490                           ITEM_HEIGHT - frame->item_margin.top
491                           - frame->item_margin.bottom,
492                           ITEM_HEIGHT - frame->item_margin.top
493                           - frame->item_margin.bottom);
494
495         clear = ob_rr_theme->a_clear_tex;
496         RrAppearanceClearTextures(clear);
497         clear->texture[0].type = RR_TEXTURE_IMAGE;
498         clear->texture[0].data.image.image =
499             self->entry->data.normal.icon;
500         clear->texture[0].data.image.alpha =
501             self->entry->data.normal.icon_alpha;
502         clear->surface.parent = item_a;
503         clear->surface.parentx = PADDING;
504         clear->surface.parenty = frame->item_margin.top;
505         RrPaint(clear, self->icon,
506                 ITEM_HEIGHT - frame->item_margin.top
507                 - frame->item_margin.bottom,
508                 ITEM_HEIGHT - frame->item_margin.top
509                 - frame->item_margin.bottom);
510         XMapWindow(ob_display, self->icon);
511     } else if (self->entry->type == OB_MENU_ENTRY_TYPE_NORMAL &&
512                self->entry->data.normal.mask)
513     {
514         RrColor *c;
515         RrAppearance *clear;
516
517         XMoveResizeWindow(ob_display, self->icon,
518                           PADDING, frame->item_margin.top,
519                           ITEM_HEIGHT - frame->item_margin.top
520                           - frame->item_margin.bottom,
521                           ITEM_HEIGHT - frame->item_margin.top
522                           - frame->item_margin.bottom);
523
524         clear = ob_rr_theme->a_clear_tex;
525         RrAppearanceClearTextures(clear);
526         clear->texture[0].type = RR_TEXTURE_MASK;
527         clear->texture[0].data.mask.mask =
528             self->entry->data.normal.mask;
529
530         c = (self->entry->type == OB_MENU_ENTRY_TYPE_NORMAL &&
531              !self->entry->data.normal.enabled ?
532              /* disabled */
533              (self == self->frame->selected ?
534               self->entry->data.normal.mask_disabled_selected_color :
535               self->entry->data.normal.mask_disabled_color) :
536              /* enabled */
537              (self == self->frame->selected ?
538               self->entry->data.normal.mask_selected_color :
539               self->entry->data.normal.mask_normal_color));
540         clear->texture[0].data.mask.color = c;
541
542         clear->surface.parent = item_a;
543         clear->surface.parentx = PADDING;
544         clear->surface.parenty = frame->item_margin.top;
545         RrPaint(clear, self->icon,
546                 ITEM_HEIGHT - frame->item_margin.top
547                 - frame->item_margin.bottom,
548                 ITEM_HEIGHT - frame->item_margin.top
549                 - frame->item_margin.bottom);
550         XMapWindow(ob_display, self->icon);
551     } else
552         XUnmapWindow(ob_display, self->icon);
553
554     if (self->entry->type == OB_MENU_ENTRY_TYPE_SUBMENU) {
555         RrAppearance *bullet_a;
556         XMoveResizeWindow(ob_display, self->bullet,
557                           self->frame->text_x + self->frame->text_w -
558                           ITEM_HEIGHT + PADDING, PADDING,
559                           ITEM_HEIGHT - 2*PADDING,
560                           ITEM_HEIGHT - 2*PADDING);
561         bullet_a = (self == self->frame->selected ?
562                     ob_rr_theme->a_menu_bullet_selected :
563                     ob_rr_theme->a_menu_bullet_normal);
564         bullet_a->surface.parent = item_a;
565         bullet_a->surface.parentx =
566             self->frame->text_x + self->frame->text_w - ITEM_HEIGHT + PADDING;
567         bullet_a->surface.parenty = PADDING;
568         RrPaint(bullet_a, self->bullet,
569                 ITEM_HEIGHT - 2*PADDING,
570                 ITEM_HEIGHT - 2*PADDING);
571         XMapWindow(ob_display, self->bullet);
572     } else
573         XUnmapWindow(ob_display, self->bullet);
574
575     XFlush(ob_display);
576 }
577
578 /*! this code is taken from the menu_frame_render. if that changes, this won't
579   work.. */
580 static gint menu_entry_frame_get_height(ObMenuEntryFrame *self,
581                                         gboolean first_entry,
582                                         gboolean last_entry)
583 {
584     ObMenuEntryType t;
585     gint h = 0;
586
587     h += 2*PADDING;
588
589     if (self)
590         t = self->entry->type;
591     else
592         /* this is the More... entry, it's NORMAL type */
593         t = OB_MENU_ENTRY_TYPE_NORMAL;
594
595     switch (t) {
596     case OB_MENU_ENTRY_TYPE_NORMAL:
597     case OB_MENU_ENTRY_TYPE_SUBMENU:
598         h += ob_rr_theme->menu_font_height;
599         break;
600     case OB_MENU_ENTRY_TYPE_SEPARATOR:
601         if (self->entry->data.separator.label != NULL) {
602             h += ob_rr_theme->menu_title_height +
603                 (ob_rr_theme->mbwidth - PADDING) * 2;
604
605             /* if the first entry is a labeled separator, then make its border
606                overlap with the menu's outside border */
607             if (first_entry)
608                 h -= ob_rr_theme->mbwidth;
609             /* if the last entry is a labeled separator, then make its border
610                overlap with the menu's outside border */
611             if (last_entry)
612                 h -= ob_rr_theme->mbwidth;
613         } else {
614             h += ob_rr_theme->menu_sep_width +
615                 2*ob_rr_theme->menu_sep_paddingy - PADDING * 2;
616         }
617         break;
618     }
619
620     return h;
621 }
622
623 void menu_frame_render(ObMenuFrame *self)
624 {
625     gint w = 0, h = 0;
626     gint tw, th; /* temps */
627     GList *it;
628     gboolean has_icon = FALSE;
629     ObMenu *sub;
630     ObMenuEntryFrame *e;
631
632     /* find text dimensions */
633
634     STRUT_SET(self->item_margin, 0, 0, 0, 0);
635
636     if (self->entries) {
637         gint l, t, r, b;
638
639         e = self->entries->data;
640         ob_rr_theme->a_menu_text_normal->texture[0].data.text.string = "";
641         tw = RrMinWidth(ob_rr_theme->a_menu_text_normal);
642         tw += 2*PADDING;
643
644         th = ITEM_HEIGHT;
645
646         RrMargins(ob_rr_theme->a_menu_normal, &l, &t, &r, &b);
647         STRUT_SET(self->item_margin,
648                   MAX(self->item_margin.left, l),
649                   MAX(self->item_margin.top, t),
650                   MAX(self->item_margin.right, r),
651                   MAX(self->item_margin.bottom, b));
652         RrMargins(ob_rr_theme->a_menu_selected, &l, &t, &r, &b);
653         STRUT_SET(self->item_margin,
654                   MAX(self->item_margin.left, l),
655                   MAX(self->item_margin.top, t),
656                   MAX(self->item_margin.right, r),
657                   MAX(self->item_margin.bottom, b));
658         RrMargins(ob_rr_theme->a_menu_disabled, &l, &t, &r, &b);
659         STRUT_SET(self->item_margin,
660                   MAX(self->item_margin.left, l),
661                   MAX(self->item_margin.top, t),
662                   MAX(self->item_margin.right, r),
663                   MAX(self->item_margin.bottom, b));
664         RrMargins(ob_rr_theme->a_menu_disabled_selected, &l, &t, &r, &b);
665         STRUT_SET(self->item_margin,
666                   MAX(self->item_margin.left, l),
667                   MAX(self->item_margin.top, t),
668                   MAX(self->item_margin.right, r),
669                   MAX(self->item_margin.bottom, b));
670     }
671
672     /* render the entries */
673
674     for (it = self->entries; it; it = g_list_next(it)) {
675         RrAppearance *text_a;
676         e = it->data;
677
678         /* if the first entry is a labeled separator, then make its border
679            overlap with the menu's outside border */
680         if (it == self->entries &&
681             e->entry->type == OB_MENU_ENTRY_TYPE_SEPARATOR &&
682             e->entry->data.separator.label)
683         {
684             h -= ob_rr_theme->mbwidth;
685         }
686
687         if (e->entry->type == OB_MENU_ENTRY_TYPE_SEPARATOR &&
688             e->entry->data.separator.label)
689         {
690             e->border = ob_rr_theme->mbwidth;
691         }
692
693         RECT_SET_POINT(e->area, 0, h+e->border);
694         XMoveWindow(ob_display, e->window,
695                     e->area.x-e->border, e->area.y-e->border);
696         XSetWindowBorderWidth(ob_display, e->window, e->border);
697         XSetWindowBorder(ob_display, e->window,
698                          RrColorPixel(ob_rr_theme->menu_border_color));
699
700         text_a = (e->entry->type == OB_MENU_ENTRY_TYPE_NORMAL &&
701                   !e->entry->data.normal.enabled ?
702                   /* disabled */
703                   (e == self->selected ?
704                    ob_rr_theme->a_menu_text_disabled_selected :
705                    ob_rr_theme->a_menu_text_disabled) :
706                   /* enabled */
707                   (e == self->selected ?
708                    ob_rr_theme->a_menu_text_selected : 
709                    ob_rr_theme->a_menu_text_normal));
710         switch (e->entry->type) {
711         case OB_MENU_ENTRY_TYPE_NORMAL:
712             text_a->texture[0].data.text.string = e->entry->data.normal.label;
713             tw = RrMinWidth(text_a);
714             tw = MIN(tw, MAX_MENU_WIDTH);
715             th = ob_rr_theme->menu_font_height;
716
717             if (e->entry->data.normal.icon ||
718                 e->entry->data.normal.mask)
719                 has_icon = TRUE;
720             break;
721         case OB_MENU_ENTRY_TYPE_SUBMENU:
722             sub = e->entry->data.submenu.submenu;
723             text_a->texture[0].data.text.string = sub ? sub->title : "";
724             tw = RrMinWidth(text_a);
725             tw = MIN(tw, MAX_MENU_WIDTH);
726             th = ob_rr_theme->menu_font_height;
727
728             if (e->entry->data.normal.icon ||
729                 e->entry->data.normal.mask)
730                 has_icon = TRUE;
731
732             tw += ITEM_HEIGHT - PADDING;
733             break;
734         case OB_MENU_ENTRY_TYPE_SEPARATOR:
735             if (e->entry->data.separator.label != NULL) {
736                 ob_rr_theme->a_menu_text_title->texture[0].data.text.string =
737                     e->entry->data.separator.label;
738                 tw = RrMinWidth(ob_rr_theme->a_menu_text_title) +
739                     2*ob_rr_theme->paddingx;
740                 tw = MIN(tw, MAX_MENU_WIDTH);
741                 th = ob_rr_theme->menu_title_height +
742                     (ob_rr_theme->mbwidth - PADDING) *2;
743             } else {
744                 tw = 0;
745                 th = ob_rr_theme->menu_sep_width +
746                     2*ob_rr_theme->menu_sep_paddingy - 2*PADDING;
747             }
748             break;
749         }
750         tw += 2*PADDING;
751         th += 2*PADDING;
752         w = MAX(w, tw);
753         h += th;
754     }
755
756     /* if the last entry is a labeled separator, then make its border
757        overlap with the menu's outside border */
758     it = g_list_last(self->entries);
759     e = it ? it->data : NULL;
760     if (e && e->entry->type == OB_MENU_ENTRY_TYPE_SEPARATOR &&
761         e->entry->data.separator.label)
762     {
763         h -= ob_rr_theme->mbwidth;
764     }
765
766     self->text_x = PADDING;
767     self->text_w = w;
768
769     if (self->entries) {
770         if (has_icon) {
771             w += ITEM_HEIGHT + PADDING;
772             self->text_x += ITEM_HEIGHT + PADDING;
773         }
774     }
775
776     if (!w) w = 10;
777     if (!h) h = 3;
778
779     XResizeWindow(ob_display, self->window, w, h);
780
781     self->inner_w = w;
782
783     RrPaint(self->a_items, self->window, w, h);
784
785     for (it = self->entries; it; it = g_list_next(it))
786         menu_entry_frame_render(it->data);
787
788     w += ob_rr_theme->mbwidth * 2;
789     h += ob_rr_theme->mbwidth * 2;
790
791     RECT_SET_SIZE(self->area, w, h);
792
793     XFlush(ob_display);
794 }
795
796 static void menu_frame_update(ObMenuFrame *self)
797 {
798     GList *mit, *fit;
799     Rect *a;
800     gint h;
801
802     menu_pipe_execute(self->menu);
803     menu_find_submenus(self->menu);
804
805     self->selected = NULL;
806
807     /* start at show_from */
808     mit = g_list_nth(self->menu->entries, self->show_from);
809
810     /* go through the menu's and frame's entries and connect the frame entries
811        to the menu entries */
812     for (fit = self->entries; mit && fit;
813          mit = g_list_next(mit), fit = g_list_next(fit))
814     {
815         ObMenuEntryFrame *f = fit->data;
816         f->entry = mit->data;
817     }
818
819     /* if there are more menu entries than in the frame, add them */
820     while (mit) {
821         ObMenuEntryFrame *e = menu_entry_frame_new(mit->data, self);
822         self->entries = g_list_append(self->entries, e);
823         mit = g_list_next(mit);
824     }
825
826     /* if there are more frame entries than menu entries then get rid of
827        them */
828     while (fit) {
829         GList *n = g_list_next(fit);
830         menu_entry_frame_free(fit->data);
831         self->entries = g_list_delete_link(self->entries, fit);
832         fit = n;
833     }
834
835     /* * make the menu fit on the screen */
836
837     /* calculate the height of the menu */
838     h = 0;
839     for (fit = self->entries; fit; fit = g_list_next(fit))
840         h += menu_entry_frame_get_height(fit->data,
841                                          fit == self->entries,
842                                          g_list_next(fit) == NULL);
843     /* add the border at the top and bottom */
844     h += ob_rr_theme->mbwidth * 2;
845
846     a = screen_physical_area_monitor(self->monitor);
847
848     if (h > a->height) {
849         GList *flast, *tmp;
850         gboolean last_entry = TRUE;
851
852         /* take the height of our More... entry into account */
853         h += menu_entry_frame_get_height(NULL, FALSE, TRUE);
854
855         /* start at the end of the entries */
856         flast = g_list_last(self->entries);
857
858         /* pull out all the entries from the frame that don't
859            fit on the screen, leaving at least 1 though */
860         while (h > a->height && g_list_previous(flast) != NULL) {
861             /* update the height, without this entry */
862             h -= menu_entry_frame_get_height(flast->data, FALSE, last_entry);
863
864             /* destroy the entry we're not displaying */
865             tmp = flast;
866             flast = g_list_previous(flast);
867             menu_entry_frame_free(tmp->data);
868             self->entries = g_list_delete_link(self->entries, tmp);
869
870             /* only the first one that we see is the last entry in the menu */
871             last_entry = FALSE;
872         };
873
874         {
875             ObMenuEntry *more_entry;
876             ObMenuEntryFrame *more_frame;
877             /* make the More... menu entry frame which will display in this
878                frame.
879                if self->menu->more_menu is NULL that means that this is already
880                More... menu, so just use ourself.
881             */
882             more_entry = menu_get_more((self->menu->more_menu ?
883                                         self->menu->more_menu :
884                                         self->menu),
885                                        /* continue where we left off */
886                                        self->show_from +
887                                        g_list_length(self->entries));
888             more_frame = menu_entry_frame_new(more_entry, self);
889             /* make it get deleted when the menu frame goes away */
890             menu_entry_unref(more_entry);
891
892             /* add our More... entry to the frame */
893             self->entries = g_list_append(self->entries, more_frame);
894         }
895     }
896
897     g_free(a);
898
899     menu_frame_render(self);
900 }
901
902 static gboolean menu_frame_is_visible(ObMenuFrame *self)
903 {
904     return !!(g_list_find(menu_frame_visible, self));
905 }
906
907 static gboolean menu_frame_show(ObMenuFrame *self)
908 {
909     GList *it;
910
911     /* determine if the underlying menu is already visible */
912     for (it = menu_frame_visible; it; it = g_list_next(it)) {
913         ObMenuFrame *f = it->data;
914         if (f->menu == self->menu)
915             break;
916     }
917     if (!it) {
918         if (self->menu->update_func)
919             if (!self->menu->update_func(self, self->menu->data))
920                 return FALSE;
921     }
922
923     if (menu_frame_visible == NULL) {
924         /* no menus shown yet */
925
926         /* grab the pointer in such a way as to pass through "owner events"
927            so that we can get enter/leave notifies in the menu. */
928         if (!grab_pointer(TRUE, FALSE, OB_CURSOR_POINTER))
929             return FALSE;
930         if (!grab_keyboard()) {
931             ungrab_pointer();
932             return FALSE;
933         }
934     }
935
936     menu_frame_update(self);
937
938     menu_frame_visible = g_list_prepend(menu_frame_visible, self);
939
940     if (self->menu->show_func)
941         self->menu->show_func(self, self->menu->data);
942
943     return TRUE;
944 }
945
946 gboolean menu_frame_show_topmenu(ObMenuFrame *self, gint x, gint y,
947                                  gboolean mouse)
948 {
949     gint px, py;
950     guint i;
951
952     if (menu_frame_is_visible(self))
953         return TRUE;
954     if (!menu_frame_show(self))
955         return FALSE;
956
957     if (self->menu->place_func)
958         self->menu->place_func(self, &x, &y, mouse, self->menu->data);
959     else
960         menu_frame_place_topmenu(self, &x, &y);
961
962     menu_frame_move(self, x, y);
963
964     XMapWindow(ob_display, self->window);
965
966     if (screen_pointer_pos(&px, &py)) {
967         ObMenuEntryFrame *e = menu_entry_frame_under(px, py);
968         if (e && e->frame == self)
969             e->ignore_enters++;
970     }
971
972     return TRUE;
973 }
974
975 gboolean menu_frame_show_submenu(ObMenuFrame *self, ObMenuFrame *parent,
976                                  ObMenuEntryFrame *parent_entry)
977 {
978     gint x, y, dx, dy;
979     gint px, py;
980
981     if (menu_frame_is_visible(self))
982         return TRUE;
983
984     self->monitor = parent->monitor;
985     self->parent = parent;
986     self->parent_entry = parent_entry;
987
988     /* set up parent's child to be us */
989     if (parent->child)
990         menu_frame_hide(parent->child);
991     parent->child = self;
992
993     if (!menu_frame_show(self))
994         return FALSE;
995
996     menu_frame_place_submenu(self, &x, &y);
997     menu_frame_move_on_screen(self, x, y, &dx, &dy);
998
999     if (dx != 0) {
1000         /*try the other side */
1001         self->direction_right = !self->direction_right;
1002         menu_frame_place_submenu(self, &x, &y);
1003         menu_frame_move_on_screen(self, x, y, &dx, &dy);
1004     }
1005     menu_frame_move(self, x + dx, y + dy);
1006
1007     XMapWindow(ob_display, self->window);
1008
1009     if (screen_pointer_pos(&px, &py)) {
1010         ObMenuEntryFrame *e = menu_entry_frame_under(px, py);
1011         if (e && e->frame == self)
1012             e->ignore_enters++;
1013     }
1014
1015     return TRUE;
1016 }
1017
1018 static void menu_frame_hide(ObMenuFrame *self)
1019 {
1020     GList *it = g_list_find(menu_frame_visible, self);
1021
1022     if (!it)
1023         return;
1024
1025     if (self->menu->hide_func)
1026         self->menu->hide_func(self, self->menu->data);
1027
1028     if (self->child)
1029         menu_frame_hide(self->child);
1030
1031     if (self->parent)
1032         self->parent->child = NULL;
1033     self->parent = NULL;
1034     self->parent_entry = NULL;
1035
1036     menu_frame_visible = g_list_delete_link(menu_frame_visible, it);
1037
1038     if (menu_frame_visible == NULL) {
1039         /* last menu shown */
1040         ungrab_pointer();
1041         ungrab_keyboard();
1042     }
1043
1044     XUnmapWindow(ob_display, self->window);
1045
1046     menu_frame_free(self);
1047 }
1048
1049 void menu_frame_hide_all(void)
1050 {
1051     GList *it;
1052
1053     if (config_submenu_show_delay) {
1054         /* remove any submenu open requests */
1055         ob_main_loop_timeout_remove(ob_main_loop,
1056                                     menu_entry_frame_submenu_timeout);
1057     }
1058     if ((it = g_list_last(menu_frame_visible)))
1059         menu_frame_hide(it->data);
1060 }
1061
1062 void menu_frame_hide_all_client(ObClient *client)
1063 {
1064     GList *it = g_list_last(menu_frame_visible);
1065     if (it) {
1066         ObMenuFrame *f = it->data;
1067         if (f->client == client) {
1068             if (config_submenu_show_delay) {
1069                 /* remove any submenu open requests */
1070                 ob_main_loop_timeout_remove(ob_main_loop,
1071                                             menu_entry_frame_submenu_timeout);
1072             }
1073             menu_frame_hide(f);
1074         }
1075     }
1076 }
1077
1078 ObMenuFrame* menu_frame_under(gint x, gint y)
1079 {
1080     ObMenuFrame *ret = NULL;
1081     GList *it;
1082
1083     for (it = menu_frame_visible; it; it = g_list_next(it)) {
1084         ObMenuFrame *f = it->data;
1085
1086         if (RECT_CONTAINS(f->area, x, y)) {
1087             ret = f;
1088             break;
1089         }
1090     }
1091     return ret;
1092 }
1093
1094 ObMenuEntryFrame* menu_entry_frame_under(gint x, gint y)
1095 {
1096     ObMenuFrame *frame;
1097     ObMenuEntryFrame *ret = NULL;
1098     GList *it;
1099
1100     if ((frame = menu_frame_under(x, y))) {
1101         x -= ob_rr_theme->mbwidth + frame->area.x;
1102         y -= ob_rr_theme->mbwidth + frame->area.y;
1103
1104         for (it = frame->entries; it; it = g_list_next(it)) {
1105             ObMenuEntryFrame *e = it->data;
1106
1107             if (RECT_CONTAINS(e->area, x, y)) {
1108                 ret = e;
1109                 break;
1110             }
1111         }
1112     }
1113     return ret;
1114 }
1115
1116 static gboolean menu_entry_frame_submenu_timeout(gpointer data)
1117 {
1118     g_assert(menu_frame_visible);
1119     menu_entry_frame_show_submenu((ObMenuEntryFrame*)data);
1120     return FALSE;
1121 }
1122
1123 void menu_frame_select(ObMenuFrame *self, ObMenuEntryFrame *entry,
1124                        gboolean immediate)
1125 {
1126     ObMenuEntryFrame *old = self->selected;
1127     ObMenuFrame *oldchild = self->child;
1128
1129     if (entry && entry->entry->type == OB_MENU_ENTRY_TYPE_SEPARATOR)
1130         entry = old;
1131
1132     if (old == entry) return;
1133
1134     if (config_submenu_show_delay) {
1135         /* remove any submenu open requests */
1136         ob_main_loop_timeout_remove(ob_main_loop,
1137                                     menu_entry_frame_submenu_timeout);
1138     }
1139
1140     self->selected = entry;
1141
1142     if (old)
1143         menu_entry_frame_render(old);
1144     if (oldchild)
1145         menu_frame_hide(oldchild);
1146
1147     if (self->selected) {
1148         menu_entry_frame_render(self->selected);
1149
1150         if (self->selected->entry->type == OB_MENU_ENTRY_TYPE_SUBMENU) {
1151             if (config_submenu_show_delay && !immediate) {
1152                 /* initiate a new submenu open request */
1153                 ob_main_loop_timeout_add(ob_main_loop,
1154                                          config_submenu_show_delay * 1000,
1155                                          menu_entry_frame_submenu_timeout,
1156                                          self->selected, g_direct_equal,
1157                                          NULL);
1158             } else {
1159                 menu_entry_frame_show_submenu(self->selected);
1160             }
1161         }
1162     }
1163 }
1164
1165 void menu_entry_frame_show_submenu(ObMenuEntryFrame *self)
1166 {
1167     ObMenuFrame *f;
1168
1169     if (!self->entry->data.submenu.submenu) return;
1170
1171     f = menu_frame_new(self->entry->data.submenu.submenu,
1172                        self->entry->data.submenu.show_from,
1173                        self->frame->client);
1174     /* pass our direction on to our child */
1175     f->direction_right = self->frame->direction_right;
1176
1177     menu_frame_show_submenu(f, self->frame, self);
1178 }
1179
1180 void menu_entry_frame_execute(ObMenuEntryFrame *self, guint state)
1181 {
1182     if (self->entry->type == OB_MENU_ENTRY_TYPE_NORMAL &&
1183         self->entry->data.normal.enabled)
1184     {
1185         /* grab all this shizzle, cuz when the menu gets hidden, 'self'
1186            gets freed */
1187         ObMenuEntry *entry = self->entry;
1188         ObMenuExecuteFunc func = self->frame->menu->execute_func;
1189         gpointer data = self->frame->menu->data;
1190         GSList *acts = self->entry->data.normal.actions;
1191         ObClient *client = self->frame->client;
1192         ObMenuFrame *frame = self->frame;
1193
1194         /* release grabs before executing the shit */
1195         if (!(state & ControlMask)) {
1196             menu_frame_hide_all();
1197             frame = NULL;
1198         }
1199
1200         if (func)
1201             func(entry, frame, client, state, data);
1202         else
1203             actions_run_acts(acts, OB_USER_ACTION_MENU_SELECTION,
1204                              state, -1, -1, 0, OB_FRAME_CONTEXT_NONE, client);
1205     }
1206 }
1207
1208 void menu_frame_select_previous(ObMenuFrame *self)
1209 {
1210     GList *it = NULL, *start;
1211
1212     if (self->entries) {
1213         start = it = g_list_find(self->entries, self->selected);
1214         while (TRUE) {
1215             ObMenuEntryFrame *e;
1216
1217             it = it ? g_list_previous(it) : g_list_last(self->entries);
1218             if (it == start)
1219                 break;
1220
1221             if (it) {
1222                 e = it->data;
1223                 if (e->entry->type == OB_MENU_ENTRY_TYPE_SUBMENU)
1224                     break;
1225                 if (e->entry->type == OB_MENU_ENTRY_TYPE_NORMAL)
1226                     break;
1227             }
1228         }
1229     }
1230     menu_frame_select(self, it ? it->data : NULL, TRUE);
1231 }
1232
1233 void menu_frame_select_next(ObMenuFrame *self)
1234 {
1235     GList *it = NULL, *start;
1236
1237     if (self->entries) {
1238         start = it = g_list_find(self->entries, self->selected);
1239         while (TRUE) {
1240             ObMenuEntryFrame *e;
1241
1242             it = it ? g_list_next(it) : self->entries;
1243             if (it == start)
1244                 break;
1245
1246             if (it) {
1247                 e = it->data;
1248                 if (e->entry->type == OB_MENU_ENTRY_TYPE_SUBMENU)
1249                     break;
1250                 if (e->entry->type == OB_MENU_ENTRY_TYPE_NORMAL)
1251                     break;
1252             }
1253         }
1254     }
1255     menu_frame_select(self, it ? it->data : NULL, TRUE);
1256 }