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