123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376(** 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 we should make it work (?) *)openPrintfopenB_utilsmoduleLayout=B_layoutmoduleWidget=B_widgetmoduleTheme=B_thememoduleVar=B_varmoduleTvar=B_tvarmoduleDraw=B_drawmoduleSelection=B_selectionmoduleLabel=B_labelmoduleLong_list=B_long_list(* 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 *)width:intoption;}typesort=|Ascending(* increasing *)|Descending(* decreasing *)typecolumn_private={title:string;rows:int->Layout.t;compare:(int->int->int)option;mutablewidth:int;mutablesort:sortoption}typet={length:int;data:column_privatearray;selection:Selection.tVar.t;(* selection of rows *)mutablelast_selected:intoption;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 *)}lettitle_margin=5lettitle_background=Layout.color_bgDraw.(set_alpha40blue)letrow_hl=Layout.color_bgDraw.(set_alpha30blue)letrow_selected=Layout.opaque_bgDraw.(pale(pale(paleblue)))leticon_color=Draw.(set_alpha40grey)(* max_width returns the max width of the first n_max entries of the column c *)letmax_width?(n_max=50)(c:column)=letn_max=iminn_maxc.lengthinletrecloopim=ifi=n_maxthenmelseletw=Layout.width(c.rowsi)inloop(i+1)(imaxmw)inloop00letmake_title(c:column)=(* we compute the label widget *)letlabel=Widget.labelc.titlein(* if no width is specified, we compute the max width of the first entries of
the column *)letlw,_=Widget.default_sizelabelinletw=matchc.widthwith|Somew->w|None->(imaxlw(max_widthc))inletlayout=ifc.compare=Nonethen(* first encapsulate in order to then left-align *)Layout.flat_of_w~sep:0[label]elsebegin(* add icon for sorting *)letsort_indicator=Widget.icon~fg:icon_color"sort"inletsw,_=Widget.default_sizesort_indicatorinletlw,lh=Widget.default_sizelabelinletw'=w-sw-lwinifw'>=0then(* we can add sort_indicator *)Layout.flat_of_w~sep:0~align:(Draw.Max)[label;Widget.empty~w:w'~h:lh();sort_indicator]elseLayout.flat_of_w~sep:0[label]endinLayout.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(* 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]"(* extracts the sort_indicator widget from the title layout *)(* Warning: this depends on the way title is created in make_title *)letget_indicatortitle=letopenLayoutinmatchtitle.contentwith|Rooms[{content=Rooms[_;_;sort_indicator];_};_](* see AAA *)->Some(widgetsort_indicator)|_->Noneletget_row__(* t i *)=()(* ??? *)letmake_column(c:column)w:column_private={title=c.title;rows=c.rows;compare=c.compare;width=w;sort=None}letmake_columns(columns:columnlist)widths=List.map2make_columncolumnswidthsletmake_table?row_height(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,Layout.height(c0.rows0)inletrow_height=defaultrow_heightrwinlettitles=List.mapmake_titlecolumnsinletwidths=List.mapLayout.widthtitlesinletdata=make_columnscolumnswidths|>Array.of_listin{length;data;selection=Var.createSelection.empty;last_selected=None;order=Array.initlength(funi->i);titles=Array.of_listtitles;(* useful ? we have the layout below *)row_height;layout=Var.createNone(* will be computed afterwards *)}(* entry number i in the original array and in position ii in the display *)letget_backgroundtiii=ifSelection.mem(Var.gett.selection)ithenSomerow_selectedelseifiimod2=1thenSome(Layout.color_bgDraw.(set_alpha20grey))elseNoneletmake_long_list~w~ht=(* generate row #i: *)letgenerate=funii->leti=t.order.(ii)inletbackground=get_backgroundtiiiinletleft_margin=Widget.empty~w:title_margin~h:t.row_height()inletclick_area=Widget.empty~w~h:t.row_height()inletca=Layout.resident~name:(sprintf"click_area %u(%u)"iii)click_areainletrow=Array.mapi(funjc->letwidth=Layout.widtht.titles.(j)inletname=sprintf"entry[%u,%u]"ijinletr=Layout.flat~sep:0~hmargin:0~vmargin:0~name[c.rowsi]inLayout.set_widthr(width+title_margin);r)t.data|>Array.to_list|>List.cons(Layout.residentleft_margin)|>(Layout.flat~sep:0~hmargin:0~vmargin:0?background)inletenter_=(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_=(t.last_selected<-Somei;letnew_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: *)Selection.toggle(Var.gett.selection)iinVar.sett.selectionnew_sel;Layout.set_backgroundrow(get_backgroundtiii);)inWidget.on_click~clickclick_area;Layout.(superpose[row;ca])inletheight_fn_=Somet.row_heightinLong_list.create~w~h~generate~height_fn~length:t.length()letmake_layout?w~ht=letalign=Draw.Maxin(* bottom align *)lettitles_list=Array.to_listt.titlesinletw=matchwwith|Somew->w|None->title_margin+(List.fold_left(funyr->y+title_margin+Layout.widthr)0titles_list)inlettitles_row=Layout.flat~sep:title_margin~hmargin:title_margin~vmargin:title_margin~background:title_background~aligntitles_listinletlong=make_long_listt~w~h:(h-Layout.heighttitles_row)intitles_row,long(* in-place reverse bijection of array *)letreverse_arraya=letl=Array.lengtha-1infori=0tol/2doletx=Array.unsafe_getaiinArray.unsafe_setai(Array.unsafe_geta(l-i));Array.unsafe_seta(l-i)xdone(* sets the sort-indicator symbol. Does nothing if column is not sortable *)letset_indicatortj=ift.data.(j).compare=Nonethen()elsebeginletsort=t.data.(j).sortindo_option(get_indicatort.titles.(j))(funindicator->letlabel=Widget.get_labelindicatorinLabel.setlabel(matchsortwith|None->Theme.fa_symbol"sort"(* terminology in font_a is reversed *)|SomeAscending->Theme.fa_symbol"sort-desc"|SomeDescending->Theme.fa_symbol"sort-asc"))end(* refreshes the table by creating a new long_list *)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,g,titles_row=letopenLayoutinmatchr.contentwith|Rooms[titles_row;long_old]->widthtitles_row,heightlong_old,long_old.geometry,titles_row|_->failwith"table.ml: layout content is corrupted"(* TODO don't crash ? *)inletlong=make_long_list~w~htin(* this is the dangerous part: *)Layout.(long.geometry<-g);Layout.(long.current_geom<-to_current_geomg);(* = not really necessary, because I have removed do_adjust in set_rooms *)Layout.set_roomsr[titles_row;long])(* changes 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=Descendingthenreverse_arrayt.order;(* TODO modify t.titles.(j) *)refresht;column.sort<-Somesort;fori=0toArray.lengtht.titles-1doifi<>jthent.data.(i).sort<-None;set_indicatortidone)letconnect_titletj=ift.data.(j).compare=Nonethen()elsebeginletwidget=get_areat.titles.(j)inletclick_=letsort=matcht.data.(j).sortwith|None->Ascending|SomeAscending->Descending|SomeDescending->Ascendinginchange_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. *)letmake_selection_tvart=lett_fromsel=selin(* the user can access the selection via this *)lett_tosel=(* this is what is done when the user will modifiy the
selection using Tvar.set *)Var.sett.selectionsel;(* this is redundant with Tvar.set, but we need it
to be done *before* refresh... *)refresht;selinTvar.create(t.selection)~t_from~t_to(* this returns the main layout and the selection variable *)letcreate?w~h?row_height?(name="table")(columns:columnlist)=lett=make_tablecolumns?row_heightinlettitles_row,long=make_layout?w~htinletlayout=Layout.tower~sep:0~hmargin:0~vmargin:0~name[titles_row;long]inVar.sett.layout(Somelayout);forj=0toList.lengthcolumns-1doconnect_titletjdone;layout,make_selection_tvart(* Create table from text array a.(i).(j) : row i, column j *)letof_array?w~h?widths?row_height?nameheadersa=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.labela.(i).(j)));compare=Some(funi1i2->comparea.(i1).(j)a.(i2).(j));width=widths.(j)})|>Array.to_listincreate?w~h?row_height?namecolumns(* 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?w~h?widths?row_height?name=function|[]->failwith"Cannot create table with empty list."|headers::rows->leta=List.mapArray.of_listrows|>Array.of_listinof_array?w~h?widths?row_height?nameheadersa(* * * * *)