123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496(*********************************************************************************)(* 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 *)(* *)(*********************************************************************************)(** Notebook widget. *)openMiscopenTsdlopenWidgetopenContaineropenPack(** The ["active_page"] property, representing the 0-based index
of the active (i.e. displayed) page in a notebook. *)letactive_page=Props.int_prop~inherited:false"active_page"(** Property ["tab_props"] to store properties used for notebook tabs. *)lettab_props=Props.props_prop~default:(Props.empty())"tab_props"(** Notebook widget.
A notebook widget is a [[Widget.widget] Container.container], i.e. a
widget containing contents widgets (the pages), each one being associated to
a widget representing this page in the list of displayed tabs.
A notebook can be oriented vertically (tabs are on the left and
and are packed vertically) or horizontally (tabs are on top and
and are packed horizontally).
*)classnotebook?classes?name?props?wdata()=object(self)inherit[Widget.widget]Container.container_list?classes?name?props?wdata()assuperinheritWidget.orientedasoriented(**/**)methodkind="notebook"valmutabletab_box=newPack.box()methodprivatetab_box=tab_boxmethodset_orientationo=oriented#set_orientationo;tab_box#set_pProps.orientationo;self#apply_thememethod!do_apply_theme~root~parentparent_pathrules=super#do_apply_theme~root~parentparent_pathrules;letpath=self#css_path~parent_path()intab_box#do_apply_theme~root~parent:theme_propspathrules;min_width<-None;min_height<-None(**/**)methodactive_page=self#opt_pactive_page(** Make page [p] the active page (0-based index). *)methodset_active_pagep=self#set_active_page_~force:falsep(** [nb#get_page None] returns [None].
[nb#get_page (Some n)] returns [None] if [n]-th page does not exist,
or [Some (label_widget, page_widget)].*)methodget_nthn=matchnwith|None->None|Somen->matchList.nth_optself#childrennwith|None->None|Somec->Some(c.data,c.widget)(**/**)methodprivateset_active_page_?(force=false)p=[%debug"%s#set_active_page_ %d"self#mep];(*Log.warn (fun m ->
List.iter
(fun c -> m "begin %s#set_active_page_ child %s visible: %b" self#me
c.widget#me c.widget#visible) children);*)letlen=List.lengthself#childreninifp<0||p>=lenthenfalseelse(let()=matchself#active_pagewith|Someprevwhenprev<len&&(prev<>p||force)->(letc=List.nthself#childrenprevinc.widget#set_visiblefalse;[%debug"%s#set_active_page_: set is_focus to false for %s"self#mec.widget#me];c.widget#set_pProps.is_focusfalse;c.data#set_selectedfalse;matchc.data#parentwith|None->()|Somep->p#set_selectedfalse)|_->()inletc=List.nthself#childrenpinc.widget#set_visibletrue;let()=c.data#set_selectedtrue;matchc.data#parentwith|None->()|Somep->p#set_selectedtrueinself#set_pactive_pagep;[%debug"%s#set_active_page_: set is_focus to true for %s"self#mec.widget#me];c.widget#set_pProps.is_focustrue;(*Log.warn (fun m ->
List.iter
(fun c -> m "end %s#set_active_page_ child %s visible: %b" self#me
c.widget#me c.widget#visible) children);*)true)method!wtree=N(self#coerce,tab_box#wtree::List.map(func->c.widget#wtree)children)methodprivatetab_widget_by_coords~x~y=List.find_opt(funw->G.inside~x~yw#geometry)tab_box#children_widgetsmethod!privatechild_by_coords~x~y=let(px,py)=letg=tab_box#geometryin(x-g.x-g_inner.x,y-g.y-g_inner.y)inList.find_opt(func->G.inside~x:px~y:pyc.data#geometry||(c.widget#visible&&G.inside~x~yc.widget#geometry))childrenmethodprivatetabs_width=tab_box#min_widthmethodprivatetabs_height=tab_box#min_heightmethod!privatemin_width_=letwidth_tabs=self#tabs_widthinsuper#min_width_+(matchself#orientationwith|Props.Horizontal->maxwidth_tabs(matchself#active_pagewith|None->0|Somep->matchList.nth_optself#childrenpwith|None->0|Somec->c.widget#min_width)|Vertical->width_tabs+(matchself#active_pagewith|None->0|Somep->matchList.nth_optself#childrenpwith|None->0|Somec->c.widget#min_width))method!privatemin_height_=letheight_tabs=self#tabs_heightinsuper#min_height_+(matchself#orientationwith|Horizontal->height_tabs+(matchself#active_pagewith|None->0|Somep->matchList.nth_optself#childrenpwith|None->0|Somec->c.widget#min_height)|Vertical->maxheight_tabs(matchself#active_pagewith|None->0|Somep->matchList.nth_optself#childrenpwith|None->0|Somec->c.widget#min_height))method!max_width=Nonemethod!max_height=Nonemethodprivatepack_label?posw=letb=Bin.bin~classes:["tab"]()inb#set_handle_hoveringtrue;let_=b#connectWidget.Clicked(funbev->ifbev.button=1thenmatchtab_box#widget_indexb#as_widgetwith|None->false|Somep->self#set_active_page_pelsefalse)inb#set_childw;tab_box#pack?pos~hexpand:0~vexpand:0~data:wb#as_widget;bmethodprivatepack_pos?pos~label(w:Widget.widget)=[%debug"%s#add %s"self#mew#me];letold_len=List.lengthself#childreninw#set_visiblefalse;matchsuper#add?poswlabel#coercewith|false->None|true->self#pack_label?poslabel;matchself#widget_indexwwith|None->None|Some0whenold_len<=0->(* setting active page will set widget visibilty to true and
will trigger a need_resize in our container *)ifself#set_active_page_0thenSome0else(Log.err(funm->m"%s#set_active_page_: page not set active ??"self#me);None)|Somen->matchself#active_pagewith|None->Log.err(funm->m"%s: no active page but page added in position %d"self#men);ifself#set_active_page_0thenSome0elseNone|Somep->let_=ifn<=pthenself#set_active_page_~force:true(p+1)elsefalseinSomep(**/**)(** [#pack ~label w] adds a new page with contents widget [w]
and label widget [label]. Optional argument [pos] can be
used to specify a 0-based position for the new page. Default
is to append the page. *)methodpack?pos~labelw=let_=self#pack_pos?pos~labelwin()(**/**)method!child_reparentedw=self#unpackw(**/**)(** [unpack w] removes the page corresponding to the (contents) widget [w]. *)methodunpack(w:Widget.widget)=matchself#widget_indexwwith|None->()|Somei->letc=List.nthself#childreniin[%debug"%s#unpack %s List.nth self#children %d done"self#mew#mei];matchsuper#removewwith|false->()|true->(matchc.data#parentwith|None->Log.warn(funm->m"Tab label %s has no parent!"c.data#me)|Somep->tab_box#unpackp);matchself#active_pagewith|None->self#need_resize|Somep->ifp=ithen(* removing the active page *)letlen=List.lengthself#childreniniflen<=0then((* no more pages *)Props.set_optpropsactive_pageNone;self#need_resize;self#need_render~layer:(self#get_pProps.layer)g)else((* set active page the next one, or the previous one
if the removed page was the last one *)letp=ifi<lenthenielsemax(len-1)(i-1)inlet_=self#set_active_page_pin())elseifp<ithenself#need_resizeelse(* active page > i, decrement the active page *)let_=self#set_active_page_(p-1)in()(** [remove_page i] removes the page at index [i]. *)methodremove_pagei=matchList.nth_optchildreniwith|None->Log.warn(funm->m"%s#remove_page i=%d page not found"self#mei);|Somec->self#unpackc.widget(** [find_child p] returns the first page widget [c] for which [p c] returns [true],
if any. *)methodfind_childpred=self#find_child_optpred(** [widget_index w] returns the page index corresponding to widget [w], if any. *)method!widget_index=super#widget_index(**/**)methodprivateset_geometry_tab_box=letg=matchself#orientationwith|Horizontal->G.{x=0;y=0;w=g_inner.w;h=tab_box#min_height}|Vertical->G.{x=0;y=0;w=tab_box#min_width;h=g_inner.h}intab_box#set_geometrygmethod!set_geometrygeom=super#set_geometrygeom;[%debug"%s#set_geometry g=%a"self#meG.ppg];self#set_geometry_tab_box;letremain=letgt=tab_box#geometryinmatchself#orientationwith|Horizontal->lety=gt.y+gt.hinG.{x=0;y;w=g_inner.w;h=g_inner.h-y}|Vertical->letx=gt.x+gt.winG.{x;y=0;w=g_inner.w-x;h=g_inner.h}inList.iter(func->c.widget#set_geometryremain)self#childrenmethod!privaterender_me~layerrend~offset:(x,y)rg=letoff_x=g.x+g_inner.xinletoff_y=g.y+g_inner.yinletoffset=(x+off_x,y+off_y)inletrg=G.translate~x:(-off_x)~y:(-off_y)rgintab_box#render~layerrend~offsetrg;matchself#active_pagewith|None->()|Somep->matchList.nth_optself#childrenpwith|None->()|Somec->c.widget#render~layer~offsetrendrgmethod!child_need_resizew=ifOid.equalw#idtab_box#idthenself#need_resizeelsesuper#child_need_resizewmethod!on_sdl_event_down~oldpospose=ifself#sensitivethenmatchletf(x,y)=(x-g.x-g_inner.x,y-g.y-g_inner.y)inletoldpos=Option.mapfoldposinletpos=Option.mapfposinmatchSdl.Event.(enum(getetyp))with|`Key_down|`Key_up|`Text_input|`Text_editing->(matchList.find_opt(func->c.widget#get_pProps.is_focus)childrenwith|None->false|Somec->c.widget#on_sdl_event_down~oldpospose)|_->List.fold_left(funaccw->if(matcholdposwith|Some(x,y)->G.inside~x~yw#geometry|None->false)||matchposwith|Some(x,y)->G.inside~x~yw#geometry|None->truethen((*Log.warn (fun m -> m "%s#on_sdl_event_down: propagating event to %s"
self#me w#me);*)letb=w#on_sdl_event_down~oldposposeinacc||b)elseacc)false(letl=matchself#active_pagewith|None->[]|Somep->matchList.nth_optself#childrenpwith|None->[]|Somec->[c.widget]inl@[tab_box#as_widget])with|true->true|false->self#on_sdl_eventposeelsefalsemethod!leaf_widget_at~x~y=letx=x-g.x-g_inner.xinlety=y-g.y-g_inner.yinmatchself#child_by_coords~x~ywith|None->None|Somec->ifG.inside~x~yc.widget#geometrythenc.widget#leaf_widget_at~x~yelselet(px,py)=letg=tab_box#geometryin(x-g.x-g_inner.x,y-g.y-g_inner.y)inc.data#leaf_widget_at~x:px~y:pymethod!next_widget?inside~looppredw=matchwwith|Somewwhenw#equaltab_box#coerce->super#next_widget?inside~looppredNone|_->tab_box#next_widget?inside~looppredwmethod!prev_widget?inside~looppredw=letreciter=function|[]->tab_box#prev_widget?inside~looppredNone|c::qwhenpredc.widget->Somec.widget|c::q->matchc.widget#prev_widget?inside~looppredNonewith|None->iterq|x->xinmatchwwith|None->iter(List.revchildren)|Somewwhenw#equaltab_box#coerce->(matchinside,parentwith|Somei,_whenself#equali->ifloopthenself#prev_widget?inside~looppredNoneelseNone|_,None->None|_,Somep->p#prev_widget?inside~looppred(Someself#coerce))|Somew->letrecfind=function|[]->iter[]|c::qwhenc.widget#equalw->iterq|_::q->findqinfind(List.revchildren)initializertab_box#set_parent?with_rend:with_renderer(Someself#coerce)end(** Convenient function to create a {!class-notebook}.
Default [orientation] is [Horizontal].
The widget has class ["vertical"] or ["horizontal"] depending on orientation.
See {!Widget.widget_arguments} for other arguments. *)letnotebook?(orientation=Props.Horizontal)?classes?name?props?wdata?pack()=letw=newnotebook?classes?name?props?wdata()inw#set_orientationorientation;Widget.may_pack?packw;w(** Same as {!val-notebook} but orientation is already fixed to [Horizontal].*)lethnotebook?classes?name?props?wdata?pack()=notebook~orientation:Horizontal?classes?name?props?wdata?pack()(** Same as {!val-notebook} but orientation is already fixed to [Vertical].*)letvnotebook?classes?name?props?wdata?pack()=notebook~orientation:Vertical?classes?name?props?wdata?pack()