add a menuOverlap property to themes, and use it in the submenu placement
[dana/openbox.git] / openbox / menu.c
1 #include "menu.h"
2 #include "openbox.h"
3 #include "stacking.h"
4 #include "client.h"
5 #include "grab.h"
6 #include "screen.h"
7 #include "geom.h"
8 #include "plugin.h"
9 #include "misc.h"
10
11 GHashTable *menu_hash = NULL;
12 GList *menu_visible = NULL;
13
14 #define FRAME_EVENTMASK (ButtonPressMask |ButtonMotionMask | EnterWindowMask |\
15                          LeaveWindowMask)
16 #define TITLE_EVENTMASK (ButtonPressMask | ButtonMotionMask)
17 #define ENTRY_EVENTMASK (EnterWindowMask | LeaveWindowMask | \
18                          ButtonPressMask | ButtonReleaseMask)
19
20 static void parse_menu(xmlDocPtr doc, xmlNodePtr node, void *data)
21 {
22     parse_menu_full(doc, node, data, TRUE);
23 }
24
25
26 void parse_menu_full(xmlDocPtr doc, xmlNodePtr node, void *data,
27                        gboolean newmenu)
28 {
29     Action *act;
30     xmlNodePtr nact;
31
32     gchar *id = NULL, *title = NULL, *label = NULL, *plugin;
33     ObMenu *menu = NULL, *parent;
34
35     if (newmenu == TRUE) {
36         if (!parse_attr_string("id", node->parent, &id))
37             goto parse_menu_fail;
38         if (!parse_attr_string("label", node->parent, &title))
39             goto parse_menu_fail;
40
41         g_message("menu label %s", title);
42
43         menu = menu_new(title, id, data ? *((ObMenu**)data) : NULL);
44
45         if (data)
46             *((ObMenu**)data) = menu;
47     } else {
48         menu = (ObMenu *)data;
49     }
50     
51     while (node) {
52         if (!xmlStrcasecmp(node->name, (const xmlChar*) "menu")) {
53             if (parse_attr_string("plugin", node, &plugin)) {
54                 PluginMenuCreateData data = {
55                     .doc = doc,
56                     .node = node,
57                     .parent = menu
58                 };
59                 parent = plugin_create(plugin, &data);
60                 g_free(plugin);
61             } else {
62                 parent = menu;
63                 parse_menu(doc, node->xmlChildrenNode, &parent);
64                 menu_add_entry(menu, menu_entry_new_submenu(parent->label,
65                                                             parent));
66             }
67
68         }
69         else if (!xmlStrcasecmp(node->name, (const xmlChar*) "item")) {
70             if (parse_attr_string("label", node, &label)) {
71                 if ((nact = parse_find_node("action", node->xmlChildrenNode)))
72                     act = action_parse(doc, nact);
73                 else
74                     act = NULL;
75                 if (act)
76                     menu_add_entry(menu, menu_entry_new(label, act));
77                 else
78                     menu_add_entry(menu, menu_entry_new_separator(label));
79                 g_free(label);
80             }
81         }
82         node = node->next;
83     }
84
85 parse_menu_fail:
86     g_free(id);
87     g_free(title);
88 }
89
90 void menu_control_show(ObMenu *self, int x, int y, ObClient *client);
91
92 void menu_destroy_hash_key(ObMenu *menu)
93 {
94     g_free(menu);
95 }
96
97 void menu_destroy_hash_value(ObMenu *self)
98 {
99     GList *it;
100
101     for (it = self->entries; it; it = it->next)
102         menu_entry_free(it->data);
103     g_list_free(self->entries);
104
105     g_free(self->label);
106     g_free(self->name);
107
108     g_hash_table_remove(window_map, &self->title);
109     g_hash_table_remove(window_map, &self->frame);
110     g_hash_table_remove(window_map, &self->items);
111
112     stacking_remove(self);
113
114     RrAppearanceFree(self->a_title);
115     RrAppearanceFree(self->a_items);
116     XDestroyWindow(ob_display, self->title);
117     XDestroyWindow(ob_display, self->frame);
118     XDestroyWindow(ob_display, self->items);
119
120     g_free(self);
121 }
122
123 void menu_entry_free(ObMenuEntry *self)
124 {
125     g_free(self->label);
126     action_free(self->action);
127
128     g_hash_table_remove(window_map, &self->item);
129
130     RrAppearanceFree(self->a_item);
131     RrAppearanceFree(self->a_disabled);
132     RrAppearanceFree(self->a_hilite);
133     XDestroyWindow(ob_display, self->item);
134
135     g_free(self);
136 }
137     
138 void menu_startup()
139 {
140 /*
141     ObMenu *m;
142     ObMenu *s;
143     ObMenu *t;
144     Action *a;
145 */
146
147     menu_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
148                                       (GDestroyNotify)menu_destroy_hash_key,
149                                       (GDestroyNotify)menu_destroy_hash_value);
150
151     parse_register("menu", parse_menu, NULL);
152
153 /*
154     m = menu_new("sex menu", "root", NULL);
155  
156     a = action_from_string("execute");
157     a->data.execute.path = g_strdup("xterm");
158     menu_add_entry(m, menu_entry_new("xterm", a));
159     a = action_from_string("restart");
160     menu_add_entry(m, menu_entry_new("restart", a));
161     menu_add_entry(m, menu_entry_new_separator("--"));
162     a = action_from_string("exit");
163     menu_add_entry(m, menu_entry_new("exit", a));
164 */
165
166     /*
167     s = menu_new("subsex menu", "submenu", m);
168     a = action_from_string("execute");
169     a->data.execute.path = g_strdup("xclock");
170     menu_add_entry(s, menu_entry_new("xclock", a));
171
172     menu_add_entry(m, menu_entry_new_submenu("subz", s));
173
174     s = menu_new("empty", "chub", m);
175     menu_add_entry(m, menu_entry_new_submenu("empty", s));
176
177     s = menu_new("", "s-club", m);
178     menu_add_entry(m, menu_entry_new_submenu("empty", s));
179
180     s = menu_new(NULL, "h-club", m);
181     menu_add_entry(m, menu_entry_new_submenu("empty", s));
182
183     s = menu_new(NULL, "g-club", m);
184
185     a = action_from_string("execute");
186     a->data.execute.path = g_strdup("xterm");
187     menu_add_entry(s, menu_entry_new("xterm", a));
188     a = action_from_string("restart");
189     menu_add_entry(s, menu_entry_new("restart", a));
190     menu_add_entry(s, menu_entry_new_separator("--"));
191     a = action_from_string("exit");
192     menu_add_entry(s, menu_entry_new("exit", a));
193
194     menu_add_entry(m, menu_entry_new_submenu("long", s));
195     */
196 }
197
198 void menu_shutdown()
199 {
200     g_hash_table_destroy(menu_hash);
201 }
202
203 static Window createWindow(Window parent, unsigned long mask,
204                            XSetWindowAttributes *attrib)
205 {
206     return XCreateWindow(ob_display, parent, 0, 0, 1, 1, 0,
207                          RrDepth(ob_rr_inst), InputOutput,
208                          RrVisual(ob_rr_inst), mask, attrib);
209                        
210 }
211
212 ObMenu *menu_new_full(char *label, char *name, ObMenu *parent, 
213                     menu_controller_show show, menu_controller_update update)
214 {
215     XSetWindowAttributes attrib;
216     ObMenu *self;
217
218     self = g_new0(ObMenu, 1);
219     self->obwin.type = Window_Menu;
220     self->label = g_strdup(label);
221     self->name = g_strdup(name);
222     self->parent = parent;
223     self->open_submenu = NULL;
224
225     self->entries = NULL;
226     self->shown = FALSE;
227     self->invalid = TRUE;
228
229     /* default controllers */
230     self->show = show;
231     self->hide = NULL;
232     self->update = update;
233     self->mouseover = NULL;
234     self->selected = NULL;
235
236     self->plugin = NULL;
237     self->plugin_data = NULL;
238
239     attrib.override_redirect = TRUE;
240     attrib.event_mask = FRAME_EVENTMASK;
241     self->frame = createWindow(RootWindow(ob_display, ob_screen),
242                                CWOverrideRedirect|CWEventMask, &attrib);
243     attrib.event_mask = TITLE_EVENTMASK;
244     self->title = createWindow(self->frame, CWEventMask, &attrib);
245     self->items = createWindow(self->frame, 0, &attrib);
246
247     self->a_title = self->a_items = NULL;
248
249     XMapWindow(ob_display, self->title);
250     XMapWindow(ob_display, self->items);
251
252     g_hash_table_insert(window_map, &self->frame, self);
253     g_hash_table_insert(window_map, &self->title, self);
254     g_hash_table_insert(window_map, &self->items, self);
255     g_hash_table_insert(menu_hash, g_strdup(name), self);
256
257     stacking_add(MENU_AS_WINDOW(self));
258     stacking_raise(MENU_AS_WINDOW(self));
259
260     return self;
261 }
262
263 void menu_free(char *name)
264 {
265     g_hash_table_remove(menu_hash, name);
266 }
267
268 ObMenuEntry *menu_entry_new_full(char *label, Action *action,
269                                ObMenuEntryRenderType render_type,
270                                gpointer submenu)
271 {
272     ObMenuEntry *menu_entry = g_new0(ObMenuEntry, 1);
273
274     menu_entry->label = g_strdup(label);
275     menu_entry->render_type = render_type;
276     menu_entry->action = action;
277
278     menu_entry->hilite = FALSE;
279     menu_entry->enabled = TRUE;
280
281     menu_entry->submenu = submenu;
282
283     return menu_entry;
284 }
285
286 void menu_entry_set_submenu(ObMenuEntry *entry, ObMenu *submenu)
287 {
288     g_assert(entry != NULL);
289     
290     entry->submenu = submenu;
291
292     if(entry->parent != NULL)
293         entry->parent->invalid = TRUE;
294 }
295
296 void menu_add_entry(ObMenu *menu, ObMenuEntry *entry)
297 {
298     XSetWindowAttributes attrib;
299
300     g_assert(menu != NULL);
301     g_assert(entry != NULL);
302     g_assert(entry->item == None);
303
304     menu->entries = g_list_append(menu->entries, entry);
305     entry->parent = menu;
306
307     attrib.event_mask = ENTRY_EVENTMASK;
308     entry->item = createWindow(menu->items, CWEventMask, &attrib);
309     XMapWindow(ob_display, entry->item);
310
311     entry->a_item = entry->a_disabled = entry->a_hilite = NULL;
312
313     menu->invalid = TRUE;
314
315     g_hash_table_insert(window_map, &entry->item, menu);
316 }
317
318 void menu_show(char *name, int x, int y, ObClient *client)
319 {
320     ObMenu *self;
321   
322     self = g_hash_table_lookup(menu_hash, name);
323     if (!self) {
324         g_warning("Attempted to show menu '%s' but it does not exist.",
325                   name);
326         return;
327     }
328
329     menu_show_full(self, x, y, client);
330 }  
331
332 void menu_show_full(ObMenu *self, int x, int y, ObClient *client)
333 {
334     g_assert(self != NULL);
335        
336     menu_render(self);
337     
338     self->client = client;
339
340     if (!self->shown) {
341         if (!(self->parent && self->parent->shown)) {
342             grab_pointer(TRUE, None);
343             grab_keyboard(TRUE);
344         }
345         menu_visible = g_list_append(menu_visible, self);
346     }
347
348     if (self->show) {
349         self->show(self, x, y, client);
350     } else {
351       menu_control_show(self, x, y, client);
352     }
353 }
354
355 void menu_hide(ObMenu *self) {
356     if (self->shown) {
357         XUnmapWindow(ob_display, self->frame);
358         self->shown = FALSE;
359         if (self->open_submenu)
360             menu_hide(self->open_submenu);
361         if (self->parent && self->parent->open_submenu == self)
362             self->parent->open_submenu = NULL;
363
364         if (!(self->parent && self->parent->shown)) {
365             grab_keyboard(FALSE);
366             grab_pointer(FALSE, None);
367         }
368         menu_visible = g_list_remove(menu_visible, self);
369     }
370 }
371
372 void menu_clear(ObMenu *self) {
373     GList *it;
374   
375     for (it = self->entries; it; it = it->next) {
376         ObMenuEntry *entry = it->data;
377         menu_entry_free(entry);
378     }
379     self->entries = NULL;
380     self->invalid = TRUE;
381 }
382
383
384 ObMenuEntry *menu_find_entry(ObMenu *menu, Window win)
385 {
386     GList *it;
387
388     for (it = menu->entries; it; it = it->next) {
389         ObMenuEntry *entry = it->data;
390         if (entry->item == win)
391             return entry;
392     }
393     return NULL;
394 }
395
396 ObMenuEntry *menu_find_entry_by_pos(ObMenu *menu, int x, int y)
397 {
398     if (x < 0 || x >= menu->size.width || y < 0 || y >= menu->size.height)
399         return NULL;
400
401     y -= menu->title_h + ob_rr_theme->bwidth;
402     if (y < 0) return NULL;
403     
404     g_message ("%d %p", y/menu->item_h, g_list_nth_data(menu->entries, y / menu->item_h));
405     return g_list_nth_data(menu->entries, y / menu->item_h);
406 }
407
408 void menu_entry_fire(ObMenuEntry *self)
409 {
410     ObMenu *m;
411
412     if (self->action) {
413         self->action->data.any.c = self->parent->client;
414         self->action->func(&self->action->data);
415
416         /* hide the whole thing */
417         m = self->parent;
418         while (m->parent) m = m->parent;
419         menu_hide(m);
420     }
421 }
422
423 /* 
424    Default menu controller action for showing.
425 */
426
427 void menu_control_show(ObMenu *self, int x, int y, ObClient *client)
428 {
429     guint i;
430     Rect *a = NULL;
431
432     g_assert(!self->invalid);
433     
434     for (i = 0; i < screen_num_monitors; ++i) {
435         a = screen_physical_area_monitor(i);
436         if (RECT_CONTAINS(*a, x, y))
437             break;
438     }
439     g_assert(a != NULL);
440     self->xin_area = i;
441
442     POINT_SET(self->location,
443               MIN(x, a->x + a->width - 1 - self->size.width), 
444               MIN(y, a->y + a->height - 1 - self->size.height));
445     XMoveWindow(ob_display, self->frame, self->location.x, self->location.y);
446
447     if (!self->shown) {
448         XMapWindow(ob_display, self->frame);
449         stacking_raise(MENU_AS_WINDOW(self));
450         self->shown = TRUE;
451     } else if (self->shown && self->open_submenu) {
452         menu_hide(self->open_submenu);
453     }
454 }
455
456 void menu_control_mouseover(ObMenuEntry *self, gboolean enter)
457 {
458     int x;
459     Rect *a;
460
461     self->hilite = enter;
462   
463     if (enter) {
464         if (self->parent->open_submenu && self->submenu 
465             != self->parent->open_submenu)
466             menu_hide(self->parent->open_submenu);
467         
468         if (self->submenu && self->parent->open_submenu != self->submenu) {
469             self->parent->open_submenu = self->submenu;
470
471             /* shouldn't be invalid since it must be displayed */
472             g_assert(!self->parent->invalid);
473             /* TODO: I don't understand why these bevels should be here.
474                Something must be wrong in the width calculation */
475             x = self->parent->location.x + self->parent->size.width + 
476                 ob_rr_theme->bwidth - ob_rr_theme->menu_overlap;
477
478             /* need to get the width. is this bad?*/
479             menu_render(self->submenu);
480
481             a = screen_physical_area_monitor(self->parent->xin_area);
482
483             if (self->submenu->size.width + x >= a->x + a->width)
484                 x = self->parent->location.x - self->submenu->size.width - 
485                     ob_rr_theme->bwidth + ob_rr_theme->menu_overlap;
486             
487             menu_show_full(self->submenu, x,
488                            self->parent->location.y + self->y,
489                            self->parent->client);
490         } 
491     }
492 }
493
494 ObMenuEntry *menu_control_keyboard_nav(ObMenuEntry *over, ObKey key)
495 {
496     GList *it = NULL;
497         
498     switch (key) {
499     case OB_KEY_DOWN: {
500         if (over != NULL) {
501             if (over->parent->mouseover)
502                 over->parent->mouseover(over, FALSE);
503             else
504                 menu_control_mouseover(over, FALSE);
505             menu_entry_render(over);
506                 
507             it = over->parent->entries;
508             while (it != NULL && it->data != over)
509                 it = it->next;
510         }
511             
512         if (it && it->next)
513             over = (ObMenuEntry *)it->next->data;
514         else if (over == NULL) {
515             if (menu_visible && ((ObMenu *)menu_visible->data)->entries)
516                 over = (ObMenuEntry *)
517                     (((ObMenu *)menu_visible->data)->entries)->data;
518             else
519                 over = NULL;
520         } else {
521             over = (over->parent->entries != NULL ?
522                     over->parent->entries->data : NULL);
523         }
524
525         if (over) {
526             if (over->parent->mouseover)
527                 over->parent->mouseover(over, TRUE);
528             else
529                 menu_control_mouseover(over, TRUE);
530             menu_entry_render(over);
531         }
532         
533         break;
534     }
535     case OB_KEY_UP: {
536         if (over != NULL) {
537             if (over->parent->mouseover)
538                 over->parent->mouseover(over, FALSE);
539             else
540                 menu_control_mouseover(over, FALSE);
541             menu_entry_render(over);
542                 
543             it = g_list_last(over->parent->entries);
544             while (it != NULL && it->data != over)
545                 it = it->prev;
546         } 
547             
548         if (it && it->prev)
549             over = (ObMenuEntry *)it->prev->data;
550         else if (over == NULL) {
551             it = g_list_last(menu_visible);
552             if (it != NULL) {
553                 it = g_list_last(((ObMenu *)it->data)->entries);
554                 over = (ObMenuEntry *)(it != NULL ? it->data : NULL);
555             }
556         } else
557             over = (over->parent->entries != NULL ?
558                     g_list_last(over->parent->entries)->data :
559                     NULL);
560
561         if (over->parent->mouseover)
562             over->parent->mouseover(over, TRUE);
563         else
564             menu_control_mouseover(over, TRUE);
565         menu_entry_render(over);
566         break;
567     }
568     case OB_KEY_RETURN: {
569         if (over == NULL)
570             return over;
571
572         if (over->submenu) {
573             if (over->parent->mouseover)
574                 over->parent->mouseover(over, FALSE);
575             else
576                 menu_control_mouseover(over, FALSE);
577             menu_entry_render(over);
578
579             if (over->submenu->entries)
580                 over = over->submenu->entries->data;
581
582             if (over->parent->mouseover)
583                 over->parent->mouseover(over, TRUE);
584             else
585                 menu_control_mouseover(over, TRUE);
586             menu_entry_render(over);
587         }
588         else {
589             if (over->parent->mouseover)
590                 over->parent->mouseover(over, FALSE);
591             else
592                 menu_control_mouseover(over, FALSE);
593             menu_entry_render(over);
594
595             menu_entry_fire(over);
596         }
597         break;
598     }
599     case OB_KEY_ESCAPE: {
600         if (over != NULL) {
601             if (over->parent->mouseover)
602                 over->parent->mouseover(over, FALSE);
603             else
604                 menu_control_mouseover(over, FALSE);
605             menu_entry_render(over);
606
607             menu_hide(over->parent);
608         } else {
609             it  = g_list_last(menu_visible);
610             if (it) {
611                 menu_hide((ObMenu *)it->data);
612             }
613         }
614         
615         over = NULL;
616         break;
617     }
618     default:
619         g_error("Unknown key");
620     }
621
622     return over;
623 }