123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662(*********************************************************************************)(* OCaml-Stk *)(* *)(* Copyright (C) 2023-2024 INRIA All rights reserved. *)(* Author: Maxence Guesdon, INRIA Saclay *)(* *)(* This program is free software; you can redistribute it and/or modify *)(* it under the terms of the GNU General Public License as *)(* published by the Free Software Foundation, version 3 of the License. *)(* *)(* This program is distributed in the hope that it will be useful, *)(* but WITHOUT ANY WARRANTY; without even the implied warranty of *)(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *)(* GNU General Public License for more details. *)(* *)(* You should have received a copy of the GNU General Public *)(* License along with this program; if not, write to the Free Software *)(* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA *)(* 02111-1307 USA *)(* *)(* As a special exception, you have permission to link this program *)(* with the OCaml compiler and distribute executables, as long as you *)(* follow the requirements of the GNU GPL in regard to all of the *)(* software in the executable aside from the OCaml compiler. *)(* *)(* Contact: Maxence.Guesdon@inria.fr *)(* *)(*********************************************************************************)(** Menus and menubars. *)openMiscopenWidgetopenTsdl(** A menu item with a menu attached can be either
[Folded] or [Unfolded]. *)typeitem_menu_state=Folded|Unfoldedletitem_menu_state_wrapper=letto_string=function|Folded->"folded"|Unfolded->"unfolded"inletof_string=function|"folded"->Folded|"unfolded"->Unfolded|s->Ocf.invalid_value(`Strings)inOcf.Wrapper.string_to_stringof_string(**/**)moduleTItem_menu_state=structtypet=item_menu_stateletcompare=Stdlib.compareletwrapper=Someitem_menu_state_wrapperlettransition=NoneendmodulePItem_menu_state=Props.Add_prop_type(TItem_menu_state)(**/**)(** Property ["item_menu_state"] for menu state. Default is [Folded]. *)letitem_menu_state:item_menu_stateProps.prop=PItem_menu_state.mk_prop~default:Folded"item_menu_state"(** Keyboard ["shortcut"] property for a menu. *)letshortcut=Props.keystate_prop~inherited:false"shortcut"(** An entry in a menu. The child of the entry is displayed in
the menu. A (sub)menu can be attached to the item. In this case,
when the item is unfolded, its attached menu is displayed.
*)classmenuitem?classes?name?props?wdata()=object(self)inheritBin.bin?classes?name?props?wdata()assuperinheritWidget.orientedasoriented(** {2 Properties} *)(**/**)methodkind="menuitem"methodset_orientationo=oriented#set_orientationo;let(exp_w,exp_h)=matchowith|Horizontal->1,0|Vertical->0,1inProps.setpropsProps.hexpandexp_w;Props.setpropsProps.vexpandexp_h(**/**)methodshortcut=self#opt_pshortcutmethodset_shortcut=self#set_pshortcut(** {2 Submenu} *)(**/**)valmutablemenu=(None:menuoption)(**/**)methodremove_menu=matchmenuwith|None->()|Some_->menu<-Nonemethodset_menum=self#remove_menu;menu<-Somem;ignore((m:>widget)#connectWidget.Activated(self#on_menu_activatedm))(**/**)methodon_menu_activated(m:menu)()=matchmenuwith(* keep menu in callback so that we can compare to current menu ;
indeed, a menu could be associated to this item then
removed and associated to another item. *)|SomemenuwhenOid.equalmenu#idm#id->self#trigger_unit_eventWidget.Activated()|_->()method!wtree=letN(w,l)=super#wtreeinletl=matchmenuwith|None->l|Somem->l@[m#wtree]inN(w,l)method!set_p:'a.'aProps.prop->?delay:float->?propagate:bool->'a->unit=funp?delay?(propagate=false)v->super#set_p?delay~propagatepv;matchdelay,Props.transitionpwith|Some_,Some_->()|_->ifpropagatethenmatchmenuwith|None->()|Somem->(m:>widget)#set_p~propagatepvelse()(**/**)(** {2 Actions} *)(** Activates the item. If no submenu is attached, triggers
the the [Widget.Activated] event. *)methodactivate=[%debug"%s activated"self#me];self#set_selectedtrue;matchmenuwith|None->self#trigger_unit_eventWidget.Activated()|Some_->(*self#unfold*)()(**/**)methodprivatefold=matchmenuwith|None->[%debug"%s#unfold: no menu"self#me];|Some(menu:menu)->[%debug"%s#unfold: closing menu %s"self#memenu#me];menu#close;Props.setpropsitem_menu_stateFoldedmethodprivatepopup_coords(m:menu)=letw=m#min_widthinleth=m#min_heightinlet(x,x2,y,y2)=matchself#orientationwith|Vertical->(-g_inner.x,-g_inner.x+g.w-w,-g_inner.y+g.h,-g_inner.y-h)|Horizontal->(-g_inner.x+g.w,-g_inner.x-w,-g_inner.y,-g_inner.y+g.h-h)inletdi=matchself#top_windowwith|None->Log.warn(funm->m"%s#top_window = None"self#me);0|Somew->let>di=Sdl.get_window_display_indexwin[%debug"%s display window index = %d"self#medi];diinlet>r=Sdl.get_display_boundsdiinletr=G.of_rectrinlet(x,y)=self#to_desktop_coords~x~yinlet(x2,y2)=self#to_desktop_coords~x:x2~y:y2inletx=ifx+w<=r.x+r.w||x2<0thenxelsex2inlety=ify+h<=r.y+r.h||y2<0thenyelsey2in(x,y)methodprivateunfold=[%debug"%s#unfold"self#me];matchProps.getpropsitem_menu_statewith|Unfolded->()|Folded->matchmenuwith|None->()|Some(m:menu)->let(x,y)=self#popup_coordsmin[%debug"%s#unfold x=%d, y=%d"self#mexy];leton_closelast=matchparentwith|None->()|Somep->[%debug"%s#unfold/on_close parent=%s, last=%b"self#mep#melast];Props.setpropsitem_menu_stateFoldedinProps.setpropsitem_menu_stateUnfolded;m#popup?x:(Somex)?y:(Somey)?on_close:(Someon_close)()methodprivateon_item_menu_state_changed~prev~now=matchnowwith|Unfolded->self#unfold|Folded->self#foldmethodon_selected_changed~prev~now=matchmenuwith|None->()|Some_->ifnowthenself#unfoldelseself#foldmethod!destroy=let()=matchmenuwith|None->()|Somem->m#destroyinsuper#destroymethod!set_parent?with_rendw=super#set_parent?with_rendwinitializerself#set_orientationself#orientation;let_id=self#connect(Object.Prop_changeditem_menu_state)self#on_item_menu_state_changedinlet_id=self#connect(Object.Prop_changedProps.selected)self#on_selected_changedinlet_id=self#connectWidget.Button_released(funb->ifb.Widget.button=1thenlet_=self#activateintrueelsefalse)inlet_id=self#connectWidget.Button_pressed(funb->ifb.Widget.button=1then((matchmenuwith|None->()|Some_->self#unfold);false)elsefalse)in()end(** Menu widget.
A menu inherited from {!Pack.class-box} to arrange its items.
Regular menus have vertical orientation (i.e. items are packed vertically,
but horizontal orientation is supported.
*)andmenu?classes?name?(orientation=Props.Vertical)?props?wdata()=object(self)inherit[menuitem*Events.callback_id]Pack.box?classes?name?props?wdata()assuper(**/**)methodkind="menu"valmutablewin=(None:Window.windowoption)(**/**)(** Close the menu window if it exists (i.e. if menu is displayed).
This also closes windows attached to the submenus of its items.
*)methodclose=matchwinwith|None->[%debug"%s#close: no win !"self#me];|Somew->[%debug"%s#close"self#me];win<-None;w#close;List.iter(func->c.Container.widget#set_selectedfalse)children(** Create a new window to display the menu.
Optional parameters:
{ul
{- [x] and [y] for top-left coordoninates of the window.
Default is to use mouse position.}
{- [on_close] specifies a function to be called when the
menu is closed. Argument indicates if the menu is the last
still displayed.}
}*)methodpopup?x?y?on_close()=self#close;leton_closelast=self#close;matchon_closewith|None->()|Somef->flastinletw=App.popup_menu~on_close?x?y(self:>Widget.widget)inwin<-Somew(**/**)methodprivateselected_child=List.find_opt(func->c.Container.widget#selected)self#childrenmethodon_item_activatedmi()=mi#set_selected?delay:None?propagate:Nonefalse;self#trigger_unit_eventWidget.Activated();self#close;(* App.close_menu_windows ()*)methodon_mouse_leave=(matchself#selected_childwith|None->()|Somec->matchc.widget#get_pitem_menu_statewith|Unfolded->(* keep unfolded item selected *)()|_->c.widget#set_selectedfalse);super#on_mouse_leavemethodon_mouse_motionev=matchself#child_by_coords~x:ev.x~y:ev.ywith|None->super#on_mouse_motionev|Somec->ifnotc.widget#selectedthen((matchself#selected_childwith|None->()|Somec->c.widget#set_selectedfalse);c.widget#set_selectedtrue);super#on_mouse_motionev(**/**)(** Menu items must be added using this method rather than (inherited) [#pack].
[pos] can be used to specify the 0-based position of the item among
items already present. Default is to insert after all existing items.
*)methodadd_item:?pos:int->menuitem->unit=fun?posmi->letcb=mi#connectWidget.Activated(self#on_item_activatedmi)inself#pack?pos~data:(mi,cb)mi#coerce;mi#set_orientation(matchorientationwith|Props.Vertical->Props.Horizontal|Horizontal->Vertical);(** Same as {!class-menu.add_item} but insert item as last item.*)methodappend_itemmi=self#add_item?pos:Nonemi(** Same as {!class-menu.add_item} but insert item as first item.*)methodprepend_itemmi=self#add_item~pos:0mi(** Menu items must be removed using this method rather than (inherited) [#unpack].*)methodremove_item:menuitem->unit=funmi->matchself#child_by_widgetmi#coercewith|None->()|Somec->self#unpackmi#coerce;matchc.datawith|None->()|Some(_,id)->mi#disconnectid(** Removes all items. *)methodclear_items=List.iter(func->matchc.Container.datawith|None->()|Some(mi,_)->self#remove_itemmi)self#childreninitializerself#set_orientationorientation;Props.setprops(matchself#orientationwith|Horizontal->Props.vexpand|Vertical->Props.hexpand)0;end(** Menubar widget.
This widget inherited from {!Pack.class-box}, with horizontal [orientation ]by
default, but vertical orientation is supported.*)classmenubar?classes?name?(orientation=Props.Horizontal)?props?wdata()=object(self)inherit[menuitem*Events.callback_id]Pack.box?classes?name?props?wdata()assuper(**/**)methodkind="menubar"valmutableprev_focused_widget=Nonevalmutableactive=falsemethodprivatetake_focus=matchself#get_focuswith|None->false|Some_->truemethodon_button_pressedev=[%debug"menubar %s clicked (active=%b)"self#meactive];ifev.button=1thenmatchactivewith|false->letw=self#top_widgetinprev_focused_widget<-w#focused_widget;active<-true;self#set_can_focustrue;self#set_focusabletrue;letb=self#take_focusin[%debug"%s#on_button_pressed take_focus=>%b"self#meb];(matchprev_focused_widgetwith|None->Log.warn(funm->m"%s: did not stole focus (top_widget=%s)"self#mew#me)|Somew->[%debug"%s: stole focus from %s"self#mew#me]);(*if not b then prev_focused_widget <- None;*)true|true->(* no need to do anything because with the window taking
back the focus, the menu windows are destroyed; let's just
set active to [false]. *)(*(match self#selected_child with
| None -> [%debug
(fun m -> m "%s#on_button_pressed: no selected child" self#me)
| Some c -> c.Container.widget#set_selected false
);*)self#set_inactive;trueelsefalsemethodon_item_activatedmi()=mi#set_selected?delay:None?propagate:Nonefalse;active<-false;self#set_focusablefalse;matchprev_focused_widgetwith|None->()|Somew->prev_focused_widget<-None;ignore(w#grab_focus());methodprivateselected_child=List.find_opt(func->c.Container.widget#selected)self#childrenmethod!on_mouse_leave=(matchself#selected_childwith|None->()|Somec->matchc.widget#get_pitem_menu_statewith|Unfolded->(* keep unfolded item selected if not a top menu *)()|_->c.widget#set_selectedfalse);super#on_mouse_leavemethod!on_mouse_motionev=matchactivewith|false->(* with don't have focus, do nothing *)super#on_mouse_motionev|true->matchself#child_by_coords~x:ev.x~y:ev.ywith|None->super#on_mouse_motionev|Somec->ifnotc.widget#selectedthen((* select child before unselected previous child,
or else desotry the menu window then creating
the new one will trigger a gained focus event
on the normal window, which will make us delete
all menu windows, including the new one. *)letprev_select_child=self#selected_childinc.widget#set_selectedtrue;matchprev_select_childwith|None->()|Somec->c.widget#set_selectedfalse);super#on_mouse_motionev(**/**)(** Menu items must be added using this method rather than (inherited) [#pack].
[pos] can be used to specify the 0-based position of the item among
items already present. Default is to insert after all existing items.*)methodadd_item:?pos:int->menuitem->unit=fun?posmi->letcb=mi#connectWidget.Activated(self#on_item_activatedmi)inself#pack?pos~data:(mi,cb)mi#coerce;mi#set_orientation(matchorientationwith|Props.Vertical->Props.Horizontal|Horizontal->Vertical);(** Same as {!class-menubar.add_item} but insert item as last item.*)methodappend_itemmi=self#add_item?pos:Nonemi(** Same as {!class-menubar.add_item} but insert item as first item.*)methodprepend_itemmi=self#add_item~pos:0mi(** Menu items must be removed using this method rather than (inherited) [#unpack].*)methodremove_item:menuitem->unit=funmi->matchself#child_by_widgetmi#coercewith|None->()|Somec->self#unpackmi#coerce;matchc.datawith|None->()|Some(_,id)->mi#disconnectid(** Removes all items. *)methodclear_items=List.iter(func->matchc.Container.datawith|None->()|Some(mi,_)->self#remove_itemmi)self#children(**/**)methodprivateset_inactive=active<-false;matchself#selected_childwith|None->()|Somec->c.Container.widget#set_selectedfalsemethod!release_focus=self#set_inactive;super#release_focusinitializerself#set_orientationorientation;Props.setprops(matchself#orientationwith|Horizontal->Props.vexpand|Vertical->Props.hexpand)0;ignore(self#connectWidget.Button_pressedself#on_button_pressed)end(** Convenient function to create a {!class-menubar}.
Optional argument [orientation] defines vertical or horizontal
orientation; default is [Horizontal].
See {!Widget.widget_arguments} for other arguments. *)letmenubar?classes?name?orientation?props?wdata?pack()=letw=newmenubar?classes?name?orientation?props?wdata()inWidget.may_pack?packw;w(** Convenient function to create a {!class-menuitem}.
Optional argument [shortcut] defines keyboard shortcut.
See {!Widget.widget_arguments} for other arguments. *)letmenuitem?classes?name?props?wdata?shortcut?pack()=letw=newmenuitem?classes?name?props?wdata()inOption.iterw#set_shortcutshortcut;Option.iter(funf->fw)pack;w(**/**)letshortcut_prop=shortcut(**/**)(** Convenient function to create a {!class-menuitem} with a
horizontal {!Pack.class-box} child.
Two label widgets are added to the box. The first one is the label
of the item (with initial [text] if provided).
Optional argument [shortcut] defines keyboard shortcut and it is
displayed in the second label (which has class ["menuitem_shortcut"]).
See {!Widget.widget_arguments} for other arguments.
The function returns the menu item and the first label (the one
for the menu item text).*)letlabel_menuitem?classes?name?props?wdata?text?shortcut?pack()=letmi=menuitem?classes?name?props?wdata?shortcut?pack()inlethbox=Pack.hbox~pack:mi#set_child()inletlab=Text.label~pack:hbox#pack?text()inletshortcut_lab=Text.label~classes:["menuitem_shortcut"]~pack:(hbox#pack~hexpand:0)()inshortcut_lab#set_visiblefalse;Option.iter(funks->shortcut_lab#set_visibletrue;shortcut_lab#set_text(Key.string_of_keystateks))shortcut;let_=mi#connect(Object.Prop_changedshortcut_prop)(fun~prev~now->shortcut_lab#set_visibletrue;shortcut_lab#set_text(Key.string_of_keystatenow))inhbox#set_bg_colorColor.transparent;(mi,lab)(** Convenient function to create a {!class-menu}.
Optional argument [orientation] defines vertical or horizontal
orientation; default is [Vertical].
See {!Widget.widget_arguments} for other arguments. *)letmenu?classes?name?props?wdata?pack()=letw=newmenu?classes?name?props?wdata()inOption.iter(funf->fw)pack;w(** {2 Utilities for popup menus} *)(** Menu entry description:
{ul
{- [`I (text, cb)] is a label item with [text], and [cb] is called
when item is activated.}
{- [`C (text, init, cb)] is a checkbox item with [text]; [init] indicates
whether the box is checked and [cb] is called with state when
item is activated.}
{- [`R [(text1, init1, cb1) ; ...] ] is same as [`C] but describes
a list of radiobuttons.}
{- [`M (text, entries)] describes a submenu with [text] for
the menu item and a list of entries.}
}
*)typemenu_entry=[`Iofstring*(unit->unit)|`Cofstring*bool*(bool->unit)|`Rof(string*bool*(bool->unit))list|`Mofstring*menu_entrylist](**/**)letfill_menumentries=letreciterm=function|[]->()|e::q->let()=matchewith|`I(text,cb)->let(mi,_)=label_menuitem~text~pack:m#append_item()inlet_=mi#connectWidget.Activatedcbin()|`C_->Log.warn(funm->m"`C menu entry not implemented yet")|`R_->Log.warn(funm->m"`R menu entry not implemented yet")|`M(text,entries)->let(mi,_)=label_menuitem~text~pack:m#append_item()inletm=menu~pack:mi#set_menu()initermentriesinitermqinitermentries(**/**)(** [popup_menu_entries entries] create a menu according to the [entries]
description and pops it up. The menu is closed when an item is activated
(or an event occurs closing menu windows). This function is typically
used for contextual menus. *)letpopup_menu_entries(entries:menu_entrylist)=letmenu=menu()infill_menumenuentries;menu#popup()