123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540(* This file is part of BOGUE, by San Vu Ngoc *)(* Table layout. *)(* il faut pouvoir changer la largeur des colonnes. Donc garder accès aux
layouts... ou au moins les recréer *)(* TODO add line number/label *)(* TODO it raises exception if length = 0, but should we make it work
anyways? *)openPrintfopenB_utilsmoduleButton=B_buttonmoduleDraw=B_drawmoduleLayout=B_layoutmoduleLabel=B_labelmoduleLong_list=B_long_listmoduleSelection=B_selectionmoduleSpace=B_spacemoduleTheme=B_thememoduleTvar=B_tvarmoduleTrigger=B_triggermoduleVar=B_varmoduleWidget=B_widget(* this is the public, non-mutable type *)typecolumn={title:string;length:int;rows:int->Layout.t;compare:(int->int->int)option;(* use "compare i1 i2" in order to compare entries i1 and i2 *)min_width:intoption;align:Draw.alignoption}typesort=|Ascending(* increasing *)|Descending(* decreasing *)typecolumn_private={title:string;rows:int->Layout.t;compare:(int->int->int)option;mutablewidth:int;align:Draw.alignoption;mutablesort:sortoption;mutableset_sort:(sortoption->unit)option}typeinternal={length:int;(* number of rows *)data:column_privatearray;selection:(Selection.t,Selection.t)Tvar.t;(* selection of rows *)max_selected:intoption;(* maximal size of selection. If max_selected=1 then the clicked entry will
always be selected (and the previously selected entry discarded.)
Otherwise, clicking a row will only toggle the row. *)mutablelast_selected:intoption;(* index ii in the currently sorted view *)on_click:(internal->int->unit)option;order:intarray;(* we keep here the bijection ith entry --> jth displayed *)titles:Layout.tarray;row_height:int;layout:(Layout.toption)Var.t;(* the global layout *)long_list:(Long_list.toption)Var.t;min_width:int;min_height:int;(* min_width and min_height are hints, it's up to the user
to respect them or not. *)}typet=internalletredrawt=do_option(Var.gett.long_list)Long_list.redrawletget_layout(t:t)=remove_option(Var.gett.layout)letget_selectiont=Tvar.gett.selectionletset_selectiontsel=Tvar.sett.selectionsel;ifSelection.sizesel=1thenSelection.iter(funi->t.last_selected<-array_find_index(funj->i=j)t.order)selelset.last_selected<-None;(* We need to regenerate the entries to update their background. *)(* refresh t *)(* abusif? *)do_option(Var.gett.long_list)Long_list.regenerateletget_llt=remove_option(Var.gett.long_list)letset_scrolltx=Long_list.set_scroll(get_llt)xletget_scrollt=Long_list.get_scroll(get_llt)letmin_widtht=t.min_widthletmin_heightt=t.min_heightlettitle_margin=4lettitle_background=Layout.color_bgDraw.(set_alpha90Button.color_off)letrow_hl=Layout.color_bgDraw.(set_alpha40Button.color_off)letrow_selected=Layout.opaque_bgDraw.(paleButton.color_off)leticon_color=Draw.(set_alpha100grey)(* [max_width c] returns the max width of the entries of the column c,
optionally limited to the first n_max entries. *)letmax_width?(n_max=50)(c:column)=letn_max=iminn_maxc.lengthinletrecloopim=ifi=n_maxthenmelseletw=Layout.width(c.rowsi)inloop(i+1)(imaxmw)inloop00(* Sets the sort-indicator icon. Does nothing if column is not sortable. *)letset_indicatoricont=ift.compare=Nonethen()elsebeginLabel.seticon(matcht.sortwith|None->Theme.fa_symbol"sort"(* terminology in font_a is reversed *)|SomeAscending->Theme.fa_symbol"sort-desc"|SomeDescending->Theme.fa_symbol"sort-asc")endletset_sorticontsort=t.sort<-sort;set_indicatoricont(* Make title labels with given (or guessed) minimal width, and optional icons
for sorting columns. *)letmake_title(c:column)=(* We compute the label widget. *)letlabel=Widget.label~align:Draw.Minc.titlein(* If no width is specified, we compute the max width of the first entries of
the column *)letlw,_=Widget.default_sizelabelinletw=matchc.min_widthwith|Somew->w|None->(imaxlw(max_widthc))inletlayout,icon=ifc.compare=Nonethen(* First encapsulate in order to then left-align. *)Layout.flat_of_w~sep:0[label],Noneelsebegin(* add icon for sorting *)letsort_indicator=Widget.icon~fg:icon_color"sort"inleticon=Widget.get_labelsort_indicatorinletsort_indicator=Layout.residentsort_indicatorinLayout.setxsort_indicator(w-Layout.widthsort_indicator);letlayout=Layout.superpose~w[Layout.residentlabel;sort_indicator]inSpace.keep_right~reset_scaling:truesort_indicator;layout,SomeiconendinLayout.set_widthlayoutw;(* not necessary in the case of sort_indicator *)let(_,h)=Widget.default_sizelabelinletclick_area=Widget.empty~w~h()inlettitle=Layout.(superpose[layout;residentclick_area])in(*AAA*)title,icon(* [get_area] extracts the click_area widget from the title layout *)(* Warning: this depends on the way title is created in make_title, see
(*AAA*) *)letget_areatitle=letopenLayoutinmatchtitle.contentwith|Rooms[_;area]->widgetarea(* see AAA *)|_->failwith"table.ml: The title layout should contain [layout; area]"letget_row__(* t i *)=()(* ??? *)letmake_column(c:column)(w,icon):column_private=lett={title=c.title;rows=c.rows;compare=c.compare;width=w;align=c.align;sort=None;set_sort=None}inlet()=matchc.compare,iconwith|None,_|_,None->()|_,Someicon->t.set_sort<-Some(set_sorticont)intletmake_columns(columns:columnlist)widths_icons=List.map2make_columncolumnswidths_iconsletmake_table?row_height?selection?max_selected?on_click?on_select(columns:columnlist)=letlength,rw=matchcolumnswith|[]->failwith"Cannot create empty table"|c0::_->List.iter(fun(c:column)->ifc.length<>c0.lengththenfailwith"Table columns must have same length")columns;c0.length,ifc0.length>0thenLayout.height(c0.rows0)else(printddebug_warning"Table column has zero element";10)inletrow_height=defaultrow_heightrwinlettitles_icons=List.mapmake_titlecolumnsinletmin_width=List.fold_left(funmti->m+Layout.widthti)0(List.mapfsttitles_icons)inletmin_height=Layout.height(fst(List.hdtitles_icons))inletwidths_icons=titles_icons|>List.map(fun(title,icon)->(Layout.widthtitle,icon))inletdata=make_columnscolumnswidths_icons|>Array.of_listinletsel=defaultselectionSelection.emptyinleton_select=defaulton_selectnopinletselection=Tvar.create(Var.createsel)~t_from:(funx->x)~t_to:(funs->on_selects;s)in{length;data;selection=selection;last_selected=None;max_selected;on_click;order=Array.initlength(funi->i);titles=Array.of_list(List.mapfsttitles_icons);(* : useful ? we have the layout below *)row_height;layout=Var.createNone;(* will be computed afterwards *)long_list=Var.createNone;(* will be computed afterwards *)min_width;min_height}letunselected_bgii=ifiimod2=1thenSome(Layout.color_bgDraw.(set_alpha20grey))elseNone(* [get_background] returns the background to use for this entry. [i] is the
entry number in the original array and in position [ii] in the display. *)letget_backgroundtiii=ifSelection.mem(Tvar.gett.selection)ithenSomerow_selectedelseunselected_bgiiletis_valid_sel_sizetsel=matcht.max_selectedwith|None->true|Somem->Selection.sizesel<=m(* Get the ieth row without the click_area, but only if it is computed and
currently in use by the Long_list. Warning, [get_row] uses the specific
structure of the row generated by [generate] below. *)letget_ll_rowtii=check_option(Var.gett.long_list)(funll->matchll.ll.array.(ii)with|Computedroom->(matchroom.contentwith|Rooms[row;_ca]->Somerow|_->None)|_->None)letmake_long_list~w~ht=(* let debug_bg = Layout.color_bg Draw.(set_alpha 40 green) in *)(* Generate row #ii: *)letgenerate=funii->leti=t.order.(ii)inletbackground=get_backgroundtiiiinletclick_area=Widget.empty~w~h:t.row_height()inletca=Layout.resident~name:(sprintf"click_area %u(%u)"iii)click_areainletrow=letopenLayoutinArray.mapi(funjc->letwj=widtht.titles.(j)inletname=sprintf"entry[%u,%u]"ijinletalign=t.data.(j).aligninletr=tower~clip:true~resize:Resize.Linear(* ~background:debug_bg *)~hmargin:title_margin~sep:title_margin~vmargin:0?align~name[c.rowsi]inset_widthr(wj+title_margin);(* r.resize <- (fun _ -> *)(* let open Resize in *)(* set_width r (width t.titles.(j) + title_margin)); *)r)t.data|>Array.to_list(* |> List.cons (resident left_margin) *)|>flat~margins:0?backgroundinletenter_=(Layout.set_backgroundca(Somerow_hl)(* Layout.fade_in ca ~duration:150 *))inletleave_=Layout.set_backgroundcaNone(* Layout.fade_out ca ~duration:150 *)in(* TODO: PROBLEM if one adds Layout.fade_in/out animations here, it becomes
very slow when one tries to scroll at the same time ==> cf
"check_mouse_motion board" dans bogue.ml *)Widget.mouse_over~enter~leaveclick_area;(* TODO click is not good with touchscreen *)letclick_=printddebug_event"Table: click on entry %i"i;let()=matcht.on_clickwithNone->()|Somef->ftiinifTrigger.shift_pressed()thenbegin(* Click + shift *)printddebug_event"Table: shift-click";do_optiont.last_selected(funii0->letsel=Selection.fold(funiis->Selection.addst.order.(ii))(Selection.range(iminii0ii,imaxii0ii))Selection.emptyinletnew_sel=Selection.unionsel(Tvar.gett.selection)inifis_valid_sel_sizetnew_selthenTvar.sett.selectionnew_sel;do_option(Var.gett.long_list)Long_list.regenerate)endelseletnew_sel=(* if Trigger.ctrl_pressed () *)(* then Selection.toggle t.selection i *)(* else if Trigger.shift_pressed () *)(* then (match t.last_selected with *)(* | Some i0 -> *)(* Selection.(union t.selection [Range (min i i0, max i i0)]) *)(* | None -> Selection.[Range (i,i)]) *)(* else Selection.[Range (i,i)] in *)(* TODO: At this point this (standard) selection mechanism
with CRTL and SHIFT does not work because we need to
recompute the how long list to update all backgrounds. This
will have to be added afterwards. For the moment we only
toggle: *)ift.max_selected=Some1thenSelection.range(i,i)elseSelection.toggle(Tvar.gett.selection)iinifis_valid_sel_sizetnew_selthenTvar.sett.selectionnew_sel;Layout.set_backgroundrow(get_backgroundtiii);ift.max_selected=Some1(* We need to reset the previously selected background. Problem, from
here we don't know the previous [row]... we have to use
[get_ll_row]. *)thendo_optiont.last_selected(funli->ifli<>iithendo_option(get_ll_rowtli)(funrow->Layout.set_backgroundrow(unselected_bgli)));ifSelection.memnew_selithent.last_selected<-SomeiiinWidget.on_click~clickclick_area;(* Selection using keyboard *)letkeyboard__ev=letopenTsdl.Sdlin(* Select all on CTRL-A *)if(t.max_selected=None||remove_optiont.max_selected>=t.length)&&Event.(getevkeyboard_keycode)=K.a&&Event.(getevkeyboard_keymod)landKmod.ctrl<>0thenbeginTvar.sett.selection(Selection.range(0,t.length-1));do_option(Var.gett.long_list)Long_list.regenerateendinletc=Widget.connect_mainclick_areaclick_areakeyboard[Trigger.key_down]inWidget.add_connectionclick_areac;Layout.(set_widthca(widthrow));(* [width row] might be different from [w] in case of resizing. *)Layout.(superpose[row;ca])inletheight_fn_=Somet.row_heightinLong_list.create~w~h~generate~height_fn~scale_width:true~length:t.length()letmake_layout~ht=letalign=Draw.Maxin(* bottom align *)lettitles_list=Array.to_listt.titlesinlettitles_row=Layout.flat~name:"titles_row"~margins:title_margin~background:title_background~aligntitles_listinletw=Layout.widthtitles_row(* title_margin + (List.fold_left (fun y r -> y + title_margin + Layout.width r) *)(* 0 titles_list) *)inletlong=make_long_listt~w~h:(h-Layout.heighttitles_row)intitles_row,long(* In-place reverse bijection of array *)letreverse_arraya=ifArray.lengtha>0thenletl=Array.lengtha-1infori=0tol/2doletx=Array.unsafe_getaiinArray.unsafe_setai(Array.unsafe_geta(l-i));Array.unsafe_seta(l-i)xdone(* Refreshes the table by creating a new long_list. Warning this changes the
height variable [h] and hence the max value of the slider. *)letrefresht=Var.with_protectt.layout(function|None->failwith"table.ml: field t.layout should not be None"(* TODO don't crash here and provide a default ? But this should never
happen *)|Somer->letw,h,titles_row=letopenLayoutinmatchr.contentwith|Rooms[titles_row;long_old]->widthtitles_row,heightlong_old,titles_row|_->failwith"table.ml: layout content is corrupted"(* TODO don't crash ? *)inletlong=make_long_list~w~htinVar.sett.long_list(Somelong);letlong_room=Long_list.get_layoutlongin(* this is the dangerous part: *)(* Layout.(long.geometry <- g); *)(* Layout.(long.current_geom <- to_current_geom g); *)(* = not really necessary, because I have removed do_adjust in set_rooms *)Layout.set_rooms~sync:falser[titles_row;long_room];Layout.retower~duration:0~margins:0r;Layout.rec_set_layertitles_rowr.layer;Layout.rec_set_layerlong_roomr.layer;(* Layout.update_current_geom r; *)(* Layout.resize_tower ~hmargin:0 ~vmargin:0 ~sep:0 ~align:Draw.Min r; *)(* long.resize (Layout.get_size r); *)Layout.resize_follow_widthtitles_row)(* Change sorting order. We don't try to modify the long_list in-place, we
create a new one *)letchange_ordertjsort=letcolumn=t.data.(j)indo_optioncolumn.compare(funcompare->Array.stable_sortcomparet.order;ifsort=SomeDescendingthenreverse_arrayt.order;apply_optioncolumn.set_sortsort;fori=0toArray.lengtht.titles-1doifi<>jthenapply_optiont.data.(i).set_sortNonedone;refresht)(* Return Some (j, true) if jeth column is sorted in reverse order, (false for
normal order). FIXME: this does not give information about the secondary
order of other columns (which at this point we didn't record anywhere,
anyway... This depends on the [sort] algo that we used, whether is preserve
secondary orders or not) *)letget_sorted_columnt=letn=Array.lengtht.titlesinletrecloopj=ifj=nthenNoneelsematcht.data.(j).sortwith|Somesort->Some(j,sort=Descending)|None->loop(j+1)inloop0letconnect_titletj=ift.data.(j).compare=Nonethen()elsebeginletwidget=get_areat.titles.(j)inletclick_=letsort=matcht.data.(j).sortwith|None->SomeAscending|SomeAscending->SomeDescending|SomeDescending->SomeAscendinginchange_ordertjsortinWidget.on_click~clickwidget;letenter_=lettitle=t.titles.(j)inLayout.set_backgroundtitle(Sometitle_background)inletleave_=lettitle=t.titles.(j)inLayout.set_backgroundtitleNoneinWidget.mouse_over~enter~leavewidget;end(* We just share the selection variable via a Tvar to automatically update the
layout when the selection is changed. *)(* let make_selection_tvar t = *)(* let t_from sel = sel in (\* the user can access the selection via this *\) *)(* let t_to sel = (\* this is what is done when the user will modifiy the *)(* selection using Tvar.set *\) *)(* TVar.set t.selection sel; (\* this is redundant with Tvar.set, but we need it *)(* to be done *before* refresh... *\) *)(* refresh t; *)(* sel in *)(* Tvar.create (t.selection) ~t_from ~t_to *)(* this returns the main layout and the selection variable *)letcreate~h?row_height?(name="table")?on_click?max_selected?selection?on_select(columns:columnlist)=lett=make_tablecolumns?row_height?on_click?max_selected?selection?on_selectinlettitles_row,long=make_layout~htinletlong_layout=Long_list.get_layoutlonginlettable=Layout.tower~margins:0~name[titles_row;long_layout]inLayout.resize_follow_widthtitles_row;Layout.resize_keep_marginslong_layout;Var.sett.layout(Sometable);forj=0toList.lengthcolumns-1doconnect_titletjdone;Var.sett.long_list(Somelong);letw0,h0=Layout.get_sizetitles_rowintable.resize<-(letopenLayout.Resizeinfun(w,h)->set_sizetable~w:(imaxw0w)~h:(imax(2*h0)h));t(* Create table from text array a.(i).(j) : row i, column j *)letof_array~h?widths?row_height?name?on_click?max_selected?selection?on_select?alignheadersa=lethead=Array.of_listheadersinletni=Array.lengthainifni=0thenfailwith"Cannot create table with empty array."elseletnj=Array.lengtha.(0)inifnj<>Array.lengthheadthenfailwith"Cannot create table: headers size does not fit the number of \
columns."elseletwidths=matchwidthswith|None->List.map(fun_->None)headers|Somelist->listinletwidths=Array.of_listwidthsinifArray.lengthwidths<>njthenfailwith"Cannot create table: list of widths does not fit the number \
of columns."elseletcolumns=head|>Array.mapi(funjtitle->{title;length=ni;rows=(funi->Layout.resident(Widget.label?aligna.(i).(j)));compare=Some(funi1i2->comparea.(i1).(j)a.(i2).(j));min_width=widths.(j);align})|>Array.to_listincreate~h?on_click?on_select?row_height?name?max_selected?selectioncolumns(* From a Csv.t style list of rows (first row must be the header). Warning: this
functions first converts to an array, ie. the data is likely to be duplicated
in memory *)letof_list~h?widths?row_height?name?max_selected?selection?on_select?align=function|[]->failwith"Cannot create table with empty list."|headers::rows->leta=List.mapArray.of_listrows|>Array.of_listinof_array~h?on_select?widths?row_height?name?max_selected?selection?alignheadersaletsort_columnt?(reverse=false)j=change_ordertj(ifreversethenSomeDescendingelseSomeAscending)(* * * * *)(* Bizarre, [table] rame bcp plus que juste [long_list]. *)