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