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