unison

Fork of Unison, a bi-directional file synchronization tool
git clone git://git.laack.co/unison.git
Log | Files | Refs | README | LICENSE

uigtk3.ml (175502B)


      1 (* Unison file synchronizer: src/uigtk3.ml *)
      2 (* Copyright 1999-2020, Benjamin C. Pierce
      3 
      4     This program is free software: you can redistribute it and/or modify
      5     it under the terms of the GNU General Public License as published by
      6     the Free Software Foundation, either version 3 of the License, or
      7     (at your option) any later version.
      8 
      9     This program is distributed in the hope that it will be useful,
     10     but WITHOUT ANY WARRANTY; without even the implied warranty of
     11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12     GNU General Public License for more details.
     13 
     14     You should have received a copy of the GNU General Public License
     15     along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16 *)
     17 
     18 
     19 open Common
     20 open Lwt
     21 
     22 module Private = struct
     23 
     24 let debug = Trace.debug "ui"
     25 
     26 let myNameCapitalized = String.capitalize_ascii Uutil.myName
     27 
     28 (**********************************************************************
     29                            LOW-LEVEL STUFF
     30  **********************************************************************)
     31 
     32 (**********************************************************************
     33  Some message strings (build them here because they look ugly in the
     34  middle of other code.
     35  **********************************************************************)
     36 
     37 let tryAgainMessage =
     38   Printf.sprintf
     39 "You can use %s to synchronize a local directory with another local directory,
     40 or with a remote directory.
     41 You can also synchronize a single file if you enter a file name.
     42 
     43 Please enter the first (local) directory that you want to synchronize."
     44 myNameCapitalized
     45 
     46 (* ---- *)
     47 
     48 let helpmessage = Printf.sprintf
     49 "%s can synchronize a local directory with another local directory, or with
     50 a directory on a remote machine. You can also synchronize a single file
     51 by entering a file name instead of a directory.
     52 
     53 To synchronize with a local directory, just enter the file name.
     54 
     55 To synchronize with a remote directory, you must first choose a protocol
     56 that %s will use to connect to the remote machine.  Each protocol has
     57 different requirements:
     58 
     59 1) To synchronize using SSH, there must be an SSH client installed on
     60 this machine and an SSH server installed on the remote machine.  You
     61 must enter the host to connect to, a user name (if different from
     62 your user name on this machine), and the directory on the remote machine
     63 (relative to your home directory on that machine).
     64 
     65 2) To synchronize using %s's socket protocol, there must be a %s
     66 server running on the remote machine, listening to the port that you
     67 specify here.  (Use \"%s -socket xxx\" on the remote machine to
     68 start the %s server.)  You must enter the host, port, and the directory
     69 on the remote machine (relative to the working directory of the
     70 %s server running on that machine)."
     71 myNameCapitalized myNameCapitalized myNameCapitalized myNameCapitalized myNameCapitalized myNameCapitalized myNameCapitalized
     72 
     73 (**********************************************************************
     74  Font preferences
     75  **********************************************************************)
     76 
     77 let fontMonospace = lazy (Pango.Font.from_string "monospace")
     78 let fontBold = lazy (Pango.Font.from_string "bold")
     79 let fontItalic = lazy (Pango.Font.from_string "italic")
     80 
     81 (**********************************************************************
     82  Unison icon
     83  **********************************************************************)
     84 
     85 (* This does not work with the current version of Lablgtk, due to a bug
     86 let icon =
     87   GdkPixbuf.from_data ~width:48 ~height:48 ~has_alpha:true
     88     (Gpointer.region_of_bytes Pixmaps.icon_data)
     89 *)
     90 let icon =
     91   lazy begin
     92     let p = GdkPixbuf.create ~width:48 ~height:48 ~has_alpha:true () in
     93     Gpointer.blit
     94       ~src:(Gpointer.region_of_bytes (Bytes.of_string Pixmaps.icon_data))
     95       ~dst:(GdkPixbuf.get_pixels p);
     96     p
     97   end
     98 
     99 let leftPtrWatch =
    100   lazy (Gdk.Cursor.create `WATCH)
    101 
    102 let make_busy w =
    103   Gdk.Window.set_cursor w#misc#window (Lazy.force leftPtrWatch)
    104 let make_interactive w =
    105   (* HACK: setting the cursor to NULL restore the default cursor *)
    106   Gdk.Window.set_cursor w#misc#window (Obj.magic Gpointer.boxed_null)
    107 
    108 (*********************************************************************
    109   UI state variables
    110  *********************************************************************)
    111 
    112 type stateItem = { mutable ri : reconItem;
    113                    mutable bytesTransferred : Uutil.Filesize.t;
    114                    mutable bytesToTransfer : Uutil.Filesize.t;
    115                    mutable whatHappened : (Util.confirmation * string option) option}
    116 let theState = ref [||]
    117 let unsynchronizedPaths = ref None
    118 
    119 (* ---- *)
    120 
    121 let theToplevelWindow = ref None
    122 let setToplevelWindow w = theToplevelWindow := Some w
    123 let toplevelWindow () =
    124   match !theToplevelWindow with
    125     Some w -> w
    126   | None   -> raise (Util.Fatal "Main window not initialized; check your DISPLAY setup")
    127 
    128 (*********************************************************************
    129   Lock management
    130  *********************************************************************)
    131 
    132 let busy = ref false
    133 
    134 let getLock f =
    135   let protect ~(finally : unit -> unit) f =
    136     (* Very simple [protect] when we know that [finally] does not raise *)
    137     (* FIXME: Switch to [Fun.protect] once OCaml 4.09 is the minimum? *)
    138     try let () = f () in finally () with
    139     | e ->
    140         finally ();
    141         raise e
    142   in
    143   if !busy then
    144     Trace.status "Synchronizer is busy, please wait.."
    145   else begin
    146     busy := true; protect ~finally:(fun () -> busy := false) f
    147   end
    148 
    149 (**********************************************************************
    150   Miscellaneous
    151  **********************************************************************)
    152 
    153 let sync_action = ref None
    154 
    155 let last = ref (0.)
    156 
    157 let gtk_sync forced =
    158   let t = Unix.gettimeofday () in
    159   if !last = 0. || forced || t -. !last > 0.05 then begin
    160     last := t;
    161     begin match !sync_action with
    162       Some f -> f ()
    163     | None   -> ()
    164     end;
    165     while Glib.Main.iteration false do () done
    166   end
    167 
    168 (**********************************************************************
    169                       CHARACTER SET TRANSCODING
    170 ***********************************************************************)
    171 
    172 (* Transcodage from Microsoft Windows Codepage 1252 to Unicode *)
    173 
    174 (* Unison currently uses the "ASCII" Windows filesystem API.  With
    175    this API, filenames are encoded using a proprietary character
    176    encoding.  This encoding depends on the Windows setup, but in
    177    Western Europe, the Windows Codepage 1252 is usually used.
    178    GTK, on the other hand, uses the UTF-8 encoding.  This code perform
    179    the translation from Codepage 1252 to UTF-8.  A call to [transcode]
    180    should be wrapped around every string below that might contain
    181    non-ASCII characters. *)
    182 
    183 let code =
    184   [| 0x0020; 0x0001; 0x0002; 0x0003; 0x0004; 0x0005; 0x0006; 0x0007;
    185      0x0008; 0x0009; 0x000A; 0x000B; 0x000C; 0x000D; 0x000E; 0x000F;
    186      0x0010; 0x0011; 0x0012; 0x0013; 0x0014; 0x0015; 0x0016; 0x0017;
    187      0x0018; 0x0019; 0x001A; 0x001B; 0x001C; 0x001D; 0x001E; 0x001F;
    188      0x0020; 0x0021; 0x0022; 0x0023; 0x0024; 0x0025; 0x0026; 0x0027;
    189      0x0028; 0x0029; 0x002A; 0x002B; 0x002C; 0x002D; 0x002E; 0x002F;
    190      0x0030; 0x0031; 0x0032; 0x0033; 0x0034; 0x0035; 0x0036; 0x0037;
    191      0x0038; 0x0039; 0x003A; 0x003B; 0x003C; 0x003D; 0x003E; 0x003F;
    192      0x0040; 0x0041; 0x0042; 0x0043; 0x0044; 0x0045; 0x0046; 0x0047;
    193      0x0048; 0x0049; 0x004A; 0x004B; 0x004C; 0x004D; 0x004E; 0x004F;
    194      0x0050; 0x0051; 0x0052; 0x0053; 0x0054; 0x0055; 0x0056; 0x0057;
    195      0x0058; 0x0059; 0x005A; 0x005B; 0x005C; 0x005D; 0x005E; 0x005F;
    196      0x0060; 0x0061; 0x0062; 0x0063; 0x0064; 0x0065; 0x0066; 0x0067;
    197      0x0068; 0x0069; 0x006A; 0x006B; 0x006C; 0x006D; 0x006E; 0x006F;
    198      0x0070; 0x0071; 0x0072; 0x0073; 0x0074; 0x0075; 0x0076; 0x0077;
    199      0x0078; 0x0079; 0x007A; 0x007B; 0x007C; 0x007D; 0x007E; 0x007F;
    200      0x20AC; 0x1234; 0x201A; 0x0192; 0x201E; 0x2026; 0x2020; 0x2021;
    201      0x02C6; 0x2030; 0x0160; 0x2039; 0x0152; 0x1234; 0x017D; 0x1234;
    202      0x1234; 0x2018; 0x2019; 0x201C; 0x201D; 0x2022; 0x2013; 0x2014;
    203      0x02DC; 0x2122; 0x0161; 0x203A; 0x0153; 0x1234; 0x017E; 0x0178;
    204      0x00A0; 0x00A1; 0x00A2; 0x00A3; 0x00A4; 0x00A5; 0x00A6; 0x00A7;
    205      0x00A8; 0x00A9; 0x00AA; 0x00AB; 0x00AC; 0x00AD; 0x00AE; 0x00AF;
    206      0x00B0; 0x00B1; 0x00B2; 0x00B3; 0x00B4; 0x00B5; 0x00B6; 0x00B7;
    207      0x00B8; 0x00B9; 0x00BA; 0x00BB; 0x00BC; 0x00BD; 0x00BE; 0x00BF;
    208      0x00C0; 0x00C1; 0x00C2; 0x00C3; 0x00C4; 0x00C5; 0x00C6; 0x00C7;
    209      0x00C8; 0x00C9; 0x00CA; 0x00CB; 0x00CC; 0x00CD; 0x00CE; 0x00CF;
    210      0x00D0; 0x00D1; 0x00D2; 0x00D3; 0x00D4; 0x00D5; 0x00D6; 0x00D7;
    211      0x00D8; 0x00D9; 0x00DA; 0x00DB; 0x00DC; 0x00DD; 0x00DE; 0x00DF;
    212      0x00E0; 0x00E1; 0x00E2; 0x00E3; 0x00E4; 0x00E5; 0x00E6; 0x00E7;
    213      0x00E8; 0x00E9; 0x00EA; 0x00EB; 0x00EC; 0x00ED; 0x00EE; 0x00EF;
    214      0x00F0; 0x00F1; 0x00F2; 0x00F3; 0x00F4; 0x00F5; 0x00F6; 0x00F7;
    215      0x00F8; 0x00F9; 0x00FA; 0x00FB; 0x00FC; 0x00FD; 0x00FE; 0x00FF |]
    216 
    217 let rec transcodeRec buf s i l =
    218   if i < l then begin
    219     let c = code.(Char.code s.[i]) in
    220     if c < 0x80 then
    221       Buffer.add_char buf (Char.chr c)
    222     else if c < 0x800 then begin
    223       Buffer.add_char buf (Char.chr (c lsr 6 + 0xC0));
    224       Buffer.add_char buf (Char.chr (c land 0x3f + 0x80))
    225     end else if c < 0x10000 then begin
    226       Buffer.add_char buf (Char.chr (c lsr 12 + 0xE0));
    227       Buffer.add_char buf (Char.chr ((c lsr 6) land 0x3f + 0x80));
    228       Buffer.add_char buf (Char.chr (c land 0x3f + 0x80))
    229     end;
    230     transcodeRec buf s (i + 1) l
    231   end
    232 
    233 let transcodeDoc s =
    234   let buf = Buffer.create 1024 in
    235   transcodeRec buf s 0 (String.length s);
    236   Buffer.contents buf
    237 
    238 (****)
    239 
    240 let escapeMarkup s = Glib.Markup.escape_text s
    241 
    242 let transcodeFilename s =
    243   if Prefs.read Case.unicodeEncoding then
    244     Unicode.protect s
    245   else if Sys.win32 then transcodeDoc s else
    246   try
    247     Glib.Convert.filename_to_utf8 s
    248   with Glib.Convert.Error _ ->
    249     Unicode.protect s
    250 
    251 let transcode s =
    252   if Prefs.read Case.unicodeEncoding then
    253     Unicode.protect s
    254   else
    255   try
    256     Glib.Convert.locale_to_utf8 s
    257   with Glib.Convert.Error _ ->
    258     Unicode.protect s
    259 
    260 (**********************************************************************
    261                        USEFUL LOW-LEVEL WIDGETS
    262  **********************************************************************)
    263 
    264 class scrolled_text ?editable ?shadow_type ?(wrap_mode=`WORD) ?packing ?show
    265     () =
    266   let sw =
    267     GBin.scrolled_window ?packing ~show:false
    268       ?shadow_type ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
    269   in
    270   let text = GText.view ?editable ~wrap_mode ~packing:sw#add () in
    271   let () = text#set_left_margin 4
    272   and () = text#set_right_margin 4 in
    273   object
    274     inherit GObj.widget_full sw#as_widget
    275     method text = text
    276     method insert s = text#buffer#set_text s;
    277     method show () = sw#misc#show ()
    278     initializer
    279       if show <> Some false then sw#misc#show ()
    280   end
    281 
    282 (* ------ *)
    283 
    284 (* Display a message in a window and wait for the user
    285    to hit the button. *)
    286 let okBox ~parent ~title ~typ ~message =
    287   let t =
    288     GWindow.message_dialog
    289       ~parent ~title ~message_type:typ ~message ~modal:true
    290       ~buttons:GWindow.Buttons.ok () in
    291   ignore (t#run ()); t#destroy ()
    292 
    293 (* ------ *)
    294 
    295 let primaryText msg =
    296   Printf.sprintf "<span weight=\"bold\" size=\"larger\">%s</span>"
    297     (escapeMarkup msg)
    298 
    299 (* twoBox: Display a message in a window and wait for the user
    300    to hit one of two buttons.  Return true if the first button is
    301    chosen, false if the second button is chosen. *)
    302 let twoBox ?(kind=`DIALOG_WARNING) ~parent ~title ~astock ~bstock message =
    303   let t =
    304     GWindow.dialog ~parent ~title ~border_width:6 ~modal:true
    305       ~resizable:false () in
    306   t#vbox#set_spacing 12;
    307   let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
    308   ignore (GMisc.image ~stock:kind ~icon_size:`DIALOG
    309             ~yalign:0. ~packing:h1#pack ());
    310   let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
    311   ignore (GMisc.label
    312             ~markup:(primaryText title ^ "\n\n" ^ escapeMarkup message)
    313             ~selectable:true ~yalign:0. ~packing:v1#add ());
    314   t#add_button_stock bstock `NO;
    315   t#add_button_stock astock `YES;
    316   t#set_default_response `NO;
    317   t#show();
    318   let res = t#run () in
    319   t#destroy ();
    320   res = `YES
    321 
    322 (* ------ *)
    323 
    324 (* Avoid recursive invocations of the function below (a window receives
    325    delete events even when it is not sensitive) *)
    326 let inExit = ref false
    327 
    328 let doExit () = Lwt_unix.run (Update.unlockArchives ()); exit 0
    329 
    330 let safeExit () =
    331   if not !inExit then begin
    332     inExit := true;
    333     if not !busy then exit 0 else
    334     if twoBox ~parent:(toplevelWindow ()) ~title:"Premature exit"
    335         ~astock:`YES ~bstock:`NO
    336         "Unison is working, exit anyway ?"
    337     then exit 0;
    338     inExit := false
    339   end
    340 
    341 (* ------ *)
    342 
    343 (* warnBox: Display a warning message in a window and wait (unless
    344    we're in batch mode) for the user to hit "OK" or "Exit". *)
    345 let warnBox ~parent title message =
    346   let message = transcode message in
    347   if Prefs.read Globals.batch then begin
    348     (* In batch mode, just pop up a window and go ahead *)
    349     let t =
    350       GWindow.dialog ~parent ~title
    351         ~border_width:6 ~modal:true ~resizable:false () in
    352     t#vbox#set_spacing 12;
    353     let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
    354     ignore (GMisc.image ~stock:`DIALOG_INFO ~icon_size:`DIALOG
    355               ~yalign:0. ~packing:h1#pack ());
    356     let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
    357     ignore (GMisc.label ~markup:(primaryText title ^ "\n\n" ^
    358                                  escapeMarkup message)
    359               ~selectable:true ~yalign:0. ~packing:v1#add ());
    360     t#add_button_stock `CLOSE `CLOSE;
    361     t#set_default_response `CLOSE;
    362     ignore (t#connect#response ~callback:(fun _ -> t#destroy ()));
    363     t#show ()
    364   end else begin
    365     inExit := true;
    366     let ok =
    367       twoBox ~parent:(toplevelWindow ()) ~title ~astock:`OK ~bstock:`QUIT
    368         message in
    369     if not(ok) then doExit ();
    370     inExit := false
    371   end
    372 
    373 (****)
    374 
    375 let accel_paths = Hashtbl.create 17
    376 let underscore_re = Str.regexp_string "_"
    377 class ['a] gMenuFactory
    378     ?(accel_group=GtkData.AccelGroup.create ())
    379     ?(accel_path="<DEFAULT ROOT>/")
    380     ?(accel_modi=[`CONTROL])
    381     ?(accel_flags=[`VISIBLE]) (menu_shell : 'a) =
    382   object (self)
    383     val menu_shell : #GMenu.menu_shell = menu_shell
    384     val group = accel_group
    385     val m = accel_modi
    386     val flags = (accel_flags:Gtk.Tags.accel_flag list)
    387     val accel_path = accel_path
    388     method menu = menu_shell
    389     method accel_group = group
    390     method accel_path = accel_path
    391     method private bind
    392         ?(modi=m) ?key ?callback label ?(name=label) (item : GMenu.menu_item) =
    393       menu_shell#append item;
    394       let accel_path = accel_path ^ name in
    395       let accel_path = Str.global_replace underscore_re "" accel_path in
    396       (* Default accel path value *)
    397       if not (Hashtbl.mem accel_paths accel_path) then begin
    398         Hashtbl.add accel_paths accel_path ();
    399         GtkData.AccelMap.add_entry accel_path ?key ~modi
    400       end;
    401       (* Register this accel path *)
    402       GtkBase.Widget.set_accel_path item#as_widget accel_path accel_group;
    403       Gaux.may callback ~f:(fun callback -> item#connect#activate ~callback)
    404     method add_item ?key ?modi ?callback ?submenu ?bindname label =
    405       let item = GMenu.menu_item  ~use_mnemonic:true ~label () in
    406       self#bind ?modi ?key ?callback label ?name:bindname item;
    407       Gaux.may (submenu : GMenu.menu option) ~f:item#set_submenu;
    408       item
    409     method add_image_item ?(image : GObj.widget option)
    410         ?modi ?key ?callback ?stock ?name label =
    411       (* GTK 3 does not provide image menu items (there is a way to
    412          manually create a workaround but that does not work with
    413          lablgtk. Let's create a regular menu item instead. *)
    414       let item =
    415         GMenu.menu_item ~use_mnemonic:true ~label () in
    416       match stock  with
    417       | None ->
    418           self#bind ?modi ?key ?callback label ?name item;
    419           item
    420       | Some s ->
    421           try
    422             let st = GtkStock.Item.lookup s in
    423             self#bind
    424               ?modi ?key:(if st.GtkStock.keyval=0 then key else None)
    425               ?callback label ?name item;
    426             item
    427           with Not_found -> item
    428 
    429     method add_check_item ?active ?modi ?key ?callback label =
    430       let item = GMenu.check_menu_item ~label ~use_mnemonic:true ?active () in
    431       self#bind label ?modi ?key
    432         ?callback:(Gaux.may_map callback ~f:(fun f () -> f item#active))
    433         (item : GMenu.check_menu_item :> GMenu.menu_item);
    434       item
    435     method add_separator () = GMenu.separator_item ~packing:menu_shell#append ()
    436     method add_submenu label =
    437       let item = GMenu.menu_item ~use_mnemonic:true ~label () in
    438       self#bind label item;
    439       (GMenu.menu ~packing:item#set_submenu (), item)
    440     method replace_submenu (item : GMenu.menu_item) =
    441       GMenu.menu ~packing:item#set_submenu ()
    442 end
    443 
    444 (**********************************************************************
    445                          HIGHER-LEVEL WIDGETS
    446 ***********************************************************************)
    447 
    448 (* FIXME: This is a lowest-effort port of GTK2 pixmap-based code to GTK3.
    449    It works but is probably needlessly inefficient(??). It should be
    450    rewritten from scratch to match the new GTK(+Cairo) API and only draw
    451    updated regions. *)
    452 class stats width height =
    453   let area =
    454     let d = GMisc.drawing_area () in
    455     d#set_width_request width;
    456     d#set_height_request height;
    457     d#set_margin_left 4;
    458     d#set_margin_right 4;
    459     d#set_margin_top 8;
    460     d#set_margin_bottom 8;
    461     d#set_hexpand true;
    462     d#set_vexpand true;
    463     d
    464   in
    465   object (self)
    466     inherit GObj.widget_full area#as_widget
    467     val mutable maxim = ref 0.
    468     val mutable scale = ref 1.
    469     val mutable min_scale = 1.
    470     val mutable values = Array.make width 0.
    471     val mutable active = false
    472     val mutable width = float_of_int width
    473     val mutable height = float_of_int height
    474     initializer
    475       ignore (area#misc#connect#size_allocate ~callback:self#resize);
    476       ignore (area#misc#connect#draw ~callback:self#redraw)
    477 
    478     method resize rect =
    479       let oldw = truncate width in
    480       let neww = min rect.Gtk.width 640 in
    481       if neww > oldw then
    482         values <- Array.append (Array.make (neww - oldw) 0.) (Array.sub values 0 oldw)
    483       else if neww < oldw then begin
    484         Array.blit values (oldw - neww) values 0 neww
    485       end;
    486       width <- float_of_int neww;
    487       height <- float_of_int (min rect.Gtk.height 200);
    488       area#misc#queue_draw ()
    489 
    490     method redraw cr =
    491       scale := min_scale;
    492       while !maxim > !scale do
    493         scale := !scale *. 1.5
    494       done;
    495       Cairo.set_source_rgb cr 1. 1. 1.;
    496       Cairo.rectangle cr 0. 0. ~w:width ~h:height;
    497       Cairo.fill cr;
    498       for i = 0 to truncate width - 1 do
    499         self#rect cr i values.(max 0 (i - 1)) values.(i)
    500       done;
    501       true
    502 
    503     method activate a = active <- a; if a then area#misc#queue_draw ()
    504 
    505     method scale h = height *. h /. !scale
    506 
    507     method private rect cr i v' v =
    508       let h = self#scale v in
    509       let h' = self#scale v' in
    510       let h1 = min h' h in
    511       let h2 = max h' h in
    512       Cairo.set_source_rgb cr 0. 0. 0.;
    513       Cairo.rectangle cr (float_of_int i) (height -. h1) ~w:1. ~h:h1;
    514       Cairo.fill cr;
    515       for h = (truncate h1) + 1 to (truncate h2) do
    516         let v = ((float h -. h1) /. (h2 -. h1)) in
    517         Cairo.set_source_rgb cr v v v;
    518         Cairo.rectangle cr (float_of_int i) (height -. float h) ~w:1. ~h:1.;
    519         Cairo.fill cr;
    520         ()
    521       done
    522 
    523     method push v =
    524       let width = truncate width in
    525       let need_max = values.(0) = !maxim in
    526       for i = 0 to width - 2 do
    527         values.(i) <- values.(i + 1)
    528       done;
    529       values.(width - 1) <- v;
    530       if need_max then begin
    531         maxim := 0.;
    532         for i = 0 to width - 1 do maxim := max !maxim values.(i) done
    533       end else
    534         maxim := max !maxim v;
    535       if active then begin
    536         area#misc#queue_draw ()
    537       end
    538   end
    539 
    540 let clientWritten = ref 0.
    541 let serverWritten = ref 0.
    542 let emitRate2 = ref 0.
    543 let receiveRate2 = ref 0.
    544 
    545 let rate2str v =
    546   if v > 9.9e3 then begin
    547     if v > 9.9e6 then
    548       Format.sprintf "%1.0f MiB/s" (v /. 1e6)
    549     else if v > 999e3 then
    550       Format.sprintf "%1.1f MiB/s" (v /. 1e6)
    551     else
    552       Format.sprintf "%1.0f KiB/s" (v /. 1e3)
    553   end else begin
    554     if v > 990. then
    555       Format.sprintf "%1.1f KiB/s" (v /. 1e3)
    556     else if v > 99. then
    557       Format.sprintf "%1.2f KiB/s" (v /. 1e3)
    558     else
    559       " "
    560   end
    561 
    562 let mib = 1024. *. 1024.
    563 let kib2str v =
    564   if v > 100_000_000. then
    565     Format.sprintf "%.0f MiB" (v /. mib)
    566   else if v > 1_000_000. then
    567     Format.sprintf "%.1f MiB" (v /. mib)
    568   else if v > 1024. then
    569     Format.sprintf "%.1f KiB" (v /. 1024.)
    570   else
    571     Format.sprintf "%.0f B" v
    572 
    573 let statistics () =
    574   let title = "Statistics" in
    575   let t = GWindow.dialog ~title ~parent:(toplevelWindow ()) () in
    576   let t_dismiss = GButton.button ~stock:`CLOSE ~packing:t#action_area#add () in
    577   t_dismiss#grab_default ();
    578   let dismiss () = t#misc#hide () in
    579   ignore (t_dismiss#connect#clicked ~callback:dismiss);
    580   ignore (t#event#connect#delete ~callback:(fun _ -> dismiss (); true));
    581 
    582   let emission = new stats 320 50 in
    583   t#vbox#pack ~expand:false ~padding:4 (emission :> GObj.widget);
    584   let reception = new stats 320 50 in
    585   t#vbox#pack ~expand:false ~padding:4 (reception :> GObj.widget);
    586 
    587   let cols = new GTree.column_list in
    588   let c_1 = cols#add Gobject.Data.string in
    589   let c_client = cols#add Gobject.Data.string in
    590   let c_server = cols#add Gobject.Data.string in
    591   let c_total = cols#add Gobject.Data.string in
    592   let lst = GTree.list_store cols in
    593   let l = GTree.view ~model:lst ~enable_search:false ~packing:(t#vbox#add) () in
    594   l#selection#set_mode `NONE;
    595   ignore (l#append_column (GTree.view_column ~title:""
    596     ~renderer:(GTree.cell_renderer_text [], ["text", c_1]) ()));
    597   ignore (l#append_column (GTree.view_column ~title:"Client"
    598     ~renderer:(GTree.cell_renderer_text [`XALIGN 1.0], ["text", c_client]) ()));
    599   ignore (l#append_column (GTree.view_column ~title:"Server"
    600     ~renderer:(GTree.cell_renderer_text [`XALIGN 1.0], ["text", c_server]) ()));
    601   ignore (l#append_column (GTree.view_column ~title:"Total"
    602     ~renderer:(GTree.cell_renderer_text [`XALIGN 1.0], ["text", c_total]) ()));
    603   let rate_row = lst#append () in
    604   ignore (lst#set ~row:rate_row ~column:c_1 "Reception rate");
    605   let receive_row = lst#append () in
    606   ignore (lst#set ~row:receive_row ~column:c_1 "Data received");
    607   let data_row = lst#append () in
    608   ignore (lst#set ~row:data_row ~column:c_1 "File data written");
    609 
    610   ignore (t#event#connect#map ~callback:(fun _ ->
    611     emission#activate true;
    612     reception#activate true;
    613     false));
    614   ignore (t#event#connect#unmap ~callback:(fun _ ->
    615     emission#activate false;
    616     reception#activate false;
    617     false));
    618 
    619   let delay = 0.5 in
    620   let a = 0.5 in
    621   let b = 0.8 in
    622 
    623   let emittedBytes = ref 0. in
    624   let emitRate = ref 0. in
    625   let receivedBytes = ref 0. in
    626   let receiveRate = ref 0. in
    627 
    628   let stopCounter = ref 0 in
    629 
    630   let updateTable () =
    631     let row = rate_row in
    632     lst#set ~row ~column:c_client (rate2str !receiveRate2);
    633     lst#set ~row ~column:c_server (rate2str !emitRate2);
    634     lst#set ~row ~column:c_total (rate2str (!receiveRate2 +. !emitRate2));
    635     let row = receive_row in
    636     lst#set ~row ~column:c_client (kib2str !receivedBytes);
    637     lst#set ~row ~column:c_server (kib2str !emittedBytes);
    638     lst#set ~row ~column:c_total (kib2str (!receivedBytes +. !emittedBytes));
    639     let row = data_row in
    640     lst#set ~row ~column:c_client (kib2str !clientWritten);
    641     lst#set ~row ~column:c_server (kib2str !serverWritten);
    642     lst#set ~row ~column:c_total (kib2str (!clientWritten +. !serverWritten))
    643   in
    644   let timeout _ =
    645     emitRate :=
    646       a *. !emitRate +.
    647       (1. -. a) *. (!Remote.emittedBytes -. !emittedBytes) /. delay;
    648     emitRate2 :=
    649       b *. !emitRate2 +.
    650       (1. -. b) *. (!Remote.emittedBytes -. !emittedBytes) /. delay;
    651     emission#push !emitRate;
    652     receiveRate :=
    653       a *. !receiveRate +.
    654       (1. -. a) *. (!Remote.receivedBytes -. !receivedBytes) /. delay;
    655     receiveRate2 :=
    656       b *. !receiveRate2 +.
    657       (1. -. b) *. (!Remote.receivedBytes -. !receivedBytes) /. delay;
    658     reception#push !receiveRate;
    659     emittedBytes := !Remote.emittedBytes;
    660     receivedBytes := !Remote.receivedBytes;
    661     if !stopCounter > 0 then decr stopCounter;
    662     if !stopCounter = 0 then begin
    663       emitRate2 := 0.; receiveRate2 := 0.;
    664     end;
    665     updateTable ();
    666     !stopCounter <> 0
    667   in
    668   let startStats () =
    669     if !stopCounter = 0 then begin
    670       emittedBytes := !Remote.emittedBytes;
    671       receivedBytes := !Remote.receivedBytes;
    672       stopCounter := -1;
    673       ignore (GMain.Timeout.add ~ms:(truncate (delay *. 1000.))
    674                 ~callback:timeout)
    675     end else
    676       stopCounter := -1
    677   in
    678   let stopStats () = stopCounter := 10 in
    679   (t, startStats, stopStats)
    680 
    681 (* ------ *)
    682 
    683 let globalGTKInited = ref false
    684 
    685 let gui_safe_eprintf fmt =
    686   Printf.ksprintf (fun s ->
    687     if System.has_stderr ~info:s then Printf.eprintf "%s%!" s) fmt
    688 
    689 let fatalError ?(quit=false) message =
    690   let title = if quit then "Fatal error" else "Error" in
    691   let () =
    692     Trace.sendLogMsgsToStderr := false; (* We don't know if stderr is available *)
    693     try Trace.log (title ^ ": " ^ message ^ "\n")
    694     with Util.Fatal _ -> () in (* Can't allow fatal errors in fatal error handler *)
    695   let toplevelWindow =
    696     try Some (toplevelWindow ())
    697     with Util.Fatal err ->
    698       begin
    699         gui_safe_eprintf "\n%s:\n%s\n\n%s\n\n" title err message;
    700         if not !globalGTKInited then exit 1 else None
    701       end
    702   in
    703   let t =
    704     GWindow.dialog ?parent:toplevelWindow ~title
    705       ~border_width:6 ~modal:true ~resizable:false () in
    706   t#vbox#set_spacing 12;
    707   let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
    708   ignore (GMisc.image ~stock:`DIALOG_ERROR ~icon_size:`DIALOG
    709             ~yalign:0. ~packing:h1#pack ());
    710   let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
    711   ignore (GMisc.label
    712             ~markup:(primaryText title ^ "\n\n" ^
    713                      escapeMarkup (transcode message))
    714             ~line_wrap:true ~selectable:true ~yalign:0. ~packing:v1#add ());
    715   t#add_button_stock `QUIT `QUIT;
    716   if not quit then t#add_button_stock `CLOSE `CLOSE;
    717   t#set_default_response (if quit then `QUIT else `CLOSE);
    718   ignore (t#connect#response
    719             ~callback:(function `QUIT -> exit 1 | _ -> ()));
    720   t#show(); ignore (t#run ()); t#destroy ();
    721   if quit then exit 1
    722 
    723 let fatalErrorHandler = ref (fatalError ~quit:true)
    724 
    725 let stackOverflowNoQuitMsg () =
    726   "Stack overflow. This could indicate a programming error.\n\
    727     You should be able to continue without having to quit \
    728     the application but the error may repeat.\n\n\
    729     Technical information in case you need to report a bug:\n"
    730   ^ (Printexc.get_backtrace ())
    731 
    732 (* ------ *)
    733 
    734 let getFirstRoot () =
    735   let t = GWindow.dialog ~parent:(toplevelWindow ()) ~title:"Root selection"
    736       ~modal:true ~resizable:true () in
    737   t#misc#grab_focus ();
    738 
    739   let hb = GPack.hbox
    740       ~packing:(t#vbox#pack ~expand:false ~padding:15) () in
    741   ignore(GMisc.label ~text:tryAgainMessage
    742            ~justify:`LEFT
    743            ~packing:(hb#pack ~expand:false ~padding:15) ());
    744 
    745   let f1 = GPack.hbox ~spacing:4
    746       ~packing:(t#vbox#pack ~expand:true ~padding:4) () in
    747   ignore (GMisc.label ~text:"Dir:" ~packing:(f1#pack ~expand:false) ());
    748   let fileE = GEdit.entry ~packing:f1#add () in
    749   fileE#misc#grab_focus ();
    750   let b = GFile.chooser_button ~action:`SELECT_FOLDER
    751     ~title:"Select a local directory"
    752     ~packing:(f1#pack ~expand:false) () in
    753   ignore (b#connect#selection_changed ~callback:(fun () ->
    754             if not fileE#is_focus then
    755               fileE#set_text (match b#filename with None -> "" | Some s -> s)));
    756   ignore (fileE#connect#changed ~callback:(fun () ->
    757             if fileE#is_focus then ignore (b#set_filename fileE#text)));
    758 
    759   let f3 = t#action_area in
    760   let result = ref None in
    761   let contCommand() =
    762     result := Some (Util.trimWhitespace fileE#text);
    763     t#destroy () in
    764   let cancelButton = GButton.button ~stock:`CANCEL ~packing:f3#add () in
    765   ignore (cancelButton#connect#clicked
    766             ~callback:(fun () -> result := None; t#destroy()));
    767   let contButton = GButton.button ~stock:`OK ~packing:f3#add () in
    768   ignore (contButton#connect#clicked ~callback:contCommand);
    769   ignore (fileE#connect#activate ~callback:contCommand);
    770   contButton#grab_default ();
    771   t#show ();
    772   ignore (t#connect#destroy ~callback:GMain.Main.quit);
    773   GMain.Main.main ();
    774   match !result with None -> None
    775   | Some file ->
    776       Some(Clroot.clroot2string(Clroot.ConnectLocal(Some file)))
    777 
    778 (* ------ *)
    779 
    780 let getSecondRoot () =
    781   let t = GWindow.dialog ~parent:(toplevelWindow ()) ~title:"Root selection"
    782       ~modal:true ~resizable:true () in
    783   t#misc#grab_focus ();
    784 
    785   let message = "Please enter the second directory you want to synchronize." in
    786 
    787   let vb = t#vbox in
    788   let hb = GPack.hbox ~packing:(vb#pack ~expand:false ~padding:15) () in
    789   ignore(GMisc.label ~text:message
    790            ~justify:`LEFT
    791            ~packing:(hb#pack ~expand:false ~padding:15) ());
    792   let helpB = GButton.button ~stock:`HELP ~packing:hb#add () in
    793   ignore (helpB#connect#clicked
    794             ~callback:(fun () -> okBox ~parent:t ~title:"Picking roots" ~typ:`INFO
    795                 ~message:helpmessage));
    796 
    797   let result = ref None in
    798 
    799   let f = GPack.vbox ~packing:(vb#pack ~expand:false) () in
    800 
    801   let f1 = GPack.hbox ~spacing:4 ~packing:f#add () in
    802   ignore (GMisc.label ~text:"Directory:" ~packing:(f1#pack ~expand:false) ());
    803   let fileE = GEdit.entry ~packing:f1#add () in
    804   fileE#misc#grab_focus ();
    805   let b = GFile.chooser_button ~action:`SELECT_FOLDER
    806     ~title:"Select a local directory"
    807     ~packing:(f1#pack ~expand:false) () in
    808   ignore (b#connect#selection_changed ~callback:(fun () ->
    809             if not fileE#is_focus then
    810               fileE#set_text (match b#filename with None -> "" | Some s -> s)));
    811   ignore (fileE#connect#changed ~callback:(fun () ->
    812             if fileE#is_focus then ignore (b#set_filename fileE#text)));
    813 
    814   let f0 = GPack.hbox ~spacing:4 ~packing:f#add () in
    815   let localB = GButton.radio_button ~packing:(f0#pack ~expand:false)
    816       ~label:"Local" () in
    817   let sshB = GButton.radio_button ~group:localB#group
    818       ~packing:(f0#pack ~expand:false)
    819       ~label:"SSH" () in
    820   let socketB = GButton.radio_button ~group:sshB#group
    821       ~packing:(f0#pack ~expand:false) ~label:"Socket" () in
    822 
    823   let f2 = GPack.hbox ~spacing:4 ~packing:f#add () in
    824   ignore (GMisc.label ~text:"Host:" ~packing:(f2#pack ~expand:false) ());
    825   let hostE = GEdit.entry ~packing:f2#add () in
    826 
    827   ignore (GMisc.label ~text:"(Optional) User:"
    828             ~packing:(f2#pack ~expand:false) ());
    829   let userE = GEdit.entry ~packing:f2#add () in
    830 
    831   ignore (GMisc.label ~text:"Port:"
    832             ~packing:(f2#pack ~expand:false) ());
    833   let portE = GEdit.entry ~packing:f2#add () in
    834 
    835   let varLocalRemote = ref (`Local : [`Local|`SSH|`SOCKET]) in
    836   let localState() =
    837     varLocalRemote := `Local;
    838     hostE#misc#set_sensitive false;
    839     userE#misc#set_sensitive false;
    840     portE#misc#set_sensitive false;
    841     b#misc#set_sensitive true in
    842   let remoteState() =
    843     hostE#misc#set_sensitive true;
    844     b#misc#set_sensitive false;
    845     match !varLocalRemote with
    846       `SOCKET ->
    847         (portE#misc#set_sensitive true; userE#misc#set_sensitive false)
    848     | _ ->
    849         (portE#misc#set_sensitive false; userE#misc#set_sensitive true) in
    850   let protoState x =
    851     varLocalRemote := x;
    852     remoteState() in
    853   ignore (localB#connect#clicked ~callback:localState);
    854   ignore (sshB#connect#clicked ~callback:(fun () -> protoState(`SSH)));
    855   ignore (socketB#connect#clicked ~callback:(fun () -> protoState(`SOCKET)));
    856   localState();
    857   let getRoot() =
    858     let file = Util.trimWhitespace fileE#text in
    859     let user = Util.trimWhitespace userE#text in
    860     let host = Util.trimWhitespace hostE#text in
    861     let port = Util.trimWhitespace portE#text in
    862     match !varLocalRemote with
    863       `Local ->
    864         Clroot.clroot2string(Clroot.ConnectLocal(Some file))
    865     | `SSH ->
    866         Clroot.clroot2string(Clroot.fixHost(
    867         Clroot.ConnectByShell("ssh",
    868                               host,
    869                               (if user="" then None else Some user),
    870                               (if port="" then None else Some port),
    871                               Some file)))
    872     | `SOCKET ->
    873         Clroot.clroot2string(Clroot.fixHost(
    874         (* FIX: report an error if the port entry is not well formed *)
    875         Clroot.ConnectBySocket(host,
    876                                portE#text,
    877                                Some file))) in
    878   let contCommand() =
    879     try
    880       let root = getRoot() in
    881       result := Some root;
    882       t#destroy ()
    883     with Failure _ ->
    884       if portE#text="" then
    885         okBox ~parent:t ~title:"Error" ~typ:`ERROR ~message:"Please enter a port"
    886       else okBox ~parent:t ~title:"Error" ~typ:`ERROR
    887           ~message:"The port you specify must be an integer"
    888     | Util.Transient s | Util.Fatal s | Invalid_argument s | Prefs.IllegalValue s ->
    889       okBox ~parent:t ~title:"Error" ~typ:`ERROR
    890         ~message:("Something's wrong with the values you entered, try again.\n" ^ s) in
    891   let f3 = t#action_area in
    892   let cancelButton =
    893     GButton.button ~stock:`CANCEL ~packing:f3#add () in
    894   ignore (cancelButton#connect#clicked
    895             ~callback:(fun () -> result := None; t#destroy ()));
    896   let contButton =
    897     GButton.button ~stock:`OK ~packing:f3#add () in
    898   ignore (contButton#connect#clicked ~callback:contCommand);
    899   contButton#grab_default ();
    900   ignore (fileE#connect#activate ~callback:contCommand);
    901 
    902   t#show ();
    903   ignore (t#connect#destroy ~callback:GMain.Main.quit);
    904   GMain.Main.main ();
    905   !result
    906 
    907 let promptForRoots () =
    908   match getFirstRoot () with
    909   | None -> None
    910   | Some r1 ->
    911       begin match getSecondRoot () with
    912       | None -> None
    913       | Some r2 -> Some (r1, r2)
    914       end
    915 
    916 (* ------ *)
    917 
    918 type 'a pwdDialog = {
    919   labelAppend : string -> unit;
    920   presentAndRun : unit -> unit;
    921   closeInput : unit -> unit;
    922 }
    923 let passwordDialogs = ref []
    924 
    925 let createPasswordDialog passwordDialog rootName msg response =
    926   let t =
    927     GWindow.dialog ~parent:(toplevelWindow ())
    928       ~title:"Unison: SSH connection" ~position:`CENTER
    929       ~modal:true ~resizable:false ~border_width:6 () in
    930   t#misc#grab_focus ();
    931 
    932   t#vbox#set_spacing 12;
    933 
    934   let header =
    935     primaryText
    936       (Format.sprintf "Connecting to '%s'..." (Unicode.protect rootName)) in
    937 
    938   let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
    939   ignore (GMisc.image ~stock:`DIALOG_AUTHENTICATION ~icon_size:`DIALOG
    940             ~yalign:0. ~packing:h1#pack ());
    941   let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
    942   let msgLbl = (GMisc.label ~markup:(header ^ "\n\n" ^
    943                               escapeMarkup (Unicode.protect msg))
    944            ~selectable:true ~yalign:0. ~packing:v1#pack ()) in
    945 
    946   let passwordE = GEdit.entry ~packing:v1#pack ~visibility:false () in
    947   passwordE#misc#grab_focus ();
    948 
    949   t#add_button_stock `QUIT `QUIT;
    950   t#add_button_stock `OK `OK;
    951   t#set_default_response `OK;
    952   ignore (passwordE#connect#activate ~callback:(fun _ -> t#response `OK));
    953 
    954   t#show();
    955 
    956   let labelAppend msg =
    957     msgLbl#set_label (msgLbl#label ^ escapeMarkup (Unicode.protect msg)) in
    958   let presentAndRun () =
    959     try t#present (); ignore (t#run ()) with Failure _ -> () in
    960   let closeInput () =
    961     passwordE#set_editable false; passwordE#set_visible false; passwordE#set_text "" in
    962   passwordDialog := Some { labelAppend; presentAndRun; closeInput };
    963 
    964   let callback res =
    965     passwordDialog := None;
    966     let pwd = passwordE#text in
    967     let editable = passwordE#editable in
    968     t#destroy ();
    969     gtk_sync true;
    970     match res with
    971     | `DELETE_EVENT | `QUIT -> safeExit ()
    972     | `OK -> if editable then response pwd
    973   in
    974   ignore (t#connect#response ~callback)
    975 
    976 let getPassword passwordDialog rootName msg response =
    977   match !passwordDialog with
    978   | Some { labelAppend; _ } -> labelAppend msg
    979   | None -> createPasswordDialog passwordDialog rootName msg response
    980 
    981 let disablePassword passwordDialog () =
    982   match !passwordDialog with
    983   | Some { closeInput; _ } -> closeInput ()
    984   | None -> ()
    985 
    986 let waitForPasswordWindowClosing () =
    987   let present x =
    988     match !x with
    989     | Some { presentAndRun; _ } -> presentAndRun ()
    990     | None -> ()
    991   in
    992   passwordDialogs :=
    993     Safelist.filter (fun x -> present x; !x <> None) !passwordDialogs
    994 
    995 let termInteract rootName =
    996   let d = ref None in
    997   passwordDialogs := d :: !passwordDialogs;
    998   { Terminal.userInput = getPassword d rootName; endInput = disablePassword d }
    999 
   1000 (* ------ *)
   1001 
   1002 module React = struct
   1003   type 'a t = { mutable state : 'a; mutable observers : ('a -> unit) list }
   1004 
   1005   let make v =
   1006     let res = { state = v; observers = [] } in
   1007     let update v =
   1008       if res.state <> v then begin
   1009         res.state <- v; List.iter (fun f -> f v) res.observers
   1010       end
   1011     in
   1012     (res, update)
   1013 
   1014   let const v = fst (make v)
   1015 
   1016   let add_observer x f = x.observers <- f :: x.observers
   1017 
   1018   let state x = x.state
   1019 
   1020   let lift f x =
   1021     let (res, update) = make (f (state x)) in
   1022     add_observer x (fun v -> update (f v));
   1023     res
   1024 
   1025   let lift2 f x y =
   1026     let (res, update) = make (f (state x) (state y)) in
   1027     add_observer x (fun v -> update (f v (state y)));
   1028     add_observer y (fun v -> update (f (state x) v));
   1029     res
   1030 
   1031   let lift3 f x y z =
   1032     let (res, update) = make (f (state x) (state y) (state z)) in
   1033     add_observer x (fun v -> update (f v (state y) (state z)));
   1034     add_observer y (fun v -> update (f (state x) v (state z)));
   1035     add_observer z (fun v -> update (f (state x) (state y) v));
   1036     res
   1037 
   1038   let iter f x = f (state x); add_observer x f
   1039 
   1040   type 'a event = { mutable ev_observers : ('a -> unit) list }
   1041 
   1042   let make_event () =
   1043     let res = { ev_observers = [] } in
   1044     let trigger v = List.iter (fun f -> f v) res.ev_observers in
   1045     (res, trigger)
   1046 
   1047   let add_ev_observer x f = x.ev_observers <- f :: x.ev_observers
   1048 
   1049   let hold v e =
   1050     let (res, update) = make v in
   1051     add_ev_observer e update;
   1052     res
   1053 
   1054   let iter_ev f e = add_ev_observer e f
   1055 
   1056   let lift_ev f e =
   1057     let (res, trigger) = make_event () in
   1058     add_ev_observer e (fun x -> trigger (f x));
   1059     res
   1060 
   1061   module Ops = struct
   1062     let (>>) x f = lift f x
   1063     let (>|) x f = iter f x
   1064 
   1065     let (>>>) x f = lift_ev f x
   1066     let (>>|) x f = iter_ev f x
   1067   end
   1068 end
   1069 
   1070 module GtkReact = struct
   1071   let entry (e : #GEdit.entry) =
   1072     let (res, update) = React.make e#text in
   1073     ignore (e#connect#changed ~callback:(fun () -> update (e#text)));
   1074     res
   1075 
   1076   let text_combo ((c, _) : _ GEdit.text_combo) =
   1077     let (res, update) = React.make c#active in
   1078     ignore (c#connect#changed ~callback:(fun () -> update (c#active)));
   1079     res
   1080 
   1081   let toggle_button (b : #GButton.toggle_button) =
   1082     let (res, update) = React.make b#active in
   1083     ignore (b#connect#toggled ~callback:(fun () -> update (b#active)));
   1084     res
   1085 
   1086   let file_chooser (c : #GFile.chooser) =
   1087     let (res, update) = React.make c#filename in
   1088     ignore (c#connect#selection_changed
   1089               ~callback:(fun () -> update (c#filename)));
   1090     res
   1091 
   1092   let current_tree_view_selection (t : #GTree.view) =
   1093     let m =t#model in
   1094     Safelist.map (fun p -> m#get_row_reference p) t#selection#get_selected_rows
   1095 
   1096   let tree_view_selection_changed t =
   1097     let (res, trigger) = React.make_event () in
   1098     ignore (t#selection#connect#changed
   1099               ~callback:(fun () -> trigger (current_tree_view_selection t)));
   1100     res
   1101 
   1102   let tree_view_selection t =
   1103     React.hold (current_tree_view_selection t) (tree_view_selection_changed t)
   1104 
   1105   let label (l : #GMisc.label) x = React.iter (fun v -> l#set_text v) x
   1106 
   1107   let label_underlined (l : #GMisc.label) x =
   1108     React.iter (fun v -> l#set_text v; l#set_use_underline true) x
   1109 
   1110   let label_markup (l : #GMisc.label) x =
   1111     React.iter (fun v -> l#set_text v; l#set_use_markup true) x
   1112 
   1113   let show w x =
   1114     React.iter (fun b -> if b then w#misc#show () else w#misc#hide ()) x
   1115   let set_sensitive w x = React.iter (fun b -> w#misc#set_sensitive b) x
   1116 end
   1117 
   1118 open React.Ops
   1119 
   1120 (* ------ *)
   1121 
   1122 (* Resize an object (typically, a label with line wrapping) so that it
   1123    use all its available space *)
   1124 let adjustSize (w : #GObj.widget) =
   1125   let notYet = ref true in
   1126   ignore
   1127     (w#misc#connect#size_allocate ~callback:(fun r ->
   1128        if !notYet then begin
   1129          notYet := false;
   1130          (* JV: I have no idea where the 12 comes from.  Without it,
   1131             a window resize may happen. *)
   1132          w#misc#set_size_request ~width:(max 10 (r.Gtk.width - 12)) ()
   1133        end))
   1134 
   1135 let createProfile parent =
   1136   let assistant = GAssistant.assistant ~modal:true () in
   1137   assistant#set_transient_for parent#as_window;
   1138   assistant#set_modal true;
   1139   assistant#set_title "Profile Creation";
   1140 
   1141   let empty s = s = "" in
   1142   let nonEmpty s = s <> "" in
   1143 (*
   1144   let integerRe =
   1145     Str.regexp "\\([+-]?[0-9]+\\|0o[0-7]+\\|0x[0-9a-zA-Z]+\\)" in
   1146 *)
   1147   let integerRe = Str.regexp "[0-9]+" in
   1148   let isInteger s =
   1149     Str.string_match integerRe s 0 && Str.matched_string s = s in
   1150 
   1151   (* Introduction *)
   1152   let intro =
   1153     GMisc.label
   1154       ~xpad:12 ~ypad:12
   1155       ~text:"Welcome to the Unison Profile Creation Assistant.\n\n\
   1156              Click \"Next\" to begin."
   1157     () in
   1158   ignore
   1159     (assistant#append_page
   1160        ~title:"Profile Creation"
   1161        ~page_type:`INTRO
   1162        ~complete:true
   1163       intro#as_widget);
   1164 
   1165   (* Profile name and description *)
   1166   let description = GPack.vbox ~border_width:12 ~spacing:6 () in
   1167   adjustSize
   1168     (GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
   1169        ~text:"Please enter the name of the profile and \
   1170               possibly a short description."
   1171        ~packing:(description#pack ~expand:false) ());
   1172   let tbl =
   1173     let al = GBin.alignment ~packing:(description#pack ~expand:false) () in
   1174     al#set_left_padding 12;
   1175     GPack.table ~rows:2 ~columns:2 ~col_spacings:12 ~row_spacings:6
   1176       ~packing:(al#add) () in
   1177   let nameEntry =
   1178     GEdit.entry ~activates_default:true
   1179       ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X) () in
   1180   let name = GtkReact.entry nameEntry in
   1181   ignore (GMisc.label ~text:"Profile _name:" ~xalign:0.
   1182             ~use_underline:true ~mnemonic_widget:nameEntry
   1183             ~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
   1184   let labelEntry =
   1185     GEdit.entry ~activates_default:true
   1186        ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) () in
   1187   let label = GtkReact.entry labelEntry in
   1188   ignore (GMisc.label ~text:"_Description:" ~xalign:0.
   1189             ~use_underline:true ~mnemonic_widget:labelEntry
   1190             ~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
   1191   let existingProfileLabel =
   1192     GMisc.label ~xalign:1. ~packing:(description#pack ~expand:false) ()
   1193   in
   1194   adjustSize existingProfileLabel;
   1195   GtkReact.label_markup existingProfileLabel
   1196     (name >> fun s -> Format.sprintf " <i>Profile %s already exists.</i>"
   1197                         (escapeMarkup s));
   1198   let profileExists =
   1199     name >> fun s -> s <> "" && System.file_exists (Prefs.profilePathname s)
   1200   in
   1201   GtkReact.show existingProfileLabel profileExists;
   1202 
   1203   ignore
   1204     (assistant#append_page
   1205        ~title:"Profile Description"
   1206        ~page_type:`CONTENT
   1207        description#as_widget);
   1208   let setPageComplete page b = assistant#set_page_complete page#as_widget b in
   1209   React.lift2 (&&) (name >> nonEmpty) (profileExists >> not)
   1210     >| setPageComplete description;
   1211 
   1212   let connection = GPack.vbox ~border_width:12 ~spacing:12 () in
   1213   let vb =
   1214     GPack.vbox ~spacing:6 ~packing:(connection#pack ~expand:false) () in
   1215   adjustSize
   1216     (GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
   1217        ~text:"You can use Unison to synchronize a local directory \
   1218               with another local directory, or with a remote directory."
   1219        ~packing:(vb#pack ~expand:false) ());
   1220   adjustSize
   1221     (GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
   1222        ~text:"Please select the kind of synchronization \
   1223               you want to perform."
   1224        ~packing:(vb#pack ~expand:false) ());
   1225   let tbl =
   1226     let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
   1227     al#set_left_padding 12;
   1228     GPack.table ~rows:2 ~columns:2 ~col_spacings:12 ~row_spacings:6
   1229       ~packing:(al#add) () in
   1230   ignore (GMisc.label ~text:"Description:" ~xalign:0. ~yalign:0.
   1231             ~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
   1232   let kindCombo =
   1233     let al =
   1234       GBin.alignment ~xscale:0. ~xalign:0.
   1235         ~packing:(tbl#attach ~left:1 ~top:0) () in
   1236     GEdit.combo_box_text
   1237       ~strings:["Local"; "Using SSH";
   1238                 "Through a plain TCP connection"]
   1239       ~active:0 ~packing:(al#add) ()
   1240   in
   1241   ignore (GMisc.label ~text:"Synchronization _kind:" ~xalign:0.
   1242             ~use_underline:true ~mnemonic_widget:(fst kindCombo)
   1243             ~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
   1244   let kind =
   1245     GtkReact.text_combo kindCombo
   1246       >> fun i -> List.nth [`Local; `SSH; `SOCKET] i
   1247   in
   1248   let isLocal = kind >> fun k -> k = `Local in
   1249   let isSSH = kind >> fun k -> k = `SSH in
   1250   let isSocket = kind >> fun k -> k = `SOCKET in
   1251   let descrLabel =
   1252     GMisc.label ~xalign:0. ~line_wrap:true
   1253        ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) ()
   1254   in
   1255   adjustSize descrLabel;
   1256   GtkReact.label descrLabel
   1257     (kind >> fun k ->
   1258      match k with
   1259        `Local ->
   1260           "Local synchronization."
   1261      | `SSH ->
   1262           "This is the recommended way to synchronize \
   1263            with a remote machine.  A\xc2\xa0remote instance of Unison is \
   1264            automatically started via SSH."
   1265      | `SOCKET ->
   1266           "Synchronization with a remote machine by connecting \
   1267            to an instance of Unison already listening \
   1268            on a specific TCP port.");
   1269   let vb = GPack.vbox ~spacing:6 ~packing:(connection#add) () in
   1270   GtkReact.show vb (isLocal >> not);
   1271   ignore (GMisc.label ~markup:"<b>Configuration</b>" ~xalign:0.
   1272             ~packing:(vb#pack ~expand:false) ());
   1273   let al = GBin.alignment ~packing:(vb#add) () in
   1274   al#set_left_padding 12;
   1275   let vb = GPack.vbox ~spacing:6 ~packing:(al#add) () in
   1276   let requirementLabel =
   1277     GMisc.label ~xalign:0. ~line_wrap:true
   1278        ~packing:(vb#pack ~expand:false) ()
   1279   in
   1280   adjustSize requirementLabel;
   1281   GtkReact.label requirementLabel
   1282     (kind >> fun k ->
   1283      match k with
   1284        `Local ->
   1285           ""
   1286      | `SSH ->
   1287           "There must be an SSH client installed on this machine, \
   1288            and Unison and an SSH server installed on the remote machine."
   1289      | `SOCKET ->
   1290           "There must be a Unison server running on the remote machine, \
   1291            listening on the port that you specify here.  \
   1292            (Use \"Unison -socket xxx\" on the remote machine to start \
   1293            the Unison server.)");
   1294   let connDescLabel =
   1295     GMisc.label ~xalign:0. ~line_wrap:true
   1296        ~packing:(vb#pack ~expand:false) ()
   1297   in
   1298   adjustSize connDescLabel;
   1299   GtkReact.label connDescLabel
   1300     (kind >> fun k ->
   1301      match k with
   1302        `Local  -> ""
   1303      | `SSH    -> "Please enter the host to connect to and a user name, \
   1304                    if different from your user name on this machine."
   1305      | `SOCKET -> "Please enter the host and port to connect to.");
   1306   let tbl =
   1307     let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
   1308     al#set_left_padding 12;
   1309     GPack.table ~rows:2 ~columns:2 ~col_spacings:12 ~row_spacings:6
   1310       ~packing:(al#add) () in
   1311   let hostEntry =
   1312     GEdit.entry ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X) () in
   1313   let host = GtkReact.entry hostEntry in
   1314   ignore (GMisc.label ~text:"_Host:" ~xalign:0.
   1315             ~use_underline:true ~mnemonic_widget:hostEntry
   1316             ~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
   1317   let userEntry =
   1318     GEdit.entry ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) ()
   1319   in
   1320   GtkReact.show userEntry (isSocket >> not);
   1321   let user = GtkReact.entry userEntry in
   1322   GtkReact.show
   1323     (GMisc.label ~text:"_User:" ~xalign:0. ~yalign:0.
   1324        ~use_underline:true ~mnemonic_widget:userEntry
   1325        ~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ())
   1326     (isSocket >> not);
   1327   let portEntry =
   1328     GEdit.entry ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) ()
   1329   in
   1330   GtkReact.show portEntry isSocket;
   1331   let port = GtkReact.entry portEntry in
   1332   GtkReact.show
   1333     (GMisc.label ~text:"_Port:" ~xalign:0. ~yalign:0.
   1334        ~use_underline:true ~mnemonic_widget:portEntry
   1335        ~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ())
   1336     isSocket;
   1337   let compressLabel =
   1338     GMisc.label ~xalign:0. ~line_wrap:true
   1339       ~text:"Data compression can greatly improve performance \
   1340              on slow connections.  However, it may slow down \
   1341              things on (fast) local networks."
   1342       ~packing:(vb#pack ~expand:false) ()
   1343   in
   1344   adjustSize compressLabel;
   1345   GtkReact.show compressLabel isSSH;
   1346   let compressButton =
   1347     let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
   1348     al#set_left_padding 12;
   1349     (GButton.check_button ~label:"Enable _compression" ~use_mnemonic:true
   1350        ~active:false ~packing:al#add ())
   1351   in
   1352   GtkReact.show compressButton isSSH;
   1353   let compress = GtkReact.toggle_button compressButton in
   1354 (*XXX Disabled for now... *)
   1355 (*
   1356   adjustSize
   1357     (GMisc.label ~xalign:0. ~line_wrap:true
   1358        ~text:"If this is possible, it is recommended that Unison \
   1359               attempts to connect immediately to the remote machine, \
   1360               so that it can perform some auto-detections."
   1361        ~packing:(vb#pack ~expand:false) ());
   1362   let connectImmediately =
   1363     let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
   1364     al#set_left_padding 12;
   1365     GtkReact.toggle_button
   1366       (GButton.check_button ~label:"Connect _immediately" ~use_mnemonic:true
   1367          ~active:true ~packing:(al#add) ())
   1368   in
   1369   let connectImmediately =
   1370     React.lift2 (&&) connectImmediately (isLocal >> not) in
   1371 *)
   1372   let isNotUnixPath s = String.length s > 0 && s.[0] <> '{' in
   1373   let isTCPsocket = React.lift2 (&&) isSocket (host >> isNotUnixPath) in
   1374   let pageComplete =
   1375     React.lift2 (||) isLocal
   1376       (React.lift2 (&&) (host >> nonEmpty)
   1377           (React.lift2 (||)
   1378               (React.lift2 (&&) isTCPsocket (port >> isInteger))
   1379               (React.lift2 (&&) (isTCPsocket >> not) (port >> empty))))
   1380   in
   1381   ignore
   1382     (assistant#append_page
   1383        ~title:"Connection Setup"
   1384        ~page_type:`CONTENT
   1385        connection#as_widget);
   1386   pageComplete >| setPageComplete connection;
   1387 
   1388   (* Connection to server *)
   1389 (*XXX Disabled for now... Fill in this page
   1390   let connectionInProgress = GMisc.label ~text:"..." () in
   1391   let p =
   1392     assistant#append_page
   1393       ~title:"Connecting to Server..."
   1394       ~page_type:`PROGRESS
   1395       connectionInProgress#as_widget
   1396   in
   1397   ignore
   1398     (assistant#connect#prepare (fun () ->
   1399        if assistant#current_page = p then begin
   1400          if React.state connectImmediately then begin
   1401            (* XXXX start connection... *)
   1402            assistant#set_page_complete connectionInProgress#as_widget true
   1403          end else
   1404            assistant#set_current_page (p + 1)
   1405        end));
   1406 *)
   1407 
   1408   (* Directory selection *)
   1409   let directorySelection = GPack.vbox ~border_width:12 ~spacing:6 () in
   1410   let dirhb = GPack.hbox ~packing:(directorySelection#pack ~expand:false) () in
   1411   adjustSize
   1412     (GMisc.label ~xalign:0. ~line_wrap:false ~justify:`LEFT
   1413        ~text:"Please select the two "
   1414        ~packing:(dirhb#pack ~expand:false) ());
   1415   let dirKindCombo =
   1416     GEdit.combo_box_text
   1417       ~strings:["directories"; "files"]
   1418       ~active:0 ~packing:(dirhb#pack ~expand:false) () in
   1419   let dirKind =
   1420     GtkReact.text_combo dirKindCombo
   1421       >> fun i -> List.nth [`Dir; `File] i
   1422   in
   1423   adjustSize
   1424     (GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
   1425        ~text:" that you want to synchronize."
   1426        ~packing:(dirhb#pack ~expand:false) ());
   1427   (* Not sure what's going on here, but when setting the focus on an element,
   1428      it's actually the next element that gets the focus by default. We want
   1429      the focus to be on the first directory selector. Setting the focus on the
   1430      combo here achieves exactly that... *)
   1431   ignore ((fst dirKindCombo)#misc#connect#map ~callback:(fst dirKindCombo)#misc#grab_focus);
   1432   let secondDirLabel1 =
   1433     GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
   1434       ~text:""
   1435       ~packing:(directorySelection#pack ~expand:false) ()
   1436   in
   1437   adjustSize secondDirLabel1;
   1438   GtkReact.show secondDirLabel1 ((React.lift2 (||) isLocal isSocket) >> not);
   1439   GtkReact.label secondDirLabel1 (dirKind >> function
   1440     | `Dir  -> "The second directory is relative to your home \
   1441                 directory on the remote machine."
   1442     | `File -> "The second file is relative to your home \
   1443                 directory on the remote machine.");
   1444   let secondDirLabel2 =
   1445     GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
   1446       ~text:""
   1447       ~packing:(directorySelection#pack ~expand:false) ()
   1448   in
   1449   adjustSize secondDirLabel2;
   1450   GtkReact.show secondDirLabel2 isSocket;
   1451   GtkReact.label secondDirLabel2 (dirKind >> function
   1452     | `Dir  -> "The second directory is relative to \
   1453                 the working directory of the Unison server \
   1454                 running on the remote machine."
   1455     | `File -> "The second file is relative to \
   1456                 the working directory of the Unison server \
   1457                 running on the remote machine.");
   1458   let tbl =
   1459     let al =
   1460       GBin.alignment ~packing:(directorySelection#pack ~expand:false) () in
   1461     al#set_left_padding 12;
   1462     GPack.table ~rows:2 ~columns:2 ~col_spacings:12 ~row_spacings:6
   1463       ~packing:(al#add) () in
   1464   let firstDirButton =
   1465     GFile.chooser_button ~action:`SELECT_FOLDER ~title:"First Directory"
   1466        ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X) ()
   1467   in
   1468   React.lift2
   1469     (fun local dirkind ->
   1470       firstDirButton#set_action (
   1471         match dirkind with
   1472         | `Dir  -> `SELECT_FOLDER
   1473         | `File -> `OPEN
   1474       );
   1475       firstDirButton#set_title (
   1476         match local, dirkind with
   1477         | true, `Dir   -> "First Directory"
   1478         | false, `Dir  -> "Local Directory"
   1479         | true, `File  -> "First File"
   1480         | false, `File -> "Local File"
   1481       )
   1482     ) isLocal dirKind |> ignore;
   1483 
   1484   GtkReact.label_underlined
   1485     (GMisc.label ~xalign:0.
   1486        ~mnemonic_widget:firstDirButton
   1487        ~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ())
   1488     (React.lift2 (fun local dirkind ->
   1489         match local, dirkind with
   1490         | true, `Dir -> "_First directory:"
   1491         | false, `Dir -> "_Local directory:"
   1492         | true, `File -> "_First file:"
   1493         | false, `File -> "_Local file:"
   1494       ) isLocal dirKind);
   1495   let noneToEmpty o = match o with None -> "" | Some s -> s in
   1496   let firstDir = GtkReact.file_chooser firstDirButton >> noneToEmpty in
   1497 
   1498   let secondDirButton =
   1499     GFile.chooser_button ~action:`SELECT_FOLDER ~title:"Second Directory"
   1500        ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) () in
   1501   dirKind >| (function
   1502     | `Dir  -> secondDirButton#set_action `SELECT_FOLDER;
   1503                secondDirButton#set_title "Second Directory"
   1504     | `File -> secondDirButton#set_action `OPEN;
   1505                secondDirButton#set_title "Second File"
   1506   );
   1507 
   1508   let secondDirLabel =
   1509     GMisc.label ~xalign:0.
   1510       ~text:"Se_cond directory:"
   1511       ~use_underline:true ~mnemonic_widget:secondDirButton
   1512       ~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) () in
   1513   GtkReact.show secondDirButton isLocal;
   1514   GtkReact.show secondDirLabel isLocal;
   1515   GtkReact.label_underlined secondDirLabel
   1516     (dirKind >> function `Dir -> "Se_cond directory:" | `File -> "Se_cond file:");
   1517   let remoteDirEdit =
   1518     GEdit.entry ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) ()
   1519   in
   1520   let remoteDirLabel =
   1521     GMisc.label ~xalign:0.
   1522       ~text:"_Remote directory:"
   1523       ~use_underline:true ~mnemonic_widget:remoteDirEdit
   1524       ~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ()
   1525   in
   1526   GtkReact.show remoteDirEdit (isLocal >> not);
   1527   GtkReact.show remoteDirLabel (isLocal >> not);
   1528   GtkReact.label_underlined remoteDirLabel
   1529     (dirKind >> function `Dir -> "_Remote directory:" | `File -> "_Remote file:");
   1530   let secondDir =
   1531     React.lift3 (fun b l r -> if b then l else r) isLocal
   1532       (GtkReact.file_chooser secondDirButton >> noneToEmpty)
   1533       (GtkReact.entry remoteDirEdit)
   1534   in
   1535   ignore
   1536     (assistant#append_page
   1537        ~title:"Directory Selection"
   1538        ~page_type:`CONTENT
   1539        directorySelection#as_widget);
   1540   React.lift2 (||) (isLocal >> not) (React.lift2 (<>) firstDir secondDir)
   1541     >| setPageComplete directorySelection;
   1542 
   1543   (* Specific options *)
   1544   let options = GPack.vbox ~border_width:12 ~spacing:12 () in
   1545   (* Do we need to set specific options for FAT partitions?
   1546      If under Windows, then all the options are set properly, except for
   1547      ignoreinodenumbers in case one replica is on a FAT partition on a
   1548      remote non-Windows machine.  As this is unlikely, we do not
   1549      handle this case. *)
   1550   let fat =
   1551     if Sys.win32 then
   1552       React.const false
   1553     else begin
   1554       let vb =
   1555         GPack.vbox ~spacing:6 ~packing:(options#pack ~expand:false) () in
   1556       let fatLabel =
   1557         GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
   1558           ~text:"Select the following option if one of your \
   1559                  directory is on a FAT partition.  This is typically \
   1560                  the case for a USB stick."
   1561           ~packing:(vb#pack ~expand:false) ()
   1562       in
   1563       adjustSize fatLabel;
   1564       let fatButton =
   1565         let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
   1566         al#set_left_padding 12;
   1567         (GButton.check_button
   1568            ~label:"Synchronization involving a _FAT partition"
   1569            ~use_mnemonic:true ~active:false ~packing:(al#add) ())
   1570       in
   1571       GtkReact.toggle_button fatButton
   1572     end
   1573   in
   1574   (* Fastcheck is safe except on FAT partitions and on Windows when
   1575      not in Unicode mode where there is a very slight chance of
   1576      missing an update when a file is moved onto another with the same
   1577      modification time.  Nowadays, FAT is rarely used on working
   1578      partitions.  In most cases, we should be in Unicode mode.
   1579      Thus, it seems sensible to always enable fastcheck. *)
   1580 (*
   1581   let fastcheck = isLocal >> not >> (fun b -> b || Sys.win32) in
   1582 *)
   1583   (* Unicode mode can be problematic when the source machine is under
   1584      Windows and the remote machine is not, as Unison may have already
   1585      been used using the legacy Latin 1 encoding.  Cygwin also did not
   1586      handle Unicode before version 1.7. *)
   1587   let vb = GPack.vbox ~spacing:6 ~packing:(options#pack ~expand:false) () in
   1588   let askUnicode = React.const false in
   1589 (* isLocal >> not >> fun b -> (b && Sys.win32) || Sys.cygwin in*)
   1590   GtkReact.show vb askUnicode;
   1591   adjustSize
   1592     (GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
   1593        ~text:"When synchronizing in case insensitive mode, \
   1594               Unison has to make some assumptions regarding \
   1595               filename encoding.  If unsure, use Unicode."
   1596        ~packing:(vb#pack ~expand:false) ());
   1597   let vb =
   1598     let al = GBin.alignment
   1599       ~xscale:0. ~xalign:0. ~packing:(vb#pack ~expand:false) () in
   1600     al#set_left_padding 12;
   1601     GPack.vbox ~spacing:0 ~packing:(al#add) ()
   1602   in
   1603   ignore
   1604     (GMisc.label ~xalign:0. ~text:"Filename encoding:"
   1605        ~packing:(vb#pack ~expand:false) ());
   1606   let hb =
   1607     let al = GBin.alignment
   1608       ~xscale:0. ~xalign:0. ~packing:(vb#pack ~expand:false) () in
   1609     al#set_left_padding 12;
   1610     GPack.button_box `VERTICAL ~layout:`START
   1611       ~spacing:0 ~packing:(al#add) ()
   1612   in
   1613   let unicodeButton =
   1614     GButton.radio_button ~label:"_Unicode" ~use_mnemonic:true ~active:true
   1615       ~packing:(hb#add) ()
   1616   in
   1617   ignore
   1618     (GButton.radio_button ~label:"_Latin 1" ~use_mnemonic:true
   1619        ~group:unicodeButton#group ~packing:(hb#add) ());
   1620 (*
   1621   let unicode =
   1622     React.lift2 (||) (askUnicode >> not) (GtkReact.toggle_button unicodeButton)
   1623   in
   1624 *)
   1625   let p =
   1626     assistant#append_page
   1627       ~title:"Specific Options" ~complete:true
   1628       ~page_type:`CONTENT
   1629       options#as_widget
   1630   in
   1631   ignore
   1632     (assistant#connect#prepare ~callback:(fun () ->
   1633        if assistant#current_page = p &&
   1634           not (not Sys.win32 || React.state askUnicode)
   1635        then
   1636          assistant#set_current_page (p + 1)));
   1637 
   1638   let conclusionOk = "You have now finished filling in the profile.\n\n\
   1639              Click \"Apply\" to create it."
   1640   and conclusionFail = "There was an error when preparing the profile.\n\n\
   1641              Click \"Back\" to review what you entered." in
   1642   let conclusion =
   1643     GMisc.label
   1644       ~xpad:12 ~ypad:12
   1645       ~text:conclusionOk
   1646     () in
   1647   let conclusionp =
   1648     (assistant#append_page
   1649        ~title:"Done" ~complete:true
   1650        ~page_type:`CONFIRM
   1651        conclusion#as_widget) in
   1652 
   1653   let makeRemoteRoot () =
   1654     let secondDir = Util.trimWhitespace (React.state secondDir) in
   1655     let host = Util.trimWhitespace (React.state host) in
   1656     let user = match React.state user with "" -> None | u -> Some (Util.trimWhitespace u) in
   1657     let secondRoot =
   1658       match React.state kind with
   1659         `Local  -> Clroot.ConnectLocal (Some secondDir)
   1660       | `SSH    -> Clroot.ConnectByShell
   1661                      ("ssh", host, user, None, Some secondDir)
   1662       | `SOCKET -> Clroot.ConnectBySocket
   1663                      (host, React.state port, Some secondDir)
   1664     in
   1665     try
   1666       let root = Clroot.clroot2string (Clroot.fixHost secondRoot) in
   1667       ignore (Clroot.parseRoot root);
   1668       Some root
   1669     with
   1670     | Util.Transient s | Util.Fatal s | Invalid_argument s | Prefs.IllegalValue s ->
   1671         begin
   1672           okBox ~parent ~title:"Error" ~typ:`ERROR
   1673             ~message:("There was a problem with the remote root "
   1674               ^ "data you entered.\n\n" ^ s);
   1675           None
   1676         end
   1677   in
   1678   ignore (assistant#connect#prepare ~callback:(fun () ->
   1679     if assistant#current_page = conclusionp then
   1680     let ok = (React.state kind = `Local) || (makeRemoteRoot () <> None) in
   1681     let () = setPageComplete conclusion ok in
   1682     if ok then conclusion#set_text conclusionOk
   1683     else conclusion#set_text conclusionFail));
   1684 
   1685   let profileName = ref None in
   1686   let saveProfile () =
   1687     let filename = Prefs.profilePathname (React.state name) in
   1688     begin try
   1689       let ch =
   1690         System.open_out_gen [Open_wronly; Open_creat; Open_excl] 0o600 filename
   1691       in
   1692       let close_on_error f =
   1693         try f () with e -> close_out_noerr ch; raise e
   1694       in
   1695       close_on_error (fun () ->
   1696       Printf.fprintf ch "# Unison preferences\n";
   1697       let label = React.state label in
   1698       if label <> "" then Printf.fprintf ch "label = %s\n" label;
   1699       Printf.fprintf ch "root = %s\n" (React.state firstDir);
   1700       let secondRoot =
   1701         match makeRemoteRoot () with
   1702         | None -> assert false (* We should never reach here due to validation above *)
   1703         | Some s -> s
   1704       in
   1705       Printf.fprintf ch "root = %s\n" secondRoot;
   1706       if React.state compress && React.state kind = `SSH then
   1707         Printf.fprintf ch "sshargs = -C\n";
   1708 (*
   1709       if React.state fastcheck then
   1710         Printf.fprintf ch "fastcheck = true\n";
   1711       if React.state unicode then
   1712         Printf.fprintf ch "unicode = true\n";
   1713 *)
   1714       if React.state fat then Printf.fprintf ch "fat = true\n";
   1715       close_out ch);
   1716       profileName := Some (React.state name)
   1717     with Sys_error errmsg ->
   1718       okBox ~parent:assistant ~typ:`ERROR ~title:"Could not save profile"
   1719         ~message:("Error when saving profile: " ^ errmsg)
   1720     end;
   1721     assistant#destroy ();
   1722   in
   1723   ignore (assistant#connect#close ~callback:saveProfile);
   1724   ignore (assistant#connect#destroy ~callback:GMain.Main.quit);
   1725   ignore (assistant#connect#cancel ~callback:assistant#destroy);
   1726   assistant#show ();
   1727   GMain.Main.main ();
   1728   !profileName
   1729 
   1730 (* ------ *)
   1731 
   1732 let nameOfType t =
   1733   match t with
   1734     `BOOL        -> "boolean"
   1735   | `BOOLDEF     -> "boolean"
   1736   | `INT         -> "integer"
   1737   | `STRING      -> "text"
   1738   | `STRING_LIST -> "text list"
   1739   | `CUSTOM      -> "custom"
   1740   | `UNKNOWN     -> "unknown"
   1741 
   1742 let defaultValue t =
   1743   match t with
   1744     `BOOL        -> ["true"]
   1745   | `BOOLDEF     -> ["true"]
   1746   | `INT         -> ["0"]
   1747   | `STRING      -> [""]
   1748   | `STRING_LIST -> []
   1749   | `CUSTOM      -> []
   1750   | `UNKNOWN     -> []
   1751 
   1752 let editPreference parent nm ty vl =
   1753   let t =
   1754     GWindow.dialog ~parent ~border_width:12
   1755       ~title:"Edit the Preference"
   1756       ~modal:true () in
   1757   let vb = t#vbox in
   1758   vb#set_spacing 6;
   1759 
   1760   let isList =
   1761     match ty with
   1762       `STRING_LIST | `CUSTOM | `UNKNOWN -> true
   1763     | _ -> false
   1764   in
   1765   let columns = if isList then 3 else 2 in
   1766   let rows = if isList then 3 else 2 in
   1767   let tbl =
   1768     GPack.table ~rows ~columns ~col_spacings:12 ~row_spacings:6
   1769       ~packing:(vb#pack ~expand:true) () in
   1770   ignore (GMisc.label ~text:"Preference:" ~xalign:0.
   1771             ~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
   1772   ignore (GMisc.label ~text:"Description:" ~xalign:0.
   1773             ~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
   1774   ignore (GMisc.label ~text:"Type:" ~xalign:0.
   1775             ~packing:(tbl#attach ~left:0 ~top:2 ~expand:`NONE) ());
   1776   ignore (GMisc.label ~text:(Unicode.protect nm) ~xalign:0. ~selectable:true ()
   1777             ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X));
   1778   let (doc, _) = Prefs.documentation nm in
   1779   ignore (GMisc.label ~text:doc ~xalign:0. ~selectable:true ()
   1780             ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X));
   1781   ignore (GMisc.label ~text:(nameOfType ty) ~xalign:0. ~selectable:true ()
   1782             ~packing:(tbl#attach ~left:1 ~top:2 ~expand:`X));
   1783   let newValue =
   1784     if isList then begin
   1785       let valueLabel =
   1786         GMisc.label ~text:"V_alue:" ~use_underline:true ~xalign:0. ~yalign:0.
   1787           ~packing:(tbl#attach ~left:0 ~top:3 ~expand:`NONE) ()
   1788       in
   1789       let cols = new GTree.column_list in
   1790       let c_value = cols#add Gobject.Data.string in
   1791       let c_ml = cols#add Gobject.Data.caml in
   1792       let lst_store = GTree.list_store cols in
   1793       let lst =
   1794         let sw =
   1795           GBin.scrolled_window ~packing:(tbl#attach ~left:1 ~top:3 ~expand:`BOTH)
   1796             ~shadow_type:`IN ~height:200 ~width:400
   1797             ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC () in
   1798         GTree.view ~model:lst_store ~headers_visible:false
   1799           ~reorderable:true ~packing:sw#add () in
   1800       valueLabel#set_mnemonic_widget (Some (lst :> GObj.widget));
   1801       let column =
   1802         GTree.view_column
   1803           ~renderer:(GTree.cell_renderer_text [], ["text", c_value]) ()
   1804       in
   1805       ignore (lst#append_column column);
   1806       let vb =
   1807         GPack.button_box
   1808           `VERTICAL ~layout:`START ~spacing:6
   1809           ~packing:(tbl#attach ~left:2 ~top:3 ~expand:`NONE) ()
   1810       in
   1811       let selection = GtkReact.tree_view_selection lst in
   1812       let hasSel = selection >> fun l -> l <> [] in
   1813       let addB =
   1814         GButton.button ~stock:`ADD ~packing:(vb#pack ~expand:false) () in
   1815       let removeB =
   1816         GButton.button ~stock:`REMOVE ~packing:(vb#pack ~expand:false) () in
   1817       let editB =
   1818         GButton.button ~stock:`EDIT ~packing:(vb#pack ~expand:false) () in
   1819       let upB =
   1820         GButton.button ~stock:`GO_UP ~packing:(vb#pack ~expand:false) () in
   1821       let downB =
   1822         GButton.button ~stock:`GO_DOWN ~packing:(vb#pack ~expand:false) () in
   1823       List.iter (fun b -> b#set_xalign 0.) [addB; removeB; editB; upB; downB];
   1824       GtkReact.set_sensitive removeB hasSel;
   1825       let editLabel =
   1826         GMisc.label ~text:"Edited _item:"
   1827           ~use_underline:true ~xalign:0.
   1828           ~packing:(tbl#attach ~left:0 ~top:4 ~expand:`NONE) ()
   1829       in
   1830       let editEntry =
   1831         GEdit.entry ~packing:(tbl#attach ~left:1 ~top:4 ~expand:`X) () in
   1832       editLabel#set_mnemonic_widget (Some (editEntry :> GObj.widget));
   1833       let edit = GtkReact.entry editEntry in
   1834       let edited =
   1835         React.lift2
   1836           (fun l txt ->
   1837              match l with
   1838                [rf] -> lst_store#get ~row:rf#iter ~column:c_ml <> txt
   1839              | _    -> false)
   1840           selection edit
   1841       in
   1842       GtkReact.set_sensitive editB edited;
   1843       let selectionChange = GtkReact.tree_view_selection_changed lst in
   1844       selectionChange >>| (fun s ->
   1845         match s with
   1846           [rf] -> editEntry#set_text
   1847                     (lst_store#get ~row:rf#iter ~column:c_value)
   1848         | _    -> ());
   1849       let add () =
   1850         let txt = editEntry#text in
   1851         let row = lst_store#append () in
   1852         lst_store#set ~row ~column:c_value txt;
   1853         lst_store#set ~row ~column:c_ml txt;
   1854         lst#selection#select_iter row;
   1855         lst#scroll_to_cell (lst_store#get_path row) column
   1856       in
   1857       ignore (addB#connect#clicked ~callback:add);
   1858       ignore (editEntry#connect#activate ~callback:add);
   1859       let remove () =
   1860         match React.state selection with
   1861           [rf] -> let i = rf#iter in
   1862                   if lst_store#iter_next i then
   1863                     lst#selection#select_iter i
   1864                   else begin
   1865                     let p = rf#path in
   1866                     if GTree.Path.prev p then
   1867                       lst#selection#select_path p
   1868                   end;
   1869                   ignore (lst_store#remove rf#iter)
   1870         | _    -> ()
   1871       in
   1872       ignore (removeB#connect#clicked ~callback:remove);
   1873       let edit () =
   1874         match React.state selection with
   1875           [rf] -> let row = rf#iter in
   1876                   let txt = editEntry#text in
   1877                   lst_store#set ~row ~column:c_value txt;
   1878                   lst_store#set ~row ~column:c_ml txt
   1879         | _    -> ()
   1880       in
   1881       ignore (editB#connect#clicked ~callback:edit);
   1882       let updateUpDown l =
   1883         let (upS, downS) =
   1884           match l with
   1885               [rf] -> (GTree.Path.prev rf#path, lst_store#iter_next rf#iter)
   1886           | _      -> (false, false)
   1887         in
   1888         upB#misc#set_sensitive upS;
   1889         downB#misc#set_sensitive downS
   1890       in
   1891       selectionChange >>| updateUpDown;
   1892       ignore (lst_store#connect#after#row_deleted
   1893                 ~callback:(fun _ -> updateUpDown (React.state selection)));
   1894       let go_up () =
   1895         match React.state selection with
   1896           [rf] -> let p = rf#path in
   1897                   if GTree.Path.prev p then begin
   1898                     let i = rf#iter in
   1899                     let i' = lst_store#get_iter p in
   1900                     ignore (lst_store#swap i i');
   1901                     lst#scroll_to_cell (lst_store#get_path i) column
   1902                   end;
   1903                   updateUpDown (React.state selection)
   1904         | _    -> ()
   1905       in
   1906       ignore (upB#connect#clicked ~callback:go_up);
   1907       let go_down () =
   1908         match React.state selection with
   1909           [rf] -> let i = rf#iter in
   1910                   if lst_store#iter_next i then begin
   1911                     let i' = rf#iter in
   1912                     ignore (lst_store#swap i i');
   1913                     lst#scroll_to_cell (lst_store#get_path i') column
   1914                   end;
   1915                   updateUpDown (React.state selection)
   1916         | _    -> ()
   1917       in
   1918       ignore (downB#connect#clicked ~callback:go_down);
   1919       List.iter
   1920         (fun v ->
   1921            let row = lst_store#append () in
   1922            lst_store#set ~row ~column:c_value (Unicode.protect v);
   1923            lst_store#set ~row ~column:c_ml v)
   1924         vl;
   1925      (fun () ->
   1926         let l = ref [] in
   1927         lst_store#foreach
   1928           (fun _ row -> l := lst_store#get ~row ~column:c_ml :: !l; false);
   1929         List.rev !l)
   1930     end else begin
   1931       let v = List.hd vl in
   1932       begin match ty with
   1933         `BOOL | `BOOLDEF ->
   1934           let hb =
   1935             GPack.button_box `HORIZONTAL ~layout:`START
   1936               ~packing:(tbl#attach ~left:1 ~top:3 ~expand:`X) ()
   1937           in
   1938           let isTrue = v = "true" || v = "yes" in
   1939           let trueB =
   1940             GButton.radio_button ~label:"_True" ~use_mnemonic:true
   1941               ~active:isTrue ~packing:(hb#add) ()
   1942           in
   1943           ignore
   1944             (GButton.radio_button ~label:"_False" ~use_mnemonic:true
   1945                ~group:trueB#group ~active:(not isTrue) ~packing:(hb#add) ());
   1946            ignore
   1947              (GMisc.label ~text:"Value:" ~xalign:0.
   1948                 ~packing:(tbl#attach ~left:0 ~top:3 ~expand:`NONE) ());
   1949           (fun () -> [if trueB#active then "true" else "false"])
   1950       | `INT | `STRING ->
   1951            let valueEntry =
   1952              GEdit.entry ~text:v ~width_chars: 40
   1953                ~activates_default:true
   1954                ~packing:(tbl#attach ~left:1 ~top:3 ~expand:`X) ()
   1955            in
   1956            ignore
   1957              (GMisc.label ~text:"V_alue:" ~use_underline:true ~xalign:0.
   1958                 ~mnemonic_widget:valueEntry
   1959                 ~packing:(tbl#attach ~left:0 ~top:3 ~expand:`NONE) ());
   1960            (fun () -> [valueEntry#text])
   1961       | `STRING_LIST | `CUSTOM | `UNKNOWN ->
   1962            assert false
   1963       end
   1964     end
   1965   in
   1966 
   1967   let res = ref None in
   1968   let cancelCommand () = t#destroy () in
   1969   let cancelButton =
   1970     GButton.button ~stock:`CANCEL ~packing:t#action_area#add () in
   1971   ignore (cancelButton#connect#clicked ~callback:cancelCommand);
   1972   let okCommand _ = res := Some (newValue ()); t#destroy () in
   1973   let okButton =
   1974     GButton.button ~stock:`OK ~packing:t#action_area#add () in
   1975   ignore (okButton#connect#clicked ~callback:okCommand);
   1976   okButton#grab_default ();
   1977   ignore (t#connect#destroy ~callback:GMain.Main.quit);
   1978   t#show ();
   1979   GMain.Main.main ();
   1980   !res
   1981 
   1982 
   1983 let markupRe = Str.regexp "<\\([a-z]+\\)>\\|</\\([a-z]+\\)>\\|&\\([a-z]+\\);"
   1984 let entities =
   1985   [("amp", "&"); ("lt", "<"); ("gt", ">"); ("quot", "\""); ("apos", "'")]
   1986 
   1987 let rec insertMarkupRec tags (t : #GText.view) s i tl =
   1988   try
   1989     let j = Str.search_forward markupRe s i in
   1990     if j > i then
   1991       t#buffer#insert ~tags:(List.flatten tl) (String.sub s i (j - i));
   1992     let tag = try Some (Str.matched_group 1 s) with Not_found -> None in
   1993     match tag with
   1994       Some tag ->
   1995         insertMarkupRec tags t s (Str.group_end 0)
   1996           ((try [List.assoc tag tags] with Not_found -> []) :: tl)
   1997     | None ->
   1998         let entity = try Some (Str.matched_group 3 s) with Not_found -> None in
   1999         match entity with
   2000           None ->
   2001             insertMarkupRec tags t s (Str.group_end 0) (List.tl tl)
   2002         | Some ent ->
   2003             begin try
   2004               t#buffer#insert ~tags:(List.flatten tl) (List.assoc ent entities)
   2005             with Not_found -> () end;
   2006             insertMarkupRec tags t s (Str.group_end 0) tl
   2007   with Not_found ->
   2008     let j = String.length s in
   2009     if j > i then
   2010       t#buffer#insert ~tags:(List.flatten tl) (String.sub s i (j - i))
   2011 
   2012 let insertMarkup tags t s =
   2013   t#buffer#set_text ""; insertMarkupRec tags t s 0 []
   2014 
   2015 let documentPreference ~compact ~packing =
   2016   let vb = GPack.vbox ~spacing:6 ~packing () in
   2017   ignore (GMisc.label ~markup:"<b>Documentation</b>" ~xalign:0.
   2018             ~packing:(vb#pack ~expand:false) ());
   2019   let al = GBin.alignment ~packing:(vb#pack ~expand:true ~fill:true) () in
   2020   al#set_left_padding 12;
   2021   let columns = if compact then 3 else 2 in
   2022   let tbl =
   2023     GPack.table ~rows:2 ~columns ~col_spacings:12 ~row_spacings:6
   2024       ~packing:(al#add) () in
   2025   tbl#misc#set_sensitive false;
   2026   ignore (GMisc.label ~text:"Short description:" ~xalign:0.
   2027             ~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
   2028   ignore (GMisc.label ~text:"Long description:" ~xalign:0. ~yalign:0.
   2029             ~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
   2030   let shortDescr =
   2031     GMisc.label ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X)
   2032       ~xalign:0. ~selectable:true () in
   2033   let longDescr =
   2034     let sw =
   2035       if compact then
   2036         GBin.scrolled_window ~height:128 ~width:640
   2037           ~packing:(tbl#attach ~left:0 ~top:2 ~right:2 ~expand:`BOTH)
   2038           ~shadow_type:`IN ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
   2039       else
   2040         GBin.scrolled_window ~height:128 ~width:640
   2041           ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`BOTH)
   2042           ~shadow_type:`IN ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
   2043     in
   2044     GText.view ~editable:false ~packing:sw#add ~wrap_mode:`WORD ()
   2045   in
   2046   let () = longDescr#set_left_margin 4
   2047   and () = longDescr#set_right_margin 4
   2048   and () = longDescr#set_top_margin 1
   2049   and () = longDescr#set_bottom_margin 2 in
   2050   let (>>>) x f = f x in
   2051   let newlineRe = Str.regexp "\n *" in
   2052   let styleRe = Str.regexp "{\\\\\\([a-z]+\\) \\([^{}]*\\)}" in
   2053   let verbRe = Str.regexp "\\\\verb|\\([^|]*\\)|" in
   2054   let argRe = Str.regexp "\\\\ARG{\\([^{}]*\\)}" in
   2055   let textttRe = Str.regexp "\\\\texttt{\\([^{}]*\\)}" in
   2056   let emphRe = Str.regexp "\\\\emph{\\([^{}]*\\)}" in
   2057   let sectionRe = Str.regexp "\\\\sectionref{\\([^{}]*\\)}{\\([^{}]*\\)}" in
   2058   let emdash = Str.regexp_string "---" in
   2059   let parRe = Str.regexp "\\\\par *" in
   2060   let underRe = Str.regexp "\\\\_ *" in
   2061   let dollarRe = Str.regexp "\\\\\\$ *" in
   2062   let formatDoc doc =
   2063     doc >>>
   2064     Str.global_replace newlineRe " " >>>
   2065     escapeMarkup >>>
   2066     Str.global_substitute styleRe
   2067       (fun s ->
   2068          try
   2069            let tag =
   2070              match Str.matched_group 1 s with
   2071                "em" -> "i"
   2072              | "tt" -> "tt"
   2073              | _ -> raise Exit
   2074            in
   2075            Format.sprintf "<%s>%s</%s>" tag (Str.matched_group 2 s) tag
   2076          with Exit ->
   2077            Str.matched_group 0 s) >>>
   2078     Str.global_replace verbRe "<tt>\\1</tt>" >>>
   2079     Str.global_replace argRe "<tt>\\1</tt>" >>>
   2080     Str.global_replace textttRe "<tt>\\1</tt>" >>>
   2081     Str.global_replace emphRe "<i>\\1</i>" >>>
   2082     Str.global_replace sectionRe "Section '\\2'" >>>
   2083     Str.global_replace emdash "\xe2\x80\x94" >>>
   2084     Str.global_replace parRe "\n" >>>
   2085     Str.global_replace underRe "_" >>>
   2086     Str.global_replace dollarRe "_"
   2087   in
   2088   let tags =
   2089     let create = longDescr#buffer#create_tag in
   2090     [("i", create [`FONT_DESC (Lazy.force fontItalic)]);
   2091      ("tt", create [`FONT_DESC (Lazy.force fontMonospace)])]
   2092   in
   2093   fun nm ->
   2094     let (short, long) =
   2095       match nm with
   2096         Some nm ->
   2097           tbl#misc#set_sensitive true;
   2098           Prefs.documentation nm
   2099       | _ ->
   2100           tbl#misc#set_sensitive false;
   2101           ("", "")
   2102     in
   2103     shortDescr#set_text (String.capitalize_ascii short);
   2104     insertMarkup tags longDescr (formatDoc long)
   2105 (*    longDescr#buffer#set_text (formatDoc long)*)
   2106 
   2107 let addPreference parent =
   2108   let t =
   2109     GWindow.dialog ~parent ~border_width:0
   2110       ~title:"Add a Preference"
   2111       ~modal:true () in
   2112   t#set_default_height 575;
   2113   let vb = t#vbox in
   2114   (* The border_width of dialog used to be 12 (now 0). Instead, now the
   2115      margins of the inner box are set to 12 to get the same visual result.
   2116      The top margin is reduced because otherwise there would be too much
   2117      space due to [expand_all_btn]. *)
   2118   vb#set_margin 12;
   2119   vb#set_margin_top 0;
   2120   vb#set_spacing 12;
   2121   let paned = GPack.paned `VERTICAL ~packing:(vb#pack ~expand:true) () in
   2122 
   2123   let lvb = GPack.vbox ~spacing:1 ~packing:(paned#pack1 ~resize:true) () in
   2124   let lvhb = GPack.hbox ~spacing:6 ~packing:(lvb#pack ~expand:false) () in
   2125   let preferenceLabel =
   2126     GMisc.label
   2127       ~text:"_Preferences:" ~use_underline:true
   2128       ~xalign:0. ~yalign:1. ~packing:(lvhb#pack ~expand:true) ()
   2129   in
   2130   (* The spacing of [lvb] used to be 6. Now it's set to 1 and additionally
   2131      the bottom margin of [preferenceLabel] is set to 5 to get the same
   2132      visual result. This is done because otherwise there would be too much
   2133      space due to [expand_all_btn]. *)
   2134   preferenceLabel#set_margin_bottom 5;
   2135   let cols = new GTree.column_list in
   2136   let c_name = cols#add Gobject.Data.string in
   2137   let c_font = cols#add Gobject.Data.string in
   2138   let store = GTree.tree_store cols in
   2139   let lst =
   2140     let sw =
   2141       GBin.scrolled_window ~packing:(lvb#pack ~expand:true)
   2142         ~shadow_type:`IN ~height:200 ~width:400
   2143         ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC () in
   2144     GTree.view ~headers_visible:false ~packing:sw#add () in
   2145   preferenceLabel#set_mnemonic_widget (Some (lst :> GObj.widget));
   2146 
   2147   let expand_all_btn =
   2148     GButton.button ~label:"Expand all" ~relief:`NONE
   2149       ~packing:(lvhb#pack ~expand:false) () in
   2150   let expander =
   2151     let xicon =
   2152       GMisc.image ~icon_name:"pan-end-symbolic"
   2153         ~packing:expand_all_btn#set_image () in
   2154     let expanded = ref false in
   2155     fun _ev ->
   2156       lst#misc#grab_focus ();
   2157       if not !expanded then begin
   2158         lst#expand_all ();
   2159         xicon#set_icon_name "pan-down-symbolic";
   2160         expand_all_btn#set_label "Collapse all";
   2161         expanded := true
   2162       end else begin
   2163         lst#collapse_all ();
   2164         xicon#set_icon_name "pan-end-symbolic";
   2165         expand_all_btn#set_label "Expand all";
   2166         expanded := false
   2167       end
   2168   in
   2169   ignore (expand_all_btn#connect#clicked ~callback:expander);
   2170 
   2171   let cell_r = GTree.cell_renderer_text [] in
   2172   let view_col = (GTree.view_column ~renderer:(cell_r, ["text", c_name]) ()) in
   2173   view_col#add_attribute cell_r "font" c_font;
   2174   ignore (lst#append_column view_col);
   2175   (*let hiddenPrefs =
   2176     ["auto"; "silent"; "terse"] in*)
   2177   let shownPrefs =
   2178     ["label"; "key"] in
   2179 
   2180   let createGroup n =
   2181     let row = store#append () in
   2182     store#set ~row ~column:c_name n;
   2183     store#set ~row ~column:c_font "bold";
   2184     row
   2185   in
   2186   let createTopic parent n =
   2187     let row = store#append ~parent () in
   2188     store#set ~row ~column:c_name n;
   2189     store#set ~row ~column:c_font "italic";
   2190     row
   2191   in
   2192   let createTopics parent g =
   2193     Safelist.map (fun t ->
   2194       let topic = g t in
   2195       (topic, (createTopic parent (Prefs.topic_title topic))))
   2196   in
   2197 
   2198   let topicsInOrder = [ `Sync; `Syncprocess; `Syncprocess_CLI; `CLI; `GUI; `Remote; `Archive ] in
   2199 
   2200   let basic = createGroup "1 — Basic preferences" in
   2201   let l = createTopics basic (fun t -> `Basic t) (`General :: topicsInOrder) in
   2202 
   2203   let adv = createGroup "2 — Advanced preferences" in
   2204   let l = l @ createTopics adv (fun t -> `Advanced t) (topicsInOrder @ [`General]) in
   2205 
   2206   let l = (`Expert, createGroup "3 — Expert preferences") :: l in
   2207 
   2208   let parents = l in
   2209   let purgeParents () =
   2210     Safelist.iter (fun (_, row) ->
   2211         if not (store#iter_has_child row) then begin
   2212           let parent = store#iter_parent row in
   2213           ignore (store#remove row);
   2214           match parent with
   2215           | None -> ()
   2216           | Some parent -> if not (store#iter_has_child parent) then
   2217                              ignore (store#remove parent)
   2218         end
   2219       ) parents
   2220   in
   2221   let categoryParent nm =
   2222     match Prefs.category nm with
   2223     | None -> None
   2224     | Some _ when List.mem nm shownPrefs -> Some basic
   2225     | Some cat -> begin
   2226         try Some (Safelist.assoc cat parents) with
   2227         | Not_found -> None
   2228       end
   2229   in
   2230   let isParent r = store#iter_has_child r in
   2231 
   2232   let () =
   2233     List.iter
   2234       (fun nm ->
   2235          let row =
   2236            match categoryParent nm with
   2237            | None -> store#append ()
   2238            | Some parent -> store#append ~parent ()
   2239          in
   2240          store#set ~row ~column:c_name nm
   2241       )
   2242       (Prefs.list false);
   2243   in
   2244   purgeParents ();
   2245 
   2246   lst#set_model (Some store#coerce);
   2247 
   2248   begin match lst#model#get_iter_first with
   2249   | None -> ()
   2250   | Some iter -> lst#expand_row (lst#model#get_path iter)
   2251   end;
   2252 
   2253   let getSelectedPref row =
   2254     if isParent row then
   2255       None
   2256     else
   2257       Some (store#get ~row ~column:c_name)
   2258   in
   2259   let getSelectedPref' () =
   2260     match lst#selection#get_selected_rows with
   2261     | [path] -> getSelectedPref (lst#model#get_iter path)
   2262     | _ -> None
   2263   in
   2264   let getSelectedPrefReact = function
   2265     | [rf] -> getSelectedPref rf#iter
   2266     | _ -> None
   2267   in
   2268   let selection = GtkReact.tree_view_selection lst in
   2269   let updateDoc = documentPreference ~compact:true ~packing:(paned#pack2 ~resize:true) in
   2270   let prefSelection = selection >> getSelectedPrefReact in
   2271   prefSelection >| updateDoc;
   2272 
   2273   lst#set_enable_search true;
   2274   let lst_expand_by_keyboard ev =
   2275     let key = GdkEvent.Key.keyval ev in
   2276     if key = GdkKeysyms._Right then begin
   2277       lst#selection#get_selected_rows
   2278       |> Safelist.iter (fun p ->
   2279           let lst_iter = lst#model#get_iter p in
   2280           if isParent lst_iter then begin
   2281             if not (lst#row_expanded p) then
   2282               lst#expand_row p
   2283             else
   2284               let chld = lst#model#iter_children (Some lst_iter) in
   2285               lst#set_cursor (lst#model#get_path chld) (lst#get_column 0)
   2286           end);
   2287       true
   2288     end else if key = GdkKeysyms._Left then begin
   2289       lst#selection#get_selected_rows
   2290       |> Safelist.iter (fun p ->
   2291           let lst_iter = lst#model#get_iter p in
   2292           if isParent lst_iter && lst#row_expanded p then
   2293             lst#collapse_row p
   2294           else
   2295             match lst#model#iter_parent lst_iter with
   2296             | None -> ()
   2297             | Some pr ->
   2298                 lst#set_cursor (lst#model#get_path pr) (lst#get_column 0));
   2299       true
   2300     end else
   2301       false
   2302   in
   2303   ignore (lst#event#connect#key_press ~callback:lst_expand_by_keyboard);
   2304 
   2305   let lst_expand_by_mouse ev =
   2306     let x = int_of_float (GdkEvent.Button.x ev)
   2307     and y = int_of_float (GdkEvent.Button.y ev) in
   2308     match lst#get_path_at_pos ~x ~y with
   2309     | None -> false
   2310     | Some (path, col, _, _) ->
   2311         lst#set_cursor path col;
   2312         lst#misc#grab_focus ();
   2313         if lst#row_expanded path then lst#collapse_row path
   2314         else lst#expand_row path;
   2315         if GdkEvent.get_type ev = `TWO_BUTTON_PRESS then
   2316           lst#row_activated path col;
   2317         (* Disable the default handler because clicking on the little expander
   2318            arrow would revert the expand/collapse that was just done. (We could
   2319            potentially check if the arrow was clicked if we created a separate
   2320            column just for the expander arrows.) *)
   2321         true
   2322   in
   2323   ignore (lst#event#connect#button_press ~callback:lst_expand_by_mouse);
   2324 
   2325   let cancelCommand () = t#destroy () in
   2326   let cancelButton =
   2327     GButton.button ~stock:`CANCEL ~packing:t#action_area#add () in
   2328   ignore (cancelButton#connect#clicked ~callback:cancelCommand);
   2329   ignore (t#event#connect#delete ~callback:(fun _ -> cancelCommand (); true));
   2330   let ok = ref false in
   2331   let addCommand _ =
   2332     match getSelectedPref' () with
   2333     | None -> ()
   2334     | Some _ -> ok := true; t#destroy ()
   2335   in
   2336   let addButton =
   2337     GButton.button ~stock:`ADD ~packing:t#action_area#add () in
   2338   ignore (addButton#connect#clicked ~callback:addCommand);
   2339   GtkReact.set_sensitive addButton (prefSelection >> fun nm -> nm <> None);
   2340   ignore (lst#connect#row_activated ~callback:(fun _ _ -> addCommand ()));
   2341   addButton#grab_default ();
   2342 
   2343   ignore (t#connect#destroy ~callback:GMain.Main.quit);
   2344   t#show ();
   2345   lst#misc#grab_focus ();
   2346   GMain.Main.main ();
   2347   if not !ok then None else
   2348   getSelectedPrefReact (React.state selection)
   2349 
   2350 let editProfile parent name =
   2351   let t =
   2352     GWindow.dialog ~parent ~border_width:12
   2353       ~title:(Format.sprintf "%s - Profile Editor" name)
   2354       ~modal:true () in
   2355   let vb = t#vbox in
   2356   t#vbox#set_spacing 12;
   2357   let paned = GPack.paned `VERTICAL ~packing:(vb#pack ~expand:true) () in
   2358 
   2359   let lvb = GPack.vbox ~spacing:6 ~packing:paned#pack1 () in
   2360   let preferenceLabel =
   2361     GMisc.label
   2362       ~text:"_Preferences:" ~use_underline:true
   2363       ~xalign:0. ~packing:(lvb#pack ~expand:false) ()
   2364   in
   2365   let hb = GPack.hbox ~spacing:12 ~packing:(lvb#add) () in
   2366   let cols = new GTree.column_list in
   2367   let c_name = cols#add Gobject.Data.string in
   2368   let c_type = cols#add Gobject.Data.string in
   2369   let c_value = cols#add Gobject.Data.string in
   2370   let c_ml = cols#add Gobject.Data.caml in
   2371   let lst_store = GTree.list_store cols in
   2372   let lst_sorted_store = GTree.model_sort lst_store in
   2373   lst_sorted_store#set_sort_column_id 0 `ASCENDING;
   2374   let lst =
   2375     let sw =
   2376       GBin.scrolled_window ~packing:(hb#pack ~expand:true)
   2377         ~shadow_type:`IN ~height:300 ~width:600
   2378         ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC () in
   2379     GTree.view ~model:lst_sorted_store ~packing:sw#add
   2380       ~headers_clickable:true () in
   2381   preferenceLabel#set_mnemonic_widget (Some (lst :> GObj.widget));
   2382   let vc_name =
   2383     GTree.view_column
   2384       ~title:"Name"
   2385       ~renderer:(GTree.cell_renderer_text [], ["text", c_name]) () in
   2386   vc_name#set_sort_column_id 0;
   2387   ignore (lst#append_column vc_name);
   2388   ignore (lst#append_column
   2389     (GTree.view_column
   2390        ~title:"Type"
   2391        ~renderer:(GTree.cell_renderer_text [], ["text", c_type]) ()));
   2392   ignore (lst#append_column
   2393     (GTree.view_column
   2394        ~title:"Value"
   2395        ~renderer:(GTree.cell_renderer_text [], ["text", c_value]) ()));
   2396   let vb =
   2397     GPack.button_box
   2398       `VERTICAL ~layout:`START ~spacing:6 ~packing:(hb#pack ~expand:false) ()
   2399   in
   2400   let selection = GtkReact.tree_view_selection lst in
   2401   let hasSel = selection >> fun l -> l <> [] in
   2402   let addB =
   2403     GButton.button ~stock:`ADD ~packing:(vb#pack ~expand:false) () in
   2404   let editB =
   2405     GButton.button ~stock:`EDIT ~packing:(vb#pack ~expand:false) () in
   2406   let deleteB =
   2407     GButton.button ~stock:`DELETE ~packing:(vb#pack ~expand:false) () in
   2408   List.iter (fun b -> b#set_xalign 0.) [addB; editB; deleteB];
   2409   GtkReact.set_sensitive editB hasSel;
   2410   GtkReact.set_sensitive deleteB hasSel;
   2411 
   2412   let (modified, setModified) = React.make false in
   2413   let formatValue vl = Unicode.protect (String.concat ", " vl) in
   2414   let deletePref () =
   2415     match React.state selection with
   2416       [rf] ->
   2417         let row = lst_sorted_store#convert_iter_to_child_iter rf#iter in
   2418         let (nm, ty, vl) = lst_store#get ~row ~column:c_ml in
   2419         if
   2420           twoBox ~kind:`DIALOG_QUESTION ~parent:t ~title:"Preference Deletion"
   2421             ~bstock:`CANCEL ~astock:`DELETE
   2422             (Format.sprintf "Do you really want to delete preference %s?"
   2423                (Unicode.protect nm))
   2424         then begin
   2425           ignore (lst_store#remove row);
   2426           setModified true
   2427         end
   2428     | _ ->
   2429         ()
   2430   in
   2431   let editPref path =
   2432     let row =
   2433       lst_sorted_store#convert_iter_to_child_iter
   2434         (lst_sorted_store#get_iter path) in
   2435     let (nm, ty, vl) = lst_store#get ~row ~column:c_ml in
   2436     match editPreference t nm ty vl with
   2437       Some [] ->
   2438         deletePref ()
   2439     | Some vl' when vl <> vl' ->
   2440         lst_store#set ~row ~column:c_ml (nm, ty, vl');
   2441         lst_store#set ~row ~column:c_value (formatValue vl');
   2442         setModified true
   2443     | _ ->
   2444         ()
   2445   in
   2446   let add () =
   2447     match addPreference t with
   2448       None ->
   2449         ()
   2450     | Some nm ->
   2451         let existing = ref false in
   2452         lst_store#foreach
   2453           (fun path row ->
   2454              let (nm', _, _) = lst_store#get ~row ~column:c_ml in
   2455              if nm = nm' then begin
   2456                existing := true; editPref path; true
   2457              end else
   2458                false);
   2459         if not !existing then begin
   2460           let ty = Prefs.typ nm in
   2461           match editPreference parent nm ty (defaultValue ty) with
   2462             Some vl when vl <> [] ->
   2463               let row = lst_store#append () in
   2464               lst_store#set ~row ~column:c_name (Unicode.protect nm);
   2465               lst_store#set ~row ~column:c_type (nameOfType ty);
   2466               lst_store#set ~row ~column:c_ml (nm, ty, vl);
   2467               lst_store#set ~row ~column:c_value (formatValue vl);
   2468               setModified true
   2469           | _ ->
   2470               ()
   2471         end
   2472   in
   2473   ignore (addB#connect#clicked ~callback:add);
   2474   ignore (editB#connect#clicked
   2475             ~callback:(fun () ->
   2476                          match React.state selection with
   2477                            [p] -> editPref p#path
   2478                          | _   -> ()));
   2479   ignore (deleteB#connect#clicked ~callback:deletePref);
   2480 
   2481   let updateDoc = documentPreference ~compact:true ~packing:paned#pack2 in
   2482   selection >|
   2483     (fun l ->
   2484        let nm =
   2485          match l with
   2486            [rf] ->
   2487              let row = rf#iter in
   2488              Some (lst_sorted_store#get ~row ~column:c_name)
   2489          | _ ->
   2490              None
   2491        in
   2492        updateDoc nm);
   2493   ignore (lst#connect#row_activated ~callback:(fun path _ -> editPref path));
   2494 
   2495   let group l =
   2496     let rec groupRec l k vl l' =
   2497       match l with
   2498         (k', v) :: r ->
   2499           if k = k' then
   2500             groupRec r k (v :: vl) l'
   2501           else
   2502             groupRec r k' [v] ((k, vl) :: l')
   2503       | [] ->
   2504           Safelist.fold_left
   2505             (fun acc (k, l) -> (k, List.rev l) :: acc) [] ((k, vl) :: l')
   2506     in
   2507     match l with
   2508       (k, v) :: r -> groupRec r k [v] []
   2509     | []          -> []
   2510   in
   2511   let lastOne l = [List.hd (Safelist.rev l)] in
   2512   let normalizeValue t vl =
   2513     match t with
   2514       `BOOL | `INT | `STRING            -> lastOne vl
   2515     | `STRING_LIST | `CUSTOM | `UNKNOWN -> vl
   2516     | `BOOLDEF ->
   2517          let l = lastOne vl in
   2518          if l = ["default"] || l = ["auto"] then [] else l
   2519   in
   2520   let (>>>) x f = f x in
   2521   Prefs.readAFile name
   2522   >>> List.map (fun (_, nm, v) -> Prefs.canonicalName nm, v)
   2523   >>> List.stable_sort (fun (nm, _) (nm', _) -> compare nm nm')
   2524   >>> group
   2525   >>> List.iter
   2526         (fun (nm, vl) ->
   2527            let nm = Prefs.canonicalName nm in
   2528            let ty = Prefs.typ nm in
   2529            let vl = normalizeValue ty vl in
   2530            if vl <> [] then begin
   2531              let row = lst_store#append () in
   2532              lst_store#set ~row ~column:c_name (Unicode.protect nm);
   2533              lst_store#set ~row ~column:c_type (nameOfType ty);
   2534              lst_store#set ~row ~column:c_value (formatValue vl);
   2535              lst_store#set ~row ~column:c_ml (nm, ty, vl)
   2536            end);
   2537 
   2538   let applyCommand _ =
   2539     if React.state modified then begin
   2540       let filename = Prefs.profilePathname name in
   2541       try
   2542         let ch =
   2543           System.open_out_gen [Open_wronly; Open_creat; Open_trunc] 0o600
   2544             filename
   2545         in
   2546         let close_on_error f =
   2547           try f () with e -> close_out_noerr ch; raise e
   2548         in
   2549         close_on_error (fun () ->
   2550   (*XXX Should trim whitespaces and check for '\n' at some point  *)
   2551         Printf.fprintf ch "# Unison preferences\n";
   2552         lst_store#foreach
   2553           (fun path row ->
   2554              let (nm, _, vl) = lst_store#get ~row ~column:c_ml in
   2555              List.iter (fun v -> Printf.fprintf ch "%s = %s\n" nm v) vl;
   2556              false);
   2557         close_out ch);
   2558         setModified false
   2559       with Sys_error errmsg ->
   2560         okBox ~parent:t ~typ:`ERROR ~title:"Could not save profile"
   2561           ~message:("Error when saving profile: " ^ errmsg)
   2562     end
   2563   in
   2564   let applyButton =
   2565     GButton.button ~stock:`APPLY ~packing:t#action_area#add () in
   2566   ignore (applyButton#connect#clicked ~callback:applyCommand);
   2567   GtkReact.set_sensitive applyButton modified;
   2568   let cancelCommand () = t#destroy () in
   2569   let cancelButton =
   2570     GButton.button ~stock:`CANCEL ~packing:t#action_area#add () in
   2571   ignore (cancelButton#connect#clicked ~callback:cancelCommand);
   2572   ignore (t#event#connect#delete ~callback:(fun _ -> cancelCommand (); true));
   2573   let okCommand _ = applyCommand (); t#destroy () in
   2574   let okButton =
   2575     GButton.button ~stock:`OK ~packing:t#action_area#add () in
   2576   ignore (okButton#connect#clicked ~callback:okCommand);
   2577   okButton#grab_default ();
   2578 (*
   2579 List.iter
   2580   (fun (nm, _, long) ->
   2581      try
   2582        let long = formatDoc long in
   2583        ignore (Str.search_forward (Str.regexp_string "\\") long 0);
   2584        Format.eprintf "%s %s@." nm long
   2585      with Not_found -> ())
   2586 (Prefs.listVisiblePrefs ());
   2587 *)
   2588 
   2589 (*
   2590 TODO:
   2591   - Extra tabs for common preferences
   2592     (should keep track of any change, or blacklist some preferences)
   2593   - Add, modify, delete
   2594   - Keep track of whether there is any change (apply button)
   2595 *)
   2596   ignore (t#connect#destroy ~callback:GMain.Main.quit);
   2597   t#show ();
   2598   GMain.Main.main ()
   2599 
   2600 (* ------ *)
   2601 
   2602 let documentationFn = ref (fun ~parent _ -> ())
   2603 
   2604 let getProfile quit =
   2605   let ok = ref false in
   2606   let parent = toplevelWindow () in
   2607   (* Make sure that a potentially open password window from a (failed) previous
   2608      session is not hidden underneath this window. *)
   2609   waitForPasswordWindowClosing ();
   2610 
   2611   (* Build the dialog *)
   2612   let t =
   2613     GWindow.dialog ~parent ~border_width:12
   2614       ~title:"Profile Selection"
   2615       ~modal:false () in
   2616   t#set_default_width 550;
   2617   (* Simulate modal dialog (allowing to open other windows, such as help) *)
   2618   parent#set_sensitive false;
   2619   ignore (t#connect#destroy ~callback:(fun () -> parent#set_sensitive true));
   2620 
   2621   let cancelCommand _ = t#destroy () in
   2622   let cancelButton =
   2623     GButton.button ~stock:(if quit then `QUIT else `CANCEL)
   2624       ~packing:t#action_area#add () in
   2625   ignore (cancelButton#connect#clicked ~callback:cancelCommand);
   2626   ignore (t#event#connect#delete ~callback:(fun _ -> cancelCommand (); true));
   2627   cancelButton#misc#set_can_default true;
   2628 
   2629   let okCommand() = ok := true; t#destroy () in
   2630   let okButton =
   2631     GButton.button ~stock:`OPEN ~packing:t#action_area#add () in
   2632   ignore (okButton#connect#clicked ~callback:okCommand);
   2633   okButton#misc#set_sensitive false;
   2634   okButton#grab_default ();
   2635 
   2636   let vb = t#vbox in
   2637   t#vbox#set_spacing 18;
   2638 
   2639   let al = GBin.alignment ~packing:(vb#pack ~expand:true) () in
   2640   al#set_left_padding 12;
   2641 
   2642   let lvb = GPack.vbox ~spacing:6 ~packing:(al#add) () in
   2643   let selectLabel =
   2644     GMisc.label
   2645       ~text:"Select a _profile:" ~use_underline:true
   2646       ~xalign:0. ~packing:(lvb#pack ~expand:false) ()
   2647   in
   2648   let hb = GPack.hbox ~spacing:12 ~packing:(lvb#add) () in
   2649   let sw =
   2650     GBin.scrolled_window ~packing:(hb#pack ~expand:true) ~height:300
   2651       ~shadow_type:`IN ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC () in
   2652   let cols = new GTree.column_list in
   2653   let c_name = cols#add Gobject.Data.string in
   2654   let c_label = cols#add Gobject.Data.string in
   2655   let c_ml = cols#add Gobject.Data.caml in
   2656   let lst_store = GTree.list_store cols in
   2657   let lst = GTree.view ~model:lst_store ~packing:sw#add () in
   2658   selectLabel#set_mnemonic_widget (Some (lst :> GObj.widget));
   2659   let vc_name =
   2660     GTree.view_column
   2661        ~title:"Profile"
   2662        ~renderer:(GTree.cell_renderer_text [], ["text", c_name]) ()
   2663   in
   2664   ignore (lst#append_column vc_name);
   2665   ignore (lst#append_column
   2666     (GTree.view_column
   2667        ~title:"Description"
   2668        ~renderer:(GTree.cell_renderer_text [], ["text", c_label]) ()));
   2669 
   2670   let vb = GPack.vbox ~spacing:6 ~packing:(vb#pack ~expand:false) () in
   2671   ignore (GMisc.label ~markup:"<b>Summary</b>" ~xalign:0.
   2672             ~packing:(vb#pack ~expand:false) ());
   2673   let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
   2674   al#set_left_padding 12;
   2675   let tbl =
   2676     GPack.table ~rows:2 ~columns:2 ~col_spacings:12 ~row_spacings:6
   2677       ~packing:(al#add) () in
   2678   tbl#misc#set_sensitive false;
   2679   ignore (GMisc.label ~text:"First root:" ~xalign:0.
   2680             ~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
   2681   ignore (GMisc.label ~text:"Second root:" ~xalign:0.
   2682             ~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
   2683   let root1 =
   2684     GMisc.label ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X)
   2685       ~xalign:0. ~selectable:true ~ellipsize:`MIDDLE () in
   2686   let root2 =
   2687     GMisc.label ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X)
   2688       ~xalign:0. ~selectable:true ~ellipsize:`MIDDLE () in
   2689 
   2690   let fillLst default =
   2691     Uicommon.scanProfiles();
   2692     lst_store#clear ();
   2693     Safelist.iter
   2694       (fun (profile, info) ->
   2695          let labeltext =
   2696            match info.Uicommon.label with None -> "" | Some l -> l in
   2697          let row = lst_store#append () in
   2698          lst_store#set ~row ~column:c_name (Unicode.protect profile);
   2699          lst_store#set ~row ~column:c_label (Unicode.protect labeltext);
   2700          lst_store#set ~row ~column:c_ml (profile, info);
   2701          if Some profile = default then begin
   2702            lst#selection#select_iter row;
   2703            lst#scroll_to_cell (lst_store#get_path row) vc_name
   2704          end)
   2705       (Safelist.sort (fun (p, _) (p', _) -> compare p p') !Uicommon.profilesAndRoots)
   2706   in
   2707   let selection = GtkReact.tree_view_selection lst in
   2708   let hasSel = selection >> fun l -> l <> [] in
   2709   let selInfo =
   2710     selection >> fun l ->
   2711       match l with
   2712         [rf] -> Some (lst_store#get ~row:rf#iter ~column:c_ml, rf)
   2713       | _    -> None
   2714   in
   2715   selInfo >|
   2716     (fun info ->
   2717        match info with
   2718          Some ((profile, info), _) ->
   2719            begin match info.Uicommon.roots with
   2720              [r1; r2] -> root1#set_text (Unicode.protect r1);
   2721                          root2#set_text (Unicode.protect r2);
   2722                          tbl#misc#set_sensitive true
   2723            | _        -> root1#set_text ""; root2#set_text "";
   2724                          tbl#misc#set_sensitive false
   2725            end
   2726        | None ->
   2727            root1#set_text ""; root2#set_text "";
   2728            tbl#misc#set_sensitive false);
   2729   GtkReact.set_sensitive okButton hasSel;
   2730 
   2731   let box = GPack.vbox ~packing:(hb#pack ~expand:false) () in
   2732   let vb =
   2733     GPack.button_box
   2734       `VERTICAL ~layout:`START ~spacing:6 ~packing:(box#pack ~expand:false) ()
   2735   in
   2736   let addButton =
   2737     GButton.button ~stock:`ADD ~packing:(vb#pack ~expand:false) () in
   2738   ignore (addButton#connect#clicked
   2739      ~callback:(fun () ->
   2740                   match createProfile t with
   2741                     Some p -> fillLst (Some p) | None -> ()));
   2742   let editButton =
   2743     GButton.button ~stock:`EDIT ~packing:(vb#pack ~expand:false) () in
   2744   ignore (editButton#connect#clicked
   2745             ~callback:(fun () -> match React.state selInfo with
   2746                                    None ->
   2747                                      ()
   2748                                  | Some ((p, _), _) ->
   2749                                      editProfile t p; fillLst (Some p)));
   2750   GtkReact.set_sensitive editButton hasSel;
   2751   let deleteProfile () =
   2752     match React.state selInfo with
   2753       Some ((profile, _), rf) ->
   2754        if
   2755          twoBox ~kind:`DIALOG_QUESTION ~parent:t ~title:"Profile Deletion"
   2756            ~bstock:`CANCEL ~astock:`DELETE
   2757            (Format.sprintf "Do you really want to delete profile %s?"
   2758               (transcode profile))
   2759        then begin
   2760          try
   2761            System.unlink (Prefs.profilePathname profile);
   2762            ignore (lst_store#remove rf#iter)
   2763          with Unix.Unix_error _ -> ()
   2764        end
   2765     | None ->
   2766        ()
   2767   in
   2768   let deleteButton =
   2769     GButton.button ~stock:`DELETE ~packing:(vb#pack ~expand:false) () in
   2770   ignore (deleteButton#connect#clicked ~callback:deleteProfile);
   2771   GtkReact.set_sensitive deleteButton hasSel;
   2772   List.iter (fun b -> b#set_xalign 0.) [addButton; editButton; deleteButton];
   2773 
   2774   ignore (GPack.vbox ~packing:(box#pack ~expand:true) ());
   2775   let helpButton =
   2776     GButton.button ~stock:`HELP ~packing:(box#pack ~expand:false) () in
   2777   helpButton#set_xalign 0.;
   2778   ignore (helpButton#connect#clicked
   2779      ~callback:(fun () -> !documentationFn ~parent:t ""));
   2780 
   2781   ignore (lst#connect#row_activated ~callback:(fun _ _ -> okCommand ()));
   2782   fillLst None;
   2783   ignore (t#connect#destroy ~callback:GMain.Main.quit);
   2784   t#show ();
   2785   GMain.Main.main ();
   2786   match React.state selInfo with
   2787     Some ((p, _), _) when !ok -> Some p
   2788   | _                         -> None
   2789 
   2790 (* ------ *)
   2791 
   2792 let get_size_chars obj ?desc ?lang ~height ~width () =
   2793   let metrics = obj#misc#pango_context#get_metrics ?desc ?lang () in
   2794   (width * GPango.to_pixels metrics#approx_digit_width,
   2795    height * GPango.to_pixels (metrics#ascent+metrics#descent))
   2796 
   2797 let documentation ~parent sect =
   2798   let title = "Documentation" in
   2799   let t = GWindow.dialog ~title ~parent () in
   2800   let t_dismiss =
   2801     GButton.button ~stock:`CLOSE ~packing:t#action_area#add () in
   2802   t_dismiss#grab_default ();
   2803   let dismiss () = t#destroy () in
   2804   ignore (t_dismiss#connect#clicked ~callback:dismiss);
   2805   ignore (t#event#connect#delete ~callback:(fun _ -> dismiss (); true));
   2806 
   2807   let nb = GPack.notebook ~show_tabs:true ~tab_pos:`LEFT ~border_width:5
   2808     ~packing:(t#vbox#pack ~expand:true) () in
   2809 
   2810   let sect_idx = ref 0 in
   2811   let add_nb_page label active w =
   2812     let i = nb#append_page ~tab_label:label#coerce w in
   2813     if active then sect_idx := i
   2814   in
   2815 
   2816   let lw = ref 1 in
   2817   let addDocSection (shortname, (name, docstr)) =
   2818     if shortname = "" || name = "" then () else
   2819     let namelen = String.length name in
   2820     if namelen <= 20 then lw := max !lw namelen;
   2821     let label = GMisc.label ~markup:("<b>" ^ name ^ "</b>")
   2822                   ~xalign:1. ~justify:`RIGHT ~ellipsize:`NONE
   2823                   ~line_wrap:(namelen > 20) () in
   2824     label#set_width_chars 20;
   2825     let box = GBin.frame ~border_width:8
   2826                 ~packing:(add_nb_page label (shortname = sect)) () in
   2827     let text = new scrolled_text ~editable:false ~wrap_mode:`NONE
   2828                  ~packing:box#add () in
   2829     text#insert docstr
   2830   in
   2831   Safelist.iter addDocSection Strings.docs;
   2832 
   2833   nb#goto_page !sect_idx;
   2834 
   2835   let (width, height) = get_size_chars t ~width:(80 + !lw) ~height:25 () in
   2836   t#set_default_size ~width ~height;
   2837 
   2838   t#show ()
   2839 let () = documentationFn := documentation
   2840 
   2841 (* ------ *)
   2842 
   2843 let messageBox ~title ?(action = fun t -> t#destroy) ?styleText message =
   2844   let utitle = transcode title in
   2845   let t = GWindow.dialog ~title:utitle ~parent:(toplevelWindow ())
   2846             ~position:`CENTER () in
   2847   let t_dismiss = GButton.button ~stock:`CLOSE ~packing:t#action_area#add () in
   2848   t_dismiss#grab_default ();
   2849   ignore (t_dismiss#connect#clicked ~callback:(action t));
   2850   let t_text =
   2851     new scrolled_text ~editable:false ~wrap_mode:`NONE
   2852       ~packing:(t#vbox#pack ~expand:true) ()
   2853   in
   2854   t_text#insert message;
   2855   let () = match styleText with None -> () | Some fn -> fn t_text in
   2856   let (width, height) = get_size_chars t_text ~width:82 ~height:20 () in
   2857   t#set_default_size ~width ~height;
   2858   ignore (t#event#connect#delete ~callback:(fun _ -> action t (); true));
   2859   t#show ()
   2860 
   2861 (* twoBoxAdvanced: Display a message in a window and wait for the user
   2862    to hit one of two buttons.  Return true if the first button is
   2863    chosen, false if the second button is chosen. Also has a button for
   2864    showing more details to the user in a messageBox dialog *)
   2865 let twoBoxAdvanced
   2866       ~parent ~title ~message ~longtext ~advLabel ~astock ~bstock =
   2867   let t =
   2868     GWindow.dialog ~parent ~border_width:6 ~modal:true
   2869       ~resizable:false () in
   2870   t#vbox#set_spacing 12;
   2871   let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
   2872   ignore (GMisc.image ~stock:`DIALOG_QUESTION ~icon_size:`DIALOG
   2873             ~yalign:0. ~packing:h1#pack ());
   2874   let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
   2875   ignore (GMisc.label
   2876             ~markup:(primaryText title ^ "\n\n" ^ escapeMarkup message)
   2877             ~selectable:true ~yalign:0. ~packing:v1#add ());
   2878   t#add_button_stock `CANCEL `NO;
   2879   let cmd () =
   2880     messageBox ~title:"Details" longtext
   2881   in
   2882   t#add_button advLabel `HELP;
   2883   t#add_button_stock `APPLY `YES;
   2884   t#set_default_response `NO;
   2885   let res = ref false in
   2886   let setRes signal =
   2887     match signal with
   2888       `YES -> res := true; t#destroy ()
   2889     | `NO -> res := false; t#destroy ()
   2890     | `HELP -> cmd ()
   2891     | _ -> ()
   2892   in
   2893   ignore (t#connect#response ~callback:setRes);
   2894   ignore (t#connect#destroy ~callback:GMain.Main.quit);
   2895   t#show();
   2896   GMain.Main.main();
   2897   !res
   2898 
   2899 let summaryBox ~parent ~title ~message ~f =
   2900   let t =
   2901     GWindow.dialog ~parent ~border_width:6 ~modal:true
   2902       ~resizable:true ~focus_on_map:true () in
   2903   t#vbox#set_spacing 12;
   2904   let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
   2905   ignore (GMisc.image ~stock:`DIALOG_INFO ~icon_size:`DIALOG
   2906             ~yalign:0. ~packing:h1#pack ());
   2907   let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
   2908   ignore (GMisc.label
   2909             ~markup:(primaryText title ^ "\n\n" ^ escapeMarkup message)
   2910             ~selectable:true ~xalign:0. ~yalign:0. ~packing:(v1#pack ~expand:false) ());
   2911   let exp = GBin.expander ~spacing:12 ~label:"Show details"
   2912               ~packing:(v1#pack ~expand:true) () in
   2913   let t_text =
   2914     new scrolled_text ~editable:false ~shadow_type:`IN ~packing:exp#add () in
   2915   t_text#set_expand true;
   2916   let (width, height) = get_size_chars t_text ~width:60 ~height:10 () in
   2917   t_text#set_width_request width;
   2918   t_text#set_height_request height;
   2919   f (t_text#text);
   2920   t#add_button_stock `OK `OK;
   2921   t#set_default_response `OK;
   2922   let setRes signal = t#destroy () in
   2923   ignore (t#connect#response ~callback:setRes);
   2924   ignore (t#connect#destroy ~callback:GMain.Main.quit);
   2925   t#show();
   2926   GMain.Main.main()
   2927 
   2928 (**********************************************************************
   2929                              TOP-LEVEL WINDOW
   2930  **********************************************************************)
   2931 
   2932 let displayWaitMessage () =
   2933   make_busy (toplevelWindow ());
   2934   Trace.status (Uicommon.contactingServerMsg ())
   2935 
   2936 let prepDebug () =
   2937   if Sys.win32 then
   2938     (* As a side-effect, this allocates a console if the process doesn't
   2939        have one already. This call is here only for the side-effect,
   2940        because debugging output is produced on stderr and the GUI will
   2941        crash if there is no stderr. *)
   2942     try ignore (System.terminalStateFunctions ())
   2943     with Unix.Unix_error _ -> ()
   2944 
   2945 (* ------ *)
   2946 
   2947 type status = NoStatus | Done | Failed
   2948 
   2949 let createToplevelWindow () =
   2950   let toplevelWindow =
   2951     GWindow.window ~kind:`TOPLEVEL ~position:`CENTER
   2952       ~title:myNameCapitalized ()
   2953   in
   2954   setToplevelWindow toplevelWindow;
   2955   (* There is already a default icon under Windows, and transparent
   2956      icons are not supported by all version of Windows *)
   2957   if not Sys.win32 then toplevelWindow#set_icon (Some (Lazy.force icon));
   2958   let toplevelVBox = GPack.vbox ~packing:toplevelWindow#add () in
   2959 
   2960   (*******************************************************************
   2961    Statistic window
   2962    *******************************************************************)
   2963 
   2964   let (statWin, startStats, stopStats) = statistics () in
   2965 
   2966   (*******************************************************************
   2967    Groups of things that are sensitive to interaction at the same time
   2968    *******************************************************************)
   2969   let grAction = ref [] in
   2970   let grDiff = ref [] in
   2971   let grGo = ref [] in
   2972   let grRescan = ref [] in
   2973   let grStop = ref [] in
   2974   let grDetail = ref [] in
   2975   let grAdd gr w = gr := w#misc::!gr in
   2976   let grSet gr st = Safelist.iter (fun x -> x#set_sensitive st) !gr in
   2977   let grDisactivateAll () =
   2978     grSet grAction false;
   2979     grSet grDiff false;
   2980     grSet grGo false;
   2981     grSet grRescan false;
   2982     grSet grStop false;
   2983     grSet grDetail false
   2984   in
   2985 
   2986   (*********************************************************************
   2987     Create the menu bar
   2988    *********************************************************************)
   2989   let topHBox = GPack.hbox ~packing:(toplevelVBox#pack ~expand:false) () in
   2990 
   2991   let menuBar =
   2992     GMenu.menu_bar ~border_width:0
   2993       ~packing:(topHBox#pack ~expand:true) () in
   2994   let menus = new gMenuFactory ~accel_modi:[] menuBar in
   2995   let accel_group = menus#accel_group in
   2996   toplevelWindow#add_accel_group accel_group;
   2997   let add_submenu ?(modi=[]) label =
   2998     let (menu, item) = menus#add_submenu label in
   2999     (new gMenuFactory ~accel_group:(menus#accel_group)
   3000        ~accel_path:(menus#accel_path ^ label ^ "/")
   3001        ~accel_modi:modi menu,
   3002      item)
   3003   in
   3004   let replace_submenu ?(modi=[]) label item =
   3005     let menu = menus#replace_submenu item in
   3006     new gMenuFactory ~accel_group:(menus#accel_group)
   3007       ~accel_path:(menus#accel_path ^ label ^ "/")
   3008       ~accel_modi:modi menu
   3009   in
   3010 
   3011   let profileLabel =
   3012     GMisc.label ~text:"" ~packing:(topHBox#pack ~expand:false ~padding:2) () in
   3013 
   3014   let displayNewProfileLabel () =
   3015     let p = match !Prefs.profileName with None -> "" | Some p -> p in
   3016     let label = Prefs.read Uicommon.profileLabel in
   3017     let s =
   3018       match p, label with
   3019         "",        _  -> ""
   3020       | _,         "" -> p
   3021       | "default", _  -> label
   3022       | _             -> Format.sprintf "%s (%s)" p label
   3023     in
   3024     let roots = String.concat " ↔ " (Globals.rawRoots ()) in
   3025     let roots = if roots = "" then "" else "   |   " ^ roots in
   3026     toplevelWindow#set_title
   3027       (if s = "" then myNameCapitalized else
   3028        Format.sprintf "%s [%s]%s" myNameCapitalized s roots);
   3029     let s = if s="" then "No profile" else "Profile: " ^ s in
   3030     profileLabel#set_text (transcode s)
   3031   in
   3032   displayNewProfileLabel ();
   3033 
   3034   (*********************************************************************
   3035     Create the menus
   3036    *********************************************************************)
   3037   let (fileMenu, _) = add_submenu "_Synchronization" in
   3038   let (actionMenu, actionItem) = add_submenu "_Actions" in
   3039   let (ignoreMenu, _) = add_submenu ~modi:[`SHIFT] "_Ignore" in
   3040   let (sortMenu, _) = add_submenu "S_ort" in
   3041   let (helpMenu, _) = add_submenu "_Help" in
   3042   let (expertMenu, expertItem) = add_submenu "Expert" in
   3043   let () = expertItem#set_visible false in (* Expert menu hidden by default *)
   3044 
   3045   (*********************************************************************
   3046     Action bar
   3047    *********************************************************************)
   3048   let actionBar =
   3049     GButton.toolbar ~style:`BOTH
   3050       (* 2003-0519 (stse): how to set space size in gtk 2.0? *)
   3051       (* Answer from Jacques Garrigue: this can only be done in
   3052          the user's.gtkrc, not programmatically *)
   3053       ~orientation:`HORIZONTAL (* ~space_size:10 *)
   3054       ~packing:(toplevelVBox#pack ~expand:false) () in
   3055   actionBar#set_icon_size `SMALL_TOOLBAR;
   3056   (* [show_arrow] is initially false to produce a better default width. *)
   3057   actionBar#set_show_arrow false;
   3058   ignore (toplevelWindow#misc#connect#show
   3059     ~callback:(fun () -> actionBar#set_show_arrow true));
   3060 
   3061   (*********************************************************************
   3062     Create the main window
   3063    *********************************************************************)
   3064   let mainWindowSW =
   3065       GBin.scrolled_window ~packing:(toplevelVBox#pack ~expand:true)
   3066         ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
   3067   in
   3068   let cols = new GTree.column_list in
   3069   let c_replica1 = cols#add Gobject.Data.string in
   3070   let c_action   = cols#add Gobject.Data.gobject in
   3071   let c_replica2 = cols#add Gobject.Data.string in
   3072   let c_status   = cols#add Gobject.Data.gobject_option in
   3073   let c_statust  = cols#add Gobject.Data.string in
   3074   let c_path     = cols#add Gobject.Data.string in
   3075   (*let c_rowid    = cols#add Gobject.Data.uint in*)
   3076   (* With current implementation the [list_store] view model and [theState]
   3077      array have one-to-one correspondence, so that list_store's tree path index
   3078      is the same as theState array index.
   3079      This changes when, for example, [tree_store] would be used instead of
   3080      list_store, or a separate view-only sorting is implemented without sorting
   3081      the backing theState array. In that case, the column [c_rowid] must be
   3082      used to store the index of [theState] array in the view model. Tree path
   3083      index must not be used directly as [theState] array index and vice versa. *)
   3084   let mainWindowModel = GTree.list_store cols in
   3085   let mainWindow =
   3086     GTree.view ~model:mainWindowModel ~packing:(mainWindowSW#add)
   3087       ~headers_clickable:false ~enable_search:false () in
   3088   mainWindow#selection#set_mode `MULTIPLE;
   3089   ignore (mainWindow#append_column
   3090     (GTree.view_column
   3091        ~title:(" ")
   3092        ~renderer:(GTree.cell_renderer_text [], ["text", c_replica1]) ()));
   3093   ignore (mainWindow#append_column
   3094     (GTree.view_column ~title:"  Action  "
   3095        ~renderer:(GTree.cell_renderer_pixbuf [], ["pixbuf", c_action]) ()));
   3096   ignore (mainWindow#append_column
   3097     (GTree.view_column
   3098        ~title:(" ")
   3099        ~renderer:(GTree.cell_renderer_text [], ["text", c_replica2]) ()));
   3100   let status_view_col = GTree.view_column ~title:"  Status  "
   3101        ~renderer:(GTree.cell_renderer_pixbuf [], ["pixbuf", c_status]) () in
   3102   let status_t_rend = GTree.cell_renderer_text [] in
   3103   status_view_col#pack ~expand:false ~from:`END status_t_rend;
   3104   status_view_col#add_attribute status_t_rend "text" c_statust;
   3105   ignore (mainWindow#append_column status_view_col);
   3106   ignore (mainWindow#append_column
   3107     (GTree.view_column ~title:"  Path  "
   3108        ~renderer:(GTree.cell_renderer_text [], ["text", c_path]) ()));
   3109 
   3110   let setMainWindowColumnHeaders roots =
   3111     let escape s = String.split_on_char '_' s |> String.concat "__" in
   3112     let (r1, r2) = Uicommon.roots2niceStrings 15 roots in
   3113     Array.iteri
   3114       (fun i data ->
   3115          (mainWindow#get_column i)#set_title data)
   3116       [| " " ^ Unicode.protect (escape r1) ^ " "; "  Action  ";
   3117          " " ^ Unicode.protect (escape r2) ^ " "; "  Status  ";
   3118          " Path" |];
   3119   in
   3120 
   3121   (* See above for comment about tree path index and [theState] array index
   3122      equivalence. *)
   3123   let siOfRow f path =
   3124     let row = mainWindowModel#get_iter path in
   3125     let i = (GTree.Path.get_indices path).(0) in
   3126     (*let i = mainWindowModel#get ~row ~column:c_rowid in*)
   3127     f i !theState.(i) row
   3128   in
   3129   let rowOfSi i = GTree.Path.create [i] in
   3130   let currentNumberRows () = mainWindow#selection#count_selected_rows in
   3131   let currentRow () =
   3132     match currentNumberRows () with
   3133     | 1 -> siOfRow (fun i si row -> Some (i, !theState.(i), row))
   3134              (List.hd mainWindow#selection#get_selected_rows)
   3135     | _ -> None
   3136   in
   3137   let currentSelectedIter f =
   3138     Safelist.iter (fun r -> siOfRow f r)
   3139       mainWindow#selection#get_selected_rows
   3140   in
   3141   let currentSelectedFold f a =
   3142     Safelist.fold_left (fun a r -> siOfRow (fun _ si _ -> f a si) r)
   3143       a mainWindow#selection#get_selected_rows
   3144   in
   3145   let currentSelectedExists pred =
   3146     Safelist.exists (fun r -> siOfRow (fun _ si _ -> pred si) r)
   3147       mainWindow#selection#get_selected_rows
   3148   in
   3149 
   3150   (*********************************************************************
   3151     Create the details window
   3152    *********************************************************************)
   3153 
   3154   let showDetCommand () =
   3155     let details =
   3156       match currentRow () with
   3157         None ->
   3158           None
   3159       | Some (_, si, _) ->
   3160           let path = Path.toString si.ri.path1 in
   3161           match si.whatHappened with
   3162             Some (Util.Failed _, Some det) ->
   3163               Some ("Merge execution details for file" ^
   3164                     transcodeFilename path,
   3165                     det)
   3166           | _ ->
   3167               match si.ri.replicas with
   3168                 Problem err ->
   3169                   Some ("Errors for file " ^ transcodeFilename path, err)
   3170               | Different diff ->
   3171                   let prefix s l =
   3172                     Safelist.map (fun err -> Format.sprintf "%s%s\n" s err) l
   3173                   in
   3174                   let errors =
   3175                     Safelist.append
   3176                       (prefix "[root 1]: " diff.errors1)
   3177                       (prefix "[root 2]: " diff.errors2)
   3178                   in
   3179                   let errors =
   3180                     match si.whatHappened with
   3181                        Some (Util.Failed err, _) -> err :: errors
   3182                     |  _                         -> errors
   3183                   in
   3184                   Some ("Errors for file " ^ transcodeFilename path,
   3185                         String.concat "\n" errors)
   3186     in
   3187     match details with
   3188       None                  -> ((* Should not happen *))
   3189     | Some (title, details) -> messageBox ~title (transcode details)
   3190   in
   3191 
   3192   let detailsWindowSW =
   3193     GBin.scrolled_window ~packing:(toplevelVBox#pack ~expand:false)
   3194         ~shadow_type:`IN ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
   3195   in
   3196   let detailsWindow =
   3197     GText.view ~editable:false ~packing:detailsWindowSW#add ()
   3198   in
   3199   let (width, height) = get_size_chars detailsWindow ~height:4 ~width:112 () in
   3200   let () = detailsWindowSW#set_height_request height in
   3201   (* width is set in [sizeMainWindow] *)
   3202 
   3203   let detailsWindowPath = detailsWindow#buffer#create_tag [] in
   3204   let detailsWindowInfo =
   3205     detailsWindow#buffer#create_tag [`FONT_DESC (Lazy.force fontMonospace)] in
   3206   let detailsWindowError =
   3207     detailsWindow#buffer#create_tag [`WRAP_MODE `WORD] in
   3208   detailsWindow#misc#set_can_focus false;
   3209 
   3210   let updateButtons () =
   3211     if not !busy then
   3212       let actionPossible si =
   3213         match si.whatHappened, si.ri.replicas with
   3214           None, Different _ -> true
   3215         | _                 -> false
   3216       in
   3217       match currentRow () with
   3218         None ->
   3219           grSet grAction (currentSelectedExists actionPossible);
   3220           grSet grDiff false;
   3221           grSet grDetail false
   3222       | Some (_, si, _) ->
   3223           let details =
   3224             begin match si.ri.replicas with
   3225               Different diff -> diff.errors1 <> [] || diff.errors2 <> []
   3226             | Problem _      -> true
   3227             end
   3228               ||
   3229             begin match si.whatHappened with
   3230               Some (Util.Failed _, _) -> true
   3231             | _                       -> false
   3232             end
   3233           in
   3234           grSet grDetail details;
   3235           let activateAction = actionPossible si in
   3236           let activateDiff =
   3237             activateAction &&
   3238             match si.ri.replicas with
   3239               Different {rc1 = {typ = `FILE}; rc2 = {typ = `FILE}} ->
   3240                 true
   3241             | _ ->
   3242                 false
   3243           in
   3244           grSet grAction activateAction;
   3245           grSet grDiff activateDiff
   3246   in
   3247 
   3248   let makeRowVisible row =
   3249     mainWindow#scroll_to_cell row status_view_col (* just a dummy column *)
   3250   in
   3251 
   3252 (*
   3253   let makeFirstUnfinishedVisible pRiInFocus =
   3254     let im = Array.length !theState in
   3255     let rec find i =
   3256       if i >= im then makeRowVisible im else
   3257       match pRiInFocus (!theState.(i).ri), !theState.(i).whatHappened with
   3258         true, None -> makeRowVisible i
   3259       | _ -> find (i+1) in
   3260     find 0
   3261   in
   3262 *)
   3263 
   3264   let updateDetails () =
   3265     begin match currentRow () with
   3266       None ->
   3267         detailsWindow#buffer#set_text ""
   3268     | Some (_, si, _) ->
   3269         let (formated, details) =
   3270           match si.whatHappened with
   3271           | Some(Util.Failed(s), _) ->
   3272                (false, s)
   3273           | None | Some(Util.Succeeded, _) ->
   3274               match si.ri.replicas with
   3275                 Problem _ ->
   3276                   (false, Uicommon.details2string si.ri "  ")
   3277               | Different _ ->
   3278                   (true, Uicommon.details2string si.ri "  ")
   3279         in
   3280         let path = Path.toString si.ri.path1 in
   3281         detailsWindow#buffer#set_text "";
   3282         detailsWindow#buffer#insert ~tags:[detailsWindowPath]
   3283           (transcodeFilename path);
   3284         let len = String.length details in
   3285         let details =
   3286           if details.[len - 1] = '\n' then String.sub details 0 (len - 1)
   3287           else details
   3288         in
   3289         if details <> "" then
   3290           detailsWindow#buffer#insert
   3291              ~tags:[if formated then detailsWindowInfo else detailsWindowError]
   3292              ("\n" ^ transcode details)
   3293     end;
   3294     (* Display text *)
   3295     updateButtons () in
   3296 
   3297   (*********************************************************************
   3298     Status window
   3299    *********************************************************************)
   3300 
   3301   let statusHBox = GPack.hbox ~packing:(toplevelVBox#pack ~expand:false) () in
   3302 
   3303   let progressBar =
   3304     GRange.progress_bar ~packing:(statusHBox#pack ~expand:false) () in
   3305 
   3306   progressBar#misc#modify_font detailsWindow#misc#pango_context#font_description;
   3307   let (w, _) = get_size_chars progressBar ~width:28 ~height:1 () in
   3308   progressBar#set_width_request w;
   3309   progressBar#set_show_text true;
   3310   progressBar#set_pulse_step 0.02;
   3311   let progressBarPulse = ref false in
   3312 
   3313   let statusWindow =
   3314     GMisc.statusbar ~packing:(statusHBox#pack ~expand:true) () in
   3315   statusWindow#set_margin 0;
   3316   let statusContext = statusWindow#new_context ~name:"status" in
   3317   ignore (statusContext#push "");
   3318 
   3319   let displayStatus m =
   3320     statusContext#pop ();
   3321     if !progressBarPulse then progressBar#pulse ();
   3322     ignore (statusContext#push (transcode m));
   3323     (* Force message to be displayed immediately *)
   3324     gtk_sync false
   3325   in
   3326 
   3327   let formatStatus major minor = (Util.padto 30 (major ^ "  ")) ^ minor in
   3328 
   3329   (* Tell the Trace module about the status printer *)
   3330   Trace.messageDisplayer := displayStatus;
   3331   Trace.statusFormatter := formatStatus;
   3332   Trace.sendLogMsgsToStderr := false;
   3333 
   3334 
   3335   (* Window is created before initPrefs but we don't want the size to
   3336      jump around after window has been shown (which is inevitable when
   3337      height is specified in a profile). Scan the command line to check
   3338      for height preference. *)
   3339   begin try
   3340     let prefName = List.hd (Prefs.name Uicommon.mainWindowHeight) in
   3341     let clHeight = List.hd (Util.StringMap.find prefName (Prefs.scanCmdLine "")) in
   3342     Prefs.set Uicommon.mainWindowHeight (int_of_string clHeight)
   3343   with Not_found | Invalid_argument _ | Util.Fatal _ -> () end;
   3344 
   3345   let calcWinSize () =
   3346     (* (Poor) approximation of row height. It is impossible to get real
   3347        GTK TreeView row height (and it depends on theme). *)
   3348     let row_height = (List.hd mainWindow#all_children)#misc#allocation.height in
   3349     let height =
   3350       if row_height < 2 then   (* Oops, sizes clearly not allocated yet *)
   3351         let metrics = mainWindowSW#misc#pango_context#get_metrics () in
   3352         let h = GPango.to_pixels (metrics#ascent + metrics#descent) in
   3353         (h + 8) * (8 + (Prefs.read Uicommon.mainWindowHeight)) (* rought default *)
   3354       else
   3355           topHBox#misc#allocation.height
   3356         + actionBar#misc#allocation.height
   3357         + 2 * mainWindow#border_width  (* top and bottom *)
   3358         + row_height  (* column headers *)
   3359         + (row_height - 3) * (Prefs.read Uicommon.mainWindowHeight)
   3360         + detailsWindowSW#misc#allocation.height
   3361         + statusHBox#misc#allocation.height
   3362     in
   3363     let height = min height (Gdk.Screen.height ~screen:toplevelWindow#screen ()) in
   3364     let width =
   3365       let metrics = mainWindowSW#misc#pango_context#get_metrics () in
   3366       let w = GPango.to_pixels metrics#approx_digit_width in
   3367       max (w * 112) 860
   3368     in
   3369     let width = min width (Gdk.Screen.width ~screen:toplevelWindow#screen ()) in
   3370     (height, width)
   3371   in
   3372 
   3373   let prevHeightPref = ref 0 in
   3374 
   3375   let sizeMainWindow () =
   3376     (* Only update height if the preference changed, otherwise risk undoing
   3377        user's manual height adjustments. Also assume no change if the
   3378        preference is at the default value. *)
   3379     let prefHeight = Prefs.read Uicommon.mainWindowHeight in
   3380     if !prevHeightPref <> prefHeight &&
   3381         (!prevHeightPref = 0 ||
   3382           prefHeight <> Prefs.readDefault Uicommon.mainWindowHeight) then begin
   3383       let (height, _) = calcWinSize ()
   3384       and width = toplevelWindow#misc#allocation.width in
   3385       toplevelWindow#resize ~height ~width
   3386     end;
   3387     prevHeightPref := prefHeight
   3388   in
   3389   let (height, width) = calcWinSize () in
   3390   toplevelWindow#set_default_size ~height ~width;
   3391   ignore (toplevelWindow#misc#connect#show ~callback:sizeMainWindow);
   3392 
   3393   (*********************************************************************
   3394     Functions used to print in the main window
   3395    *********************************************************************)
   3396   let delayUpdates = ref false in
   3397 
   3398   let select row scroll =
   3399     delayUpdates := true;
   3400     mainWindow#selection#unselect_all ();
   3401     mainWindow#selection#select_path row;
   3402     mainWindow#set_cursor row status_view_col (* just a dummy column *);
   3403     delayUpdates := false;
   3404     if scroll then makeRowVisible row;
   3405     updateDetails ()
   3406   in
   3407   let selectI i scroll = select (rowOfSi i) scroll in
   3408 
   3409   ignore (mainWindow#selection#connect#changed ~callback:
   3410       (fun () -> if not !delayUpdates then updateDetails ()));
   3411 
   3412   let nextInteresting () =
   3413     let l = Array.length !theState in
   3414     let start = match currentRow () with Some (i, _, _) -> i + 1 | None -> 0 in
   3415     let rec loop i =
   3416       if i < l then
   3417         match !theState.(i).ri.replicas with
   3418           Different {direction = dir}
   3419               when not (Prefs.read Uicommon.auto) || isConflict dir ->
   3420             selectI i true
   3421         | _ ->
   3422             loop (i + 1) in
   3423     loop start in
   3424   let selectSomethingIfPossible () =
   3425     if currentNumberRows () = 0 then nextInteresting () in
   3426 
   3427   let columnsOf si =
   3428     let oldPath = Path.empty in
   3429     let status =
   3430       match si.ri.replicas with
   3431         Different {direction = Conflict _} | Problem _ ->
   3432           NoStatus
   3433       | _ ->
   3434           match si.whatHappened with
   3435             None                     -> NoStatus
   3436           | Some (Util.Succeeded, _) -> Done
   3437           | Some (Util.Failed _, _)  -> Failed
   3438     in
   3439     let (r1, action, r2, path) =
   3440       Uicommon.reconItem2stringList oldPath si.ri in
   3441     (r1, action, r2, status, path)
   3442   in
   3443 
   3444   let greenPixel  = "00dd00" in
   3445   let redPixel    = "ff2040" in
   3446   let lightbluePixel = "8888FF" in
   3447   let orangePixel = "ff9303" in
   3448 (*
   3449   let yellowPixel = "999900" in
   3450   let blackPixel  = "000000" in
   3451 *)
   3452   let buildPixmap p =
   3453     Pixmaps.to_pixbuf p in
   3454   let buildPixmaps f c1 =
   3455     (buildPixmap (f c1), buildPixmap (f lightbluePixel)) in
   3456 
   3457   let doneIcon = buildPixmap Pixmaps.success in
   3458   let failedIcon = buildPixmap Pixmaps.failure in
   3459   let rightArrow = buildPixmaps Pixmaps.copyAB greenPixel in
   3460   let leftArrow = buildPixmaps Pixmaps.copyBA greenPixel in
   3461   let orangeRightArrow = buildPixmaps Pixmaps.copyAB orangePixel in
   3462   let orangeLeftArrow = buildPixmaps Pixmaps.copyBA orangePixel in
   3463   let ignoreAct = buildPixmaps Pixmaps.ignore redPixel in
   3464   let failedIcons = (failedIcon, failedIcon) in
   3465   let mergeLogo = buildPixmaps Pixmaps.mergeLogo greenPixel in
   3466 (*
   3467   let rightArrowBlack = buildPixmap (Pixmaps.copyAB blackPixel) in
   3468   let leftArrowBlack = buildPixmap (Pixmaps.copyBA blackPixel) in
   3469   let mergeLogoBlack = buildPixmap (Pixmaps.mergeLogo blackPixel) in
   3470 *)
   3471 
   3472   let getArrow j action =
   3473     let changedFromDefault = match !theState.(j).ri.replicas with
   3474         Different diff -> diff.direction <> diff.default_direction
   3475       | _ -> false in
   3476     let sel pixmaps =
   3477       if changedFromDefault then snd pixmaps else fst pixmaps in
   3478     let pixmaps =
   3479       match action with
   3480         Uicommon.AError      -> failedIcons
   3481       | Uicommon.ASkip _     -> ignoreAct
   3482       | Uicommon.ALtoR false -> rightArrow
   3483       | Uicommon.ALtoR true  -> orangeRightArrow
   3484       | Uicommon.ARtoL false -> leftArrow
   3485       | Uicommon.ARtoL true  -> orangeLeftArrow
   3486       | Uicommon.AMerge      -> mergeLogo
   3487     in
   3488     sel pixmaps
   3489   in
   3490 
   3491 
   3492   let getStatusIcon = function
   3493     | Failed   -> Some failedIcon
   3494     | Done     -> Some doneIcon
   3495     | NoStatus -> None in
   3496 
   3497   let displayRowAction row i action =
   3498     mainWindowModel#set ~row ~column:c_action (getArrow i action) in
   3499   let displayRowStatus row status =
   3500     mainWindowModel#set ~row ~column:c_status (getStatusIcon status);
   3501     if status <> NoStatus then
   3502       mainWindowModel#set ~row ~column:c_statust "" in
   3503   let displayRowPath row path =
   3504     mainWindowModel#set ~row ~column:c_path (transcodeFilename path) in
   3505   let displayRow row i r1 r2 action status path =
   3506     mainWindowModel#set ~row ~column:c_replica1 r1;
   3507     mainWindowModel#set ~row ~column:c_replica2 r2;
   3508     displayRowAction row i action;
   3509     displayRowStatus row status;
   3510     displayRowPath row path;
   3511     (*mainWindowModel#set ~row ~column:c_rowid i;*)
   3512   in
   3513 
   3514   let displayMain() =
   3515     (* The call to mainWindow#clear below side-effect current,
   3516        so we save the current value before we clear out the main window and
   3517        rebuild it. *)
   3518     let savedCurrent = mainWindow#selection#get_selected_rows in
   3519     mainWindow#set_model None;
   3520     mainWindowModel#clear ();
   3521     let tot = Array.length !theState - 1 in
   3522     let totf = float_of_int (tot + 1) in
   3523     progressBar#set_text (Printf.sprintf "Displaying %i items..." (tot + 1));
   3524     for i = 0 to tot do
   3525       if i mod 1024 = 0 then begin
   3526         progressBar#set_fraction (max 0. (min 1. ((float_of_int i) /. totf)));
   3527         gtk_sync false
   3528       end;
   3529 
   3530       let (r1, action, r2, status, path) = columnsOf !theState.(i) in
   3531 
   3532       let row = mainWindowModel#append () in
   3533       displayRow row i r1 r2 action status path;
   3534     done;
   3535     mainWindow#set_model (Some mainWindowModel#coerce);
   3536     begin match savedCurrent with
   3537     | []  -> selectSomethingIfPossible ()
   3538     | [x] -> select x true
   3539     | _   -> Safelist.iter (fun p -> mainWindow#selection#select_path p) savedCurrent
   3540     end;
   3541 
   3542     progressBar#set_text ""; progressBar#set_fraction 0.;
   3543     updateDetails ();  (* Do we need this line? *)
   3544  in
   3545 
   3546   let redisplay i si iter =
   3547     let (_, action, _, status, path) = columnsOf si in
   3548     displayRowAction iter i action;
   3549     displayRowStatus iter status;
   3550     if status = Failed then displayRowPath iter (path ^
   3551                "       [failed: click on this line for details]");
   3552   in
   3553 
   3554   let fastRedisplay i =
   3555     let si = !theState.(i) in
   3556     let iter = mainWindowModel#get_iter (rowOfSi i) in
   3557     let (_, action, _, status, path) = columnsOf si in
   3558     displayRowStatus iter status;
   3559     if status = Failed then begin
   3560       displayRowPath iter (path ^
   3561                "       [failed: click on this line for details]");
   3562       match currentRow () with
   3563       | Some (_, csi, _) when csi = si -> updateDetails ()
   3564       | Some _ | None -> ()
   3565     end
   3566   in
   3567 
   3568   let updateRowStatus i newstatus =
   3569     let row = mainWindowModel#get_iter (rowOfSi i) in
   3570     let oldstatus = mainWindowModel#get ~row ~column:c_statust in
   3571     if oldstatus <> newstatus then mainWindowModel#set ~row ~column:c_statust newstatus
   3572   in
   3573 
   3574   let totalBytesToTransfer = ref Uutil.Filesize.zero in
   3575   let totalBytesTransferred = ref Uutil.Filesize.zero in
   3576 
   3577   let t1 = ref 0. in
   3578   let lastFrac = ref 0. in
   3579   let sta = ref (Uicommon.Stats.init (Uutil.Filesize.zero)) in
   3580   let displayGlobalProgress v =
   3581     if v = 0. || abs_float (v -. !lastFrac) > 1. then begin
   3582       lastFrac := v;
   3583       progressBar#set_fraction (max 0. (min 1. (v /. 100.)))
   3584     end;
   3585     if v < 0.001 then
   3586       progressBar#set_text " "
   3587     else begin
   3588       let t = Unix.gettimeofday () in
   3589       Uicommon.Stats.update !sta t !totalBytesTransferred;
   3590       let delta = t -. !t1 in
   3591       if delta >= 0.5 then begin
   3592         t1 := t;
   3593         let remTime =
   3594           if v >= 100. then "00:00 remaining" else
   3595           (Uicommon.Stats.eta !sta "--:--") ^ " remaining"
   3596         in
   3597         let rate = Uicommon.Stats.avgRate1 !sta in
   3598         let txt =
   3599           if rate > 99. then
   3600             Format.sprintf "%s  (%s)" remTime (rate2str rate)
   3601           else
   3602             remTime
   3603         in
   3604         progressBar#set_text txt
   3605       end
   3606     end
   3607   in
   3608 
   3609   let showGlobalProgress b =
   3610     (* Concatenate the new message *)
   3611     totalBytesTransferred := Uutil.Filesize.add !totalBytesTransferred b;
   3612     let v =
   3613       (Uutil.Filesize.percentageOfTotalSize
   3614          !totalBytesTransferred !totalBytesToTransfer)
   3615     in
   3616     displayGlobalProgress v
   3617   in
   3618 
   3619   let root1IsLocal = ref true in
   3620   let root2IsLocal = ref true in
   3621 
   3622   let initGlobalProgress b =
   3623     let (root1,root2) = Globals.roots () in
   3624     root1IsLocal := fst root1 = Local;
   3625     root2IsLocal := fst root2 = Local;
   3626     totalBytesToTransfer := b;
   3627     totalBytesTransferred := Uutil.Filesize.zero;
   3628     t1 := Unix.gettimeofday ();
   3629     sta := Uicommon.Stats.init !totalBytesToTransfer;
   3630     displayGlobalProgress 0.
   3631   in
   3632 
   3633   let showProgress i bytes dbg =
   3634     let i = Uutil.File.toLine i in
   3635     let item = !theState.(i) in
   3636     item.bytesTransferred <- Uutil.Filesize.add item.bytesTransferred bytes;
   3637     let b = item.bytesTransferred in
   3638     let len = item.bytesToTransfer in
   3639     let newstatus =
   3640       if b = Uutil.Filesize.zero || len = Uutil.Filesize.zero then "start "
   3641       else if len = Uutil.Filesize.zero then
   3642         Printf.sprintf "%5s " (Uutil.Filesize.toString b)
   3643       else Util.percent2string (Uutil.Filesize.percentageOfTotalSize b len) in
   3644     let dbg = if Trace.enabled "progress" then dbg ^ "/" else "" in
   3645     let newstatus = dbg ^ newstatus in
   3646     updateRowStatus i newstatus;
   3647     showGlobalProgress bytes;
   3648     gtk_sync false;
   3649     begin match item.ri.replicas with
   3650       Different diff ->
   3651         begin match diff.direction with
   3652           Replica1ToReplica2 ->
   3653             if !root2IsLocal then
   3654               clientWritten := !clientWritten +. Uutil.Filesize.toFloat bytes
   3655             else
   3656               serverWritten := !serverWritten +. Uutil.Filesize.toFloat bytes
   3657         | Replica2ToReplica1 ->
   3658             if !root1IsLocal then
   3659               clientWritten := !clientWritten +. Uutil.Filesize.toFloat bytes
   3660             else
   3661               serverWritten := !serverWritten +. Uutil.Filesize.toFloat bytes
   3662         | Conflict _ | Merge ->
   3663             (* Diff / merge *)
   3664             clientWritten := !clientWritten +. Uutil.Filesize.toFloat bytes
   3665         end
   3666     | _ ->
   3667         assert false
   3668     end
   3669   in
   3670 
   3671   (* Install showProgress so that we get called back by low-level
   3672      file transfer stuff *)
   3673   Uutil.setProgressPrinter showProgress;
   3674 
   3675   (* Apply new ignore patterns to the current state, expecting that the
   3676      number of reconitems will grow smaller. Adjust the display, being
   3677      careful to keep the cursor as near as possible to its position
   3678      before the new ignore patterns take effect. *)
   3679   let ignoreAndRedisplay () =
   3680     let lst = Array.to_list !theState in
   3681     (* FIX: we should actually test whether any prefix is now ignored *)
   3682     let keep sI = not (Globals.shouldIgnore sI.ri.path1) in
   3683     theState := Array.of_list (Safelist.filter keep lst);
   3684     displayMain() in
   3685 
   3686   let sortAndRedisplay () =
   3687     let compareRIs = Sortri.compareReconItems() in
   3688     Array.stable_sort (fun si1 si2 -> compareRIs si1.ri si2.ri) !theState;
   3689     displayMain() in
   3690 
   3691   (******************************************************************
   3692    Main detect-updates-and-reconcile logic
   3693    ******************************************************************)
   3694 
   3695   let commitUpdates () =
   3696     Trace.status "Updating synchronizer state";
   3697     let t = Trace.startTimer "Updating synchronizer state" in
   3698     gtk_sync true;
   3699     Update.commitUpdates();
   3700     Trace.showTimer t
   3701   in
   3702 
   3703   let clearMainWindow () =
   3704     grDisactivateAll ();
   3705     make_busy toplevelWindow;
   3706     mainWindow#set_model None;
   3707     mainWindowModel#clear ();
   3708     mainWindow#set_model (Some mainWindowModel#coerce);
   3709     theState := [||];
   3710     detailsWindow#buffer#set_text ""
   3711   in
   3712 
   3713   let detectUpdatesAndReconcile () =
   3714     clearMainWindow ();
   3715     startStats ();
   3716     progressBarPulse := true;
   3717     sync_action := Some (fun () -> progressBar#pulse ());
   3718     let findUpdates () =
   3719       let t = Trace.startTimer "Checking for updates" in
   3720       Trace.status "Looking for changes";
   3721       let updates = Update.findUpdates ~wantWatcher:true !unsynchronizedPaths in
   3722       Trace.showTimer t;
   3723       updates in
   3724     let reconcile updates =
   3725       let t = Trace.startTimer "Reconciling" in
   3726       let reconRes = Recon.reconcileAll ~allowPartial:true updates in
   3727       Trace.showTimer t;
   3728       reconRes in
   3729     let (reconItemList, thereAreEqualUpdates, dangerousPaths) =
   3730       reconcile (findUpdates ()) in
   3731     if not !Update.foundArchives then commitUpdates ();
   3732     if reconItemList = [] then begin
   3733       if !Update.foundArchives then commitUpdates ();
   3734       if thereAreEqualUpdates then
   3735         Trace.status
   3736           "Replicas have been changed only in identical ways since last sync"
   3737       else
   3738         Trace.status "Everything is up to date"
   3739     end else
   3740       Trace.status "Check and/or adjust selected actions; then press Go";
   3741     theState :=
   3742       Array.of_list
   3743          (Safelist.map
   3744             (fun ri -> { ri = ri;
   3745                          bytesTransferred = Uutil.Filesize.zero;
   3746                          bytesToTransfer = Uutil.Filesize.zero;
   3747                          whatHappened = None })
   3748             reconItemList);
   3749     unsynchronizedPaths :=
   3750       Some (Safelist.map (fun ri -> ri.path1) reconItemList, []);
   3751     progressBarPulse := false; sync_action := None; displayGlobalProgress 0.;
   3752     displayMain();
   3753     progressBarPulse := false; sync_action := None; displayGlobalProgress 0.;
   3754     stopStats ();
   3755     grSet grGo (Array.length !theState > 0);
   3756     grSet grRescan true;
   3757     make_interactive toplevelWindow;
   3758     if Prefs.read Globals.confirmBigDeletes then begin
   3759       if dangerousPaths <> [] then begin
   3760         Prefs.set Globals.batch false;
   3761         Util.warn (Uicommon.dangerousPathMsg dangerousPaths)
   3762       end;
   3763     end;
   3764   in
   3765 
   3766   (*********************************************************************
   3767     Help menu
   3768    *********************************************************************)
   3769   let addDocSection (shortname, (name, docstr)) =
   3770     let parent = toplevelWindow in
   3771     if shortname = "about" then
   3772       ignore (helpMenu#add_image_item
   3773                 ~stock:`ABOUT ~callback:(fun () -> documentation ~parent shortname)
   3774                 name)
   3775     else if shortname <> "" && name <> "" then
   3776       ignore (helpMenu#add_item
   3777                 ~callback:(fun () -> documentation ~parent shortname)
   3778                 name) in
   3779   Safelist.iter addDocSection Strings.docs;
   3780 
   3781   (*********************************************************************
   3782     Ignore menu
   3783    *********************************************************************)
   3784   let addRegExpByPath pathfunc =
   3785     Util.StringSet.iter (fun pat -> Uicommon.addIgnorePattern pat)
   3786       (currentSelectedFold
   3787          (fun s si -> Util.StringSet.add (pathfunc si.ri.path1) s)
   3788          Util.StringSet.empty);
   3789     ignoreAndRedisplay ()
   3790   in
   3791   grAdd grAction
   3792     (ignoreMenu#add_item ~key:GdkKeysyms._i
   3793        ~callback:(fun () -> getLock (fun () ->
   3794           addRegExpByPath Uicommon.ignorePath))
   3795        "Permanently Ignore This _Path");
   3796   grAdd grAction
   3797     (ignoreMenu#add_item ~key:GdkKeysyms._E
   3798        ~callback:(fun () -> getLock (fun () ->
   3799           addRegExpByPath Uicommon.ignoreExt))
   3800        "Permanently Ignore Files with this _Extension");
   3801   grAdd grAction
   3802     (ignoreMenu#add_item ~key:GdkKeysyms._N
   3803        ~callback:(fun () -> getLock (fun () ->
   3804           addRegExpByPath Uicommon.ignoreName))
   3805        "Permanently Ignore Files with this _Name (in any Dir)");
   3806 
   3807   (*
   3808   grAdd grRescan
   3809     (ignoreMenu#add_item ~callback:
   3810        (fun () -> getLock ignoreDialog) "Edit ignore patterns");
   3811   *)
   3812 
   3813   (*********************************************************************
   3814     Sort menu
   3815    *********************************************************************)
   3816   grAdd grRescan
   3817     (sortMenu#add_item
   3818        ~callback:(fun () -> getLock (fun () ->
   3819           Sortri.sortByName();
   3820           sortAndRedisplay()))
   3821        "Sort by _Name");
   3822   grAdd grRescan
   3823     (sortMenu#add_item
   3824        ~callback:(fun () -> getLock (fun () ->
   3825           Sortri.sortBySize();
   3826           sortAndRedisplay()))
   3827        "Sort by _Size");
   3828   grAdd grRescan
   3829     (sortMenu#add_item
   3830        ~callback:(fun () -> getLock (fun () ->
   3831           Sortri.sortNewFirst();
   3832           sortAndRedisplay()))
   3833        "Sort Ne_w Entries First (toggle)");
   3834   grAdd grRescan
   3835     (sortMenu#add_item
   3836        ~callback:(fun () -> getLock (fun () ->
   3837           Sortri.restoreDefaultSettings();
   3838           sortAndRedisplay()))
   3839        "_Default Ordering");
   3840 
   3841   (*********************************************************************
   3842     Main function : synchronize
   3843    *********************************************************************)
   3844   let synchronize () =
   3845     if Array.length !theState = 0 then
   3846       Trace.status "Nothing to synchronize"
   3847     else begin
   3848       grDisactivateAll ();
   3849       make_busy toplevelWindow;
   3850 
   3851       Trace.status "Propagating changes";
   3852       Uicommon.transportStart ();
   3853       grSet grStop true;
   3854       let totalLength =
   3855         Array.fold_left
   3856           (fun l si ->
   3857              si.bytesTransferred <- Uutil.Filesize.zero;
   3858              let len =
   3859                if si.whatHappened = None then Common.riLength si.ri else
   3860                Uutil.Filesize.zero
   3861              in
   3862              si.bytesToTransfer <- len;
   3863              Uutil.Filesize.add l len)
   3864           Uutil.Filesize.zero !theState in
   3865       initGlobalProgress totalLength;
   3866       let t = Trace.startTimer "Propagating changes" in
   3867       let uiWrapper i theSI =
   3868         match theSI.whatHappened with
   3869           None ->
   3870             let textDetailed = ref None in
   3871             catch (fun () ->
   3872                      Transport.transportItem
   3873                        theSI.ri (Uutil.File.ofLine i)
   3874                        (fun title text ->
   3875                          textDetailed := (Some text);
   3876                          if Prefs.read Uicommon.confirmmerge then
   3877                            twoBoxAdvanced
   3878                              ~parent:toplevelWindow
   3879                              ~title:title
   3880                              ~message:("Do you want to commit the changes to"
   3881                                        ^ " the replicas ?")
   3882                              ~longtext:text
   3883                              ~advLabel:"View details..."
   3884                              ~astock:`YES
   3885                              ~bstock:`NO
   3886                          else
   3887                            true)
   3888                      >>= (fun () ->
   3889                        return Util.Succeeded))
   3890                    (fun e ->
   3891                      match e with
   3892                        Util.Transient s ->
   3893                          return (Util.Failed s)
   3894                      | _ ->
   3895                          fail e)
   3896               >>= (fun res ->
   3897                 let rem =
   3898                   Uutil.Filesize.sub
   3899                     theSI.bytesToTransfer theSI.bytesTransferred
   3900                 in
   3901                 if rem <> Uutil.Filesize.zero then
   3902                   showProgress (Uutil.File.ofLine i) rem "done";
   3903                 theSI.whatHappened <- Some (res, !textDetailed);
   3904             fastRedisplay i;
   3905             gtk_sync false;
   3906             return ())
   3907         | Some _ ->
   3908             return () (* Already processed this one (e.g. merged it) *)
   3909       in
   3910       startStats ();
   3911       Uicommon.transportItems !theState (fun {ri; _} -> not (Common.isDeletion ri)) uiWrapper;
   3912       Uicommon.transportItems !theState (fun {ri; _} -> Common.isDeletion ri) uiWrapper;
   3913       Uicommon.transportFinish ();
   3914       grSet grStop false;
   3915       Trace.showTimer t;
   3916       commitUpdates ();
   3917       stopStats ();
   3918 
   3919       let failureList =
   3920         Array.fold_right
   3921           (fun si l ->
   3922              match si.whatHappened with
   3923                Some (Util.Failed err, _) ->
   3924                  (si, [err], "transport failure") :: l
   3925              | _ ->
   3926                  l)
   3927           !theState []
   3928       in
   3929       let failureCount = List.length failureList in
   3930       let failures =
   3931         if failureCount = 0 then [] else
   3932         [Printf.sprintf "%d failure%s"
   3933            failureCount (if failureCount = 1 then "" else "s")]
   3934       in
   3935       let partialList =
   3936         Array.fold_right
   3937           (fun si l ->
   3938              match si.whatHappened with
   3939                Some (Util.Succeeded, _)
   3940                when partiallyProblematic si.ri &&
   3941                     not (problematic si.ri) ->
   3942                  let errs =
   3943                    match si.ri.replicas with
   3944                      Different diff -> diff.errors1 @ diff.errors2
   3945                    | _              -> assert false
   3946                  in
   3947                  (si, errs,
   3948                   "partial transfer (errors during update detection)") :: l
   3949              | _ ->
   3950                  l)
   3951           !theState []
   3952       in
   3953       let partialCount = List.length partialList in
   3954       let partials =
   3955         if partialCount = 0 then [] else
   3956         [Printf.sprintf "%d partially transferred" partialCount]
   3957       in
   3958       let skippedList =
   3959         Array.fold_right
   3960           (fun si l ->
   3961              match si.ri.replicas with
   3962                Problem err ->
   3963                  (si, [err], "error during update detection") :: l
   3964              | Different diff when isConflict diff.direction ->
   3965                  (si, [],
   3966                   if isConflict diff.default_direction then
   3967                     "conflict"
   3968                   else "skipped") :: l
   3969              | _ ->
   3970                  l)
   3971           !theState []
   3972       in
   3973       let skippedCount = List.length skippedList in
   3974       let skipped =
   3975         if skippedCount = 0 then [] else
   3976         [Printf.sprintf "%d skipped" skippedCount]
   3977       in
   3978       let nostartCount =
   3979         if not (Abort.isAll ()) then 0 else
   3980           Array.fold_left
   3981             (fun c si -> if si.whatHappened = None then c + 1 else c)
   3982             0 !theState
   3983       in
   3984       let nostart =
   3985         if nostartCount = 0 then [] else
   3986         [Printf.sprintf "%d not started" nostartCount]
   3987       in
   3988       unsynchronizedPaths :=
   3989         Some (Safelist.map (fun (si, _, _) -> si.ri.path1)
   3990                 (failureList @ partialList @ skippedList),
   3991               []);
   3992       Trace.status
   3993         (Printf.sprintf "Synchronization complete         %s"
   3994            (String.concat ", " (failures @ partials @ skipped @ nostart)));
   3995       displayGlobalProgress 0.;
   3996 
   3997       grSet grRescan true;
   3998       make_interactive toplevelWindow;
   3999 
   4000       let totalCount = failureCount + partialCount + skippedCount + nostartCount in
   4001       if totalCount > 0 then begin
   4002         let format n item sing plur =
   4003           match n with
   4004             0 -> []
   4005           | 1 -> [Format.sprintf "one %s%s" item sing]
   4006           | n -> [Format.sprintf "%d %s%s" n item plur]
   4007         in
   4008         let infos =
   4009           format failureCount "failure" "" "s" @
   4010           format partialCount "partially transferred director" "y" "ies" @
   4011           format skippedCount "skipped item" "" "s" @
   4012           format nostartCount "not started item" "" "s"
   4013         in
   4014         let message =
   4015           (if failureCount = 0 && nostartCount = 0 then
   4016              "The synchronization was successful.\n\n"
   4017            else "") ^
   4018           "The replicas are not fully synchronized.\n" ^
   4019           (if totalCount < 2 then "There was" else "There were") ^
   4020           begin match infos with
   4021             [] -> assert false
   4022           | [x] -> " " ^ x
   4023           | l -> ":\n  - " ^ String.concat ";\n  - " l
   4024           end ^
   4025           "."
   4026         in
   4027         summaryBox ~parent:toplevelWindow
   4028           ~title:"Synchronization summary" ~message ~f:
   4029           (fun t ->
   4030              let bullet = "\xe2\x80\xa2 " in
   4031              let layout = Pango.Layout.create t#misc#pango_context#as_context in
   4032              Pango.Layout.set_text layout bullet;
   4033              let (n, _) = Pango.Layout.get_pixel_size layout in
   4034              let path =
   4035                t#buffer#create_tag [`FONT_DESC (Lazy.force fontBold)] in
   4036              let description =
   4037                t#buffer#create_tag [`FONT_DESC (Lazy.force fontItalic)] in
   4038              let errorFirstLine =
   4039                t#buffer#create_tag [`LEFT_MARGIN (n); `INDENT (- n)] in
   4040              let errorNextLines =
   4041                t#buffer#create_tag [`LEFT_MARGIN (2 * n)] in
   4042              List.iter
   4043                (fun (si, errs, desc) ->
   4044                   t#buffer#insert ~tags:[path]
   4045                     (transcodeFilename (Path.toString si.ri.path1));
   4046                   t#buffer#insert ~tags:[description]
   4047                     (" \xe2\x80\x94 " ^ desc ^ "\n");
   4048                   List.iter
   4049                     (fun err ->
   4050                        let errl =
   4051                          Str.split (Str.regexp_string "\n") (transcode err) in
   4052                        match errl with
   4053                          [] ->
   4054                            ()
   4055                        | f :: rem ->
   4056                            t#buffer#insert ~tags:[errorFirstLine]
   4057                              (bullet ^ f ^ "\n");
   4058                            List.iter
   4059                              (fun n ->
   4060                                 t#buffer#insert ~tags:[errorNextLines]
   4061                                   (n ^ "\n"))
   4062                              rem)
   4063                     errs)
   4064                (failureList @ partialList @ skippedList))
   4065       end
   4066 
   4067     end in
   4068 
   4069   (*********************************************************************
   4070     Buttons for -->, M, <--, Skip
   4071    *********************************************************************)
   4072   let doActionOnRow f i theSI iter =
   4073     begin match theSI.whatHappened, theSI.ri.replicas with
   4074       None, Different diff ->
   4075         f theSI.ri diff;
   4076         redisplay i theSI iter
   4077     | _ ->
   4078         ()
   4079     end
   4080   in
   4081   let doAction f =
   4082     match currentRow () with
   4083       Some (i, si, iter) ->
   4084         doActionOnRow f i si iter;
   4085         nextInteresting ()
   4086     | None ->
   4087         currentSelectedIter (fun i si iter -> doActionOnRow f i si iter);
   4088         updateDetails ()
   4089   in
   4090   let leftAction _ =
   4091     doAction (fun _ diff -> diff.direction <- Replica2ToReplica1) in
   4092   let rightAction _ =
   4093     doAction (fun _ diff -> diff.direction <- Replica1ToReplica2) in
   4094   let questionAction _ = doAction (fun _ diff -> diff.direction <- Conflict "") in
   4095   let mergeAction    _ = doAction (fun _ diff -> diff.direction <- Merge) in
   4096 
   4097   let insert_button (toolbar : #GButton.toolbar) ~stock ~text ~tooltip ~callback () =
   4098     let b = GButton.tool_button ~stock ~label:text ~packing:toolbar#insert () in
   4099     ignore (b#connect#clicked ~callback);
   4100     b#misc#set_tooltip_text tooltip;
   4101     b
   4102   in
   4103 
   4104 (*  actionBar#insert_space ();*)
   4105   grAdd grAction
   4106     (insert_button actionBar
   4107        ~stock:`GO_FORWARD
   4108        ~text:"Left to Right"
   4109        ~tooltip:"Propagate selected items\n\
   4110                  from the left replica to the right one"
   4111        ~callback:rightAction ());
   4112 (*  actionBar#insert_space ();*)
   4113   grAdd grAction
   4114     (insert_button actionBar ~text:"Skip"
   4115        ~stock:`NO
   4116        ~tooltip:"Skip selected items"
   4117        ~callback:questionAction ());
   4118 (*  actionBar#insert_space ();*)
   4119   grAdd grAction
   4120     (insert_button actionBar
   4121        ~stock:`GO_BACK
   4122        ~text:"Right to Left"
   4123        ~tooltip:"Propagate selected items\n\
   4124                  from the right replica to the left one"
   4125        ~callback:leftAction ());
   4126 (*  actionBar#insert_space ();*)
   4127   grAdd grAction
   4128     (insert_button actionBar
   4129        ~stock:`ADD
   4130        ~text:"Merge"
   4131        ~tooltip:"Merge selected files"
   4132        ~callback:mergeAction ());
   4133 
   4134   (*********************************************************************
   4135     Diff / merge buttons
   4136    *********************************************************************)
   4137   let diffCmd () =
   4138     match currentRow () with
   4139       Some (i, item, _) ->
   4140         getLock (fun () ->
   4141           let len =
   4142             match item.ri.replicas with
   4143               Problem _ ->
   4144                 Uutil.Filesize.zero
   4145             | Different diff ->
   4146                 snd (if !root1IsLocal then diff.rc2 else diff.rc1).size
   4147           in
   4148           item.bytesTransferred <- Uutil.Filesize.zero;
   4149           item.bytesToTransfer <- len;
   4150           initGlobalProgress len;
   4151           startStats ();
   4152           let styleDiff (t_text : scrolled_text) =
   4153             let diffAdd =
   4154               t_text#text#buffer#create_tag [`FOREGROUND "green"] in
   4155             let diffDel =
   4156               t_text#text#buffer#create_tag [`FOREGROUND "red"] in
   4157             let diffLoc =
   4158               t_text#text#buffer#create_tag [`FOREGROUND "dark cyan"; `WEIGHT `BOLD] in
   4159             let setStyle sty ~start ~stop =
   4160               t_text#text#buffer#apply_tag sty ~start ~stop
   4161             in
   4162             let rec styleDiffLine ~start =
   4163               let stop = start#forward_line in
   4164               let styleLine tag = setStyle tag ~start ~stop in
   4165               let () =
   4166                 match start#get_text ~stop:start#forward_char with
   4167                 | "+" -> styleLine diffAdd
   4168                 | "-" -> styleLine diffDel
   4169                 | "@" -> styleLine diffLoc
   4170                 | _ -> ()
   4171               in
   4172               if not (start#equal stop) then styleDiffLine ~start:stop
   4173             in
   4174             styleDiffLine ~start:(t_text#text#buffer#start_iter);
   4175           in
   4176           Uicommon.showDiffs item.ri
   4177             (fun title text ->
   4178                messageBox ~title:(transcode title) (transcode text)
   4179                  ~styleText:styleDiff)
   4180             Trace.status (Uutil.File.ofLine i);
   4181           stopStats ();
   4182           displayGlobalProgress 0.;
   4183           fastRedisplay i)
   4184     | None ->
   4185         () in
   4186 
   4187   actionBar#insert (GButton.separator_tool_item ());
   4188   grAdd grDiff (insert_button actionBar ~text:"Diff"
   4189                   ~stock:`DIALOG_INFO
   4190                   ~tooltip:"Compare the two files at each replica"
   4191                   ~callback:diffCmd ());
   4192 
   4193   (*********************************************************************
   4194     Detail button
   4195    *********************************************************************)
   4196 (*  actionBar#insert_space ();*)
   4197   grAdd grDetail (insert_button actionBar ~text:"Details"
   4198                     ~stock:`INFO
   4199                     ~tooltip:"Show detailed information about\n\
   4200                               an item, when available"
   4201                     ~callback:showDetCommand ());
   4202 
   4203   (*********************************************************************
   4204     Quit button
   4205    *********************************************************************)
   4206 (*  actionBar#insert_space ();
   4207   ignore (actionBar#insert_button ~text:"Quit"
   4208             ~icon:((GMisc.image ~stock:`QUIT ())#coerce)
   4209             ~tooltip:"Exit Unison"
   4210             ~callback:safeExit ());
   4211 *)
   4212 
   4213   (*********************************************************************
   4214     go button
   4215    *********************************************************************)
   4216   actionBar#insert (GButton.separator_tool_item ());
   4217   grAdd grGo
   4218     (insert_button actionBar ~text:"Go"
   4219        (* tooltip:"Go with displayed actions" *)
   4220        ~stock:`EXECUTE
   4221        ~tooltip:"Perform the synchronization"
   4222        ~callback:(fun () ->
   4223                     getLock synchronize) ());
   4224 
   4225   grAdd grStop
   4226     (insert_button actionBar ~text:"Stop"
   4227        ~stock:`STOP
   4228        ~tooltip:"Stop update propagation"
   4229        ~callback:Abort.all ());
   4230 
   4231   (*********************************************************************
   4232     Rescan button
   4233    *********************************************************************)
   4234   let profileInitSuccess = ref false in
   4235   let updateFromProfile = ref (fun () -> ()) in
   4236 
   4237   let loadProfile p reload =
   4238     debug (fun()-> Util.msg "Loading profile %s..." p);
   4239     Trace.status "Loading profile";
   4240     unsynchronizedPaths := None;
   4241     profileInitSuccess := false;
   4242     Uicommon.initPrefs ~profileName:p ~promptForRoots ~prepDebug ();
   4243     Uicommon.connectRoots
   4244       ~displayWaitMessage:(fun () -> if not reload then displayWaitMessage ())
   4245       ~termInteract ();
   4246     profileInitSuccess := true;
   4247     !updateFromProfile ()
   4248   in
   4249 
   4250   let reloadProfile () =
   4251     let n =
   4252       match !Prefs.profileName with
   4253         None   -> assert false
   4254       | Some n -> n
   4255     in
   4256     clearMainWindow ();
   4257     if not (Prefs.profileUnchanged ()) || not (!profileInitSuccess) then
   4258       loadProfile n true
   4259     else Uicommon.connectRoots ~displayWaitMessage ~termInteract ()
   4260   in
   4261 
   4262   let detectCmd () =
   4263     mainWindow#misc#grab_focus ();
   4264     if !profileInitSuccess then begin
   4265       getLock detectUpdatesAndReconcile;
   4266       updateDetails ();
   4267       if Prefs.read Globals.batch then begin
   4268         Prefs.set Globals.batch false; synchronize()
   4269       end
   4270     end else begin
   4271       grSet grRescan true;
   4272       make_interactive toplevelWindow
   4273     end
   4274   in
   4275   let loadAndRunProfile p =
   4276     clearMainWindow ();
   4277     loadProfile p false;
   4278     detectCmd ()
   4279   in
   4280 
   4281 (*  actionBar#insert_space ();*)
   4282   grAdd grRescan
   4283     (insert_button actionBar ~text:"Rescan"
   4284        ~stock:`REFRESH
   4285        ~tooltip:"Check for updates"
   4286        ~callback: (fun () -> reloadProfile(); detectCmd()) ());
   4287 
   4288   (*********************************************************************
   4289     Profile change button
   4290    *********************************************************************)
   4291   actionBar#insert (GButton.separator_tool_item ());
   4292   let profileChange _ =
   4293     match getProfile false with
   4294       None   -> ()
   4295     | Some p -> loadAndRunProfile p
   4296   in
   4297   grAdd grRescan (insert_button actionBar ~text:"Change Profile"
   4298                     ~stock:`OPEN
   4299                     ~tooltip:"Select a different profile"
   4300                     ~callback:profileChange ());
   4301 
   4302   (*********************************************************************
   4303     Keyboard commands
   4304    *********************************************************************)
   4305   ignore
   4306     (mainWindow#event#connect#key_press ~callback:
   4307        begin fun ev ->
   4308          let key = GdkEvent.Key.keyval ev in
   4309          if key = GdkKeysyms._Left then begin
   4310            leftAction (); GtkSignal.stop_emit (); true
   4311          end else if key = GdkKeysyms._Right then begin
   4312            rightAction (); GtkSignal.stop_emit (); true
   4313          end else
   4314            false
   4315        end);
   4316 
   4317   (*********************************************************************
   4318     Action menu
   4319    *********************************************************************)
   4320   let buildActionMenu init =
   4321     let withDelayedUpdates f x =
   4322       delayUpdates := true;
   4323       f x;
   4324       delayUpdates := false;
   4325       updateDetails () in
   4326     let actionMenu = replace_submenu "_Actions" actionItem in
   4327     grAdd grRescan
   4328       (actionMenu#add_image_item
   4329          ~callback:(fun _ -> withDelayedUpdates mainWindow#selection#select_all ())
   4330          ~image:((GMisc.image ~stock:`SELECT_ALL ~icon_size:`MENU ())#coerce)
   4331          ~modi:[`CONTROL] ~key:GdkKeysyms._A
   4332          "Select _All");
   4333     grAdd grRescan
   4334       (actionMenu#add_item
   4335          ~callback:(fun _ -> withDelayedUpdates mainWindow#selection#unselect_all ())
   4336          ~modi:[`SHIFT; `CONTROL] ~key:GdkKeysyms._A
   4337          "_Deselect All");
   4338 
   4339     ignore (actionMenu#add_separator ());
   4340 
   4341     let (loc1, loc2) =
   4342       if init then ("", "") else
   4343       let (root1,root2) = Globals.roots () in
   4344       (root2hostname root1, root2hostname root2)
   4345     in
   4346     let def_descr = "Left to Right" in
   4347     let descr =
   4348       if init || loc1 = loc2 then def_descr else
   4349       Printf.sprintf "from %s to %s" loc1 loc2 in
   4350     let left =
   4351       actionMenu#add_image_item ~key:GdkKeysyms._greater ~callback:rightAction
   4352         ~image:((GMisc.image ~stock:`GO_FORWARD ~icon_size:`MENU ())#coerce)
   4353         ~name:("Propagate " ^ def_descr) ("Propagate " ^ descr) in
   4354     grAdd grAction left;
   4355     left#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._greater;
   4356     left#add_accelerator ~group:accel_group GdkKeysyms._period;
   4357 
   4358     let def_descl = "Right to Left" in
   4359     let descl =
   4360       if init || loc1 = loc2 then def_descl else
   4361       Printf.sprintf "from %s to %s"
   4362         (Unicode.protect loc2) (Unicode.protect loc1) in
   4363     let right =
   4364       actionMenu#add_image_item ~key:GdkKeysyms._less ~callback:leftAction
   4365         ~image:((GMisc.image ~stock:`GO_BACK ~icon_size:`MENU ())#coerce)
   4366         ~name:("Propagate " ^ def_descl) ("Propagate " ^ descl) in
   4367     grAdd grAction right;
   4368     right#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._less;
   4369     right#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._comma;
   4370 
   4371     let skip =
   4372       actionMenu#add_image_item ~key:GdkKeysyms._slash ~callback:questionAction
   4373         ~image:((GMisc.image ~stock:`NO ~icon_size:`MENU ())#coerce)
   4374         "Do _Not Propagate Changes" in
   4375     grAdd grAction skip;
   4376     skip#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._minus;
   4377     skip#add_accelerator ~group:accel_group GdkKeysyms._KP_Divide;
   4378 
   4379     let merge =
   4380       actionMenu#add_image_item ~key:GdkKeysyms._m ~callback:mergeAction
   4381         ~image:((GMisc.image ~stock:`ADD ~icon_size:`MENU ())#coerce)
   4382         "_Merge the Files" in
   4383     grAdd grAction merge;
   4384   (* merge#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._m; *)
   4385 
   4386     (* Override actions *)
   4387     ignore (actionMenu#add_separator ());
   4388     grAdd grAction
   4389       (actionMenu#add_item
   4390          ~callback:(fun () ->
   4391             doAction (fun ri _ ->
   4392                         Recon.setDirection ri `Replica1ToReplica2 `Prefer))
   4393          "Resolve Conflicts in Favor of First Root");
   4394     grAdd grAction
   4395       (actionMenu#add_item
   4396          ~callback:(fun () ->
   4397             doAction (fun ri _ ->
   4398                         Recon.setDirection ri `Replica2ToReplica1 `Prefer))
   4399          "Resolve Conflicts in Favor of Second Root");
   4400     grAdd grAction
   4401       (actionMenu#add_item
   4402          ~callback:(fun () ->
   4403             doAction (fun ri _ ->
   4404                         Recon.setDirection ri `Newer `Prefer))
   4405          "Resolve Conflicts in Favor of Most Recently Modified");
   4406     grAdd grAction
   4407       (actionMenu#add_item
   4408          ~callback:(fun () ->
   4409             doAction (fun ri _ ->
   4410                         Recon.setDirection ri `Older `Prefer))
   4411          "Resolve Conflicts in Favor of Least Recently Modified");
   4412     ignore (actionMenu#add_separator ());
   4413     grAdd grAction
   4414       (actionMenu#add_item
   4415          ~callback:(fun () ->
   4416             doAction (fun ri _ -> Recon.setDirection ri `Newer `Force))
   4417          "Force Newer Files to Replace Older Ones");
   4418     grAdd grAction
   4419       (actionMenu#add_item
   4420          ~callback:(fun () ->
   4421             doAction (fun ri _ -> Recon.setDirection ri `Older `Force))
   4422          "Force Older Files to Replace Newer Ones");
   4423     ignore (actionMenu#add_separator ());
   4424     grAdd grAction
   4425       (actionMenu#add_item
   4426          ~callback:(fun () ->
   4427             doAction (fun ri _ -> Recon.revertToDefaultDirection ri))
   4428          "_Revert to Unison's Recommendation");
   4429     grAdd grAction
   4430       (actionMenu#add_item
   4431          ~callback:(fun () ->
   4432             doAction (fun ri _ -> Recon.setDirection ri `Merge `Force))
   4433          "Revert to the Merging Default, if Available");
   4434 
   4435     (* Diff *)
   4436     ignore (actionMenu#add_separator ());
   4437     grAdd grDiff (actionMenu#add_image_item ~key:GdkKeysyms._d ~callback:diffCmd
   4438         ~image:((GMisc.image ~stock:`DIALOG_INFO ~icon_size:`MENU ())#coerce)
   4439         "Show _Diffs");
   4440 
   4441     (* Details *)
   4442     grAdd grDetail
   4443       (actionMenu#add_image_item ~key:GdkKeysyms._i ~callback:showDetCommand
   4444         ~image:((GMisc.image ~stock:`INFO ~icon_size:`MENU ())#coerce)
   4445         "Detailed _Information")
   4446 
   4447   in
   4448   buildActionMenu true;
   4449 
   4450   (*********************************************************************
   4451     Synchronization menu
   4452    *********************************************************************)
   4453 
   4454   grAdd grGo
   4455     (fileMenu#add_image_item ~key:GdkKeysyms._g
   4456        ~image:(GMisc.image ~stock:`EXECUTE ~icon_size:`MENU () :> GObj.widget)
   4457        ~callback:(fun () -> getLock synchronize)
   4458        "_Go");
   4459   grAdd grRescan
   4460     (fileMenu#add_image_item ~key:GdkKeysyms._r
   4461        ~image:(GMisc.image ~stock:`REFRESH ~icon_size:`MENU () :> GObj.widget)
   4462        ~callback:(fun () -> reloadProfile(); detectCmd())
   4463        "_Rescan");
   4464   grAdd grRescan
   4465     (fileMenu#add_item ~key:GdkKeysyms._a
   4466        ~callback:(fun () ->
   4467                     reloadProfile();
   4468                     Prefs.set Globals.batch true;
   4469                     detectCmd())
   4470        "_Detect Updates and Proceed (Without Waiting)");
   4471   grAdd grRescan
   4472     (fileMenu#add_item ~key:GdkKeysyms._f
   4473        ~callback:(
   4474          fun () ->
   4475            let rec loop i acc =
   4476              if i >= Array.length (!theState) then acc else
   4477              let notok =
   4478                (match !theState.(i).whatHappened with
   4479                    None-> true
   4480                  | Some(Util.Failed _, _) -> true
   4481                  | Some(Util.Succeeded, _) -> false)
   4482               || match !theState.(i).ri.replicas with
   4483                    Problem _ -> true
   4484                  | Different diff -> isConflict diff.direction in
   4485              if notok then loop (i+1) (i::acc)
   4486              else loop (i+1) (acc) in
   4487            let failedindices = loop 0 [] in
   4488            let failedpaths =
   4489              Safelist.map (fun i -> !theState.(i).ri.path1) failedindices in
   4490            debug (fun()-> Util.msg "Rescaning with paths = %s\n"
   4491                     (String.concat ", " (Safelist.map
   4492                                            (fun p -> "'"^(Path.toString p)^"'")
   4493                                            failedpaths)));
   4494            let paths = Prefs.read Globals.paths in
   4495            let confirmBigDeletes = Prefs.read Globals.confirmBigDeletes in
   4496            Prefs.set Globals.paths failedpaths;
   4497            Prefs.set Globals.confirmBigDeletes false;
   4498            (* Modifying global paths does not play well with filesystem
   4499               monitoring, so we disable it. *)
   4500            unsynchronizedPaths := None;
   4501            detectCmd();
   4502            Prefs.set Globals.paths paths;
   4503            Prefs.set Globals.confirmBigDeletes confirmBigDeletes;
   4504            unsynchronizedPaths := None)
   4505        "Re_check Unsynchronized Items");
   4506 
   4507   ignore (fileMenu#add_separator ());
   4508 
   4509   grAdd grRescan
   4510     (fileMenu#add_image_item ~key:GdkKeysyms._p
   4511        ~callback:profileChange
   4512        ~image:(GMisc.image ~stock:`OPEN ~icon_size:`MENU () :> GObj.widget)
   4513        "Change _Profile...");
   4514 
   4515   let fastProf i key =
   4516     let item = fileMenu#add_item ~key:key ~bindname:(string_of_int i) "" in
   4517     item#misc#hide ();
   4518     grAdd grRescan item;
   4519     let show name =
   4520       match item#children with
   4521       | [] | _::_::_ -> ()
   4522       | [l] ->
   4523           let label = (GMisc.label_cast l) in
   4524           label#set_label ("Select profile " ^ name);
   4525           ignore (item#connect#activate
   4526             ~callback:(fun _ ->
   4527                if System.file_exists (Prefs.profilePathname name) then begin
   4528                  Trace.status ("Loading profile " ^ name);
   4529                  loadProfile name false; detectCmd ()
   4530                end else
   4531                  Trace.status ("Profile " ^ name ^ " not found"))
   4532             );
   4533           item#misc#show ()
   4534     in
   4535     (item#misc#hide, show) in
   4536 
   4537   let fastKeysyms =
   4538     [| GdkKeysyms._0; GdkKeysyms._1; GdkKeysyms._2; GdkKeysyms._3;
   4539        GdkKeysyms._4; GdkKeysyms._5; GdkKeysyms._6; GdkKeysyms._7;
   4540        GdkKeysyms._8; GdkKeysyms._9 |] in
   4541 
   4542   let fastKeyitems = Array.init 10 (fun i -> fastProf i fastKeysyms.(i)) in
   4543 
   4544   let updateProfileKeyMenu () =
   4545     if !Uicommon.profilesAndRoots = [] then Uicommon.scanProfiles ();
   4546 
   4547     Array.iteri
   4548       (fun i v -> match v with
   4549       | None -> (fst fastKeyitems.(i)) ()
   4550       | Some (profile, info) -> (snd fastKeyitems.(i)) profile)
   4551       Uicommon.profileKeymap
   4552   in
   4553 
   4554   ignore (fileMenu#add_separator ());
   4555   ignore (fileMenu#add_item
   4556             ~callback:(fun _ -> statWin#show ()) "Show _Statistics");
   4557 
   4558   ignore (fileMenu#add_separator ());
   4559   let quit =
   4560     fileMenu#add_image_item
   4561       ~key:GdkKeysyms._q ~callback:safeExit
   4562       ~image:((GMisc.image ~stock:`QUIT ~icon_size:`MENU ())#coerce)
   4563       "_Quit"
   4564   in
   4565   quit#add_accelerator ~group:accel_group ~modi:[`CONTROL] GdkKeysyms._q;
   4566 
   4567   (*********************************************************************
   4568     Expert menu
   4569    *********************************************************************)
   4570   let buildExpertMenu () =
   4571     let addDebugToggle modname =
   4572       ignore (expertMenu#add_check_item ~active:(Trace.enabled modname)
   4573         ~callback:(fun b -> Trace.enable modname b)
   4574         ("Debug '" ^ modname ^ "'")) in
   4575 
   4576     addDebugToggle "all";
   4577     addDebugToggle "verbose";
   4578     addDebugToggle "update";
   4579 
   4580     ignore (expertMenu#add_separator ());
   4581     ignore (expertMenu#add_item
   4582               ~callback:(fun () ->
   4583                            Printf.fprintf stderr "\nGC stats now:\n";
   4584                            Gc.print_stat stderr;
   4585                            Printf.fprintf stderr "\nAfter major collection:\n";
   4586                            Gc.full_major(); Gc.print_stat stderr;
   4587                            flush stderr)
   4588               "Show memory/GC stats")
   4589   in
   4590   buildExpertMenu ();
   4591 
   4592   let toggleExpertMenu enabled =
   4593     expertItem#set_visible enabled
   4594   in
   4595 
   4596   (*********************************************************************
   4597     Finish up
   4598    *********************************************************************)
   4599   grDisactivateAll ();
   4600 
   4601   updateFromProfile :=
   4602     (fun () ->
   4603        displayNewProfileLabel ();
   4604        setMainWindowColumnHeaders (Globals.roots ());
   4605        sizeMainWindow ();
   4606        toggleExpertMenu (Prefs.read Uicommon.expert);
   4607        buildActionMenu false);
   4608 
   4609   fatalErrorHandler :=
   4610     (fun err ->
   4611        grDisactivateAll ();
   4612        make_interactive toplevelWindow;
   4613        Trace.status ("Fatal error: " ^ err);
   4614        inExit := true;
   4615        fatalError err;
   4616        inExit := false;
   4617        match !Prefs.profileName with
   4618        | Some _ -> grSet grRescan true
   4619        | None ->  (* Normally should never get here; exceptions loading the
   4620                      very first profile are handled in the [start] function. *)
   4621            begin match getProfile true with
   4622            | None -> exit 1
   4623            | Some p -> loadAndRunProfile p
   4624            end
   4625     );
   4626 
   4627 
   4628   ignore (toplevelWindow#event#connect#delete ~callback:
   4629             (fun _ -> safeExit (); true));
   4630   toplevelWindow#show ();
   4631   fun p ->
   4632     updateProfileKeyMenu ();
   4633     loadAndRunProfile p
   4634 
   4635 
   4636 (*********************************************************************
   4637                                STARTUP
   4638  *********************************************************************)
   4639 
   4640 let start _ =
   4641   try
   4642     (* Stop GTK 3 from forcing client-side decorations *)
   4643     begin
   4644       try ignore (Unix.getenv "GTK_CSD") with
   4645       | Unix.Unix_error _ | Not_found ->
   4646           try Unix.putenv "GTK_CSD" "0" with
   4647           | Unix.Unix_error _ -> ()
   4648     end;
   4649 
   4650     (* Initialize the GTK library *)
   4651     ignore (GMain.Main.init ());
   4652     globalGTKInited := true;
   4653 
   4654     Util.warnPrinter :=
   4655       Some (fun msg -> warnBox ~parent:(toplevelWindow ()) "Warning" msg);
   4656 
   4657     GtkSignal.user_handler :=
   4658       (function
   4659        | Util.Transient s | Util.Fatal s -> !fatalErrorHandler s
   4660        | Stack_overflow -> !fatalErrorHandler (stackOverflowNoQuitMsg ());
   4661        | exn -> !fatalErrorHandler (Uicommon.exn2string exn));
   4662 
   4663     (* Ask the Remote module to call us back at regular intervals during
   4664        long network operations. *)
   4665     let rec tick () =
   4666       gtk_sync true;
   4667       Lwt_unix.sleep 0.05 >>= tick
   4668     in
   4669     ignore_result (tick ());
   4670 
   4671     let startGUI = createToplevelWindow () in
   4672 
   4673     (* Any exceptions here will be caught by the main catch handler
   4674        and the GUI will exit. *)
   4675     let getProfile () = match getProfile true with None -> exit 0 | Some x -> x in
   4676     let profileName =
   4677       match Uicommon.uiInitClRootsAndProfile ~prepDebug () with
   4678       | Error s -> begin fatalError s;
   4679                          Uicommon.clearClRoots (); getProfile () end
   4680       | Ok None -> getProfile ()
   4681       | Ok (Some s) -> s
   4682     in
   4683 
   4684     (* Exceptions from here onwards will be caught by the inner catch handler
   4685        and the GUI will not exit. Instead, the profile manager is re-opened.
   4686        User has the option to quit in the profile manager. *)
   4687     let rec initLoop profileName =
   4688       try startGUI profileName with
   4689       | Util.Transient s | Util.Fatal s ->
   4690           s |> fatalError |> Uicommon.clearClRoots |> getProfile |> initLoop
   4691       (* Since we have not started the GTK main loop yet, it is easier to
   4692          handle exceptions here directly. [GtkSignal.safe_call] could be
   4693          used but it will fail in case of subsequent exceptions without
   4694          raising, thus escaping further exception handlers.
   4695          This separate handling sequence could in theory be removed if
   4696          [startGUI] is called while the GTK main loop is running. *)
   4697     in
   4698     initLoop profileName;
   4699 
   4700     (* Display the ui *)
   4701 (*JV: not useful, as Unison does not handle any signal
   4702     ignore (GMain.Timeout.add 500 (fun _ -> true));
   4703               (* Hack: this allows signals such as SIGINT to be
   4704                  handled even when Gtk is waiting for events *)
   4705 *)
   4706     GMain.Main.main ()
   4707   with
   4708   | Util.Transient s | Util.Fatal s -> fatalError ~quit:true s
   4709   | exn -> fatalError ~quit:true (Uicommon.exn2string exn)
   4710 
   4711 end (* module Private *)
   4712 
   4713 
   4714 (*********************************************************************
   4715                             UI SELECTION
   4716  *********************************************************************)
   4717 
   4718 module Body : Uicommon.UI = struct
   4719 
   4720 let start = function
   4721     Uicommon.Text -> Uitext.Body.start Uicommon.Text
   4722   | Uicommon.Graphic ->
   4723       let displayAvailable =
   4724         Sys.win32
   4725           ||
   4726         (try System.getenv "DISPLAY" <> "" with Not_found -> false)
   4727           ||
   4728         (try System.getenv "WAYLAND_DISPLAY" <> "" with Not_found -> false)
   4729       in
   4730       if displayAvailable then Private.start Uicommon.Graphic
   4731       else begin
   4732         Util.warn "DISPLAY and WAYLAND_DISPLAY not set or empty; starting the Text UI\n";
   4733         Uitext.Body.start Uicommon.Text
   4734       end
   4735 
   4736 let defaultUi = Uicommon.Graphic
   4737 
   4738 end (* module Body *)