123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780(* This file is part of BOGUE, by San Vu Ngoc *)(* Each widget contains its personal data, and the list of connections from
it *)openTsdlopenB_utilsmoduleAvar=B_avarmoduleBox=B_boxmoduleButton=B_buttonmoduleCheck=B_checkmoduleDraw=B_drawmoduleEmpty=B_emptymoduleImage=B_imagemoduleLabel=B_labelmoduleSdl_area=B_sdl_areamoduleSlider=B_slidermoduleText_display=B_text_displaymoduleText_input=B_text_inputmoduleTimeout=B_timeoutmoduleTrigger=B_triggermoduleTvar=B_tvarmoduleUtf8=B_utf8moduleVar=B_vartypekind=|EmptyofEmpty.t|BoxofBox.t|ButtonofButton.t|CheckofCheck.t|LabelofLabel.t|TextDisplayofText_display.t|ImageofImage.t|SliderofSlider.t|TextInputofText_input.t|SdlAreaofSdl_area.t(* What to do when the same action (= same connection id) is already running? *)typeaction_priority=|Forget(* discard the new action *)|Join(* execute the new after the first one has completed *)|Replace(* kill the first action (if possible) and execute the second one *)|Main(* run in the main program. So this is blocking for all subsequent
actions *)typeactive={thread:Thread.t;(* [thread] is the thread launched by the connection with given id *)event:Sdl.event;(* [event] is the event passed to the "action". It is used also for
communication *)connect_id:int}typeaction=t->t->Sdl.event->unitandconnection={source:t;target:t;action:action;priority:action_priority;triggers:Trigger.tlist;id:int;}andt={kind:kind;(* receiver : action Event.channel; *)(* TODO: pas nécessaire ? *)actives:(activelist)Var.t;(* [actives] lists all active threads/connections for this widget. Most recent
come first in the list *)mutableconnections:connectionlist;(* the [connections] field lists all possible connections from this widget. In
the order to be executed. Particular case: the local actions are connection
from and to the same widget. *)(* mutable à cause de définition cyclique *)wid:int;mutablefresh:boolVar.t;(* is the display up-to-date? *)(* not really used anymore. TODO: check if this flag is still used *)mutableroom_id:intoption;(* [room_id] will be filled by the room id when inserted in that room *)mutablecursor:Sdl.cursoroption(* use [cursor] to override the default mouse cursor *)}letdraw_boxes=reffalse(* for debugging: draws a red rectangle around each widget layout, (fill when it
has mouse focus (might need a redraw: CTRL-l) and a blue rect around
container layouts *)letidw=w.widletget_room_idw=matchw.room_idwith|None->failwith"The widget does not belong to a room yet"|Someid->idletequalw1w2=w1.wid=w2.widlet(==)=equaltypewidget=tmoduleHash=structtypet=widgetletequal=equallethash=idendmoduleWHash=Weak.Make(Hash)letwidgets_wtable=WHash.create100letstring_of_kind=function|Empty_->"Empty"|Box_->"Box"|Button_->"Button"|Check_->"Check"|TextDisplay_->"TextDisplay"|Labell->"Label ["^xterm_red^(Label.textl)^xterm_nc^"]"|Image_->"Image"|Slider_->"Slider"|TextInput_->"TextInput"|SdlArea_->"SdlArea"(* When to use this??? *)(* in particular, when this function is called, the widget w in principle has
already been removed from widgets_wtable *)letfreew=printddebug_memory"Freeing widget #%u"w.wid;matchw.kindwith|Emptye->Empty.freee|Boxb->Box.freeb|Checkb->Check.freeb|Buttonb->Button.freeb|TextDisplayt->Text_display.freet|Imageimg->Image.freeimg|Labell->Label.freel|Sliders->Slider.frees|TextInputti->Text_input.freeti|SdlAreaa->Sdl_area.freealetis_freshw=Var.getw.fresh(* let canvas w = match w.canvas with *)(* | Some c -> c *)(* | None -> failwith "Canvas not defined";; *)(* let renderer w = *)(* (canvas w).Draw.renderer;; *)(* let set_canvas canvas w = *)(* w.canvas <- Some canvas;; *)letfresh_id=fresh_int()letfresh_wid=fresh_int()letcreate_emptykind=letwid=fresh_wid()inletw={kind;wid;actives=Var.create[];fresh=Var.createfalse;connections=[];room_id=None;cursor=None;}inWHash.addwidgets_wtablew;(*Gc.finalise free w;*)(* TODO: NOT A GOOD IDEA as this will ask to destroy
textures that maybe were already destroyed when the
window was closed *)(* However if we don't do this there is a risk that some textures are never
freed (as long as the renderer is not destroyed) *)wletdummy_widget=create_empty(Empty(Empty.create(0,0)))(*let of_id wid = Hashtbl.find widgets_table wid;;*)letof_idwid:t=tryWHash.findwidgets_wtable{dummy_widgetwithwid}with|Not_found->(printddebug_error"Cannot find widget with wid=%d"wid;raiseNot_found)(* unload all textures but the widget remains usable. (Rendering will recreate
all textures) *)letunload_texturew=printddebug_memory"Unloading texture for widget #%u"w.wid;matchw.kindwith|Emptyb->Empty.unloadb|Boxb->Box.unloadb|Checkb->Check.unloadb|Buttonb->Button.unloadb|TextDisplayt->Text_display.unloadt|Imageimg->Image.unloadimg|Labell->Label.unloadl|Sliders->Slider.unloads|TextInputti->Text_input.unloadti|SdlAreaa->Sdl_area.unloadaletdefault_sizew=matchw.kindwith|Emptye->Empty.sizee|Checkb->Check.sizeb|Boxb->Box.sizeb|TextDisplaytd->Text_display.sizetd|Labell->letx,y=Label.sizelin(x+2,y+2)|Imageimg->Image.sizeimg|Buttonb->Button.sizeb|Sliders->Slider.sizes|TextInputti->Text_input.sizeti|SdlAreaa->Sdl_area.sizealetsize=default_sizeletresizewsize=matchw.kindwith|Emptye->Empty.resizesizee|Boxb->Box.resizesizeb|Buttonb->Button.resizesizeb|Checkc->Check.resizesizec|Labell->Label.resizesizel|TextDisplayt->Text_display.resizesizet|Imagei->Image.resizesizei|Sliders->Slider.resizesizes|TextInputti->Text_input.resizesizeti|SdlAreaa->Sdl_area.resizesizealetget_cursorw=defaultw.cursor(matchw.kindwith|Empty_|Box_|Label_|TextDisplay_|SdlArea_|Image_->go(Draw.create_system_cursorSdl.System_cursor.arrow)|Button_|Check_|Slider_->go(Draw.create_system_cursorSdl.System_cursor.hand)|TextInput_->go(Draw.create_system_cursorSdl.System_cursor.ibeam))letset_cursorwcursor=w.cursor<-cursorletdisplaycanvaslayerwgeom=Var.setw.freshtrue;letgeom=Draw.scale_geomgeominmatchw.kindwith|Emptye->printddebug_board"empty box";Empty.displaycanvaslayeregeom|Boxb->printddebug_board"draw box";Box.displaycanvaslayerbgeom|Checkb->printddebug_board"check button: %b"(Check.stateb);Check.displaycanvaslayerbgeom|SdlAreaa->printddebug_board"render SDL area";Sdl_area.displayw.widcanvaslayerageom|Buttonb->printddebug_board"button [%s]"(Button.textb);Button.displaycanvaslayerbgeom|TextDisplaytd->printddebug_board"text display: %s"(Text_display.texttd);Text_display.displaycanvaslayertdgeom|Imageimg->printddebug_board"image: %s"(Image.get_fileimg);Image.displaycanvaslayerimggeom|Labell->printddebug_board"label: %s"(Label.textl);Label.displaycanvaslayerlgeom|Sliders->printddebug_board"slider: %d"(Slider.values);Slider.displaycanvaslayersgeom|TextInputti->printddebug_board"Input: %s"(Text_input.textti);Text_input.displaycanvaslayertigeom(** ask for refresh *)(* Warning: this is frequently called by other threads *)(* Warning: this *resets to 0* the user_window_id *)(* anyway, it is not clear if the user_window_id field for created event types
is really supported by (T)SDL *)letupdatew=printddebug_board"Please refresh widget #%i"w.wid;Var.setw.freshfalse;(* if !draw_boxes then Trigger.(push_event refresh_event) *)(* else *)Trigger.push_redraww.wid(* TODO... use wid et/ou window_id...*)(* refresh is not used anymore. We redraw everyhting at each frame ... *)(* before, it was not very subtle either: if !draw_boxes is false, we ask for
clearing the background before painting. Maybe some widgets can update
without clearing the whole background. But those with some transparency
probably need it. This should not be necessary in case we draw a solid
background -- for instance if draw_boxes = true *)(** create new connection *)(* if ~join:c, on donne le même id que la connexion c, ce qui permet
d'effectuer l'action conjointement avec celle de c (avec en général
la priorité Join pour effectuer à la suite de c). Attention dans ce
cas, ne pas déclancher plein de ces connexions à la suite... elles
s'attendent ! *)letconnectsourcetargetaction?(priority=Forget)?(update_target=true)?jointriggers=letaction=ifupdate_targetthenfunw1w2ev->(actionw1w2ev;updatew2)(* TODO ajouter Trigger.will_exit ev ?? *)elseactioninletaction=if!debugthenfunw1w2ev->(printddebug_thread"Executing action";lett=Unix.gettimeofday()inactionw1w2ev;printddebug_thread"End of action with time=%f"(Unix.gettimeofday()-.t))elseactioninletid=matchjoinwith|None->fresh_id()|Somec->c.idin{source;target;action;priority;triggers;id}letconnect_aftersourcetargetactiontriggers=matchList.revsource.connectionswith|[]->connectsourcetargetaction~priority:Jointriggers|c::_->connectsourcetargetaction~priority:Join~join:ctriggersletconnect_main=connect~priority:Mainletconnectionst=t.connections(* TODO à faire automatiquement après "connect" ? *)(* Not thread safe, should be used only in main thread. *)letadd_connectionwc=ifList.exists(funcc->cc.id=c.id)w.connectionsthenprintd(debug_error+debug_user)"Connection is already present in widget"elsew.connections<-List.rev(c::List.revw.connections)(* Remove connection. Not thread safe, should be used only in main thread. *)letremove_connectionwc=letclist=List.filter(funcc->cc.id<>c.id)w.connectionsinifList.compare_lengthsclistw.connections<>0thenw.connections<-clistelseprintd(debug_error+debug_user)"Cannot remove connection because it is not present in the widget."(* Remove all connection that respond to the given trigger (=event) *)letremove_triggerwtr=letclist=List.filter(funcc->not(List.memtrcc.triggers))w.connectionsinifList.compare_lengthsclistw.connections<>0thenw.connections<-clistelseprintd(debug_warning+debug_user)"[remove_trigger] There is no trigger of that kind in the list of connections."letget_boxw=matchw.kindwith|Boxb->b|_->invalid_arg"Expecting a box, not a %s"(string_of_kindw.kind)letget_checkw=matchw.kindwith|Checkb->b|_->invalid_arg"Expecting a check box, not a %s"(string_of_kindw.kind)letget_labelw=matchw.kindwith|Labell->l|_->invalid_arg"Expecting a label, not a %s"(string_of_kindw.kind)letget_buttonw=matchw.kindwith|Buttonb->b|_->invalid_arg"Expecting a button, not a %s"(string_of_kindw.kind)letget_sliderw=matchw.kindwith|Sliders->s|_->invalid_arg"Expecting a slider, not a %s"(string_of_kindw.kind)letget_text_displayw=matchw.kindwith|TextDisplaytd->td|_->invalid_arg"Expecting a text display, not a %s"(string_of_kindw.kind)letget_text_inputw=matchw.kindwith|TextInputti->ti|_->invalid_arg"Expecting a text input, not a %s"(string_of_kindw.kind)letget_imagew=matchw.kindwith|Imageim->im|_->invalid_arg"Expecting an image, not a %s"(string_of_kindw.kind)letget_sdl_areaw=matchw.kindwith|SdlAreaa->a|_->invalid_arg"Expecting an Sdl_area, not a %s"(string_of_kindw.kind)(** creation of simple widgets *)letcheck_box?state?style()=letb=create_empty(Check(Check.create?state?style()))inletaction=funw__->Check.action(get_checkw)inletc=connect_mainbbactionTrigger.buttons_downinadd_connectionbc;b(*let get_check_state b =
Check.state (get_check b)
*)letset_check_statebs=Check.set(get_checkb)sletempty~w~h()=create_empty(Empty(Empty.create(w,h)))lettext_display?w?htext=create_empty(TextDisplay(Text_display.create_from_string?w?htext))letrich_text?size?w?hparagraphs=create_empty(TextDisplay(Text_display.create?size?w?hparagraphs))letlines_display?w?hlines=create_empty(TextDisplay(Text_display.create_from_lines?w?hlines))letverbatimtext=create_empty(TextDisplay(Text_display.create_verbatimtext))lethtml?w?htext=create_empty(TextDisplay(Text_display.create_from_html?w?htext))letbox?w?h?style()=create_empty(Box(Box.create?width:w?height:h?style()))letsdl_area~w~h?style()=create_empty(SdlArea(Sdl_area.create~width:w~height:h?style()))letlabel?size?fg?font?aligntext=create_empty(Label(Label.create?size?fg?font?aligntext))(* alias for fontawesome icon labels *)leticon?size?fgname=create_empty(Label(Label.icon?size?fgname))letimage?w?h?bg?noscale?anglefile=create_empty(Image(Image.create?width:w?height:h?bg?noscale?anglefile))letimage_from_svg?w?h?bgfile=letsvg=Draw.convert_svg?w?hfileinletw,h=Draw.unscale_size(Draw.image_sizesvg)inimage~w~h?bgsvgletimage_copy?rotatew=create_empty(Image(Image.copy?rotate(get_imagew)))(* action is executed "on release" (mouse or keyboard). If you need an action
that depends on the button itself, use on_button_release instead. *)letbutton?(kind=Button.Trigger)?label?label_on?label_off?fg?bg_on?bg_off?bg_over?state?border_radius?border_color?actiontext=letb=create_empty(Button(Button.create?label?label_on?label_off?fg?bg_on?bg_off?bg_over?border_radius?border_color?state?actionkindtext))inletpress=fun___->Button.press(get_buttonb)inletc=connect_mainbbpressTrigger.buttons_downinadd_connectionbc;letrelease=matchkindwith(* move this test to Button ? *)|Button.Trigger->fun___->Button.release(get_buttonb)|Button.Switch->fun__ev->Button.switch(get_buttonb)evinletc=connect_mainbbreleaseTrigger.buttons_upinadd_connectionbc;letc=connect_mainbb(funb__->Button.mouse_enter(get_buttonb))[Trigger.mouse_enter]inadd_connectionbc;letc=connect_mainbb(funb__->Button.mouse_leave(get_buttonb))[Trigger.mouse_leave]inadd_connectionbc;letc=connect_mainbb(funb_ev->Button.receive_key(get_buttonb)ev)[Trigger.key_down;Trigger.key_up]inadd_connectionbc;b(* use ~lock if the user is not authorized to slide *)letslider?(priority=Main)?step?value?kind?var?length?thickness?tick_size?(lock=false)?w?hmaxi=letw=create_empty(Slider(Slider.create?step?value?kind?var?length?thickness?tick_size?w?hmaxi))inifnotlockthenbeginletonbutton_down=funw_ev->Slider.click(get_sliderw)evinletc=connect_mainwwonbutton_downTrigger.buttons_downinadd_connectionwc;(* let onclick = fun w _ ev -> Slider.click_focus (get_slider w) ev in *)(* let c = connect_main w w onclick [Sdl.Event.mouse_button_up] in *)(* add_connection w c; *)leton_release=funw__->Slider.release(get_sliderw)inletc=connect_mainwwon_releaseTrigger.buttons_upinadd_connectionwc;letslide=funw_ev->letti=get_sliderwinifTrigger.mm_pressedev||Trigger.event_kindev=`Finger_motionthen(Slider.slidetiev;updatew)inletc=connect~priority~update_target:falsewwslideTrigger.pointer_motioninadd_connectionwc;letget_keys=funw_ev->Slider.receive_key(get_sliderw)evinletc=connect~prioritywwget_keys[Sdl.Event.key_down]inadd_connectionwcend;w(* create a slider with a simple Tvar that executes an action each time the
local value of the slider is modified by the slider *)letslider_with_action?priority?step?kind~value?length?thickness?tick_size~actionmax=letv=Var.create(Avar.varvalue)inlett_froma=Avar.getainlett_tox=actionx;Avar.varxinletvar=Tvar.createv~t_from~t_toinslider?priority?step?kind~var?length?thickness?tick_sizemaxlettext_input?(text="")?prompt?size?filter?max_size()=letti=Text_input.create?size?prompt?filter?max_sizetextinletw=create_empty(TextInputti)inletonbutton_down=funw_ev->letti=get_text_inputwin(* = ti ! *)Text_input.button_downtievinletc=connect_mainwwonbutton_downTrigger.buttons_downinadd_connectionwc;letonclick=funw_ev->letti=get_text_inputwin(* = ti ! *)Text_input.clicktievinletc=connect_mainwwonclickTrigger.buttons_upinadd_connectionwc;letontab=funw_ev->letti=get_text_inputwin(* = ti ! *)Text_input.tabtievinletc=connect_mainwwontab[Sdl.Event.key_down]inadd_connectionwc;letselection=funw_ev->letti=get_text_inputwin(* = ti ! *)ifTrigger.mm_pressedevthen(Text_input.mouse_selecttiev;updatew)inletc=connect_main~update_target:falsewwselection[Sdl.Event.mouse_motion]inadd_connectionwc;letget_keys=funw_ev->Text_input.receive_key(get_text_inputw)evinletc2=connect_mainwwget_keysText_input.triggersinadd_connectionwc2;w(* Some generic functions or 'methods' that can make sense for one or several
types of widgets *)letget_textw=matchw.kindwith|Buttonb->Button.textb|TextDisplaytd->Text_display.texttd|Labell->Label.textl|TextInputti->Text_input.textti|_->(printddebug_error"This type of widget does not have a text function";"")letset_textwtext=matchw.kindwith|Buttonb->Button.set_labelbtext|TextDisplaytd->letpa=Text_display.paragraphs_of_stringtextinText_display.updatetdpa|Labell->Label.setltext|TextInputti->letk=Utf8.splittextinText_input.settik|_->printddebug_error"Cannot set text to this type of widget"letget_statew=matchw.kindwith|Buttonb->Button.stateb|Checkc->Check.statec|_->(printddebug_error"This type of widget does not have a state function";false)letset_statews=matchw.kindwith|Buttonb->Button.setbs|Checkc->Check.setcs|_->(printddebug_error"Cannot set the state for this type of widget.")(** creation of combined widgets *)letcheck_box_with_labeltext=letb=check_box()inletl=labeltextinletaction=fun_w_->Check.action(get_checkw)inletc=connect_mainlbactionTrigger.buttons_downinadd_connectionlc;b,l(****)(* some useful connections *)(* the disadvantage is that these functions do not take advantage of the two
widgets + event entry. Thus they are less 'functional' and require more
global variables (closures). Also, they all work with "connect_main", so are
ok only for very fast actions. *)letmouse_over?(enter=nop)?(leave=nop)w=letc=connect_mainww(funw__->enterw)[Trigger.mouse_enter]inadd_connectionwc;letc'=connect_mainww(funw__->leavew)[Trigger.mouse_leave]inadd_connectionwc'leton_click~clickw=letc=connect_mainww(funw__->clickw)Trigger.buttons_downinadd_connectionwcleton_release~releasew=letc=connect_mainww(funw__->releasew)Trigger.buttons_upinadd_connectionwcleton_button_release~releasew=letc=connect_mainww(funw_ev->ifTrigger.of_eventev<>Trigger.key_up||Button.check_key(get_buttonw)evthenreleasew)(Trigger.key_up::Trigger.buttons_up)inadd_connectionwc(****)(** check if connection is in the active list, and return the most
recent (=first in list) active, or None *)letis_activealistc=letrecloop=function|[]->None|a::rest->ifa.connect_id=c.idthenSomeaelselooprestinloopalist(** remove an 'active' from the active list of the widget *)(* it should occur only once in the list *)letremovewidgetthread_id=letreclooplistacc=matchlistwith|[]->acc|a::rest->(* if a.connect_id = active.connect_id *)(* test inutile, le suivant suffit *)ifThread.ida.thread=thread_idthenList.concat[List.revrest;acc]elselooprest(a::acc)inVar.setwidget.actives(List.rev(loop(Var.getwidget.actives)[]))letaddwidgetactive=Var.setwidget.actives(active::(Var.getwidget.actives))(** ask a thread to remove itself from a widget *)letremove_mec_idwidget=printddebug_thread"Removing connection #%d"c_id;removewidget(Thread.id(Thread.self()));decrthreads_created(* check if connection is terminated *)(* (only if the thread decided to signal this, for instance by setting the event
to Trigger.stop) *)lethas_terminatedactive=Sdl.Event.(getactive.eventtyp)<>Trigger.stop(* indicate to an active connection that its thread should terminate *)(* TODO protect this with mutex or Var *)letterminate?(timeout=50)active=printddebug_thread"Ask for terminating connection #%u"active.connect_id;Sdl.Event.(setactive.eventtyp)Trigger.stop;(* TODO send an event, now that we are using Sdl.wait_event_timeout *)ignore(Timeout.addtimeout(fun()->ifnot(has_terminatedactive)thenprintddebug_thread"Cannot terminate thread for connection #%u after %u ms."active.connect_idtimeout))(* ask for terminate and wait (blocking) until it really terminates by itself *)letwait_terminateactive=terminateactive;Thread.joinactive.thread(** activate an action (via a thread) on the connection *)letadd_actioncactionev=printddebug_thread"Create thread for connection #%d"c.id;(* Trigger.renew_my_event (); *)(* we used to create a new event for the main loop, so that "ev" can be safely
sent to the thread, and the thread can examine later, even after several
main loops, without it being altered (except when exiting is required) *)(* Now we use a more natural, solution would be to copy the event before
sending it to the thread, but there is no "copy_event" function
available... *)(* WARNING: at this point it is not possible to copy the drop_file_file field *)lete_copy=Trigger.copy_eventevinincrthreads_created;addc.source{thread=Thread.create(actionc.sourcec.target)e_copy;event=e_copy;connect_id=c.id}(** check if the trigger can wake up a connection, and if so, run the action *)letwake_upeventc=ifList.mem(Trigger.of_eventevent)c.triggersthenbeginprintddebug_thread"Activating connection #%d"c.id;(* TODO add a more precise ~test before launching the thread? *)ifc.priority=Mainthenc.actionc.sourcec.targetevent(* = direct action, no thread! Should we still add it to the active list?
*)elsebeginletaction=funw1w2ev->c.actionw1w2ev;remove_mec.idw1inletalist=Var.getc.source.activesinlettho=is_activealistcinifalist=[]||tho=Nonethenadd_actioncactioneventelsematchc.priority,thowith|Forget,_->printddebug_thread"Forgetting connection #%d"c.id|Join,Somea->letaction=funw1w2ev->(Thread.joina.thread;actionw1w2ev)inadd_actioncactionevent|Replace,Somea->begin(* printd debug_thread "Killing connection #%d" a.connect_id;*)(* Thread.kill a.thread; *)(* Thread.kill is in fact NOT
implemented... ! *)terminatea;removec.source(Thread.ida.thread);add_actioncactioneventend|_->failwith"This should not happen"endendletwake_up_allevw=List.iter(wake_upev)w.connections(** remove all active connections from this widget and ask for the threads to
terminate *)letremove_active_connectionswidget=letactives=Var.getwidget.activesinList.iterwait_terminateactives;Var.setwidget.actives[](*******************)(* Some widgets directly react to a click event to activate themselves. Some,
like text_input, even react to the TAB key. In fact, keyboard_focus is
treated globally by the main loop, therefore one could (should?) rely on
this function below instead of adding new reactions to TAB & click *)letset_keyboard_focusw=matchw.kindwith|TextInput_->()(* already done by the widget *)|Sliders->Slider.set_focuss|Buttonb->Button.set_focusb|_->()letremove_keyboard_focusw=matchw.kindwith|TextInputti->Text_input.stopti|Sliders->Slider.unfocuss|Buttonb->Button.unfocusb|_->()letguess_unset_keyboard_focusw=matchw.kindwith|TextInput_->Somefalse|Slider_->Somefalse|Button_->Somefalse|_->None(*************************)(* Some examples of "pure" actions (actions that don't depend on external
variables) *)letcopy_textw1w2_=lettext=get_textw1inset_textw2textletmap_textfw1w2_=lettext=get_textw1inset_textw2(ftext)