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