123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657(** a generic menu layout with submenus *)(* can be used with entries (layouts) at arbitrary locations *)(* VERSION2 *)openB_utilsopenTsdlmoduleLayout=B_layoutmoduleWidget=B_widgetmoduleAvar=B_avarmoduleChain=B_chainmoduleTimeout=B_timeoutmoduleTrigger=B_triggermoduleDraw=B_drawmoduleLabel=B_labelmoduleButton=B_buttonmodulePopup=B_popupmoduleStyle=B_stylemoduleEngine=struct(* A menu is a usual birectional tree, where each node is either terminal (a
leaf) and corresponds to a menu item with a action, or a submenu. However,
we don't really have to optimize functions for arbitrary trees, because it
will always be a very small tree (not deep).
The top of this tree is of type 'menu' and is the only one with a 'None'
parent_entry. *)typeaction=unit->unitandentry_type=|Menuofmenu|Actionofactionandentry={kind:entry_type;enabled:bool;mutableselected:bool;(* equivalent to highlighted *)layout:Layout.t;parent_menu:menu}andmenu={pos:(int*int)option;(* Relative position wrt the parent_entry *)mutableactive:bool;(* 'active' implies that the menu is shown. But a menu can be shown
without being active. Active implies that submenu will open on
mouse_over, and keyboard is active. *)mutablealways_shown:bool;(* If a menu is shown, it must be either 'active', or 'always_shown'. *)(* some menus (typically a menu bar, for instance) are always shown, but
not necessary always 'active' in the sense above. *)mutableentries:entrylist;room:Layout.t;(* the layout that contains all entries *)mutableparent_entry:entryoption(* the entry to which this menu is attached, or None if this is the top
menu. *)}letseparator=Action(fun()->print_endline"This action should not be launched.")(* 1. Functions for gearing menus interaction *)(* ------------------------------------------ *)(* The 'screen' layout is used for grabbing mouse even outside of the menus
themselves. Used for closing menus when clicking outside. *)letduration=200(* Duration of animations in ms. *)letscreen_enablescreen=print_endline"ENABLE";Layout.set_showscreentrueletscreen_disablescreen=print_endline"DISABLE";Layout.set_showscreenfalseletentry_is_openentry=matchentry.kindwith|Action_->false|Menumenu->menu.active||menu.always_shownletset_entry_bg?bgentry=ifentry.enabledthenLayout.set_backgroundentry.layoutbg(* the entry below mouse should always be highlighted. But we also highlight
the parent of each open menu. *)lethighlight_entry?(bg=Layout.SolidDraw.(opaquemenu_hl_color))entry=set_entry_bg~bgentry;entry.selected<-trueletreset_entry?(bg=Layout.SolidDraw.(opaquemenu_bg_color))entry=set_entry_bg~bgentry;entry.selected<-false(* Iter menu downwards *)letreciterfmenu=fmenu;List.iter(funentry->matchentry.kindwith|Action_->()|Menusubmenu->iterfsubmenu)menu.entries(* not used *)letadd_submenus_to_dst~dstmenu=letfmenu=Layout.add_room~dstmenu.room;ifnotmenu.active&¬menu.always_shownthenLayout.set_showmenu.roomfalseinList.iter(funentry->matchentry.kindwith|Action_->()|Menusubmenu->iterfsubmenu)menu.entries(* Inserts all layouts inside 'dst' at the proper position. Should be done
only once, otherwise the 'repeated widgets' error will appear. *)letadd_menu_to_dst~dstmenu=letfmenu=Layout.add_room~dstmenu.room;do_optionmenu.pos(fun(dx,dy)->letx,y=matchmenu.parent_entrywith|None->0,0|Someentry->letm=entry.parent_menu.roominletx0,y0=Layout.(getxm,getym)inletdx0,dy0=Layout.(getxentry.layout,getyentry.layout)inx0+dx0,y0+dy0inLayout.setxmenu.room(x+dx);Layout.setymenu.room(y+dy));ifnotmenu.active&¬menu.always_shownthenLayout.set_showmenu.roomfalseiniterfmenuletadd_menu_to_layermenulayer=letfmenu=Layout.global_set_layermenu.roomlayeriniterfmenu(* Return the top menu *)letrectopmenu=print_endline"TOP";matchmenu.parent_entrywith|None->menu|Someentry->topentry.parent_menuletis_topmenu=menu.parent_entry=None(* Search the top tree for the first (which should be unique) entry of Action
kind which is 'selected'. Is there a simpler way to loop? *)letselected_action_entrymenu=letrecmenuloopmenu=letcheckentry=ifentry.selectedthenmatchentry.kindwith|Action_->Someentry|Menumenu->menuloopmenuelseNoneinletrecentriesloop=function|[]->None|e::rest->matchcheckewith|Somee'->Somee'|None->entrieslooprestinentriesloopmenu.entriesinmenuloop(topmenu)(* use this for opening menus, not for closing *)letnew_timeout,clear_timeout=lett=refNonein(* there is only one global timeout variable because we assume only one user
can use only one menu at a time... *)(functionaction->do_option!tTimeout.cancel;t:=Some(Timeout.add150action)),(function()->do_option!tTimeout.cancel)letshowscreenmenu=screen_enablescreen;Layout.show~durationmenu.room;(* Layout.rec_set_show true menu.room; *)Layout.fade_in~durationmenu.roomletactivate?(timeout=false)screenmenu=ifmenu.activethen()elsebeginifnotmenu.always_showntheniftimeoutthennew_timeout(fun()->showscreenmenu)elseshowscreenmenu;menu.active<-trueendletclose?(timeout=false)screenmenu=print_endline"CLOSE";(* If the parent of this menu is the top menu, this should mean that we have
no other open menus. We can disable the screen. *)do_optionmenu.parent_entry(fune->ifis_tope.parent_menuthenscreen_disablescreen;reset_entrye);ifnotmenu.always_shown&&menu.activethenbeginmenu.active<-false;clear_timeout();letaction()=Layout.hide~duration~towards:Avar.Topmenu.room;(* il y peut y avoir des bugs qd on ouvre/ferme vite. *)Layout.fade_out~durationmenu.roominiftimeoutthenignore(Timeout.add150action)(* put 1000 for easy debugging *)elseaction()end(* We could make it more efficient and stop going down a branch as soon as a
node is aleady closed. But a Menu tree is never very long, it's probably
not worth. *)letrecclose_children?(timeout=false)screenmenu=print_endline(Printf.sprintf"CLOSE_CHILDREN with %i ENTRIES"(List.lengthmenu.entries));List.iter(funentry->matchentry.kindwith|Action_->()|Menum->beginclose_children~timeoutscreenm;close~timeoutscreenmend)menu.entries(* Close all closable menus, and un-activate the top menu *)letclose_treescreenmenu=print_endline"CLOSE_TREE";lett=topmenuinclose_childrenscreent;t.active<-falseletclose_entry~timeoutscreenentry=matchentry.kindwith|Action_->()|Menum->close~timeoutscreenm;close_children~timeoutscreenm(* Close the other menus at the same level *)letclose_others?(timeout=false)screenentry=letmenu=entry.parent_menuinletother_entries=List.filter(fune->notLayout.(e.layout==entry.layout))menu.entriesinprint_endline(Printf.sprintf"OTHER ENTRIES = %i"(List.lengthother_entries));List.iter(close_entry~timeoutscreen)other_entries(* 2. Functions for reacting to events *)(* ----------------------------------- *)(* The behaviour we code here is more or less the same as QT/KDE apps. It's
not exactly the same as GTK apps. *)(* button_down can open/close menus. It also toggles the 'active' state of the
parent menu, which is reponsible for opening submenus on mouse over or not,
and works only if the parent menu is 'always_shown'. *)letbutton_downscreenentry=print_endline"BUTTON_DOWN";ifentry.enabledthenbeginmatchentry.kindwith|Menumenu->ifmenu.activethenbeginclose_childrenscreenentry.parent_menu;highlight_entryentry;(* because closing menu will also reset the parent
entry. We don't want this here since the mouse is
over. *)ifentry.parent_menu.always_shownthenentry.parent_menu.active<-falseendelsebeginactivatescreenmenu;activatescreenentry.parent_menuend|Action_->()(* actions are executed on button_up *)endletbutton_upscreenentry=print_endline"BUTTON_UP";(* the entry here is maybe the wrong one, because it is the one that has
'focus' in the sense of main.ml, not necessarily the highlighted entry,
due to 'drag' mechanism: if the user clicked on some entry, and then
moved to another without letting the button up. So we switch:*)letentry=default(selected_action_entryentry.parent_menu)entryinifentry.enabledthenbeginmatchentry.kindwith|Menu_->()(* menus are open/closed on button_down or mouse_over *)|Actionaction->beginletbg=Layout.SolidDraw.(opaqueButton.color_on)inreset_entry~bgentry;action();(* We use a Timeout to make the colored entry visible
longer. Warning: it is possible that the menu state be scrambled
if the user is fast enough to do things in the Timeout delay...*)ignore(Timeout.add100(fun()->close_treescreenentry.parent_menu))endend(* mouse_enter (and mouse_motion?). mouse_motion will be useful only when we
add keyboard support. PROBLEM: menu should not open when using
touch. Because when touching a new entry, both mouse_enter and button_down
are triggered... so the menu opens and then quickly closes... *)letmouse_overscreenentry=print_endline"MOUSE_OVER";ifentry.enabled&¬entry.selectedthenbeginhighlight_entryentry;close_others~timeout:truescreenentry;matchentry.kindwith|Menumenu->if(notmenu.active)&&entry.parent_menu.activethenactivate~timeout:truescreenmenu|Action_->()endletmouse_leaveentry=print_endline"MOUSE_LEAVE";ifentry.enabledthenbeginifnot(entry_is_openentry)thenreset_entryentry;ifentry.parent_menu.activethenmatchentry.kindwith|Menu_->()(* if menu.active then close screen menu *)|Action_->()end(* 3. Creation of widgets and connections. *)(* --------------------------------------- *)(* First we must coat all entry layouts using the Popup module, in order to
get the correct mouse focus. This means that menus will be drawn on a
separate layer. The coat has a widget (either Empty of Box) that will
handle the connections. *)letconnect_entryscreenlayerentry=(* 'layer' is the coating layer *)letcoat=Popup.filter_screen~layerentry.layoutin(* We need a coat to get mouse focus on the whole length of the menu entry,
not only on the area of the text itself (label). *)Layout.add_room~dst:entry.layoutcoat;(* we don't use Popup.add_screen to avoid creating too many layers. *)letwidget=Layout.widgetcoatinWidget.set_cursorwidget(Some(go(Draw.create_system_cursorSdl.System_cursor.hand)));letaction___=button_downscreenentryinletc=Widget.connect_mainwidgetwidgetactionTrigger.buttons_downinWidget.add_connectionwidgetc;letaction___=button_upscreenentryinletc=Widget.connect_mainwidgetwidgetactionTrigger.buttons_upinWidget.add_connectionwidgetc;letaction___=mouse_overscreenentryinletc=Widget.connect_mainwidgetwidgetaction[(* Trigger.E.mouse_motion; *)Trigger.mouse_enter]in(* Warning do NOT add finger_motion, it will interfere with finger_down.
TODO finger doesn't work well yet. *)Widget.add_connectionwidgetc;letaction___=mouse_leaveentryinletc=Widget.connect_mainwidgetwidgetaction[Trigger.mouse_leave]inWidget.add_connectionwidgetcletrecconnect_loopscreenlayermenu=List.iter(funentry->connect_entryscreenlayerentry;matchentry.kindwith|Menusubmenu->connect_loopscreenlayersubmenu|Action_->())menu.entries(* Init, attach the menu to a destination layout. *)letinit~dstt=letdst_layer=Chain.last(Layout.get_layerdst)inletentry_layer=Popup.new_layer_abovedst_layerinadd_menu_to_layertentry_layer;letcoating_layer=Popup.new_layer_aboveentry_layerin(* the screen is used to grab all mouse focus while the submenus are open *)letscreen=Popup.filter_screen~layer:entry_layer(* ~color:Draw.(more_transp (transp green)) *)(* DEBUG*)dstinconnect_loopscreencoating_layert;add_menu_to_dst~dstt;screen_disablescreen;Layout.add_room~dstscreen;letw=Layout.widgetscreeninWidget.on_click~click:(fun_->print_endline"CLICK SCREEN";close_treescreent(* screen_disable screen *))w;end(* Now we can make a friendly API for creating elements of the menu type. *)(* example:
let file = Tower [{label = (Text "open"); content = (Action open_in)};
etc...] in
let edit = ... in
Flat [
{label = (Text "File"); content = (Menu file)};
{label = (Text "Edit"); content = (Menu edit)};
etc... ]
*)typet=Engine.menutypeaction=unit->unittypelabel=|Textofstring|LayoutofLayout.ttypeentry={label:label;content:content}(* the content type mixes two different things: Actions and submenus. Not clean
from the point of view of the programmer, but (I think) simpler from the
public viewpoint. Thus, before working with this, we convert into the Engine
types. *)andcontent=|Actionofaction|Flatofentrylist|Towerofentrylist|Customofentrylist|Separatorletseparator={label=Text"Dummy separator label";content=Separator}lettext_margin=5(* Text to Layout. w and h are only used for text. maybe remove *)letformat_label?w?h=function|Texts->letres=Layout.resident?w?h(Widget.labels)in(* : here we cannot use a resident as is because we will need to add another
room later. we need to wrap it: *)letbackground=Layout.SolidDraw.(opaquemenu_bg_color)inLayout.flat~margins:text_margin~background[res]|Layoutl->ifLayout.has_residentlthenLayout.flat~margins:0[l]elselletadd_icon_suffixlayout=letsubmenu_icon="caret-right"in(* the icon used to indicate submenus *)letsubmenu_indicator=Layout.resident(Widget.iconsubmenu_icon)inLayout.add_room~dst:layout~valign:Draw.Center~halign:Draw.Maxsubmenu_indicatormoduleTmp=struct(* We temporarily convert to a more programmer-friendly type, before
converting to Engine.menu. *)typemenukind=|Flat|Tower|Customtypemenu={entries:tentrylist;kind:menukind}andtcontent=|Actionofaction|Menuofmenu|Separatorandtentry={label:label;(* ignored in case of Separator *)content:tcontent}(* position of the submenu wrt the parent label *)typeposition=|Below|RightOfletget_layoutentry=matchentry.labelwith|Text_->failwith"get_layout should be called only when the Layout is \
generated. BUG."|Layoutl->lletcompute_suffixentry=matchentry.contentwith|Menu{kind=Tower;_}->add_icon_suffix(get_layoutentry)|_->()(* Return a copy of the tree with all Text labels replaced by Layouts *)letreccompute_layoutsentry=letlayout=matchentry.contentwith|Separator->letbackground=Layout.SolidDraw.(opaquegrey)inLayout.empty~background~w:10~h:1()|Menu_|Action_->format_labelentry.labelinletcontent=matchentry.contentwith|Action_->entry.content|Menumenu->letentries=List.mapcompute_layoutsmenu.entriesinMenu{menuwithentries}|Separator->Separatorin{label=Layoutlayout;content}letmenu_formatter=function|Flat->(funlist->letbackground=Layout.SolidDraw.(opaquemenu_bg_color)inletshadow=Style.shadow~offset:(1,1)~size:1()inLayout.flat~margins:0~background~shadowlist)|Tower->(funlist->letshadow=Style.shadow~offset:(1,1)~size:1()inletbackground=Layout.SolidDraw.(opaquemenu_bg_color)inletl=Layout.tower~margins:0~sep:0~background~shadowlistinLayout.expand_widthl;l)|Custom->(funlist->Layout.superposelist)(* Return (x,y) option, the coordinates where the submenu should be placed
when positioned in the same layout as the parent layout. *)letsubmenu_posparentposition=letw,h=Layout.get_sizeparentinmap_optionposition(function|Below->(0,h)|RightOf->(w,0))letnext_submenu_position=function|Flat->print_endline"BELOW";SomeBelow|Tower->print_endline"RIGHTOF";SomeRightOf|Custom->print_endline"NONE";Noneletget_entries=function|Menumenu->menu.entries|_->print_endline"get_entries should be called only with Menu.";[](* Compute the room containing the menu. *)letcompute_roommenu=letlayouts=List.mapget_layoutmenu.entriesinletroom=menu_formattermenu.kindlayoutsinroom(* Convert an entry to an Engine.entry. Warning, this is not an obvious
function, because Engine.entry is bidirectional, and hence cannot be
created by a simple recursive loop. We need to use mutability: some fields
are filled in later. *)(* This should be called on a well prepared entry tree where all labels are
layouts. *)letrecentry_to_enginepositionparent_menuentry=letlayout=get_layoutentryinletkind,position=matchentry.contentwith|Actiona->Engine.Actiona,None|Separator->Engine.separator,None|Menumenu->letroom=compute_roommenuin(* Now we add the suffixes: *)ifnot(Engine.is_topparent_menu)thenList.itercompute_suffixmenu.entries;letpos=submenu_pos(get_layoutentry)positioninletengine_menu=Engine.{pos;active=false;always_shown=false;entries=[];(* will be inserted later *)room;parent_entry=None}inEngine.Menuengine_menu,next_submenu_positionmenu.kindinletengine_entry=Engine.{kind;enabled=entry.content<>Separator;selected=false;layout;parent_menu}in(* second pass to recursively insert the entries field *)let_=matchengine_entry.Engine.kindwith|Engine.Action_->()|Engine.Menumenu->menu.Engine.parent_entry<-Someengine_entry;letentries=List.map(entry_to_enginepositionmenu)(get_entriesentry.content)inmenu.Engine.entries<-entries;inengine_entry(* Create an Engine.menu from a content *)letcreate_menu=function|Action_->failwith"Cannot create a menu from an Action content."|content->letdummy_parent=Layout.empty~w:0~h:0()inletentry=compute_layouts{label=Layoutdummy_parent;content}inletparent_menu=Engine.{pos=None;active=true;always_shown=true;entries=[];room=dummy_parent;parent_entry=None}inleteentry=entry_to_engineNoneparent_menuentryinletopenEngineinletmenu=matcheentry.kindwith|Action_->failwith"An Action should not show up here. BUG."|Menumenu->menuinmenu.Engine.always_shown<-true;menu.Engine.parent_entry<-None;(* remove the dummy parent *)menu(* TO BE CONTINUED... *)end(* Convert to the Tmp type *)letreccontent_to_tmp=function|Actiona->Tmp.Actiona|Flatlist->letentries=List.mapentry_to_tmplistinTmp.(Menu{entries;kind=Flat})|Towerlist->letentries=List.mapentry_to_tmplistinTmp.(Menu{entries;kind=Tower})|Customlist->letentries=List.mapentry_to_tmplistinTmp.(Menu{entries;kind=Custom})|Separator->Tmp.Separatorandentry_to_tmpentry={Tmp.label=entry.label;Tmp.content=content_to_tmpentry.content}letlayout_of_menumenu:Layout.t=menu.Engine.roomletcreate~dstcontent=lettcontent=content_to_tmpcontentinlett=Tmp.create_menutcontentinEngine.init~dstt;letroom=layout_of_menutinlet()=matchcontentwith|Flat_->Layout.(set_widthroom(widthdst))(* if the first menu is a Flat, we assume we want a menu bar, and hence make
it fill the whole width. *)|_->()inroom