123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519(*********************************************************************************)(* 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 *)(* *)(*********************************************************************************)(** Widgets for date and time display and edition. *)modulePPair_int=Props.PPair(Props.PInt)(Props.PInt)(** A month is (year * month (1..12)) *)typemonth=int*int(** Property used to store the month displayed by a calendar. *)letp_month:monthProps.prop=PPair_int.mk_prop~after:[Props.Resize]~inherited:false"month"modulePDate=Props.PTriple(Props.PInt)(Props.PInt)(Props.PInt)modulePDates=Props.PList(PDate)(** Date time, in the form (year, month(1..12), day(1..31)).*)typedate=int*int*intletp_selected_dates:datelistProps.prop=PDates.mk_prop~after:[Props.Resize]~default:[]~inherited:false"selected_dates"type_Events.ev+=|Date_selected:(date->unit)Events.ev|Date_unselected:(date->unit)Events.evtypeweekday=[`Mon|`Tue|`Wed|`Thu|`Fri|`Sat|`Sun]letwdays=[|"Monday";"Tuesday";"Wednesday";"Thursday";"Friday";"Saturday";"Sunday"|]letmonths=[|"January";"February";"March";"April";"May";"June";"July";"August";"September";"October";"November";"December"|]letarray_get_optit=ifi<Array.lengthtthenSomet.(i)elseNoneletstring_of_weekday_inti=matcharray_get_optiwdayswith|None->failwith(Printf.sprintf"invalid week day %d"i)|Somestr->strletint_of_weekday=function|`Mon->0|`Tue->1|`Wed->2|`Thu->3|`Fri->4|`Sat->5|`Sun->6letweekday_of_int=function|0->`Mon|1->`Tue|2->`Wed|3->`Thu|4->`Fri|5->`Sat|6->`Sun|n->failwith(Printf.sprintf"Invalid week day %d"n)letstring_of_weekday(wd:weekday)=string_of_weekday_int(int_of_weekdaywd)letshort_string_of_weekday_inti=String.sub(string_of_weekday_inti)02letshort_string_of_weekday(wd:weekday)=short_string_of_weekday_int(int_of_weekdaywd)letstring_of_monthi=matcharray_get_opt(i-1)monthswith|None->failwith(Printf.sprintf"invalid month %d"i)|Somestr->strlettoday()=Ptime.to_date(matchPtime.of_float_s(Unix.gettimeofday())with|None->Ptime.epoch|Somed->d)classcalendar?(class_="calendar")?name?props?wdata()=letbox_title=Pack.hbox~classes:[class_^"_box_title"]()inlet(bprev,_)=Button.text_button~classes:[class_^"_button"]~text:"◀"~pack:(box_title#pack~hexpand:0)()inlettitle=Text.label~classes:[class_^"_title"]~pack:box_title#pack()inlet(bnext,_)=Button.text_button~classes:[class_^"_button"]~text:"▶"~pack:(box_title#pack~hexpand:0)()inletcal=Table.table~classes:[class_^"_table"]~rows:1~columns:7()inobject(self)inherit[unit]Pack.box~classes:[class_]?name?props?wdata()assuper(**/**)valmutableday_widgets:(Text.label*Button.button*weekday)array=[||](**/**)(** {2 Properties} *)methodmonth:month=self#get_pp_monthmethodset_month:?delay:float->?propagate:bool->month->unit=self#set_pp_month(** [cal#prev_month] changes the {!p_month} property to previous
month, if this property was set. *)methodprev_month=matchself#opt_pp_monthwith|None->()|Some(y,m)whenm<=1->self#set_month(y-1,12)|Some(y,m)->self#set_month(y,min12(m-1))(** [cal#next_month] changes the {!p_month} property to next
month, if this property was set. *)methodnext_month=matchself#opt_pp_monthwith|None->()|Some(y,m)whenm>=12->self#set_month(y+1,1)|Some(y,m)->self#set_month(y,max1(m+1))methodeditable=self#get_pProps.editablemethodset_editable=self#set_pProps.editablemethodselected_dates=self#get_pp_selected_dates(**/**)methodprivateset_selected_dates=self#set_pp_selected_dates(**/**)methodselection_mode=self#get_pProps.selection_modemethodset_selection_mode=self#set_pProps.selection_mode(** {2 Hacking} *)(** [cal#clear] removes all day widgets. *)methodclear=forrow=1tocal#rowsdoforcolumn=0tocal#columnsdocal#unpack_at~destroy:true~row~columndonedone;day_widgets<-[||](** [cal#set_title str] changes title of calendar. Beware the title
is also set when the month changes. *)methodset_title(y,m)=letmon=string_of_monthminletstr=Printf.sprintf"%s %04d"monyintitle#set_textstr(**/**)methodprivateinsert_blankpos=ignore(Text.label~classes:[class_^"_day"]~pack:(cal#pack~pos)())methodprivatemk_daywd((y,m,day)asdate)=leton_key_pressedkev=letkey=kev.Widget.keyinletday2=matchkeywith|kwhenk=Tsdl.Sdl.K.up->Some(day-7)|kwhenk=Tsdl.Sdl.K.down->Some(day+7)|kwhenk=Tsdl.Sdl.K.left->Some(day-1)|kwhenk=Tsdl.Sdl.K.right->Some(day+1)|_->Noneinmatchday2with|Somedwhenself#valid_day~err:falsed->let(_,b,_)=day_widgets.(d-1)inb#grab_focus()|_->falseinletb=Button.button~classes:[class_^"_day_button"]()inlet_=b#connectWidget.Key_pressedon_key_pressedinletl=Text.label~classes:[class_^"_day"]~pack:b#set_child~text:(string_of_intday)()inl#set_handle_hoveringfalse;let_=b#connectWidget.Activated(fun()->self#on_day_activatedlday)inl#set_selected(List.memdateself#selected_dates);(l,b,weekday_of_intwd)methodprivateweekday_of_datedate=matchPtime.of_datedatewith|None->None|Somept->letwd=Ptime.weekday_numptin(* 0 is monday for us, but sunday in ptime *)letwd=(wd+6)mod7inSomewdmethodprivatefill_days(y,m)=letnext_day=ref0inletreciteracci=matchself#weekday_of_date(y,m,i)with|None->(* add blanks until end of week *)letacc=if!next_day>0then(List.init(7-!next_day)(fun_->None))@accelseaccinList.revacc|Somewd->letacc=ifwd<>!next_daythen(* must fill prevous days with blanks *)(List.init(wd-!next_day)(fun_->None))@accelseaccinletwidgets=self#mk_daywd(y,m,i)innext_day:=(wd+1)mod7;iter((Somewidgets)::acc)(i+1)inletlist=iter[]1inletlen=List.lengthlistincal#set_rows(1+(len/7)+(iflenmod7=0then0else1));letrecinsertacci=function|[]->Array.of_list(List.revacc)|None::q->self#insert_blank(1+i/7,imod7);insertacc(i+1)q|(Some((l,b,_)asx))::q->cal#pack~pos:(1+i/7,imod7)b#coerce;insert(x::acc)(i+1)qinday_widgets<-insert[]0listmethodprivateon_month_changed~prev~now=letold=ignore_need_resizeinself#ignore_need_resize;self#clear;self#set_titlenow;self#fill_daysnow;ifnotoldthenself#handle_need_resize;cal#need_resize;self#need_resizemethodprivatevalid_date?(err=true)((y,m,d)asdate)=matchself#weekday_of_datedatewith|None->iferrthenLog.err(funp->p"%s: invalid date (%04d,%02d,%02d)"self#meymd);false|Some_->truemethodprivatevalid_day?(err=true)?(tip="")day=letlen=Array.lengthday_widgetsinifday<=0||day>lenthen(iferrthenLog.err(funm->m"%s%s: invalid day %d (day_widgets has length %d)"self#metipdaylen);false)elsetrue(**/**)(** [cal#day_label n] returns the label widget corresponding to the given
month day (1..<last day of month>).*)methodday_labelday=ifself#valid_daydaythenlet(lab,_,_)=day_widgets.(day-1)inSomelabelseNone(** [cal#day_labels_of_weekday wd] returns the list of label widgets
for the given weekday. *)methodday_labels_of_weekdaywd=letl=ref[]inArray.iteri(funday(lab,_,w)->ifw=wdthenl:=(day,lab)::!l)day_widgets;List.rev!l(** {2 Selection} *)methodselect_date?(only=false)((y,m,d)asdate)=matchself#valid_date~err:truedatewith|false->()|true->matchself#selection_modewith|Props.Sel_none->()|sel_mode->letold_sel=self#selected_datesinifonly||sel_mode<>Sel_multiplethenList.iterself#unselect_date_self#selected_dates;self#set_selected_dates(date::self#selected_dates);ifnot(old_sel=self#selected_dates)then((matchself#opt_pp_monthwith|Some(cy,cm)whency=y&&cm=m->let(lab,_,_)=day_widgets.(d-1)in(lab:>Widget.widget)#set_selectedtrue|_->());self#trigger_unit_eventDate_selecteddate)methodprivateunselect_date_((y,m,d)asdate)=ifself#valid_datedatethen(letold_sel=self#selected_datesinself#set_selected_dates(List.filter((<>)date)self#selected_dates);ifnot(old_sel=self#selected_dates)then((matchself#opt_pp_monthwith|Some(cy,cm)whency=y&&cm=m->let(lab,_,_)=day_widgets.(d-1)inlab#set_selectedfalse|_->());self#trigger_unit_eventDate_unselecteddate))methodunselect_datedate=matchself#selection_modewith|Sel_browse->()|Sel_none->()|_->self#unselect_date_datemethodunselect_all=List.iterself#unselect_date_self#selected_dates(**/**)methodprivateselect_or_unselect_dayday=matchself#opt_pp_monthwith|None->()|Some(y,m)->letdate=(y,m,day)inifself#valid_datedatethenifList.memdateself#selected_datesthenifKey.is_mod_pressedTsdl.Sdl.Kmod.ctrlthenself#unselect_datedateelse()elseletonly=not(Key.is_mod_pressedTsdl.Sdl.Kmod.ctrl)inself#select_date~onlydatemethodprivateon_day_activatedlabelday=self#select_or_unselect_daydaymethodon_editable_changed~prev~now=bprev#set_visiblenow;bnext#set_visiblenowmethod!on_key_downposeventkeymods=[%debug"%s#on_key_down: %s"self#me(Tsdl.Sdl.get_key_namekey)];self#editable&&matchkeywith|kwhenk=Tsdl.Sdl.K.pageup&&self#editable->self#prev_month;true|kwhenk=Tsdl.Sdl.K.pagedown&&self#editable->self#next_month;true|_->falsemethodprivateon_mouse_wheelev=lety=Tsdl.Sdl.Event.(getevmouse_wheel_y)inify<0thenself#next_monthelseify>0thenself#prev_monthmethod!on_sdl_eventcoordsev=letb=ifnotself#visiblethenfalseelsematchTsdl.Sdl.Event.(enum(getevtyp))with|`Mouse_wheelwhenself#editable->(self#on_mouse_wheelev;true)|_->falseinb||super#on_sdl_eventcoordsevinitializerself#set_orientationProps.Vertical;self#pack~vexpand:0box_title#coerce;self#packcal#coerce;let_=self#connect(Object.Prop_changedp_month)self#on_month_changedinlet_=self#connect(Object.Prop_changedProps.editable)self#on_editable_changedinbprev#set_visibleself#editable;bnext#set_visibleself#editable;bprev#connectWidget.Activated(fun()->self#prev_month);bnext#connectWidget.Activated(fun()->self#next_month);fori=0to6dolet_label=Text.label~classes:[class_^"_weekday"]~text:(short_string_of_weekday_inti)~pack:(cal#pack~vexpand:0~pos:(0,i))()in()doneendletcalendar?class_?name?props?wdata?pack()=letw=newcalendar?class_?name?props?wdata()inWidget.may_pack?packw;wletdialog_calendar?classes?cal_class?behaviour?flags?rflags?resizable?x?y?w?h?(selected_dates=[])?(allow_multiple=false)title=letd=Dialog.dialog?classes?behaviour?flags?rflags?resizable?x?y?w?htitleinlet(y,m,_)=matchselected_dateswith|[]->today()|h::q->List.fold_right(fundacc->maxdacc)qhinletcal=calendar?class_:cal_class~pack:d#content_area#set_child()incal#set_month(y,m);List.itercal#select_dateselected_dates;cal#set_selection_mode(ifallow_multiplethenProps.Sel_multipleelseProps.Sel_single);d,calletdialog_select_date?classes?cal_class?behaviour?flags?rflags?resizable?x?y?w?h?(ok="Ok")?(cancel="Cancel")?selected_datetitle=let(d,cal)=dialog_calendar?classes?cal_class?behaviour?flags?rflags?resizable?x?y?w?h~selected_dates:(matchselected_datewithNone->[]|Somed->[d])titleinlet_bok=d#add_text_button~return:(fun()->matchcal#selected_dateswith[]->SomeNone|h::_->Some(Someh))~ks:(Key.keystateTsdl.Sdl.K.return)okinlet_bcancel=d#add_text_button~return:(fun()->None)~ks:(Key.keystateTsdl.Sdl.K.escape)cancelind,calletdialog_select_dates?classes?cal_class?behaviour?flags?rflags?resizable?x?y?w?h?(ok="Ok")?(cancel="Cancel")?selected_datestitle=let(d,cal)=dialog_calendar?classes?cal_class?behaviour?flags?rflags?resizable?x?y?w?h?selected_datestitleinlet_bok=d#add_text_button~return:(fun()->Somecal#selected_dates)~ks:(Key.keystateTsdl.Sdl.K.return)okinlet_bcancel=d#add_text_button~return:(fun()->None)~ks:(Key.keystateTsdl.Sdl.K.escape)cancelind,caltypeWidget.wdata+=Dateofdate(** Property used in {!date_label} to indicate whether no date can be selected.*)letp_allow_none=Props.bool_prop~inherited:false~default:true"date-allow-none"letdate_label?classes?name?props?date?allow_none?pack?(button_text="..")()=lethbox=Pack.hbox()inletlabel=Text.label?classes?name?props~pack:(hbox#pack~hexpand:0)()inOption.iter(label#set_pp_allow_none)allow_none;letwb,_=Button.text_button~text:button_text~pack:(hbox#pack~hexpand:0)()inletlabel_date()=matchlabel#wdatawith|Some(Dated)->Somed|_->Noneinletupdate_label()=matchlabel_date()with|None->label#set_text""|Some(y,m,d)->label#set_text(Printf.sprintf"%04d/%02d/%02d"ymd)inletset_date=letprev_date=refNoneinfund->(matchdwith|Somedate->prev_date:=Somedate;label#set_wdata(Some(Datedate))|None->letd=matchlabel#get_pp_allow_nonewith|true->None|false->!prev_dateinletwd=Option.map(fund->Dated)dinlabel#set_wdatawd);update_label()inset_datedate;letselect_date()=matchhbox#top_windowwith|None->()|Somew->letselected_date=label_date()inletbehaviour=matchApp.window_from_sdl(Tsdl.Sdl.get_window_idw)with|None->None|Some(w,_)->Some(`Modal_forw)inletd,_cal=dialog_select_date?behaviour~resizable:true~w:200~h:200?selected_date"Select date"ind#run_async(function|None->Lwt.return_unit|Somed->set_dated;Lwt.return_unit)inlet_=wb#connectWidget.Activatedselect_dateinWidget.may_pack?packhbox;(label,label_date,set_date,hbox)