]> git.decadent.org.uk Git - ion3.git/blob - mod_menu/menu.c
[svn-upgrade] Integrating new upstream version, ion3 (20070203)
[ion3.git] / mod_menu / menu.c
1 /*
2  * ion/mod_menu/menu.c
3  *
4  * Copyright (c) Tuomo Valkonen 1999-2007. 
5  *
6  * Ion is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU Lesser General Public License as published by
8  * the Free Software Foundation; either version 2.1 of the License, or
9  * (at your option) any later version.
10  */
11
12 #include <string.h>
13 #include <limits.h>
14
15 #include <libtu/minmax.h>
16 #include <libtu/objp.h>
17 #include <libtu/obj.h>
18 #include <libmainloop/defer.h>
19 #include <libmainloop/signal.h>
20
21 #include <ioncore/common.h>
22 #include <ioncore/window.h>
23 #include <ioncore/global.h>
24 #include <ioncore/regbind.h>
25 #include <ioncore/strings.h>
26 #include <ioncore/pointer.h>
27 #include <ioncore/focus.h>
28 #include <ioncore/event.h>
29 #include <ioncore/xwindow.h>
30 #include <ioncore/names.h>
31 #include <ioncore/gr.h>
32 #include <ioncore/gr-util.h>
33 #include "menu.h"
34 #include "main.h"
35
36
37 #define MENU_WIN(MENU) ((MENU)->win.win)
38
39
40 /*{{{ Helpers */
41
42
43 static bool extl_table_getis(ExtlTab tab, int i, const char *s, char c,
44                              void *p)
45 {
46     ExtlTab sub;
47     bool ret;
48     
49     if(!extl_table_geti_t(tab, i, &sub))
50         return FALSE;
51     ret=extl_table_get(sub, 's', c, s, p);
52     extl_unref_table(sub);
53     return ret;
54 }
55
56
57 /*}}}*/
58
59
60 /*{{{ Drawing routines */
61
62
63 static void get_outer_geom(WMenu *menu, WRectangle *geom)
64 {
65     geom->x=0;
66     geom->y=0;
67     geom->w=REGION_GEOM(menu).w;
68     geom->h=REGION_GEOM(menu).h;
69 }
70
71
72 static void get_inner_geom(WMenu *menu, WRectangle *geom)
73 {
74     GrBorderWidths bdw;
75     
76     get_outer_geom(menu, geom);
77     
78     if(menu->brush!=NULL){
79         grbrush_get_border_widths(menu->brush, &bdw);
80         geom->x+=bdw.left;
81         geom->y+=bdw.top;
82         geom->w-=bdw.left+bdw.right;
83         geom->h-=bdw.top+bdw.bottom;
84         geom->w=maxof(0, geom->w);
85         geom->h=maxof(0, geom->h);
86     }
87 }
88
89
90 GR_DEFATTR(active);
91 GR_DEFATTR(inactive);
92 GR_DEFATTR(selected);
93 GR_DEFATTR(unselected);
94 GR_DEFATTR(normal);
95 GR_DEFATTR(submenu);
96
97
98 static void init_attr()
99 {
100     GR_ALLOCATTR_BEGIN;
101     GR_ALLOCATTR(active);
102     GR_ALLOCATTR(inactive);
103     GR_ALLOCATTR(selected);
104     GR_ALLOCATTR(unselected);
105     GR_ALLOCATTR(normal);
106     GR_ALLOCATTR(submenu);
107     GR_ALLOCATTR_END;
108 }
109
110
111 static void menu_draw_entry(WMenu *menu, int i, const WRectangle *igeom,
112                             bool complete)
113 {
114     WRectangle geom;
115     GrAttr sa, aa;
116
117     aa=(REGION_IS_ACTIVE(menu) ? GR_ATTR(active) : GR_ATTR(inactive));
118     sa=(menu->selected_entry==i ? GR_ATTR(selected) : GR_ATTR(unselected));
119     
120     if(menu->entry_brush==NULL)
121         return;
122     
123     geom=*igeom;
124     geom.h=menu->entry_h;
125     geom.y+=(i-menu->first_entry)*(menu->entry_h+menu->entry_spacing);
126     
127     grbrush_begin(menu->entry_brush, &geom, GRBRUSH_AMEND|GRBRUSH_KEEP_ATTR);
128     
129     grbrush_init_attr(menu->entry_brush, &menu->entries[i].attr);
130     
131     grbrush_set_attr(menu->entry_brush, aa);
132     grbrush_set_attr(menu->entry_brush, sa);
133
134     grbrush_draw_textbox(menu->entry_brush, &geom, menu->entries[i].title, 
135                          complete);
136     
137     grbrush_end(menu->entry_brush);
138 }
139
140     
141 void menu_draw_entries(WMenu *menu, bool complete)
142 {
143     WRectangle igeom;
144     int i, mx;
145     
146     if(menu->entry_brush==NULL)
147         return;
148         
149     get_inner_geom(menu, &igeom);
150     
151     mx=menu->first_entry+menu->vis_entries;
152     mx=(mx < menu->n_entries ? mx : menu->n_entries);
153     
154     for(i=menu->first_entry; i<mx; i++)
155         menu_draw_entry(menu, i, &igeom, complete);
156 }
157
158
159 void menu_draw(WMenu *menu, bool complete)
160 {
161     GrAttr aa=(REGION_IS_ACTIVE(menu) ? GR_ATTR(active) : GR_ATTR(inactive));
162     WRectangle geom;
163     
164     if(menu->brush==NULL)
165         return;
166     
167     get_outer_geom(menu, &geom);
168     
169     grbrush_begin(menu->brush, &geom, 
170                   (complete ? 0 : GRBRUSH_NO_CLEAR_OK));
171     
172     grbrush_set_attr(menu->brush, aa);
173     
174     grbrush_draw_border(menu->brush, &geom);
175     
176     menu_draw_entries(menu, FALSE);
177     
178     grbrush_end(menu->brush);
179 }
180
181
182 /*}}}*/
183
184
185 /*{{{ Resize */
186
187
188 static void menu_calc_size(WMenu *menu, bool maxexact, 
189                            int maxw, int maxh, 
190                            int *w_ret, int *h_ret)
191 {
192     GrBorderWidths bdw, e_bdw;
193     char *str;
194     int i;
195     int nath, bdh, maxew=menu->max_entry_w;
196     
197     grbrush_get_border_widths(menu->brush, &bdw);
198     grbrush_get_border_widths(menu->entry_brush, &e_bdw);
199     
200     if(maxexact || maxew>maxw-(int)bdw.left-(int)bdw.right){
201         maxew=maxw-bdw.left-bdw.right;
202         *w_ret=maxw;
203     }else{
204         *w_ret=maxew+bdw.left+bdw.right;
205     }
206     
207     bdh=bdw.top+bdw.bottom;
208     
209     if(menu->n_entries==0){
210         *h_ret=(maxexact ? maxh : bdh);
211         menu->first_entry=0;
212         menu->vis_entries=0;
213     }else{
214         int vis=(maxh-bdh+e_bdw.spacing)/(e_bdw.spacing+menu->entry_h);
215         if(vis>menu->n_entries){
216             vis=menu->n_entries;
217             menu->first_entry=0;
218         }else if(menu->selected_entry>=0){
219             if(menu->selected_entry<menu->first_entry)
220                 menu->first_entry=menu->selected_entry;
221             else if(menu->selected_entry>=menu->first_entry+vis)
222                 menu->first_entry=menu->selected_entry-vis+1;
223         }
224         if(vis<=0)
225             vis=1;
226         menu->vis_entries=vis;
227         if(maxexact)
228             *h_ret=maxh;
229         else
230             *h_ret=vis*menu->entry_h+(vis-1)*e_bdw.spacing+bdh;
231     }
232
233     /* Calculate new shortened entry names */
234     maxew-=e_bdw.left+e_bdw.right;
235     
236     for(i=0; i<menu->n_entries; i++){
237         if(menu->entries[i].title){
238             free(menu->entries[i].title);
239             menu->entries[i].title=NULL;
240         }
241         if(maxew<=0)
242             continue;
243         
244         if(extl_table_getis(menu->tab, i+1, "name", 's', &str)){
245             menu->entries[i].title=grbrush_make_label(menu->entry_brush, 
246                                                       str, maxew);
247             free(str);
248         }
249     }
250 }
251
252
253 void calc_size(WMenu *menu, int *w, int *h)
254 {
255     if(menu->pmenu_mode){
256         menu_calc_size(menu, FALSE, INT_MAX, INT_MAX, w, h);
257     }else{
258         menu_calc_size(menu, !(menu->last_fp.mode&REGION_FIT_BOUNDS),
259                        menu->last_fp.g.w, menu->last_fp.g.h, w, h);
260     }
261 }
262     
263
264 /* Return offset from bottom-left corner of containing mplex or top-right
265  * corner of  parent menu for the respective corner of menu.
266  */
267 static void get_placement_offs(WMenu *menu, int *xoff, int *yoff)
268 {
269     GrBorderWidths bdw;
270     
271     *xoff=0; 
272     *yoff=0;
273     
274     if(menu->brush!=NULL){
275         grbrush_get_border_widths(menu->brush, &bdw);
276         *xoff+=bdw.right;
277         *yoff+=bdw.top;
278     }
279     
280     if(menu->entry_brush!=NULL){
281         grbrush_get_border_widths(menu->entry_brush, &bdw);
282         *xoff+=bdw.right;
283         *yoff+=bdw.top;
284     }
285 }
286     
287     
288 #define MINIMUM_Y_VISIBILITY 20
289 #define POINTER_OFFSET 5
290
291 static void menu_firstfit(WMenu *menu, bool submenu, const WRectangle *refg)
292 {
293     WRectangle geom;
294     
295     calc_size(menu, &(geom.w), &(geom.h));
296     
297     if(!(menu->last_fp.mode&REGION_FIT_BOUNDS)){
298         geom.x=menu->last_fp.g.x;
299         geom.y=menu->last_fp.g.y;
300     }else if(menu->pmenu_mode){
301         geom.x=refg->x;
302         geom.y=refg->y;
303         
304         if(!submenu){
305             const WRectangle *maxg = 
306                 &REGION_GEOM(REGION_PARENT((WRegion*)menu));
307             
308             geom.x-=geom.w/2;
309             geom.y+=POINTER_OFFSET;
310
311             if(geom.y+MINIMUM_Y_VISIBILITY>maxg->y+maxg->h){
312                 geom.y=maxg->y+maxg->h-MINIMUM_Y_VISIBILITY;
313                 geom.x=refg->x+POINTER_OFFSET;
314                 if(geom.x+geom.w>maxg->x+maxg->w)
315                     geom.x=refg->x-geom.w-POINTER_OFFSET;
316             }else{
317                 if(geom.x<0)
318                     geom.x=0;
319                 else if(geom.x+geom.w>maxg->x+maxg->w)
320                     geom.x=maxg->x+maxg->w-geom.w;
321             }
322         }
323     }else{
324         const WRectangle *maxg=&(menu->last_fp.g);
325         if(submenu){
326             int l, r, t, b, xoff, yoff;
327             
328             get_placement_offs(menu, &xoff, &yoff);
329             l=refg->x+xoff;
330             r=refg->x+refg->w+xoff;
331             t=refg->y-yoff;
332             b=refg->y+refg->h-yoff;
333             
334             geom.x=maxof(l, r-geom.w);
335             if(geom.x+geom.w>maxg->x+maxg->w)
336                 geom.x=maxg->x;
337
338             geom.y=minof(b-geom.h, t);
339             if(geom.y<maxg->y)
340                 geom.y=maxg->y;
341         }else{
342             geom.x=maxg->x;
343             geom.y=maxg->y+maxg->h-geom.h;
344         }
345     }
346     
347     window_do_fitrep(&menu->win, NULL, &geom);
348 }
349
350
351 static void menu_do_refit(WMenu *menu, WWindow *par, const WFitParams *oldfp)
352 {
353     WRectangle geom;
354     
355     calc_size(menu, &(geom.w), &(geom.h));
356     
357     if(!(menu->last_fp.mode&REGION_FIT_BOUNDS)){
358         geom.x=menu->last_fp.g.x;
359         geom.y=menu->last_fp.g.y;
360     }else if(menu->pmenu_mode){
361         geom.x=REGION_GEOM(menu).x;
362         geom.y=REGION_GEOM(menu).y;
363     }else{
364         const WRectangle *maxg=&(menu->last_fp.g);
365         int xdiff=REGION_GEOM(menu).x-oldfp->g.x;
366         int ydiff=(REGION_GEOM(menu).y+REGION_GEOM(menu).h
367                    -(oldfp->g.y+oldfp->g.h));
368         geom.x=maxof(0, minof(maxg->x+xdiff, maxg->x+maxg->w-geom.w));
369         geom.y=maxof(0, minof(maxg->y+maxg->h+ydiff, maxg->y+maxg->h)-geom.h);
370     }
371     
372     window_do_fitrep(&menu->win, par, &geom);
373 }
374
375
376 bool menu_fitrep(WMenu *menu, WWindow *par, const WFitParams *fp)
377 {
378     WFitParams oldfp;
379     
380     if(par!=NULL && !region_same_rootwin((WRegion*)par, (WRegion*)menu))
381         return FALSE;
382     
383     oldfp=menu->last_fp;
384     menu->last_fp=*fp;
385     menu_do_refit(menu, par, &oldfp);
386     
387     if(menu->submenu!=NULL && !menu->pmenu_mode)
388         region_fitrep((WRegion*)(menu->submenu), par, fp);
389     
390     return TRUE;
391 }
392
393
394 /*}}}*/
395
396
397 /*{{{ Brush update */
398
399
400 static void calc_entry_dimens(WMenu *menu)
401 {
402     int i, n=extl_table_get_n(menu->tab);
403     GrFontExtents fnte;
404     GrBorderWidths bdw;
405     int maxw=0;
406     char *str;
407     
408 #if 0    
409     if(extl_table_gets_s(menu->tab, title, &str)){
410         maxw=grbrush_get_text_width(title_brush, str, strlen(str));
411         free(str);
412     }
413 #endif
414     
415     for(i=1; i<=n; i++){
416         if(extl_table_getis(menu->tab, i, "name", 's', &str)){
417             int w=grbrush_get_text_width(menu->entry_brush, 
418                                          str, strlen(str));
419             if(w>maxw)
420                 maxw=w;
421             free(str);
422         }
423     }
424     
425     grbrush_get_border_widths(menu->entry_brush, &bdw);
426     grbrush_get_font_extents(menu->entry_brush, &fnte);
427     
428     menu->max_entry_w=maxw+bdw.left+bdw.right;
429     menu->entry_h=fnte.max_height+bdw.top+bdw.bottom;
430     menu->entry_spacing=bdw.spacing;
431 }
432
433
434 static bool menu_init_gr(WMenu *menu, WRootWin *rootwin, Window win)
435 {
436     GrBrush *brush, *entry_brush;
437     char *st;
438     const char *style=(menu->big_mode 
439                        ? "input-menu-big"
440                        : (menu->pmenu_mode
441                           ? "input-menu-pmenu"
442                           : "input-menu-normal"));
443
444     const char *entry_style=(menu->big_mode
445                                    ? "tab-menuentry-big"
446                                    : (menu->pmenu_mode
447                                       ? "tab-menuentry-pmenu"
448                                       : "tab-menuentry-normal"));
449     
450     brush=gr_get_brush(win, rootwin, style);
451     
452     if(brush==NULL)
453         return FALSE;
454
455     entry_brush=grbrush_get_slave(brush, rootwin, entry_style);
456     
457     if(entry_brush==NULL){
458         grbrush_release(brush);
459         return FALSE;
460     }
461     
462     if(menu->entry_brush!=NULL)
463         grbrush_release(menu->entry_brush);
464     if(menu->brush!=NULL)
465         grbrush_release(menu->brush);
466
467     menu->brush=brush;
468     menu->entry_brush=entry_brush;
469     
470     calc_entry_dimens(menu);
471     
472     return TRUE;
473 }
474
475
476 void menu_updategr(WMenu *menu)
477 {
478     if(!menu_init_gr(menu, region_rootwin_of((WRegion*)menu),
479                      MENU_WIN(menu))){
480         return;
481     }
482     
483     menu_do_refit(menu, NULL, &(menu->last_fp));
484     
485     region_updategr_default((WRegion*)menu);
486     
487     window_draw((WWindow*)menu, TRUE);
488 }
489
490
491 static void menu_release_gr(WMenu *menu)
492 {
493     if(menu->entry_brush!=NULL){
494         grbrush_release(menu->entry_brush);
495         menu->entry_brush=NULL;
496     }
497     if(menu->brush!=NULL){
498         grbrush_release(menu->brush);
499         menu->brush=NULL;
500     }
501 }
502
503
504 /*}}}*/
505
506
507 /*{{{ Init/deinit */
508
509
510 static WMenuEntry *preprocess_menu(ExtlTab tab, int *n_entries)
511 {
512     WMenuEntry *entries;
513     ExtlTab entry;
514     int i, n;
515     
516     n=extl_table_get_n(tab);
517     *n_entries=n;
518     
519     if(n<=0)
520         return NULL;
521
522     entries=ALLOC_N(WMenuEntry, n);  
523     
524     if(entries==NULL)
525         return NULL;
526         
527     init_attr();
528     
529     /* Initialise entries and check submenus */
530     for(i=1; i<=n; i++){
531         WMenuEntry *ent=&entries[i-1];
532         
533         ent->title=NULL;
534         ent->flags=0;
535         
536         gr_stylespec_init(&ent->attr);
537         
538         if(extl_table_geti_t(tab, i, &entry)){
539             char *attr;
540             ExtlTab sub;
541             ExtlFn fn;
542             
543             if(extl_table_gets_s(entry, "attr", &attr)){
544                 gr_stylespec_load_(&ent->attr, attr, TRUE);
545                 free(attr);
546             }
547             
548             if(extl_table_gets_f(entry, "submenu_fn", &fn)){
549                 ent->flags|=WMENUENTRY_SUBMENU;
550                 extl_unref_fn(fn);
551             }else if(extl_table_gets_t(entry, "submenu", &sub)){
552                 ent->flags|=WMENUENTRY_SUBMENU;
553                 extl_unref_table(sub);
554             }
555             
556             if(ent->flags&WMENUENTRY_SUBMENU)
557                 gr_stylespec_set(&ent->attr, GR_ATTR(submenu));
558             
559             extl_unref_table(entry);
560         }
561     }
562     
563     return entries;
564 }
565
566
567 static void deinit_entries(WMenu *menu);
568
569
570 bool menu_init(WMenu *menu, WWindow *par, const WFitParams *fp,
571                const WMenuCreateParams *params)
572 {
573     Window win;
574     int i;
575     
576     menu->entries=preprocess_menu(params->tab, &(menu->n_entries));
577     
578     if(menu->entries==NULL){
579         warn(TR("Empty menu."));
580         return FALSE;
581     }
582
583     menu->tab=extl_ref_table(params->tab);
584     menu->handler=extl_ref_fn(params->handler);
585     menu->pmenu_mode=params->pmenu_mode;
586     menu->big_mode=params->big_mode;
587
588     menu->last_fp=*fp;
589     
590     if(params->pmenu_mode){
591         menu->selected_entry=-1;
592     }else{
593         menu->selected_entry=params->initial-1;
594         if(menu->selected_entry<0)
595            menu->selected_entry=0;
596         if(params->initial > menu->n_entries)
597            menu->selected_entry=0;
598     }
599     
600     menu->max_entry_w=0;
601     menu->entry_h=0;
602     menu->brush=NULL;
603     menu->entry_brush=NULL;
604     menu->entry_spacing=0;
605     menu->vis_entries=menu->n_entries;
606     menu->first_entry=0;
607     menu->submenu=NULL;
608     menu->typeahead=NULL;
609     
610     menu->gm_kcb=0;
611     menu->gm_state=0;
612     
613     if(!window_init((WWindow*)menu, par, fp))
614         goto fail;
615
616     win=menu->win.win;
617     
618     if(!menu_init_gr(menu, region_rootwin_of((WRegion*)par), win))
619         goto fail2;
620         
621     init_attr();
622     
623     menu_firstfit(menu, params->submenu_mode, &(params->refg));
624     
625     window_select_input(&(menu->win), IONCORE_EVENTMASK_NORMAL);
626     
627     region_add_bindmap((WRegion*)menu, mod_menu_menu_bindmap);
628     
629     region_register((WRegion*)menu);
630     
631     return TRUE;
632
633 fail2:
634     window_deinit((WWindow*)menu);
635 fail:
636     extl_unref_table(menu->tab);
637     extl_unref_fn(menu->handler);
638     deinit_entries(menu);
639     return FALSE;
640 }
641
642
643 WMenu *create_menu(WWindow *par, const WFitParams *fp, 
644                    const WMenuCreateParams *params)
645 {
646     CREATEOBJ_IMPL(WMenu, menu, (p, par, fp, params));
647 }
648
649
650 static void deinit_entries(WMenu *menu)
651 {
652     int i;
653     
654     for(i=0; i<menu->n_entries; i++){
655         gr_stylespec_unalloc(&menu->entries[i].attr);
656         if(menu->entries[i].title!=NULL)
657             free(menu->entries[i].title);
658     }
659     
660     free(menu->entries);
661 }
662
663
664 void menu_deinit(WMenu *menu)
665 {
666     menu_typeahead_clear(menu);
667     
668     if(menu->submenu!=NULL)
669         destroy_obj((Obj*)menu->submenu);
670     
671     extl_unref_table(menu->tab);
672     extl_unref_fn(menu->handler);
673     
674     deinit_entries(menu);
675     
676     menu_release_gr(menu);
677     
678     window_deinit((WWindow*)menu);
679 }
680
681
682 /*}}}*/
683
684
685 /*{{{ Focus  */
686
687
688 static void menu_inactivated(WMenu *menu)
689 {
690     window_draw((WWindow*)menu, FALSE);
691 }
692
693
694 static void menu_activated(WMenu *menu)
695 {
696     window_draw((WWindow*)menu, FALSE);
697 }
698
699
700 /*}}}*/
701
702
703 /*{{{ Submenus */
704
705
706 static WMenu *menu_head(WMenu *menu)
707 {
708     WMenu *m=REGION_MANAGER_CHK(menu, WMenu);
709     return (m==NULL ? menu : menu_head(m));
710 }
711
712
713 static WMenu *menu_tail(WMenu *menu)
714 {
715     return (menu->submenu==NULL ? menu : menu_tail(menu->submenu));
716 }
717
718
719 static void menu_managed_remove(WMenu *menu, WRegion *sub)
720 {
721     bool mcf=region_may_control_focus((WRegion*)menu);
722     
723     if(sub!=(WRegion*)menu->submenu)
724         return;
725     
726     menu->submenu=NULL;
727
728     region_unset_manager(sub, (WRegion*)menu);
729     
730     if(mcf)
731         region_do_set_focus((WRegion*)menu, FALSE);
732 }
733
734
735 int get_sub_y_off(WMenu *menu, int n)
736 {
737     /* top + sum of above entries and spacings - top_of_sub */
738     return (menu->entry_h+menu->entry_spacing)*(n-menu->first_entry);
739 }
740
741
742 static void show_sub(WMenu *menu, int n)
743 {
744     WFitParams fp;
745     WMenuCreateParams fnp;
746     WMenu *submenu;
747     WWindow *par;
748     
749     par=REGION_PARENT(menu);
750     
751     if(par==NULL)
752         return;
753     
754     fp=menu->last_fp;
755
756     fnp.pmenu_mode=menu->pmenu_mode;
757     fnp.big_mode=menu->big_mode;
758     fnp.submenu_mode=TRUE;
759     
760     if(menu->pmenu_mode){
761         fnp.refg.x=REGION_GEOM(menu).x+REGION_GEOM(menu).w;
762         fnp.refg.y=REGION_GEOM(menu).y+get_sub_y_off(menu, n);
763         fnp.refg.w=0;
764         fnp.refg.h=0;
765     }else{
766         fnp.refg=REGION_GEOM(menu);
767     }
768
769     fnp.tab=extl_table_none();
770     {
771         ExtlFn fn;
772         if(extl_table_getis(menu->tab, n+1, "submenu_fn", 'f', &fn)){
773             extl_protect(NULL);
774             extl_call(fn, NULL, "t", &(fnp.tab));
775             extl_unprotect(NULL);
776             extl_unref_fn(fn);
777         }else{
778             extl_table_getis(menu->tab, n+1, "submenu", 't', &(fnp.tab));
779         }
780         if(fnp.tab==extl_table_none())
781             return;
782     }
783     
784     fnp.handler=extl_ref_fn(menu->handler);
785     
786     fnp.initial=0;
787     {
788         ExtlFn fn;
789         if(extl_table_getis(menu->tab, n+1, "initial", 'f', &fn)){
790             extl_protect(NULL);
791             extl_call(fn, NULL, "i", &(fnp.initial));
792             extl_unprotect(NULL);
793             extl_unref_fn(fn);
794         }else{
795             extl_table_getis(menu->tab, n+1, "initial", 'i', &(fnp.initial));
796         }
797     }
798
799     submenu=create_menu(par, &fp, &fnp);
800     
801     if(submenu==NULL)
802         return;
803     
804     menu->submenu=submenu;
805     region_set_manager((WRegion*)submenu, (WRegion*)menu);
806         
807     region_restack((WRegion*)submenu, MENU_WIN(menu), Above);
808     region_map((WRegion*)submenu);
809     
810     if(!menu->pmenu_mode && region_may_control_focus((WRegion*)menu))
811         region_do_set_focus((WRegion*)submenu, FALSE);
812 }
813
814
815 static void menu_do_set_focus(WMenu *menu, bool warp)
816 {
817     if(menu->submenu!=NULL)
818         region_do_set_focus((WRegion*)menu->submenu, warp);
819     else
820         window_do_set_focus((WWindow*)menu, warp);
821 }
822
823
824 void menu_restack(WMenu *menu, Window other, int mode)
825 {
826     xwindow_restack(MENU_WIN(menu), other, mode);
827     if(menu->submenu!=NULL)
828         region_restack((WRegion*)(menu->submenu), MENU_WIN(menu), Above);
829 }
830
831
832 void menu_stacking(WMenu *menu, Window *bottomret, Window *topret)
833 {
834     *topret=None;
835     
836     if(menu->submenu!=NULL)
837         region_stacking((WRegion*)(menu->submenu), bottomret, topret);
838     
839     *bottomret=MENU_WIN(menu);
840     if(*topret==None)
841         *topret=MENU_WIN(menu);
842
843 }
844
845     
846 /*}}}*/
847
848
849 /*{{{ Exports */
850
851
852 static void menu_do_select_nth(WMenu *menu, int n)
853 {
854     int oldn=menu->selected_entry;
855     bool drawfull=FALSE;
856     
857     if(oldn==n)
858         return;
859     
860     if(menu->submenu!=NULL)
861         destroy_obj((Obj*)menu->submenu);
862     
863     assert(menu->submenu==NULL);
864
865     menu->selected_entry=n;
866
867     if(n>=0){
868         if(n<menu->first_entry){
869             menu->first_entry=n;
870             drawfull=TRUE;
871         }else if(n>=menu->first_entry+menu->vis_entries){
872             menu->first_entry=n-menu->vis_entries+1;
873             drawfull=TRUE;
874         }
875         
876         if(menu->entries[n].flags&WMENUENTRY_SUBMENU &&
877            menu->pmenu_mode){
878             show_sub(menu, n);
879         }
880     }
881     
882     if(drawfull){
883         menu_draw_entries(menu, TRUE);
884     }else{
885         /* redraw new and old selected entry */ 
886         WRectangle igeom;
887         get_inner_geom(menu, &igeom);
888
889         /* !!!BEGIN!!! */
890         if(oldn!=-1)
891             menu_draw_entry(menu, oldn, &igeom, TRUE);
892         if(n!=-1)
893             menu_draw_entry(menu, n, &igeom, TRUE);
894     }
895 }
896
897
898 /*EXTL_DOC
899  * Select \var{n}:th entry in menu.
900  */
901 EXTL_EXPORT_MEMBER
902 void menu_select_nth(WMenu *menu, int n)
903 {
904     if(n<0)
905         n=0;
906     if(n>=menu->n_entries)
907         n=menu->n_entries-1;
908
909     menu_typeahead_clear(menu);
910     menu_do_select_nth(menu, n);
911 }
912
913
914 /*EXTL_DOC
915  * Select previous entry in menu.
916  */
917 EXTL_EXPORT_MEMBER
918 void menu_select_prev(WMenu *menu)
919 {
920     menu_select_nth(menu, (menu->selected_entry<=0 
921                            ? menu->n_entries-1
922                            : menu->selected_entry-1));
923 }
924
925
926 /*EXTL_DOC
927  * Select next entry in menu.
928  */
929 EXTL_EXPORT_MEMBER
930 void menu_select_next(WMenu *menu)
931 {
932     menu_select_nth(menu, (menu->selected_entry+1)%menu->n_entries);
933 }
934
935
936 static void menu_do_finish(WMenu *menu)
937 {
938     ExtlFn handler;
939     ExtlTab tab;
940     bool ok;
941     WMenu *head=menu_head(menu);
942     
943     handler=menu->handler;
944     menu->handler=extl_fn_none();
945     
946     ok=extl_table_geti_t(menu->tab, menu->selected_entry+1, &tab);
947     
948     if(region_manager_allows_destroying((WRegion*)head))
949         destroy_obj((Obj*)head);
950     else if(head->submenu!=NULL)
951         destroy_obj((Obj*)head->submenu);
952     
953     if(ok)
954         extl_call(handler, "t", NULL, tab);
955
956     extl_unref_fn(handler);
957     extl_unref_table(tab);
958 }
959
960
961 /*EXTL_DOC
962  * If selected entry is a submenu, display that.
963  * Otherwise destroy the menu and call handler for selected entry.
964  */
965 EXTL_EXPORT_MEMBER
966 void menu_finish(WMenu *menu)
967 {
968     menu_typeahead_clear(menu);
969
970     if(!menu->pmenu_mode && menu->selected_entry>=0 &&
971        menu->entries[menu->selected_entry].flags&WMENUENTRY_SUBMENU){
972         show_sub(menu, menu->selected_entry);
973         return;
974     }
975     
976     mainloop_defer_action((Obj*)menu, (WDeferredAction*)menu_do_finish);
977 }
978
979
980
981 /*EXTL_DOC
982  * Close \var{menu} not calling any possible finish handlers.
983  */
984 EXTL_EXPORT_MEMBER
985 void menu_cancel(WMenu *menu)
986 {
987     if(region_manager_allows_destroying((WRegion*)menu))
988         region_dispose_((WRegion*)menu);
989 }
990
991
992 /*}}}*/
993
994
995 /*{{{ Scroll */
996
997 static int scroll_time=20;
998 static int scroll_amount=3;
999 static WTimer *scroll_timer=NULL;
1000
1001
1002 static void reset_scroll_timer()
1003 {
1004     if(scroll_timer!=NULL){
1005         destroy_obj((Obj*)scroll_timer);
1006         scroll_timer=NULL;
1007     }
1008 }
1009
1010
1011 /*EXTL_DOC
1012  * Set module basic settings. The parameter table may contain the
1013  * following fields:
1014  * 
1015  * \begin{tabularx}{\linewidth}{lX}
1016  *  \tabhead{Field & Description}
1017  *  \var{scroll_amount} & Number of pixels to scroll at a time 
1018  *                        pointer-controlled menus when one extends
1019  *                        beyond a border of the screen and the pointer
1020  *                        touches that border. \\
1021  *  \var{scroll_delay}  & Time between such scrolling events in 
1022  *                        milliseconds.
1023  * \end{tabularx}
1024  */
1025 EXTL_EXPORT
1026 void mod_menu_set(ExtlTab tab)
1027 {
1028     int a, t;
1029     
1030     if(extl_table_gets_i(tab, "scroll_amount", &a))
1031         scroll_amount=maxof(0, a);
1032     if(extl_table_gets_i(tab, "scroll_delay", &t))
1033         scroll_time=maxof(0, t);
1034 }
1035
1036
1037 /*EXTL_DOC
1038  * Get module basic settings. For details, see \fnref{mod_menu.set}.
1039  */
1040 EXTL_SAFE
1041 EXTL_EXPORT
1042 ExtlTab mod_menu_get()
1043 {
1044     ExtlTab tab=extl_create_table();
1045     extl_table_sets_i(tab, "scroll_amount", scroll_amount);
1046     extl_table_sets_i(tab, "scroll_delay", scroll_time);
1047     return tab;
1048 }
1049
1050
1051 enum{
1052     D_LEFT,
1053     D_RIGHT,
1054     D_DOWN,
1055     D_UP
1056 };
1057
1058
1059 static int calc_diff(const WRectangle *mg, const WRectangle *pg, int d)
1060 {
1061     switch(d){
1062     case D_LEFT:
1063         return mg->x+mg->w-pg->w;
1064     case D_UP:
1065         return mg->y+mg->h-pg->h;
1066     case D_RIGHT:
1067         return -mg->x;
1068     case D_DOWN:
1069         return -mg->y;
1070     }
1071     return 0;
1072 }
1073
1074
1075 static int scrolld_subs(WMenu *menu, int d)
1076 {
1077     int diff=0;
1078     WRegion *p=REGION_PARENT_REG(menu);
1079     const WRectangle *pg;
1080     
1081     if(p==NULL)
1082         return 0;
1083
1084     pg=&REGION_GEOM(p);
1085     
1086     while(menu!=NULL){
1087         diff=maxof(diff, calc_diff(&REGION_GEOM(menu), pg, d));
1088         menu=menu->submenu;
1089     }
1090     
1091     return minof(maxof(0, diff), scroll_amount);
1092 }
1093
1094
1095 static void menu_select_entry_at(WMenu *menu, int px, int py);
1096
1097
1098 static void do_scroll(WMenu *menu, int xd, int yd)
1099 {
1100     WRectangle g;
1101     int px=-1, py=-1;
1102     
1103     xwindow_pointer_pos(region_root_of((WRegion*)menu), &px, &py);
1104
1105     while(menu!=NULL){
1106         g=REGION_GEOM(menu);
1107         g.x+=xd;
1108         g.y+=yd;
1109         
1110         window_do_fitrep((WWindow*)menu, NULL, &g);
1111         
1112         menu_select_entry_at(menu, px, py);
1113
1114         menu=menu->submenu;
1115     }
1116 }
1117
1118
1119 static void scroll_left(WTimer *timer, WMenu *menu)
1120 {
1121     if(menu!=NULL){
1122         do_scroll(menu, -scrolld_subs(menu, D_LEFT), 0);
1123         if(scrolld_subs(menu, D_LEFT)>0){
1124             timer_set(timer, scroll_time, (WTimerHandler*)scroll_left,
1125                       (Obj*)menu);
1126             return;
1127         }
1128     }
1129 }
1130
1131
1132 static void scroll_up(WTimer *timer, WMenu *menu)
1133 {
1134     if(menu!=NULL){
1135         do_scroll(menu, 0, -scrolld_subs(menu, D_UP));
1136         if(scrolld_subs(menu, D_UP)>0){
1137             timer_set(timer, scroll_time, (WTimerHandler*)scroll_up,
1138                       (Obj*)menu);
1139
1140             return;
1141         }
1142     }
1143 }
1144
1145
1146 static void scroll_right(WTimer *timer, WMenu *menu)
1147 {
1148     if(menu!=NULL){
1149         do_scroll(menu, scrolld_subs(menu, D_RIGHT), 0);
1150         if(scrolld_subs(menu, D_RIGHT)>0){
1151             timer_set(timer, scroll_time, (WTimerHandler*)scroll_right,
1152                       (Obj*)menu);
1153             return;
1154         }
1155     }
1156 }
1157
1158
1159 static void scroll_down(WTimer *timer, WMenu *menu)
1160 {
1161     if(menu!=NULL){
1162         do_scroll(menu, 0, scrolld_subs(menu, D_DOWN));
1163         if(scrolld_subs(menu, D_DOWN)>0){
1164             timer_set(timer, scroll_time, (WTimerHandler*)scroll_down,
1165                       (Obj*)menu);
1166             return;
1167         }
1168     }
1169 }
1170
1171
1172 static void end_scroll(WMenu *menu)
1173 {
1174     reset_scroll_timer();
1175 }
1176
1177 #define SCROLL_OFFSET 10
1178
1179 static void check_scroll(WMenu *menu, int x, int y)
1180 {
1181     WRegion *parent=REGION_PARENT_REG(menu);
1182     int rx, ry;
1183     WTimerHandler *fn=NULL;
1184
1185     if(!menu->pmenu_mode)
1186         return;
1187     
1188     if(parent==NULL){
1189         end_scroll(menu);
1190         return;
1191     }
1192
1193     region_rootpos(parent, &rx, &ry);
1194     x-=rx;
1195     y-=ry;
1196     
1197     if(x<=SCROLL_OFFSET){
1198         fn=(WTimerHandler*)scroll_right;
1199     }else if(y<=SCROLL_OFFSET){
1200         fn=(WTimerHandler*)scroll_down;
1201     }else if(x>=REGION_GEOM(parent).w-SCROLL_OFFSET){
1202         fn=(WTimerHandler*)scroll_left;
1203     }else if(y>=REGION_GEOM(parent).h-SCROLL_OFFSET){
1204         fn=(WTimerHandler*)scroll_up;
1205     }else{
1206         end_scroll(menu);
1207         return;
1208     }
1209     
1210     assert(fn!=NULL);
1211     
1212     if(scroll_timer!=NULL){
1213         if(scroll_timer->handler==(WTimerHandler*)fn &&
1214            timer_is_set(scroll_timer)){
1215             return;
1216         }
1217     }else{
1218         scroll_timer=create_timer();
1219         if(scroll_timer==NULL)
1220             return;
1221     }
1222     
1223     fn(scroll_timer, (Obj*)menu_head(menu));
1224 }
1225
1226
1227 /*}}}*/
1228
1229
1230 /*{{{ Pointer handlers */
1231
1232
1233 int menu_entry_at_root(WMenu *menu, int root_x, int root_y)
1234 {
1235     int rx, ry, x, y, entry;
1236     WRectangle ig;
1237     region_rootpos((WRegion*)menu, &rx, &ry);
1238     
1239     get_inner_geom(menu, &ig);
1240     
1241     x=root_x-rx-ig.x;
1242     y=root_y-ry-ig.y;
1243     
1244     if(x<0 || x>=ig.w || y<0  || y>=ig.h)
1245         return -1;
1246     
1247     entry=y/(menu->entry_h+menu->entry_spacing);
1248     if(entry<0 || entry>=menu->vis_entries)
1249         return -1;
1250     return entry+menu->first_entry;
1251 }
1252
1253
1254 int menu_entry_at_root_tree(WMenu *menu, int root_x, int root_y, 
1255                             WMenu **realmenu)
1256 {
1257     int entry=-1;
1258     
1259     menu=menu_tail(menu);
1260     
1261     *realmenu=menu;
1262     
1263     if(!menu->pmenu_mode)
1264         return menu_entry_at_root(menu, root_x, root_y);
1265     
1266     while(menu!=NULL){
1267         entry=menu_entry_at_root(menu, root_x, root_y);
1268         if(entry>=0){
1269             *realmenu=menu;
1270             break;
1271         }
1272         menu=REGION_MANAGER_CHK(menu, WMenu);
1273     }
1274     
1275     return entry;
1276 }
1277
1278
1279 static void menu_select_entry_at(WMenu *menu, int px, int py)
1280 {
1281     int entry=menu_entry_at_root_tree(menu, px, py, &menu);
1282     if(entry>=0)
1283         menu_do_select_nth(menu, entry);
1284 }
1285
1286
1287 void menu_release(WMenu *menu, XButtonEvent *ev)
1288 {
1289     int entry=menu_entry_at_root_tree(menu, ev->x_root, ev->y_root, &menu);
1290     end_scroll(menu);
1291     if(entry>=0){
1292         menu_select_nth(menu, entry);
1293         menu_finish(menu);
1294     }else if(menu->pmenu_mode){
1295         menu_cancel(menu_head(menu));
1296     }
1297 }
1298
1299
1300 void menu_motion(WMenu *menu, XMotionEvent *ev, int dx, int dy)
1301 {
1302     menu_select_entry_at(menu, ev->x_root, ev->y_root);
1303     check_scroll(menu, ev->x_root, ev->y_root);
1304 }
1305
1306
1307 void menu_button(WMenu *menu, XButtonEvent *ev)
1308 {
1309     int entry=menu_entry_at_root_tree(menu, ev->x_root, ev->y_root, &menu);
1310     if(entry>=0)
1311         menu_select_nth(menu, entry);
1312 }
1313
1314
1315 int menu_press(WMenu *menu, XButtonEvent *ev, WRegion **reg_ret)
1316 {
1317     menu_button(menu, ev);
1318     menu=menu_head(menu);
1319     ioncore_set_drag_handlers((WRegion*)menu,
1320                         NULL,
1321                         (WMotionHandler*)menu_motion,
1322                         (WButtonHandler*)menu_release,
1323                         NULL, 
1324                         NULL);
1325     return 0;
1326 }
1327
1328
1329 /*}}}*/
1330
1331
1332 /*{{{ Typeahead find */
1333
1334
1335 static void menu_insstr(WMenu *menu, const char *buf, size_t n)
1336 {
1337     size_t oldlen=(menu->typeahead==NULL ? 0 : strlen(menu->typeahead));
1338     char *newta=(char*)malloc(oldlen+n+1);
1339     char *newta_orig;
1340     int entry;
1341     
1342     if(newta==NULL)
1343         return;
1344     
1345     if(oldlen!=0)
1346         memcpy(newta, menu->typeahead, oldlen);
1347     if(n!=0)
1348         memcpy(newta+oldlen, buf, n);
1349     newta[oldlen+n]='\0';
1350     newta_orig=newta;
1351     
1352     while(*newta!='\0'){
1353         bool found=FALSE;
1354         entry=menu->selected_entry;
1355         do{
1356             if(menu->entries[entry].title!=NULL){
1357                 size_t l=strlen(menu->entries[entry].title);
1358                 if(libtu_strcasestr(menu->entries[entry].title, newta)){
1359                     found=TRUE;
1360                     break;
1361                 }
1362             }
1363             entry=(entry+1)%menu->n_entries;
1364         }while(entry!=menu->selected_entry);
1365         if(found){
1366             menu_do_select_nth(menu, entry);
1367             break;
1368         }
1369         newta++;
1370     }
1371     
1372     if(newta_orig!=newta){
1373         if(*newta=='\0'){
1374             free(newta_orig);
1375             newta=NULL;
1376         }else{
1377             char *p=scopy(newta);
1378             free(newta_orig);
1379             newta=p;
1380         }
1381     }
1382     if(menu->typeahead!=NULL)
1383         free(menu->typeahead);
1384     menu->typeahead=newta;
1385 }
1386
1387
1388 /*EXTL_DOC
1389  * Clear typeahead buffer.
1390  */
1391 EXTL_EXPORT_MEMBER
1392 void menu_typeahead_clear(WMenu *menu)
1393 {
1394     if(menu->typeahead!=NULL){
1395         free(menu->typeahead);
1396         menu->typeahead=NULL;
1397     }
1398 }
1399
1400
1401 /*}}}*/
1402
1403
1404 /*{{{ Dynamic function table and class implementation */
1405
1406
1407 static DynFunTab menu_dynfuntab[]={
1408     {(DynFun*)region_fitrep, (DynFun*)menu_fitrep},
1409     {region_updategr, menu_updategr},
1410     {window_draw, menu_draw},
1411     {(DynFun*)window_press, (DynFun*)menu_press},
1412     {region_managed_remove, menu_managed_remove},
1413     {region_do_set_focus, menu_do_set_focus},
1414     {region_activated, menu_activated},
1415     {region_inactivated, menu_inactivated},
1416     {window_insstr, menu_insstr},
1417     {region_restack, menu_restack},
1418     {region_stacking, menu_stacking},
1419     END_DYNFUNTAB
1420 };
1421
1422
1423 EXTL_EXPORT
1424 IMPLCLASS(WMenu, WWindow, menu_deinit, menu_dynfuntab);
1425
1426     
1427 /*}}}*/
1428
1429