(* Unison file synchronizer: src/uigtk2.ml *)
(* Copyright 1999-2018, Benjamin C. Pierce
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*)
open Common
open Lwt
module Private = struct
let debug = Trace.debug "ui"
let myNameCapitalized = String.capitalize Uutil.myName
(**********************************************************************
LOW-LEVEL STUFF
**********************************************************************)
(**********************************************************************
Some message strings (build them here because they look ugly in the
middle of other code.
**********************************************************************)
let tryAgainMessage =
Printf.sprintf
"You can use %s to synchronize a local directory with another local directory,
or with a remote directory.
Please enter the first (local) directory that you want to synchronize."
myNameCapitalized
(* ---- *)
let helpmessage = Printf.sprintf
"%s can synchronize a local directory with another local directory, or with
a directory on a remote machine.
To synchronize with a local directory, just enter the file name.
To synchronize with a remote directory, you must first choose a protocol
that %s will use to connect to the remote machine. Each protocol has
different requirements:
1) To synchronize using SSH, there must be an SSH client installed on
this machine and an SSH server installed on the remote machine. You
must enter the host to connect to, a user name (if different from
your user name on this machine), and the directory on the remote machine
(relative to your home directory on that machine).
2) To synchronize using RSH, there must be an RSH client installed on
this machine and an RSH server installed on the remote machine. You
must enter the host to connect to, a user name (if different from
your user name on this machine), and the directory on the remote machine
(relative to your home directory on that machine).
3) To synchronize using %s's socket protocol, there must be a %s
server running on the remote machine, listening to the port that you
specify here. (Use \"%s -socket xxx\" on the remote machine to
start the %s server.) You must enter the host, port, and the directory
on the remote machine (relative to the working directory of the
%s server running on that machine)."
myNameCapitalized myNameCapitalized myNameCapitalized myNameCapitalized myNameCapitalized myNameCapitalized myNameCapitalized
(**********************************************************************
Font preferences
**********************************************************************)
let fontMonospace = lazy (Pango.Font.from_string "monospace")
let fontBold = lazy (Pango.Font.from_string "bold")
let fontItalic = lazy (Pango.Font.from_string "italic")
(**********************************************************************
Unison icon
**********************************************************************)
(* This does not work with the current version of Lablgtk, due to a bug
let icon =
GdkPixbuf.from_data ~width:48 ~height:48 ~has_alpha:true
(Gpointer.region_of_bytes Pixmaps.icon_data)
*)
let icon =
let p = GdkPixbuf.create ~width:48 ~height:48 ~has_alpha:true () in
Gpointer.blit
(Gpointer.region_of_bytes Pixmaps.icon_data) (GdkPixbuf.get_pixels p);
p
let leftPtrWatch =
lazy
(let bitmap =
Gdk.Bitmap.create_from_data
~width:32 ~height:32 Pixmaps.left_ptr_watch
in
let color =
Gdk.Color.alloc ~colormap:(Gdk.Color.get_system_colormap ()) `BLACK in
Gdk.Cursor.create_from_pixmap
(bitmap :> Gdk.pixmap) ~mask:bitmap ~fg:color ~bg:color ~x:2 ~y:2)
let make_busy w =
if Util.osType <> `Win32 then
Gdk.Window.set_cursor w#misc#window (Lazy.force leftPtrWatch)
let make_interactive w =
if Util.osType <> `Win32 then
(* HACK: setting the cursor to NULL restore the default cursor *)
Gdk.Window.set_cursor w#misc#window (Obj.magic Gpointer.boxed_null)
(*********************************************************************
UI state variables
*********************************************************************)
type stateItem = { mutable ri : reconItem;
mutable bytesTransferred : Uutil.Filesize.t;
mutable bytesToTransfer : Uutil.Filesize.t;
mutable whatHappened : (Util.confirmation * string option) option}
let theState = ref [||]
let unsynchronizedPaths = ref None
module IntSet = Set.Make (struct type t = int let compare = compare end)
let current = ref IntSet.empty
let currentRow () =
if IntSet.cardinal !current = 1 then Some (IntSet.choose !current) else None
(* ---- *)
let theToplevelWindow = ref None
let setToplevelWindow w = theToplevelWindow := Some w
let toplevelWindow () =
match !theToplevelWindow with
Some w -> w
| None -> assert false
(*********************************************************************
Lock management
*********************************************************************)
let busy = ref false
let getLock f =
if !busy then
Trace.status "Synchronizer is busy, please wait.."
else begin
busy := true; f (); busy := false
end
(**********************************************************************
Miscellaneous
**********************************************************************)
let sync_action = ref None
let last = ref (0.)
let gtk_sync forced =
let t = Unix.gettimeofday () in
if !last = 0. || forced || t -. !last > 0.05 then begin
last := t;
begin match !sync_action with
Some f -> f ()
| None -> ()
end;
while Glib.Main.iteration false do () done
end
(**********************************************************************
CHARACTER SET TRANSCODING
***********************************************************************)
(* Transcodage from Microsoft Windows Codepage 1252 to Unicode *)
(* Unison currently uses the "ASCII" Windows filesystem API. With
this API, filenames are encoded using a proprietary character
encoding. This encoding depends on the Windows setup, but in
Western Europe, the Windows Codepage 1252 is usually used.
GTK, on the other hand, uses the UTF-8 encoding. This code perform
the translation from Codepage 1252 to UTF-8. A call to [transcode]
should be wrapped around every string below that might contain
non-ASCII characters. *)
let code =
[| 0x0020; 0x0001; 0x0002; 0x0003; 0x0004; 0x0005; 0x0006; 0x0007;
0x0008; 0x0009; 0x000A; 0x000B; 0x000C; 0x000D; 0x000E; 0x000F;
0x0010; 0x0011; 0x0012; 0x0013; 0x0014; 0x0015; 0x0016; 0x0017;
0x0018; 0x0019; 0x001A; 0x001B; 0x001C; 0x001D; 0x001E; 0x001F;
0x0020; 0x0021; 0x0022; 0x0023; 0x0024; 0x0025; 0x0026; 0x0027;
0x0028; 0x0029; 0x002A; 0x002B; 0x002C; 0x002D; 0x002E; 0x002F;
0x0030; 0x0031; 0x0032; 0x0033; 0x0034; 0x0035; 0x0036; 0x0037;
0x0038; 0x0039; 0x003A; 0x003B; 0x003C; 0x003D; 0x003E; 0x003F;
0x0040; 0x0041; 0x0042; 0x0043; 0x0044; 0x0045; 0x0046; 0x0047;
0x0048; 0x0049; 0x004A; 0x004B; 0x004C; 0x004D; 0x004E; 0x004F;
0x0050; 0x0051; 0x0052; 0x0053; 0x0054; 0x0055; 0x0056; 0x0057;
0x0058; 0x0059; 0x005A; 0x005B; 0x005C; 0x005D; 0x005E; 0x005F;
0x0060; 0x0061; 0x0062; 0x0063; 0x0064; 0x0065; 0x0066; 0x0067;
0x0068; 0x0069; 0x006A; 0x006B; 0x006C; 0x006D; 0x006E; 0x006F;
0x0070; 0x0071; 0x0072; 0x0073; 0x0074; 0x0075; 0x0076; 0x0077;
0x0078; 0x0079; 0x007A; 0x007B; 0x007C; 0x007D; 0x007E; 0x007F;
0x20AC; 0x1234; 0x201A; 0x0192; 0x201E; 0x2026; 0x2020; 0x2021;
0x02C6; 0x2030; 0x0160; 0x2039; 0x0152; 0x1234; 0x017D; 0x1234;
0x1234; 0x2018; 0x2019; 0x201C; 0x201D; 0x2022; 0x2013; 0x2016;
0x02DC; 0x2122; 0x0161; 0x203A; 0x0153; 0x1234; 0x017E; 0x0178;
0x00A0; 0x00A1; 0x00A2; 0x00A3; 0x00A4; 0x00A5; 0x00A6; 0x00A7;
0x00A8; 0x00A9; 0x00AA; 0x00AB; 0x00AC; 0x00AD; 0x00AE; 0x00AF;
0x00B0; 0x00B1; 0x00B2; 0x00B3; 0x00B4; 0x00B5; 0x00B6; 0x00B7;
0x00B8; 0x00B9; 0x00BA; 0x00BB; 0x00BC; 0x00BD; 0x00BE; 0x00BF;
0x00C0; 0x00C1; 0x00C2; 0x00C3; 0x00C4; 0x00C5; 0x00C6; 0x00C7;
0x00C8; 0x00C9; 0x00CA; 0x00CB; 0x00CC; 0x00CD; 0x00CE; 0x00CF;
0x00D0; 0x00D1; 0x00D2; 0x00D3; 0x00D4; 0x00D5; 0x00D6; 0x00D7;
0x00D8; 0x00D9; 0x00DA; 0x00DB; 0x00DC; 0x00DD; 0x00DE; 0x00DF;
0x00E0; 0x00E1; 0x00E2; 0x00E3; 0x00E4; 0x00E5; 0x00E6; 0x00E7;
0x00E8; 0x00E9; 0x00EA; 0x00EB; 0x00EC; 0x00ED; 0x00EE; 0x00EF;
0x00F0; 0x00F1; 0x00F2; 0x00F3; 0x00F4; 0x00F5; 0x00F6; 0x00F7;
0x00F8; 0x00F9; 0x00FA; 0x00FB; 0x00FC; 0x00FD; 0x00FE; 0x00FF |]
let rec transcodeRec buf s i l =
if i < l then begin
let c = code.(Char.code s.[i]) in
if c < 0x80 then
Buffer.add_char buf (Char.chr c)
else if c < 0x800 then begin
Buffer.add_char buf (Char.chr (c lsr 6 + 0xC0));
Buffer.add_char buf (Char.chr (c land 0x3f + 0x80))
end else if c < 0x10000 then begin
Buffer.add_char buf (Char.chr (c lsr 12 + 0xE0));
Buffer.add_char buf (Char.chr ((c lsr 6) land 0x3f + 0x80));
Buffer.add_char buf (Char.chr (c land 0x3f + 0x80))
end;
transcodeRec buf s (i + 1) l
end
let transcodeDoc s =
let buf = Buffer.create 1024 in
transcodeRec buf s 0 (String.length s);
Buffer.contents buf
(****)
let escapeMarkup s = Glib.Markup.escape_text s
let transcodeFilename s =
if Prefs.read Case.unicodeEncoding then
Unicode.protect s
else if Util.osType = `Win32 then transcodeDoc s else
try
Glib.Convert.filename_to_utf8 s
with Glib.Convert.Error _ ->
Unicode.protect s
let transcode s =
if Prefs.read Case.unicodeEncoding then
Unicode.protect s
else
try
Glib.Convert.locale_to_utf8 s
with Glib.Convert.Error _ ->
Unicode.protect s
(**********************************************************************
USEFUL LOW-LEVEL WIDGETS
**********************************************************************)
class scrolled_text ?editable ?shadow_type ?word_wrap
~width ~height ?packing ?show
() =
let sw =
GBin.scrolled_window ?packing ~show:false
?shadow_type ~hpolicy:`NEVER ~vpolicy:`AUTOMATIC ()
in
let text = GText.view ?editable ~wrap_mode:`WORD ~packing:sw#add () in
object
inherit GObj.widget_full sw#as_widget
method text = text
method insert s = text#buffer#set_text s;
method show () = sw#misc#show ()
initializer
text#misc#set_size_chars ~height ~width ();
if show <> Some false then sw#misc#show ()
end
(* ------ *)
(* Display a message in a window and wait for the user
to hit the button. *)
let okBox ~parent ~title ~typ ~message =
let t =
GWindow.message_dialog
~parent ~title ~message_type:typ ~message ~modal:true
~buttons:GWindow.Buttons.ok () in
ignore (t#run ()); t#destroy ()
(* ------ *)
let primaryText msg =
Printf.sprintf "%s"
(escapeMarkup msg)
(* twoBox: Display a message in a window and wait for the user
to hit one of two buttons. Return true if the first button is
chosen, false if the second button is chosen. *)
let twoBox ?(kind=`DIALOG_WARNING) ~parent ~title ~astock ~bstock message =
let t =
GWindow.dialog ~parent ~border_width:6 ~modal:true ~no_separator:true
~allow_grow:false () in
t#vbox#set_spacing 12;
let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
ignore (GMisc.image ~stock:kind ~icon_size:`DIALOG
~yalign:0. ~packing:h1#pack ());
let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
ignore (GMisc.label
~markup:(primaryText title ^ "\n\n" ^ escapeMarkup message)
~selectable:true ~yalign:0. ~packing:v1#add ());
t#add_button_stock bstock `NO;
t#add_button_stock astock `YES;
t#set_default_response `NO;
t#show();
let res = t#run () in
t#destroy ();
res = `YES
(* ------ *)
(* Avoid recursive invocations of the function below (a window receives
delete events even when it is not sensitive) *)
let inExit = ref false
let doExit () = Lwt_unix.run (Update.unlockArchives ()); exit 0
let safeExit () =
if not !inExit then begin
inExit := true;
if not !busy then exit 0 else
if twoBox ~parent:(toplevelWindow ()) ~title:"Premature exit"
~astock:`YES ~bstock:`NO
"Unison is working, exit anyway ?"
then exit 0;
inExit := false
end
(* ------ *)
(* warnBox: Display a warning message in a window and wait (unless
we're in batch mode) for the user to hit "OK" or "Exit". *)
let warnBox ~parent title message =
let message = transcode message in
if Prefs.read Globals.batch then begin
(* In batch mode, just pop up a window and go ahead *)
let t =
GWindow.dialog ~parent
~border_width:6 ~modal:true ~no_separator:true ~allow_grow:false () in
t#vbox#set_spacing 12;
let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
ignore (GMisc.image ~stock:`DIALOG_INFO ~icon_size:`DIALOG
~yalign:0. ~packing:h1#pack ());
let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
ignore (GMisc.label ~markup:(primaryText title ^ "\n\n" ^
escapeMarkup message)
~selectable:true ~yalign:0. ~packing:v1#add ());
t#add_button_stock `CLOSE `CLOSE;
t#set_default_response `CLOSE;
ignore (t#connect#response ~callback:(fun _ -> t#destroy ()));
t#show ()
end else begin
inExit := true;
let ok =
twoBox ~parent:(toplevelWindow ()) ~title ~astock:`OK ~bstock:`QUIT
message in
if not(ok) then doExit ();
inExit := false
end
(****)
let accel_paths = Hashtbl.create 17
let underscore_re = Str.regexp_string "_"
class ['a] gMenuFactory
?(accel_group=GtkData.AccelGroup.create ())
?(accel_path="/")
?(accel_modi=[`CONTROL])
?(accel_flags=[`VISIBLE]) (menu_shell : 'a) =
object (self)
val menu_shell : #GMenu.menu_shell = menu_shell
val group = accel_group
val m = accel_modi
val flags = (accel_flags:Gtk.Tags.accel_flag list)
val accel_path = accel_path
method menu = menu_shell
method accel_group = group
method accel_path = accel_path
method private bind
?(modi=m) ?key ?callback label ?(name=label) (item : GMenu.menu_item) =
menu_shell#append item;
let accel_path = accel_path ^ name in
let accel_path = Str.global_replace underscore_re "" accel_path in
(* Default accel path value *)
if not (Hashtbl.mem accel_paths accel_path) then begin
Hashtbl.add accel_paths accel_path ();
GtkData.AccelMap.add_entry accel_path ?key ~modi
end;
(* Register this accel path *)
GtkBase.Widget.set_accel_path item#as_widget accel_path accel_group;
Gaux.may callback ~f:(fun callback -> item#connect#activate ~callback)
method add_item ?key ?modi ?callback ?submenu label =
let item = GMenu.menu_item ~use_mnemonic:true ~label () in
self#bind ?modi ?key ?callback label item;
Gaux.may (submenu : GMenu.menu option) ~f:item#set_submenu;
item
method add_image_item ?(image : GObj.widget option)
?modi ?key ?callback ?stock ?name label =
let item =
GMenu.image_menu_item ~use_mnemonic:true ?image ~label ?stock () in
match stock with
| None ->
self#bind ?modi ?key ?callback label ?name
(item : GMenu.image_menu_item :> GMenu.menu_item);
item
| Some s ->
try
let st = GtkStock.Item.lookup s in
self#bind
?modi ?key:(if st.GtkStock.keyval=0 then key else None)
?callback label ?name
(item : GMenu.image_menu_item :> GMenu.menu_item);
item
with Not_found -> item
method add_check_item ?active ?modi ?key ?callback label =
let item = GMenu.check_menu_item ~label ~use_mnemonic:true ?active () in
self#bind label ?modi ?key
?callback:(Gaux.may_map callback ~f:(fun f () -> f item#active))
(item : GMenu.check_menu_item :> GMenu.menu_item);
item
method add_separator () = GMenu.separator_item ~packing:menu_shell#append ()
method add_submenu label =
let item = GMenu.menu_item ~use_mnemonic:true ~label () in
self#bind label item;
(GMenu.menu ~packing:item#set_submenu (), item)
method replace_submenu (item : GMenu.menu_item) =
GMenu.menu ~packing:item#set_submenu ()
end
(**********************************************************************
HIGHER-LEVEL WIDGETS
***********************************************************************)
class stats width height =
let pixmap = GDraw.pixmap ~width ~height () in
let area =
pixmap#set_foreground `WHITE;
pixmap#rectangle ~filled:true ~x:0 ~y:0 ~width ~height ();
GMisc.pixmap pixmap ~width ~height ~xpad:4 ~ypad:8 ()
in
object (self)
inherit GObj.widget_full area#as_widget
val mutable maxim = ref 0.
val mutable scale = ref 1.
val mutable min_scale = 1.
val values = Array.make width 0.
val mutable active = false
method redraw () =
scale := min_scale;
while !maxim > !scale do
scale := !scale *. 1.5
done;
pixmap#set_foreground `WHITE;
pixmap#rectangle ~filled:true ~x:0 ~y:0 ~width ~height ();
pixmap#set_foreground `BLACK;
for i = 0 to width - 1 do
self#rect i values.(max 0 (i - 1)) values.(i)
done
method activate a = active <- a; if a then self#redraw ()
method scale h = truncate ((float height) *. h /. !scale)
method private rect i v' v =
let h = self#scale v in
let h' = self#scale v' in
let h1 = min h' h in
let h2 = max h' h in
pixmap#set_foreground `BLACK;
pixmap#rectangle
~filled:true ~x:i ~y:(height - h1) ~width:1 ~height:h1 ();
for h = h1 + 1 to h2 do
let v = truncate (65535. *. (float (h - h1) /. float (h2 - h1))) in
let v = (v / 4096) * 4096 in (* Only use 16 gray levels *)
pixmap#set_foreground (`RGB (v, v, v));
pixmap#rectangle
~filled:true ~x:i ~y:(height - h) ~width:1 ~height:1 ();
done
method push v =
let need_max = values.(0) = !maxim in
for i = 0 to width - 2 do
values.(i) <- values.(i + 1)
done;
values.(width - 1) <- v;
if need_max then begin
maxim := 0.;
for i = 0 to width - 1 do maxim := max !maxim values.(i) done
end else
maxim := max !maxim v;
if active then begin
let need_resize =
!maxim > !scale || (!maxim > min_scale && !maxim < !scale /. 1.5) in
if need_resize then
self#redraw ()
else begin
pixmap#put_pixmap ~x:0 ~y:0 ~xsrc:1 (pixmap#pixmap);
pixmap#set_foreground `WHITE;
pixmap#rectangle
~filled:true ~x:(width - 1) ~y:0 ~width:1 ~height ();
self#rect (width - 1) values.(width - 2) values.(width - 1)
end;
area#misc#draw None
end
end
let clientWritten = ref 0.
let serverWritten = ref 0.
let emitRate2 = ref 0.
let receiveRate2 = ref 0.
let rate2str v =
if v > 9.9e3 then begin
if v > 9.9e6 then
Format.sprintf "%1.0f MiB/s" (v /. 1e6)
else if v > 999e3 then
Format.sprintf "%1.1f MiB/s" (v /. 1e6)
else
Format.sprintf "%1.0f KiB/s" (v /. 1e3)
end else begin
if v > 990. then
Format.sprintf "%1.1f KiB/s" (v /. 1e3)
else if v > 99. then
Format.sprintf "%1.2f KiB/s" (v /. 1e3)
else
" "
end
let statistics () =
let title = "Statistics" in
let t = GWindow.dialog ~title () in
let t_dismiss = GButton.button ~stock:`CLOSE ~packing:t#action_area#add () in
t_dismiss#grab_default ();
let dismiss () = t#misc#hide () in
ignore (t_dismiss#connect#clicked ~callback:dismiss);
ignore (t#event#connect#delete ~callback:(fun _ -> dismiss (); true));
let emission = new stats 320 50 in
t#vbox#pack ~expand:false ~padding:4 (emission :> GObj.widget);
let reception = new stats 320 50 in
t#vbox#pack ~expand:false ~padding:4 (reception :> GObj.widget);
let lst =
GList.clist
~packing:(t#vbox#add)
~titles_active:false
~titles:[""; "Client"; "Server"; "Total"] ()
in
lst#set_column ~auto_resize:true 0;
lst#set_column ~auto_resize:true ~justification:`RIGHT 1;
lst#set_column ~auto_resize:true ~justification:`RIGHT 2;
lst#set_column ~auto_resize:true ~justification:`RIGHT 3;
ignore (lst#append ["Reception rate"]);
ignore (lst#append ["Data received"]);
ignore (lst#append ["File data written"]);
for r = 0 to 2 do
lst#set_row ~selectable:false r
done;
ignore (t#event#connect#map (fun _ ->
emission#activate true;
reception#activate true;
false));
ignore (t#event#connect#unmap (fun _ ->
emission#activate false;
reception#activate false;
false));
let delay = 0.5 in
let a = 0.5 in
let b = 0.8 in
let emittedBytes = ref 0. in
let emitRate = ref 0. in
let receivedBytes = ref 0. in
let receiveRate = ref 0. in
let stopCounter = ref 0 in
let updateTable () =
let kib2str v = Format.sprintf "%.0f B" v in
lst#set_cell ~text:(rate2str !receiveRate2) 0 1;
lst#set_cell ~text:(rate2str !emitRate2) 0 2;
lst#set_cell ~text:
(rate2str (!receiveRate2 +. !emitRate2)) 0 3;
lst#set_cell ~text:(kib2str !receivedBytes) 1 1;
lst#set_cell ~text:(kib2str !emittedBytes) 1 2;
lst#set_cell ~text:
(kib2str (!receivedBytes +. !emittedBytes)) 1 3;
lst#set_cell ~text:(kib2str !clientWritten) 2 1;
lst#set_cell ~text:(kib2str !serverWritten) 2 2;
lst#set_cell ~text:
(kib2str (!clientWritten +. !serverWritten)) 2 3
in
let timeout _ =
emitRate :=
a *. !emitRate +.
(1. -. a) *. (!Remote.emittedBytes -. !emittedBytes) /. delay;
emitRate2 :=
b *. !emitRate2 +.
(1. -. b) *. (!Remote.emittedBytes -. !emittedBytes) /. delay;
emission#push !emitRate;
receiveRate :=
a *. !receiveRate +.
(1. -. a) *. (!Remote.receivedBytes -. !receivedBytes) /. delay;
receiveRate2 :=
b *. !receiveRate2 +.
(1. -. b) *. (!Remote.receivedBytes -. !receivedBytes) /. delay;
reception#push !receiveRate;
emittedBytes := !Remote.emittedBytes;
receivedBytes := !Remote.receivedBytes;
if !stopCounter > 0 then decr stopCounter;
if !stopCounter = 0 then begin
emitRate2 := 0.; receiveRate2 := 0.;
end;
updateTable ();
!stopCounter <> 0
in
let startStats () =
if !stopCounter = 0 then begin
emittedBytes := !Remote.emittedBytes;
receivedBytes := !Remote.receivedBytes;
stopCounter := -1;
ignore (GMain.Timeout.add ~ms:(truncate (delay *. 1000.))
~callback:timeout)
end else
stopCounter := -1
in
let stopStats () = stopCounter := 10 in
(t, startStats, stopStats)
(****)
(* Standard file dialog *)
let file_dialog ~parent ~title ~callback ?filename () =
let sel = GWindow.file_selection ~parent ~title ~modal:true ?filename () in
ignore (sel#cancel_button#connect#clicked ~callback:sel#destroy);
ignore (sel#ok_button#connect#clicked ~callback:
(fun () ->
let name = sel#filename in
sel#destroy ();
callback name));
sel#show ();
ignore (sel#connect#destroy ~callback:GMain.Main.quit);
GMain.Main.main ()
(* ------ *)
let fatalError message =
Trace.log (message ^ "\n");
let title = "Fatal error" in
let t =
GWindow.dialog ~parent:(toplevelWindow ())
~border_width:6 ~modal:true ~no_separator:true ~allow_grow:false () in
t#vbox#set_spacing 12;
let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
ignore (GMisc.image ~stock:`DIALOG_ERROR ~icon_size:`DIALOG
~yalign:0. ~packing:h1#pack ());
let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
ignore (GMisc.label
~markup:(primaryText title ^ "\n\n" ^
escapeMarkup (transcode message))
~line_wrap:true ~selectable:true ~yalign:0. ~packing:v1#add ());
t#add_button_stock `QUIT `QUIT;
t#set_default_response `QUIT;
t#show(); ignore (t#run ()); t#destroy ();
exit 1
(* ------ *)
let tryAgainOrQuit = fatalError
(* ------ *)
let getFirstRoot () =
let t = GWindow.dialog ~parent:(toplevelWindow ()) ~title:"Root selection"
~modal:true ~allow_grow:true () in
t#misc#grab_focus ();
let hb = GPack.hbox
~packing:(t#vbox#pack ~expand:false ~padding:15) () in
ignore(GMisc.label ~text:tryAgainMessage
~justify:`LEFT
~packing:(hb#pack ~expand:false ~padding:15) ());
let f1 = GPack.hbox ~spacing:4
~packing:(t#vbox#pack ~expand:true ~padding:4) () in
ignore (GMisc.label ~text:"Dir:" ~packing:(f1#pack ~expand:false) ());
let fileE = GEdit.entry ~packing:f1#add () in
fileE#misc#grab_focus ();
let browseCommand() =
file_dialog ~parent:t ~title:"Select a local directory"
~callback:fileE#set_text ~filename:fileE#text () in
let b = GButton.button ~label:"Browse"
~packing:(f1#pack ~expand:false) () in
ignore (b#connect#clicked ~callback:browseCommand);
let f3 = t#action_area in
let result = ref None in
let contCommand() =
result := Some(fileE#text);
t#destroy () in
let quitButton = GButton.button ~stock:`QUIT ~packing:f3#add () in
ignore (quitButton#connect#clicked
~callback:(fun () -> result := None; t#destroy()));
let contButton = GButton.button ~stock:`OK ~packing:f3#add () in
ignore (contButton#connect#clicked ~callback:contCommand);
ignore (fileE#connect#activate ~callback:contCommand);
contButton#grab_default ();
t#show ();
ignore (t#connect#destroy ~callback:GMain.Main.quit);
GMain.Main.main ();
match !result with None -> None
| Some file ->
Some(Clroot.clroot2string(Clroot.ConnectLocal(Some file)))
(* ------ *)
let getSecondRoot () =
let t = GWindow.dialog ~parent:(toplevelWindow ()) ~title:"Root selection"
~modal:true ~allow_grow:true () in
t#misc#grab_focus ();
let message = "Please enter the second directory you want to synchronize." in
let vb = t#vbox in
let hb = GPack.hbox ~packing:(vb#pack ~expand:false ~padding:15) () in
ignore(GMisc.label ~text:message
~justify:`LEFT
~packing:(hb#pack ~expand:false ~padding:15) ());
let helpB = GButton.button ~stock:`HELP ~packing:hb#add () in
ignore (helpB#connect#clicked
~callback:(fun () -> okBox ~parent:t ~title:"Picking roots" ~typ:`INFO
~message:helpmessage));
let result = ref None in
let f = GPack.vbox ~packing:(vb#pack ~expand:false) () in
let f1 = GPack.hbox ~spacing:4 ~packing:f#add () in
ignore (GMisc.label ~text:"Directory:" ~packing:(f1#pack ~expand:false) ());
let fileE = GEdit.entry ~packing:f1#add () in
fileE#misc#grab_focus ();
let browseCommand() =
file_dialog ~parent:t ~title:"Select a local directory"
~callback:fileE#set_text ~filename:fileE#text () in
let b = GButton.button ~label:"Browse"
~packing:(f1#pack ~expand:false) () in
ignore (b#connect#clicked ~callback:browseCommand);
let f0 = GPack.hbox ~spacing:4 ~packing:f#add () in
let localB = GButton.radio_button ~packing:(f0#pack ~expand:false)
~label:"Local" () in
let sshB = GButton.radio_button ~group:localB#group
~packing:(f0#pack ~expand:false)
~label:"SSH" () in
let rshB = GButton.radio_button ~group:localB#group
~packing:(f0#pack ~expand:false) ~label:"RSH" () in
let socketB = GButton.radio_button ~group:sshB#group
~packing:(f0#pack ~expand:false) ~label:"Socket" () in
let f2 = GPack.hbox ~spacing:4 ~packing:f#add () in
ignore (GMisc.label ~text:"Host:" ~packing:(f2#pack ~expand:false) ());
let hostE = GEdit.entry ~packing:f2#add () in
ignore (GMisc.label ~text:"(Optional) User:"
~packing:(f2#pack ~expand:false) ());
let userE = GEdit.entry ~packing:f2#add () in
ignore (GMisc.label ~text:"Port:"
~packing:(f2#pack ~expand:false) ());
let portE = GEdit.entry ~packing:f2#add () in
let varLocalRemote = ref (`Local : [`Local|`SSH|`RSH|`SOCKET]) in
let localState() =
varLocalRemote := `Local;
hostE#misc#set_sensitive false;
userE#misc#set_sensitive false;
portE#misc#set_sensitive false;
b#misc#set_sensitive true in
let remoteState() =
hostE#misc#set_sensitive true;
b#misc#set_sensitive false;
match !varLocalRemote with
`SOCKET ->
(portE#misc#set_sensitive true; userE#misc#set_sensitive false)
| _ ->
(portE#misc#set_sensitive false; userE#misc#set_sensitive true) in
let protoState x =
varLocalRemote := x;
remoteState() in
ignore (localB#connect#clicked ~callback:localState);
ignore (sshB#connect#clicked ~callback:(fun () -> protoState(`SSH)));
ignore (rshB#connect#clicked ~callback:(fun () -> protoState(`RSH)));
ignore (socketB#connect#clicked ~callback:(fun () -> protoState(`SOCKET)));
localState();
let getRoot() =
let file = fileE#text in
let user = userE#text in
let host = hostE#text in
let port = portE#text in
match !varLocalRemote with
`Local ->
Clroot.clroot2string(Clroot.ConnectLocal(Some file))
| `SSH | `RSH ->
Clroot.clroot2string(
Clroot.ConnectByShell((if !varLocalRemote=`SSH then "ssh" else "rsh"),
host,
(if user="" then None else Some user),
(if port="" then None else Some port),
Some file))
| `SOCKET ->
Clroot.clroot2string(
(* FIX: report an error if the port entry is not well formed *)
Clroot.ConnectBySocket(host,
portE#text,
Some file)) in
let contCommand() =
try
let root = getRoot() in
result := Some root;
t#destroy ()
with Failure "int_of_string" ->
if portE#text="" then
okBox ~parent:t ~title:"Error" ~typ:`ERROR ~message:"Please enter a port"
else okBox ~parent:t ~title:"Error" ~typ:`ERROR
~message:"The port you specify must be an integer"
| _ ->
okBox ~parent:t ~title:"Error" ~typ:`ERROR
~message:"Something's wrong with the values you entered, try again" in
let f3 = t#action_area in
let quitButton =
GButton.button ~stock:`QUIT ~packing:f3#add () in
ignore (quitButton#connect#clicked ~callback:safeExit);
let contButton =
GButton.button ~stock:`OK ~packing:f3#add () in
ignore (contButton#connect#clicked ~callback:contCommand);
contButton#grab_default ();
ignore (fileE#connect#activate ~callback:contCommand);
t#show ();
ignore (t#connect#destroy ~callback:GMain.Main.quit);
GMain.Main.main ();
!result
(* ------ *)
let getPassword rootName msg =
let t =
GWindow.dialog ~parent:(toplevelWindow ())
~title:"Unison: SSH connection" ~position:`CENTER
~no_separator:true ~modal:true ~allow_grow:false ~border_width:6 () in
t#misc#grab_focus ();
t#vbox#set_spacing 12;
let header =
primaryText
(Format.sprintf "Connecting to '%s'..." (Unicode.protect rootName)) in
let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
ignore (GMisc.image ~stock:`DIALOG_AUTHENTICATION ~icon_size:`DIALOG
~yalign:0. ~packing:h1#pack ());
let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
ignore(GMisc.label ~markup:(header ^ "\n\n" ^
escapeMarkup (Unicode.protect msg))
~selectable:true ~yalign:0. ~packing:v1#pack ());
let passwordE = GEdit.entry ~packing:v1#pack ~visibility:false () in
passwordE#misc#grab_focus ();
t#add_button_stock `QUIT `QUIT;
t#add_button_stock `OK `OK;
t#set_default_response `OK;
ignore (passwordE#connect#activate ~callback:(fun _ -> t#response `OK));
t#show();
let res = t#run () in
let pwd = passwordE#text in
t#destroy ();
gtk_sync true;
begin match res with
`DELETE_EVENT | `QUIT -> safeExit (); ""
| `OK -> pwd
end
let termInteract = Some getPassword
(* ------ *)
type profileInfo = {roots:string list; label:string option}
(* ------ *)
let profileKeymap = Array.create 10 None
let provideProfileKey filename k profile info =
try
let i = int_of_string k in
if 0<=i && i<=9 then
match profileKeymap.(i) with
None -> profileKeymap.(i) <- Some(profile,info)
| Some(otherProfile,_) ->
raise (Util.Fatal
("Error scanning profile "^
System.fspathToPrintString filename ^":\n"
^ "shortcut key "^k^" is already bound to profile "
^ otherProfile))
else
raise (Util.Fatal
("Error scanning profile "^ System.fspathToPrintString filename ^":\n"
^ "Value of 'key' preference must be a single digit (0-9), "
^ "not " ^ k))
with Failure "int_of_string" -> raise (Util.Fatal
("Error scanning profile "^ System.fspathToPrintString filename ^":\n"
^ "Value of 'key' preference must be a single digit (0-9), "
^ "not " ^ k))
(* ------ *)
module React = struct
type 'a t = { mutable state : 'a; mutable observers : ('a -> unit) list }
let make v =
let res = { state = v; observers = [] } in
let update v =
if res.state <> v then begin
res.state <- v; List.iter (fun f -> f v) res.observers
end
in
(res, update)
let const v = fst (make v)
let add_observer x f = x.observers <- f :: x.observers
let state x = x.state
let lift f x =
let (res, update) = make (f (state x)) in
add_observer x (fun v -> update (f v));
res
let lift2 f x y =
let (res, update) = make (f (state x) (state y)) in
add_observer x (fun v -> update (f v (state y)));
add_observer y (fun v -> update (f (state x) v));
res
let lift3 f x y z =
let (res, update) = make (f (state x) (state y) (state z)) in
add_observer x (fun v -> update (f v (state y) (state z)));
add_observer y (fun v -> update (f (state x) v (state z)));
add_observer z (fun v -> update (f (state x) (state y) v));
res
let iter f x = f (state x); add_observer x f
type 'a event = { mutable ev_observers : ('a -> unit) list }
let make_event () =
let res = { ev_observers = [] } in
let trigger v = List.iter (fun f -> f v) res.ev_observers in
(res, trigger)
let add_ev_observer x f = x.ev_observers <- f :: x.ev_observers
let hold v e =
let (res, update) = make v in
add_ev_observer e update;
res
let iter_ev f e = add_ev_observer e f
let lift_ev f e =
let (res, trigger) = make_event () in
add_ev_observer e (fun x -> trigger (f x));
res
module Ops = struct
let (>>) x f = lift f x
let (>|) x f = iter f x
let (>>>) x f = lift_ev f x
let (>>|) x f = iter_ev f x
end
end
module GtkReact = struct
let entry (e : #GEdit.entry) =
let (res, update) = React.make e#text in
ignore (e#connect#changed ~callback:(fun () -> update (e#text)));
res
let text_combo ((c, _) : _ GEdit.text_combo) =
let (res, update) = React.make c#active in
ignore (c#connect#changed ~callback:(fun () -> update (c#active)));
res
let toggle_button (b : #GButton.toggle_button) =
let (res, update) = React.make b#active in
ignore (b#connect#toggled ~callback:(fun () -> update (b#active)));
res
let file_chooser (c : #GFile.chooser) =
let (res, update) = React.make c#filename in
ignore (c#connect#selection_changed
~callback:(fun () -> update (c#filename)));
res
let current_tree_view_selection (t : #GTree.view) =
let m =t#model in
List.map (fun p -> m#get_row_reference p) t#selection#get_selected_rows
let tree_view_selection_changed t =
let (res, trigger) = React.make_event () in
ignore (t#selection#connect#changed
~callback:(fun () -> trigger (current_tree_view_selection t)));
res
let tree_view_selection t =
React.hold (current_tree_view_selection t) (tree_view_selection_changed t)
let label (l : #GMisc.label) x = React.iter (fun v -> l#set_text v) x
let label_underlined (l : #GMisc.label) x =
React.iter (fun v -> l#set_text v; l#set_use_underline true) x
let label_markup (l : #GMisc.label) x =
React.iter (fun v -> l#set_text v; l#set_use_markup true) x
let show w x =
React.iter (fun b -> if b then w#misc#show () else w#misc#hide ()) x
let set_sensitive w x = React.iter (fun b -> w#misc#set_sensitive b) x
end
open React.Ops
(* ------ *)
(* Resize an object (typically, a label with line wrapping) so that it
use all its available space *)
let adjustSize (w : #GObj.widget) =
let notYet = ref true in
ignore
(w#misc#connect#size_allocate ~callback:(fun r ->
if !notYet then begin
notYet := false;
(* JV: I have no idea where the 12 comes from. Without it,
a window resize may happen. *)
w#misc#set_size_request ~width:(max 10 (r.Gtk.width - 12)) ()
end))
let createProfile parent =
let assistant = GAssistant.assistant ~modal:true () in
assistant#set_transient_for parent#as_window;
assistant#set_modal true;
assistant#set_title "Profile Creation";
let nonEmpty s = s <> "" in
(*
let integerRe =
Str.regexp "\\([+-]?[0-9]+\\|0o[0-7]+\\|0x[0-9a-zA-Z]+\\)" in
*)
let integerRe = Str.regexp "[0-9]+" in
let isInteger s =
Str.string_match integerRe s 0 && Str.matched_string s = s in
(* Introduction *)
let intro =
GMisc.label
~xpad:12 ~ypad:12
~text:"Welcome to the Unison Profile Creation Assistant.\n\n\
Click \"Forward\" to begin."
() in
ignore
(assistant#append_page
~title:"Profile Creation"
~page_type:`INTRO
~complete:true
intro#as_widget);
(* Profile name and description *)
let description = GPack.vbox ~border_width:12 ~spacing:6 () in
adjustSize
(GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
~text:"Please enter the name of the profile and \
possibly a short description."
~packing:(description#pack ~expand:false) ());
let tbl =
let al = GBin.alignment ~packing:(description#pack ~expand:false) () in
al#set_left_padding 12;
GPack.table ~rows:2 ~columns:2 ~col_spacings:12 ~row_spacings:6
~packing:(al#add) () in
let nameEntry =
GEdit.entry ~activates_default:true
~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X) () in
let name = GtkReact.entry nameEntry in
ignore (GMisc.label ~text:"Profile _name:" ~xalign:0.
~use_underline:true ~mnemonic_widget:nameEntry
~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
let labelEntry =
GEdit.entry ~activates_default:true
~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) () in
let label = GtkReact.entry labelEntry in
ignore (GMisc.label ~text:"_Description:" ~xalign:0.
~use_underline:true ~mnemonic_widget:labelEntry
~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
let existingProfileLabel =
GMisc.label ~xalign:1. ~packing:(description#pack ~expand:false) ()
in
adjustSize existingProfileLabel;
GtkReact.label_markup existingProfileLabel
(name >> fun s -> Format.sprintf " Profile %s already exists."
(escapeMarkup s));
let profileExists =
name >> fun s -> s <> "" && System.file_exists (Prefs.profilePathname s)
in
GtkReact.show existingProfileLabel profileExists;
ignore
(assistant#append_page
~title:"Profile Description"
~page_type:`CONTENT
description#as_widget);
let setPageComplete page b = assistant#set_page_complete page#as_widget b in
React.lift2 (&&) (name >> nonEmpty) (profileExists >> not)
>| setPageComplete description;
let connection = GPack.vbox ~border_width:12 ~spacing:18 () in
let al = GBin.alignment ~packing:(connection#pack ~expand:false) () in
al#set_left_padding 12;
let vb =
GPack.vbox ~spacing:6 ~packing:(al#add) () in
adjustSize
(GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
~text:"You can use Unison to synchronize a local directory \
with another local directory, or with a remote directory."
~packing:(vb#pack ~expand:false) ());
adjustSize
(GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
~text:"Please select the kind of synchronization \
you want to perform."
~packing:(vb#pack ~expand:false) ());
let tbl =
let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
al#set_left_padding 12;
GPack.table ~rows:2 ~columns:2 ~col_spacings:12 ~row_spacings:6
~packing:(al#add) () in
ignore (GMisc.label ~text:"Description:" ~xalign:0. ~yalign:0.
~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
let kindCombo =
let al =
GBin.alignment ~xscale:0. ~xalign:0.
~packing:(tbl#attach ~left:1 ~top:0) () in
GEdit.combo_box_text
~strings:["Local"; "Using SSH"; "Using RSH";
"Through a plain TCP connection"]
~active:0 ~packing:(al#add) ()
in
ignore (GMisc.label ~text:"Synchronization _kind:" ~xalign:0.
~use_underline:true ~mnemonic_widget:(fst kindCombo)
~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
let kind =
GtkReact.text_combo kindCombo
>> fun i -> List.nth [`Local; `SSH; `RSH; `SOCKET] i
in
let isLocal = kind >> fun k -> k = `Local in
let isSSH = kind >> fun k -> k = `SSH in
let isSocket = kind >> fun k -> k = `SOCKET in
let descrLabel =
GMisc.label ~xalign:0. ~line_wrap:true
~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) ()
in
adjustSize descrLabel;
GtkReact.label descrLabel
(kind >> fun k ->
match k with
`Local ->
"Local synchronization."
| `SSH ->
"This is the recommended way to synchronize \
with a remote machine. A\xc2\xa0remote instance of Unison is \
automatically started via SSH."
| `RSH ->
"Synchronization with a remote machine by starting \
automatically a remote instance of Unison via RSH."
| `SOCKET ->
"Synchronization with a remote machine by connecting \
to an instance of Unison already listening \
on a specific TCP port.");
let vb = GPack.vbox ~spacing:6 ~packing:(connection#add) () in
GtkReact.show vb (isLocal >> not);
ignore (GMisc.label ~markup:"Configuration" ~xalign:0.
~packing:(vb#pack ~expand:false) ());
let al = GBin.alignment ~packing:(vb#add) () in
al#set_left_padding 12;
let vb = GPack.vbox ~spacing:6 ~packing:(al#add) () in
let requirementLabel =
GMisc.label ~xalign:0. ~line_wrap:true
~packing:(vb#pack ~expand:false) ()
in
adjustSize requirementLabel;
GtkReact.label requirementLabel
(kind >> fun k ->
match k with
`Local ->
""
| `SSH ->
"There must be an SSH client installed on this machine, \
and Unison and an SSH server installed on the remote machine."
| `RSH ->
"There must be an RSH client installed on this machine, \
and Unison and an RSH server installed on the remote machine."
| `SOCKET ->
"There must be a Unison server running on the remote machine, \
listening on the port that you specify here. \
(Use \"Unison -socket xxx\" on the remote machine to start \
the Unison server.)");
let connDescLabel =
GMisc.label ~xalign:0. ~line_wrap:true
~packing:(vb#pack ~expand:false) ()
in
adjustSize connDescLabel;
GtkReact.label connDescLabel
(kind >> fun k ->
match k with
`Local -> ""
| `SSH -> "Please enter the host to connect to and a user name, \
if different from your user name on this machine."
| `RSH -> "Please enter the host to connect to and a user name, \
if different from your user name on this machine."
| `SOCKET -> "Please enter the host and port to connect to.");
let tbl =
let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
al#set_left_padding 12;
GPack.table ~rows:2 ~columns:2 ~col_spacings:12 ~row_spacings:6
~packing:(al#add) () in
let hostEntry =
GEdit.entry ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X) () in
let host = GtkReact.entry hostEntry in
ignore (GMisc.label ~text:"_Host:" ~xalign:0.
~use_underline:true ~mnemonic_widget:hostEntry
~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
let userEntry =
GEdit.entry ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) ()
in
GtkReact.show userEntry (isSocket >> not);
let user = GtkReact.entry userEntry in
GtkReact.show
(GMisc.label ~text:"_User:" ~xalign:0. ~yalign:0.
~use_underline:true ~mnemonic_widget:userEntry
~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ())
(isSocket >> not);
let portEntry =
GEdit.entry ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) ()
in
GtkReact.show portEntry isSocket;
let port = GtkReact.entry portEntry in
GtkReact.show
(GMisc.label ~text:"_Port:" ~xalign:0. ~yalign:0.
~use_underline:true ~mnemonic_widget:portEntry
~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ())
isSocket;
let compressLabel =
GMisc.label ~xalign:0. ~line_wrap:true
~text:"Data compression can greatly improve performance \
on slow connections. However, it may slow down \
things on (fast) local networks."
~packing:(vb#pack ~expand:false) ()
in
adjustSize compressLabel;
GtkReact.show compressLabel isSSH;
let compressButton =
let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
al#set_left_padding 12;
(GButton.check_button ~label:"Enable _compression" ~use_mnemonic:true
~active:true ~packing:(al#add) ())
in
GtkReact.show compressButton isSSH;
let compress = GtkReact.toggle_button compressButton in
(*XXX Disabled for now... *)
(*
adjustSize
(GMisc.label ~xalign:0. ~line_wrap:true
~text:"If this is possible, it is recommended that Unison \
attempts to connect immediately to the remote machine, \
so that it can perform some auto-detections."
~packing:(vb#pack ~expand:false) ());
let connectImmediately =
let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
al#set_left_padding 12;
GtkReact.toggle_button
(GButton.check_button ~label:"Connect _immediately" ~use_mnemonic:true
~active:true ~packing:(al#add) ())
in
let connectImmediately =
React.lift2 (&&) connectImmediately (isLocal >> not) in
*)
let pageComplete =
React.lift2 (||) isLocal
(React.lift2 (&&) (host >> nonEmpty)
(React.lift2 (||) (isSocket >> not) (port >> isInteger)))
in
ignore
(assistant#append_page
~title:"Connection Setup"
~page_type:`CONTENT
connection#as_widget);
pageComplete >| setPageComplete connection;
(* Connection to server *)
(*XXX Disabled for now... Fill in this page
let connectionInProgress = GMisc.label ~text:"..." () in
let p =
assistant#append_page
~title:"Connecting to Server..."
~page_type:`PROGRESS
connectionInProgress#as_widget
in
ignore
(assistant#connect#prepare (fun () ->
if assistant#current_page = p then begin
if React.state connectImmediately then begin
(* XXXX start connection... *)
assistant#set_page_complete connectionInProgress#as_widget true
end else
assistant#set_current_page (p + 1)
end));
*)
(* Directory selection *)
let directorySelection = GPack.vbox ~border_width:12 ~spacing:6 () in
adjustSize
(GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
~text:"Please select the two directories that you want to synchronize."
~packing:(directorySelection#pack ~expand:false) ());
let secondDirLabel1 =
GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
~text:"The second directory is relative to your home \
directory on the remote machine."
~packing:(directorySelection#pack ~expand:false) ()
in
adjustSize secondDirLabel1;
GtkReact.show secondDirLabel1 ((React.lift2 (||) isLocal isSocket) >> not);
let secondDirLabel2 =
GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
~text:"The second directory is relative to \
the working directory of the Unison server \
running on the remote machine."
~packing:(directorySelection#pack ~expand:false) ()
in
adjustSize secondDirLabel2;
GtkReact.show secondDirLabel2 isSocket;
let tbl =
let al =
GBin.alignment ~packing:(directorySelection#pack ~expand:false) () in
al#set_left_padding 12;
GPack.table ~rows:2 ~columns:2 ~col_spacings:12 ~row_spacings:6
~packing:(al#add) () in
(*XXX Should focus on this button when becomes visible... *)
let firstDirButton =
GFile.chooser_button ~action:`SELECT_FOLDER ~title:"First Directory"
~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X) ()
in
isLocal >| (fun b -> firstDirButton#set_title
(if b then "First Directory" else "Local Directory"));
GtkReact.label_underlined
(GMisc.label ~xalign:0.
~mnemonic_widget:firstDirButton
~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ())
(isLocal >> fun b ->
if b then "_First directory:" else "_Local directory:");
let noneToEmpty o = match o with None -> "" | Some s -> s in
let firstDir = GtkReact.file_chooser firstDirButton >> noneToEmpty in
let secondDirButton =
GFile.chooser_button ~action:`SELECT_FOLDER ~title:"Second Directory"
~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) () in
let secondDirLabel =
GMisc.label ~xalign:0.
~text:"Se_cond directory:"
~use_underline:true ~mnemonic_widget:secondDirButton
~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) () in
GtkReact.show secondDirButton isLocal;
GtkReact.show secondDirLabel isLocal;
let remoteDirEdit =
GEdit.entry ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X) ()
in
let remoteDirLabel =
GMisc.label ~xalign:0.
~text:"_Remote directory:"
~use_underline:true ~mnemonic_widget:remoteDirEdit
~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ()
in
GtkReact.show remoteDirEdit (isLocal >> not);
GtkReact.show remoteDirLabel (isLocal >> not);
let secondDir =
React.lift3 (fun b l r -> if b then l else r) isLocal
(GtkReact.file_chooser secondDirButton >> noneToEmpty)
(GtkReact.entry remoteDirEdit)
in
ignore
(assistant#append_page
~title:"Directory Selection"
~page_type:`CONTENT
directorySelection#as_widget);
React.lift2 (||) (isLocal >> not) (React.lift2 (<>) firstDir secondDir)
>| setPageComplete directorySelection;
(* Specific options *)
let options = GPack.vbox ~border_width:18 ~spacing:12 () in
(* Do we need to set specific options for FAT partitions?
If under Windows, then all the options are set properly, except for
ignoreinodenumbers in case one replica is on a FAT partition on a
remote non-Windows machine. As this is unlikely, we do not
handle this case. *)
let fat =
if Util.osType = `Win32 then
React.const false
else begin
let vb =
GPack.vbox ~spacing:6 ~packing:(options#pack ~expand:false) () in
let fatLabel =
GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
~text:"Select the following option if one of your \
directory is on a FAT partition. This is typically \
the case for a USB stick."
~packing:(vb#pack ~expand:false) ()
in
adjustSize fatLabel;
let fatButton =
let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
al#set_left_padding 12;
(GButton.check_button
~label:"Synchronization involving a _FAT partition"
~use_mnemonic:true ~active:false ~packing:(al#add) ())
in
GtkReact.toggle_button fatButton
end
in
(* Fastcheck is safe except on FAT partitions and on Windows when
not in Unicode mode where there is a very slight chance of
missing an update when a file is moved onto another with the same
modification time. Nowadays, FAT is rarely used on working
partitions. In most cases, we should be in Unicode mode.
Thus, it seems sensible to always enable fastcheck. *)
(*
let fastcheck = isLocal >> not >> (fun b -> b || Util.osType = `Win32) in
*)
(* Unicode mode can be problematic when the source machine is under
Windows and the remote machine is not, as Unison may have already
been used using the legacy Latin 1 encoding. Cygwin also did not
handle Unicode before version 1.7. *)
let vb = GPack.vbox ~spacing:6 ~packing:(options#pack ~expand:false) () in
let askUnicode = React.const false in
(* isLocal >> not >> fun b -> (b || Util.isCygwin) && Util.osType = `Win32 in*)
GtkReact.show vb askUnicode;
adjustSize
(GMisc.label ~xalign:0. ~line_wrap:true ~justify:`LEFT
~text:"When synchronizing in case insensitive mode, \
Unison has to make some assumptions regarding \
filename encoding. If ensure, use Unicode."
~packing:(vb#pack ~expand:false) ());
let vb =
let al = GBin.alignment
~xscale:0. ~xalign:0. ~packing:(vb#pack ~expand:false) () in
al#set_left_padding 12;
GPack.vbox ~spacing:0 ~packing:(al#add) ()
in
ignore
(GMisc.label ~xalign:0. ~text:"Filename encoding:"
~packing:(vb#pack ~expand:false) ());
let hb =
let al = GBin.alignment
~xscale:0. ~xalign:0. ~packing:(vb#pack ~expand:false) () in
al#set_left_padding 12;
GPack.button_box `VERTICAL ~layout:`START
~spacing:0 ~packing:(al#add) ()
in
let unicodeButton =
GButton.radio_button ~label:"_Unicode" ~use_mnemonic:true ~active:true
~packing:(hb#add) ()
in
ignore
(GButton.radio_button ~label:"_Latin 1" ~use_mnemonic:true
~group:unicodeButton#group ~packing:(hb#add) ());
(*
let unicode =
React.lift2 (||) (askUnicode >> not) (GtkReact.toggle_button unicodeButton)
in
*)
let p =
assistant#append_page
~title:"Specific Options" ~complete:true
~page_type:`CONTENT
options#as_widget
in
ignore
(assistant#connect#prepare (fun () ->
if assistant#current_page = p &&
not (Util.osType <> `Win32 || React.state askUnicode)
then
assistant#set_current_page (p + 1)));
let conclusion =
GMisc.label
~xpad:12 ~ypad:12
~text:"You have now finished filling in the profile.\n\n\
Click \"Apply\" to create it."
() in
ignore
(assistant#append_page
~title:"Done" ~complete:true
~page_type:`CONFIRM
conclusion#as_widget);
let profileName = ref None in
let saveProfile () =
let filename = Prefs.profilePathname (React.state name) in
begin try
let ch =
System.open_out_gen [Open_wronly; Open_creat; Open_excl] 0o600 filename
in
Printf.fprintf ch "# Unison preferences\n";
let label = React.state label in
if label <> "" then Printf.fprintf ch "label = %s\n" label;
Printf.fprintf ch "root = %s\n" (React.state firstDir);
let secondDir = React.state secondDir in
let host = React.state host in
let user = match React.state user with "" -> None | u -> Some u in
let secondRoot =
match React.state kind with
`Local -> Clroot.ConnectLocal (Some secondDir)
| `SSH -> Clroot.ConnectByShell
("ssh", host, user, None, Some secondDir)
| `RSH -> Clroot.ConnectByShell
("rsh", host, user, None, Some secondDir)
| `SOCKET -> Clroot.ConnectBySocket
(host, React.state port, Some secondDir)
in
Printf.fprintf ch "root = %s\n" (Clroot.clroot2string secondRoot);
if React.state compress && React.state kind = `SSH then
Printf.fprintf ch "sshargs = -C\n";
(*
if React.state fastcheck then
Printf.fprintf ch "fastcheck = true\n";
if React.state unicode then
Printf.fprintf ch "unicode = true\n";
*)
if React.state fat then Printf.fprintf ch "fat = true\n";
close_out ch;
profileName := Some (React.state name)
with Sys_error _ as e ->
okBox ~parent:assistant ~typ:`ERROR ~title:"Could not save profile"
~message:(Uicommon.exn2string e)
end;
assistant#destroy ();
in
ignore (assistant#connect#close ~callback:saveProfile);
ignore (assistant#connect#destroy ~callback:GMain.Main.quit);
ignore (assistant#connect#cancel ~callback:assistant#destroy);
assistant#show ();
GMain.Main.main ();
!profileName
(* ------ *)
let nameOfType t =
match t with
`BOOL -> "boolean"
| `BOOLDEF -> "boolean"
| `INT -> "integer"
| `STRING -> "text"
| `STRING_LIST -> "text list"
| `CUSTOM -> "custom"
| `UNKNOWN -> "unknown"
let defaultValue t =
match t with
`BOOL -> ["true"]
| `BOOLDEF -> ["true"]
| `INT -> ["0"]
| `STRING -> [""]
| `STRING_LIST -> []
| `CUSTOM -> []
| `UNKNOWN -> []
let editPreference parent nm ty vl =
let t =
GWindow.dialog ~parent ~border_width:12
~no_separator:true ~title:"Edit the Preference"
~modal:true () in
let vb = t#vbox in
vb#set_spacing 6;
let isList =
match ty with
`STRING_LIST | `CUSTOM | `UNKNOWN -> true
| _ -> false
in
let columns = if isList then 5 else 4 in
let rows = if isList then 3 else 2 in
let tbl =
GPack.table ~rows ~columns ~col_spacings:12 ~row_spacings:6
~packing:(vb#pack ~expand:false) () in
ignore (GMisc.label ~text:"Preference:" ~xalign:0.
~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
ignore (GMisc.label ~text:"Description:" ~xalign:0.
~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
ignore (GMisc.label ~text:"Type:" ~xalign:0.
~packing:(tbl#attach ~left:0 ~top:2 ~expand:`NONE) ());
ignore (GMisc.label ~text:(Unicode.protect nm) ~xalign:0. ~selectable:true ()
~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X));
let (doc, _, _) = Prefs.documentation nm in
ignore (GMisc.label ~text:doc ~xalign:0. ~selectable:true ()
~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X));
ignore (GMisc.label ~text:(nameOfType ty) ~xalign:0. ~selectable:true ()
~packing:(tbl#attach ~left:1 ~top:2 ~expand:`X));
let newValue =
if isList then begin
let valueLabel =
GMisc.label ~text:"V_alue:" ~use_underline:true ~xalign:0. ~yalign:0.
~packing:(tbl#attach ~left:0 ~top:3 ~expand:`NONE) ()
in
let cols = new GTree.column_list in
let c_value = cols#add Gobject.Data.string in
let c_ml = cols#add Gobject.Data.caml in
let lst_store = GTree.list_store cols in
let lst =
let sw =
GBin.scrolled_window ~packing:(tbl#attach ~left:1 ~top:3 ~expand:`X)
~shadow_type:`IN ~height:200 ~width:400
~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC () in
GTree.view ~model:lst_store ~headers_visible:false
~reorderable:true ~packing:sw#add () in
valueLabel#set_mnemonic_widget (Some (lst :> GObj.widget));
let column =
GTree.view_column
~renderer:(GTree.cell_renderer_text [], ["text", c_value]) ()
in
ignore (lst#append_column column);
let vb =
GPack.button_box
`VERTICAL ~layout:`START ~spacing:6
~packing:(tbl#attach ~left:2 ~top:3 ~expand:`NONE) ()
in
let selection = GtkReact.tree_view_selection lst in
let hasSel = selection >> fun l -> l <> [] in
let addB =
GButton.button ~stock:`ADD ~packing:(vb#pack ~expand:false) () in
let removeB =
GButton.button ~stock:`REMOVE ~packing:(vb#pack ~expand:false) () in
let editB =
GButton.button ~stock:`EDIT ~packing:(vb#pack ~expand:false) () in
let upB =
GButton.button ~stock:`GO_UP ~packing:(vb#pack ~expand:false) () in
let downB =
GButton.button ~stock:`GO_DOWN ~packing:(vb#pack ~expand:false) () in
List.iter (fun b -> b#set_xalign 0.) [addB; removeB; editB; upB; downB];
GtkReact.set_sensitive removeB hasSel;
let editLabel =
GMisc.label ~text:"Edited _item:"
~use_underline:true ~xalign:0.
~packing:(tbl#attach ~left:0 ~top:4 ~expand:`NONE) ()
in
let editEntry =
GEdit.entry ~packing:(tbl#attach ~left:1 ~top:4 ~expand:`X) () in
editLabel#set_mnemonic_widget (Some (editEntry :> GObj.widget));
let edit = GtkReact.entry editEntry in
let edited =
React.lift2
(fun l txt ->
match l with
[rf] -> lst_store#get ~row:rf#iter ~column:c_ml <> txt
| _ -> false)
selection edit
in
GtkReact.set_sensitive editB edited;
let selectionChange = GtkReact.tree_view_selection_changed lst in
selectionChange >>| (fun s ->
match s with
[rf] -> editEntry#set_text
(lst_store#get ~row:rf#iter ~column:c_value)
| _ -> ());
let add () =
let txt = editEntry#text in
let row = lst_store#append () in
lst_store#set ~row ~column:c_value txt;
lst_store#set ~row ~column:c_ml txt;
lst#selection#select_iter row;
lst#scroll_to_cell (lst_store#get_path row) column
in
ignore (addB#connect#clicked ~callback:add);
ignore (editEntry#connect#activate ~callback:add);
let remove () =
match React.state selection with
[rf] -> let i = rf#iter in
if lst_store#iter_next i then
lst#selection#select_iter i
else begin
let p = rf#path in
if GTree.Path.prev p then
lst#selection#select_path p
end;
ignore (lst_store#remove rf#iter)
| _ -> ()
in
ignore (removeB#connect#clicked ~callback:remove);
let edit () =
match React.state selection with
[rf] -> let row = rf#iter in
let txt = editEntry#text in
lst_store#set ~row ~column:c_value txt;
lst_store#set ~row ~column:c_ml txt
| _ -> ()
in
ignore (editB#connect#clicked ~callback:edit);
let updateUpDown l =
let (upS, downS) =
match l with
[rf] -> (GTree.Path.prev rf#path, lst_store#iter_next rf#iter)
| _ -> (false, false)
in
upB#misc#set_sensitive upS;
downB#misc#set_sensitive downS
in
selectionChange >>| updateUpDown;
ignore (lst_store#connect#after#row_deleted
~callback:(fun _ -> updateUpDown (React.state selection)));
let go_up () =
match React.state selection with
[rf] -> let p = rf#path in
if GTree.Path.prev p then begin
let i = rf#iter in
let i' = lst_store#get_iter p in
ignore (lst_store#swap i i');
lst#scroll_to_cell (lst_store#get_path i) column
end;
updateUpDown (React.state selection)
| _ -> ()
in
ignore (upB#connect#clicked ~callback:go_up);
let go_down () =
match React.state selection with
[rf] -> let i = rf#iter in
if lst_store#iter_next i then begin
let i' = rf#iter in
ignore (lst_store#swap i i');
lst#scroll_to_cell (lst_store#get_path i') column
end;
updateUpDown (React.state selection)
| _ -> ()
in
ignore (downB#connect#clicked ~callback:go_down);
List.iter
(fun v ->
let row = lst_store#append () in
lst_store#set ~row ~column:c_value (Unicode.protect v);
lst_store#set ~row ~column:c_ml v)
vl;
(fun () ->
let l = ref [] in
lst_store#foreach
(fun _ row -> l := lst_store#get ~row ~column:c_ml :: !l; false);
List.rev !l)
end else begin
let v = List.hd vl in
begin match ty with
`BOOL | `BOOLDEF ->
let hb =
GPack.button_box `HORIZONTAL ~layout:`START
~packing:(tbl#attach ~left:1 ~top:3 ~expand:`X) ()
in
let isTrue = v = "true" || v = "yes" in
let trueB =
GButton.radio_button ~label:"_True" ~use_mnemonic:true
~active:isTrue ~packing:(hb#add) ()
in
ignore
(GButton.radio_button ~label:"_False" ~use_mnemonic:true
~group:trueB#group ~active:(not isTrue) ~packing:(hb#add) ());
ignore
(GMisc.label ~text:"Value:" ~xalign:0.
~packing:(tbl#attach ~left:0 ~top:3 ~expand:`NONE) ());
(fun () -> [if trueB#active then "true" else "false"])
| `INT | `STRING ->
let valueEntry =
GEdit.entry ~text:(List.hd vl) ~width_chars: 40
~activates_default:true
~packing:(tbl#attach ~left:1 ~top:3 ~expand:`X) ()
in
ignore
(GMisc.label ~text:"V_alue:" ~use_underline:true ~xalign:0.
~mnemonic_widget:valueEntry
~packing:(tbl#attach ~left:0 ~top:3 ~expand:`NONE) ());
(fun () -> [valueEntry#text])
| `STRING_LIST | `CUSTOM | `UNKNOWN ->
assert false
end
end
in
let ok = ref false in
let cancelCommand () = t#destroy () in
let cancelButton =
GButton.button ~stock:`CANCEL ~packing:t#action_area#add () in
ignore (cancelButton#connect#clicked ~callback:cancelCommand);
let okCommand _ = ok := true; t#destroy () in
let okButton =
GButton.button ~stock:`OK ~packing:t#action_area#add () in
ignore (okButton#connect#clicked ~callback:okCommand);
okButton#grab_default ();
ignore (t#connect#destroy ~callback:GMain.Main.quit);
t#show ();
GMain.Main.main ();
if !ok then Some (newValue ()) else None
let markupRe = Str.regexp "<\\([a-z]+\\)>\\|\\([a-z]+\\)>\\|&\\([a-z]+\\);"
let entities =
[("amp", "&"); ("lt", "<"); ("gt", ">"); ("quot", "\""); ("apos", "'")]
let rec insertMarkupRec tags (t : #GText.view) s i tl =
try
let j = Str.search_forward markupRe s i in
if j > i then
t#buffer#insert ~tags:(List.flatten tl) (String.sub s i (j - i));
let tag = try Some (Str.matched_group 1 s) with Not_found -> None in
match tag with
Some tag ->
insertMarkupRec tags t s (Str.group_end 0)
((try [List.assoc tag tags] with Not_found -> []) :: tl)
| None ->
let entity = try Some (Str.matched_group 3 s) with Not_found -> None in
match entity with
None ->
insertMarkupRec tags t s (Str.group_end 0) (List.tl tl)
| Some ent ->
begin try
t#buffer#insert ~tags:(List.flatten tl) (List.assoc ent entities)
with Not_found -> () end;
insertMarkupRec tags t s (Str.group_end 0) tl
with Not_found ->
let j = String.length s in
if j > i then
t#buffer#insert ~tags:(List.flatten tl) (String.sub s i (j - i))
let insertMarkup tags t s =
t#buffer#set_text ""; insertMarkupRec tags t s 0 []
let documentPreference ~compact ~packing =
let vb = GPack.vbox ~spacing:6 ~packing () in
ignore (GMisc.label ~markup:"Documentation" ~xalign:0.
~packing:(vb#pack ~expand:false) ());
let al = GBin.alignment ~packing:(vb#pack ~expand:true ~fill:true) () in
al#set_left_padding 12;
let columns = if compact then 3 else 2 in
let tbl =
GPack.table ~rows:2 ~columns ~col_spacings:12 ~row_spacings:6
~packing:(al#add) () in
tbl#misc#set_sensitive false;
ignore (GMisc.label ~text:"Short description:" ~xalign:0.
~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
ignore (GMisc.label ~text:"Long description:" ~xalign:0. ~yalign:0.
~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
let shortDescr =
GMisc.label ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X)
~xalign:0. ~selectable:true () in
let longDescr =
let sw =
if compact then
GBin.scrolled_window ~height:128 ~width:640
~packing:(tbl#attach ~left:0 ~top:2 ~right:2 ~expand:`BOTH)
~shadow_type:`IN ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
else
GBin.scrolled_window ~height:128 ~width:640
~packing:(tbl#attach ~left:1 ~top:1 ~expand:`BOTH)
~shadow_type:`IN ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
in
GText.view ~editable:false ~packing:sw#add ~wrap_mode:`WORD ()
in
let (>>>) x f = f x in
let newlineRe = Str.regexp "\n *" in
let styleRe = Str.regexp "{\\\\\\([a-z]+\\) \\([^{}]*\\)}" in
let verbRe = Str.regexp "\\\\verb|\\([^|]*\\)|" in
let argRe = Str.regexp "\\\\ARG{\\([^{}]*\\)}" in
let textttRe = Str.regexp "\\\\texttt{\\([^{}]*\\)}" in
let emphRe = Str.regexp "\\\\emph{\\([^{}]*\\)}" in
let sectionRe = Str.regexp "\\\\sectionref{\\([^{}]*\\)}{\\([^{}]*\\)}" in
let emdash = Str.regexp_string "---" in
let parRe = Str.regexp "\\\\par *" in
let underRe = Str.regexp "\\\\_ *" in
let dollarRe = Str.regexp "\\\\\\$ *" in
let formatDoc doc =
doc >>>
Str.global_replace newlineRe " " >>>
escapeMarkup >>>
Str.global_substitute styleRe
(fun s ->
try
let tag =
match Str.matched_group 1 s with
"em" -> "i"
| "tt" -> "tt"
| _ -> raise Exit
in
Format.sprintf "<%s>%s%s>" tag (Str.matched_group 2 s) tag
with Exit ->
Str.matched_group 0 s) >>>
Str.global_replace verbRe "\\1" >>>
Str.global_replace argRe "\\1" >>>
Str.global_replace textttRe "\\1" >>>
Str.global_replace emphRe "\\1" >>>
Str.global_replace sectionRe "Section '\\2'" >>>
Str.global_replace emdash "\xe2\x80\x94" >>>
Str.global_replace parRe "\n" >>>
Str.global_replace underRe "_" >>>
Str.global_replace dollarRe "_"
in
let tags =
let create = longDescr#buffer#create_tag in
[("i", create [`FONT_DESC (Lazy.force fontItalic)]);
("tt", create [`FONT_DESC (Lazy.force fontMonospace)])]
in
fun nm ->
let (short, long, _) =
match nm with
Some nm ->
tbl#misc#set_sensitive true;
Prefs.documentation nm
| _ ->
tbl#misc#set_sensitive false;
("", "", false)
in
shortDescr#set_text (String.capitalize short);
insertMarkup tags longDescr (formatDoc long)
(* longDescr#buffer#set_text (formatDoc long)*)
let addPreference parent =
let t =
GWindow.dialog ~parent ~border_width:12
~no_separator:true ~title:"Add a Preference"
~modal:true () in
let vb = t#vbox in
(* vb#set_spacing 18;*)
let paned = GPack.paned `VERTICAL ~packing:vb#add () in
let lvb = GPack.vbox ~spacing:6 ~packing:paned#pack1 () in
let preferenceLabel =
GMisc.label
~text:"_Preferences:" ~use_underline:true
~xalign:0. ~packing:(lvb#pack ~expand:false) ()
in
let cols = new GTree.column_list in
let c_name = cols#add Gobject.Data.string in
let basic_store = GTree.list_store cols in
let full_store = GTree.list_store cols in
let lst =
let sw =
GBin.scrolled_window ~packing:(lvb#pack ~expand:true)
~shadow_type:`IN ~height:200 ~width:400
~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC () in
GTree.view ~headers_visible:false ~packing:sw#add () in
preferenceLabel#set_mnemonic_widget (Some (lst :> GObj.widget));
ignore (lst#append_column
(GTree.view_column
~renderer:(GTree.cell_renderer_text [], ["text", c_name]) ()));
let hiddenPrefs =
["auto"; "doc"; "silent"; "terse"; "testserver"; "version"] in
let shownPrefs =
["label"; "key"] in
let insert (store : #GTree.list_store) all =
List.iter
(fun nm ->
if
all || List.mem nm shownPrefs ||
(let (_, _, basic) = Prefs.documentation nm in basic &&
not (List.mem nm hiddenPrefs))
then begin
let row = store#append () in
store#set ~row ~column:c_name nm
end)
(Prefs.list ())
in
insert basic_store false;
insert full_store true;
let showAll =
GtkReact.toggle_button
(GButton.check_button ~label:"_Show all preferences"
~use_mnemonic:true ~active:false ~packing:(lvb#pack ~expand:false) ())
in
showAll >|
(fun b ->
lst#set_model
(Some (if b then full_store else basic_store :> GTree.model)));
let selection = GtkReact.tree_view_selection lst in
let updateDoc = documentPreference ~compact:true ~packing:paned#pack2 in
selection >|
(fun l ->
let nm =
match l with
[rf] ->
let row = rf#iter in
let store =
if React.state showAll then full_store else basic_store in
Some (store#get ~row ~column:c_name)
| _ ->
None
in
updateDoc nm);
let cancelCommand () = t#destroy () in
let cancelButton =
GButton.button ~stock:`CANCEL ~packing:t#action_area#add () in
ignore (cancelButton#connect#clicked ~callback:cancelCommand);
ignore (t#event#connect#delete ~callback:(fun _ -> cancelCommand (); true));
let ok = ref false in
let addCommand _ = ok := true; t#destroy () in
let addButton =
GButton.button ~stock:`ADD ~packing:t#action_area#add () in
ignore (addButton#connect#clicked ~callback:addCommand);
GtkReact.set_sensitive addButton (selection >> fun l -> l <> []);
ignore (lst#connect#row_activated ~callback:(fun _ _ -> addCommand ()));
addButton#grab_default ();
ignore (t#connect#destroy ~callback:GMain.Main.quit);
t#show ();
GMain.Main.main ();
if not !ok then None else
match React.state selection with
[rf] ->
let row = rf#iter in
let store =
if React.state showAll then full_store else basic_store in
Some (store#get ~row ~column:c_name)
| _ ->
None
let editProfile parent name =
let t =
GWindow.dialog ~parent ~border_width:12
~no_separator:true ~title:(Format.sprintf "%s - Profile Editor" name)
~modal:true () in
let vb = t#vbox in
(* t#vbox#set_spacing 18;*)
let paned = GPack.paned `VERTICAL ~packing:vb#add () in
let lvb = GPack.vbox ~spacing:6 ~packing:paned#pack1 () in
let preferenceLabel =
GMisc.label
~text:"_Preferences:" ~use_underline:true
~xalign:0. ~packing:(lvb#pack ~expand:false) ()
in
let hb = GPack.hbox ~spacing:12 ~packing:(lvb#add) () in
let cols = new GTree.column_list in
let c_name = cols#add Gobject.Data.string in
let c_type = cols#add Gobject.Data.string in
let c_value = cols#add Gobject.Data.string in
let c_ml = cols#add Gobject.Data.caml in
let lst_store = GTree.list_store cols in
let lst_sorted_store = GTree.model_sort lst_store in
lst_sorted_store#set_sort_column_id 0 `ASCENDING;
let lst =
let sw =
GBin.scrolled_window ~packing:(hb#pack ~expand:true)
~shadow_type:`IN ~height:300 ~width:600
~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC () in
GTree.view ~model:lst_sorted_store ~packing:sw#add
~headers_clickable:true () in
preferenceLabel#set_mnemonic_widget (Some (lst :> GObj.widget));
let vc_name =
GTree.view_column
~title:"Name"
~renderer:(GTree.cell_renderer_text [], ["text", c_name]) () in
vc_name#set_sort_column_id 0;
ignore (lst#append_column vc_name);
ignore (lst#append_column
(GTree.view_column
~title:"Type"
~renderer:(GTree.cell_renderer_text [], ["text", c_type]) ()));
ignore (lst#append_column
(GTree.view_column
~title:"Value"
~renderer:(GTree.cell_renderer_text [], ["text", c_value]) ()));
let vb =
GPack.button_box
`VERTICAL ~layout:`START ~spacing:6 ~packing:(hb#pack ~expand:false) ()
in
let selection = GtkReact.tree_view_selection lst in
let hasSel = selection >> fun l -> l <> [] in
let addB =
GButton.button ~stock:`ADD ~packing:(vb#pack ~expand:false) () in
let editB =
GButton.button ~stock:`EDIT ~packing:(vb#pack ~expand:false) () in
let deleteB =
GButton.button ~stock:`DELETE ~packing:(vb#pack ~expand:false) () in
List.iter (fun b -> b#set_xalign 0.) [addB; editB; deleteB];
GtkReact.set_sensitive editB hasSel;
GtkReact.set_sensitive deleteB hasSel;
let (modified, setModified) = React.make false in
let formatValue vl = Unicode.protect (String.concat ", " vl) in
let deletePref () =
match React.state selection with
[rf] ->
let row = lst_sorted_store#convert_iter_to_child_iter rf#iter in
let (nm, ty, vl) = lst_store#get ~row ~column:c_ml in
if
twoBox ~kind:`DIALOG_QUESTION ~parent:t ~title:"Preference Deletion"
~bstock:`CANCEL ~astock:`DELETE
(Format.sprintf "Do you really want to delete preference %s?"
(Unicode.protect nm))
then begin
ignore (lst_store#remove row);
setModified true
end
| _ ->
()
in
let editPref path =
let row =
lst_sorted_store#convert_iter_to_child_iter
(lst_sorted_store#get_iter path) in
let (nm, ty, vl) = lst_store#get ~row ~column:c_ml in
match editPreference t nm ty vl with
Some [] ->
deletePref ()
| Some vl' when vl <> vl' ->
lst_store#set ~row ~column:c_ml (nm, ty, vl');
lst_store#set ~row ~column:c_value (formatValue vl');
setModified true
| _ ->
()
in
let add () =
match addPreference t with
None ->
()
| Some nm ->
let existing = ref false in
lst_store#foreach
(fun path row ->
let (nm', _, _) = lst_store#get ~row ~column:c_ml in
if nm = nm' then begin
existing := true; editPref path; true
end else
false);
if not !existing then begin
let ty = Prefs.typ nm in
match editPreference parent nm ty (defaultValue ty) with
Some vl when vl <> [] ->
let row = lst_store#append () in
lst_store#set ~row ~column:c_name (Unicode.protect nm);
lst_store#set ~row ~column:c_type (nameOfType ty);
lst_store#set ~row ~column:c_ml (nm, ty, vl);
lst_store#set ~row ~column:c_value (formatValue vl);
setModified true
| _ ->
()
end
in
ignore (addB#connect#clicked ~callback:add);
ignore (editB#connect#clicked
~callback:(fun () ->
match React.state selection with
[p] -> editPref p#path
| _ -> ()));
ignore (deleteB#connect#clicked ~callback:deletePref);
let updateDoc = documentPreference ~compact:true ~packing:paned#pack2 in
selection >|
(fun l ->
let nm =
match l with
[rf] ->
let row = rf#iter in
Some (lst_sorted_store#get ~row ~column:c_name)
| _ ->
None
in
updateDoc nm);
ignore (lst#connect#row_activated ~callback:(fun path _ -> editPref path));
let group l =
let rec groupRec l k vl l' =
match l with
(k', v) :: r ->
if k = k' then
groupRec r k (v :: vl) l'
else
groupRec r k' [v] ((k, vl) :: l')
| [] ->
Safelist.fold_left
(fun acc (k, l) -> (k, List.rev l) :: acc) [] ((k, vl) :: l')
in
match l with
(k, v) :: r -> groupRec r k [v] []
| [] -> []
in
let lastOne l = [List.hd (Safelist.rev l)] in
let normalizeValue t vl =
match t with
`BOOL | `INT | `STRING -> lastOne vl
| `STRING_LIST | `CUSTOM | `UNKNOWN -> vl
| `BOOLDEF ->
let l = lastOne vl in
if l = ["default"] || l = ["auto"] then [] else l
in
let (>>>) x f = f x in
Prefs.readAFile name
>>> List.map (fun (_, _, nm, v) -> Prefs.canonicalName nm, v)
>>> List.stable_sort (fun (nm, _) (nm', _) -> compare nm nm')
>>> group
>>> List.iter
(fun (nm, vl) ->
let nm = Prefs.canonicalName nm in
let ty = Prefs.typ nm in
let vl = normalizeValue ty vl in
if vl <> [] then begin
let row = lst_store#append () in
lst_store#set ~row ~column:c_name (Unicode.protect nm);
lst_store#set ~row ~column:c_type (nameOfType ty);
lst_store#set ~row ~column:c_value (formatValue vl);
lst_store#set ~row ~column:c_ml (nm, ty, vl)
end);
let applyCommand _ =
if React.state modified then begin
let filename = Prefs.profilePathname name in
try
let ch =
System.open_out_gen [Open_wronly; Open_creat; Open_trunc] 0o600
filename
in
(*XXX Should trim whitespaces and check for '\n' at some point *)
Printf.fprintf ch "# Unison preferences\n";
lst_store#foreach
(fun path row ->
let (nm, _, vl) = lst_store#get ~row ~column:c_ml in
List.iter (fun v -> Printf.fprintf ch "%s = %s\n" nm v) vl;
false);
close_out ch;
setModified false
with Sys_error _ as e ->
okBox ~parent:t ~typ:`ERROR ~title:"Could not save profile"
~message:(Uicommon.exn2string e)
end
in
let applyButton =
GButton.button ~stock:`APPLY ~packing:t#action_area#add () in
ignore (applyButton#connect#clicked ~callback:applyCommand);
GtkReact.set_sensitive applyButton modified;
let cancelCommand () = t#destroy () in
let cancelButton =
GButton.button ~stock:`CANCEL ~packing:t#action_area#add () in
ignore (cancelButton#connect#clicked ~callback:cancelCommand);
ignore (t#event#connect#delete ~callback:(fun _ -> cancelCommand (); true));
let okCommand _ = applyCommand (); t#destroy () in
let okButton =
GButton.button ~stock:`OK ~packing:t#action_area#add () in
ignore (okButton#connect#clicked ~callback:okCommand);
okButton#grab_default ();
(*
List.iter
(fun (nm, _, long) ->
try
let long = formatDoc long in
ignore (Str.search_forward (Str.regexp_string "\\") long 0);
Format.eprintf "%s %s@." nm long
with Not_found -> ())
(Prefs.listVisiblePrefs ());
*)
(*
TODO:
- Extra tabs for common preferences
(should keep track of any change, or blacklist some preferences)
- Add, modify, delete
- Keep track of whether there is any change (apply button)
*)
ignore (t#connect#destroy ~callback:GMain.Main.quit);
t#show ();
GMain.Main.main ()
(* ------ *)
let profilesAndRoots = ref []
let scanProfiles () =
Array.iteri (fun i _ -> profileKeymap.(i) <- None) profileKeymap;
profilesAndRoots :=
(Safelist.map
(fun f ->
let f = Filename.chop_suffix f ".prf" in
let filename = Prefs.profilePathname f in
let fileContents = Safelist.map (fun (_, _, n, v) -> (n, v)) (Prefs.readAFile f) in
let roots =
Safelist.map snd
(Safelist.filter (fun (n, _) -> n = "root") fileContents) in
let label =
try Some(Safelist.assoc "label" fileContents)
with Not_found -> None in
let info = {roots=roots; label=label} in
(* If this profile has a 'key' binding, put it in the keymap *)
(try
let k = Safelist.assoc "key" fileContents in
provideProfileKey filename k f info
with Not_found -> ());
(f, info))
(Safelist.filter (fun name -> not ( Util.startswith name ".#"
|| Util.startswith name Os.tempFilePrefix))
(Files.ls Os.unisonDir "*.prf")))
let getProfile quit =
let ok = ref false in
(* Build the dialog *)
let t =
GWindow.dialog ~parent:(toplevelWindow ()) ~border_width:12
~no_separator:true ~title:"Profile Selection"
~modal:true () in
t#set_default_width 550;
let cancelCommand _ = t#destroy () in
let cancelButton =
GButton.button ~stock:(if quit then `QUIT else `CANCEL)
~packing:t#action_area#add () in
ignore (cancelButton#connect#clicked ~callback:cancelCommand);
ignore (t#event#connect#delete ~callback:(fun _ -> cancelCommand (); true));
cancelButton#misc#set_can_default true;
let okCommand() = ok := true; t#destroy () in
let okButton =
GButton.button ~stock:`OPEN ~packing:t#action_area#add () in
ignore (okButton#connect#clicked ~callback:okCommand);
okButton#misc#set_sensitive false;
okButton#grab_default ();
let vb = t#vbox in
t#vbox#set_spacing 18;
let al = GBin.alignment ~packing:(vb#add) () in
al#set_left_padding 12;
let lvb = GPack.vbox ~spacing:6 ~packing:(al#add) () in
let selectLabel =
GMisc.label
~text:"Select a _profile:" ~use_underline:true
~xalign:0. ~packing:(lvb#pack ~expand:false) ()
in
let hb = GPack.hbox ~spacing:12 ~packing:(lvb#add) () in
let sw =
GBin.scrolled_window ~packing:(hb#pack ~expand:true) ~height:300
~shadow_type:`IN ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC () in
let cols = new GTree.column_list in
let c_name = cols#add Gobject.Data.string in
let c_label = cols#add Gobject.Data.string in
let c_ml = cols#add Gobject.Data.caml in
let lst_store = GTree.list_store cols in
let lst = GTree.view ~model:lst_store ~packing:sw#add () in
selectLabel#set_mnemonic_widget (Some (lst :> GObj.widget));
let vc_name =
GTree.view_column
~title:"Profile"
~renderer:(GTree.cell_renderer_text [], ["text", c_name]) ()
in
ignore (lst#append_column vc_name);
ignore (lst#append_column
(GTree.view_column
~title:"Description"
~renderer:(GTree.cell_renderer_text [], ["text", c_label]) ()));
let vb = GPack.vbox ~spacing:6 ~packing:(vb#pack ~expand:false) () in
ignore (GMisc.label ~markup:"Summary" ~xalign:0.
~packing:(vb#pack ~expand:false) ());
let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
al#set_left_padding 12;
let tbl =
GPack.table ~rows:2 ~columns:2 ~col_spacings:12 ~row_spacings:6
~packing:(al#add) () in
tbl#misc#set_sensitive false;
ignore (GMisc.label ~text:"First root:" ~xalign:0.
~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
ignore (GMisc.label ~text:"Second root:" ~xalign:0.
~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
let root1 =
GMisc.label ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X)
~xalign:0. ~selectable:true () in
let root2 =
GMisc.label ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X)
~xalign:0. ~selectable:true () in
let fillLst default =
scanProfiles();
lst_store#clear ();
Safelist.iter
(fun (profile, info) ->
let labeltext =
match info.label with None -> "" | Some l -> l in
let row = lst_store#append () in
lst_store#set ~row ~column:c_name (Unicode.protect profile);
lst_store#set ~row ~column:c_label (Unicode.protect labeltext);
lst_store#set ~row ~column:c_ml (profile, info);
if Some profile = default then begin
lst#selection#select_iter row;
lst#scroll_to_cell (lst_store#get_path row) vc_name
end)
(Safelist.sort (fun (p, _) (p', _) -> compare p p') !profilesAndRoots)
in
let selection = GtkReact.tree_view_selection lst in
let hasSel = selection >> fun l -> l <> [] in
let selInfo =
selection >> fun l ->
match l with
[rf] -> Some (lst_store#get ~row:rf#iter ~column:c_ml, rf)
| _ -> None
in
selInfo >|
(fun info ->
match info with
Some ((profile, info), _) ->
begin match info.roots with
[r1; r2] -> root1#set_text (Unicode.protect r1);
root2#set_text (Unicode.protect r2);
tbl#misc#set_sensitive true
| _ -> root1#set_text ""; root2#set_text "";
tbl#misc#set_sensitive false
end
| None ->
root1#set_text ""; root2#set_text "";
tbl#misc#set_sensitive false);
GtkReact.set_sensitive okButton hasSel;
let vb =
GPack.button_box
`VERTICAL ~layout:`START ~spacing:6 ~packing:(hb#pack ~expand:false) ()
in
let addButton =
GButton.button ~stock:`ADD ~packing:(vb#pack ~expand:false) () in
ignore (addButton#connect#clicked
~callback:(fun () ->
match createProfile t with
Some p -> fillLst (Some p) | None -> ()));
let editButton =
GButton.button ~stock:`EDIT ~packing:(vb#pack ~expand:false) () in
ignore (editButton#connect#clicked
~callback:(fun () -> match React.state selInfo with
None ->
()
| Some ((p, _), _) ->
editProfile t p; fillLst (Some p)));
GtkReact.set_sensitive editButton hasSel;
let deleteProfile () =
match React.state selInfo with
Some ((profile, _), rf) ->
if
twoBox ~kind:`DIALOG_QUESTION ~parent:t ~title:"Profile Deletion"
~bstock:`CANCEL ~astock:`DELETE
(Format.sprintf "Do you really want to delete profile %s?"
(transcode profile))
then begin
try
System.unlink (Prefs.profilePathname profile);
ignore (lst_store#remove rf#iter)
with Unix.Unix_error _ -> ()
end
| None ->
()
in
let deleteButton =
GButton.button ~stock:`DELETE ~packing:(vb#pack ~expand:false) () in
ignore (deleteButton#connect#clicked ~callback:deleteProfile);
GtkReact.set_sensitive deleteButton hasSel;
List.iter (fun b -> b#set_xalign 0.) [addButton; editButton; deleteButton];
ignore (lst#connect#row_activated ~callback:(fun _ _ -> okCommand ()));
fillLst None;
lst#misc#grab_focus ();
ignore (t#connect#destroy ~callback:GMain.Main.quit);
t#show ();
GMain.Main.main ();
match React.state selInfo with
Some ((p, _), _) when !ok -> Some p
| _ -> None
(* ------ *)
let documentation sect =
let title = "Documentation" in
let t = GWindow.dialog ~title () in
let t_dismiss =
GButton.button ~stock:`CLOSE ~packing:t#action_area#add () in
t_dismiss#grab_default ();
let dismiss () = t#destroy () in
ignore (t_dismiss#connect#clicked ~callback:dismiss);
ignore (t#event#connect#delete ~callback:(fun _ -> dismiss (); true));
let (name, docstr) = Safelist.assoc sect Strings.docs in
let docstr = transcodeDoc docstr in
let hb = GPack.hbox ~packing:(t#vbox#pack ~expand:false ~padding:2) () in
let optionmenu =
GMenu.option_menu ~packing:(hb#pack ~expand:true ~fill:false) () in
let t_text =
new scrolled_text ~editable:false
~width:80 ~height:20 ~packing:t#vbox#add ()
in
t_text#insert docstr;
let sect_idx = ref 0 in
let idx = ref 0 in
let menu = GMenu.menu () in
let addDocSection (shortname, (name, docstr)) =
if shortname <> "" && name <> "" then begin
if shortname = sect then sect_idx := !idx;
incr idx;
let item = GMenu.menu_item ~label:name ~packing:menu#append () in
let docstr = transcodeDoc docstr in
ignore
(item#connect#activate ~callback:(fun () -> t_text#insert docstr))
end
in
Safelist.iter addDocSection Strings.docs;
optionmenu#set_menu menu;
optionmenu#set_history !sect_idx;
t#show ()
(* ------ *)
let messageBox ~title ?(action = fun t -> t#destroy) message =
let utitle = transcode title in
let t = GWindow.dialog ~title:utitle ~position:`CENTER () in
let t_dismiss = GButton.button ~stock:`CLOSE ~packing:t#action_area#add () in
t_dismiss#grab_default ();
ignore (t_dismiss#connect#clicked ~callback:(action t));
let t_text =
new scrolled_text ~editable:false
~width:80 ~height:20 ~packing:t#vbox#add ()
in
t_text#insert message;
ignore (t#event#connect#delete ~callback:(fun _ -> action t (); true));
t#show ()
(* twoBoxAdvanced: Display a message in a window and wait for the user
to hit one of two buttons. Return true if the first button is
chosen, false if the second button is chosen. Also has a button for
showing more details to the user in a messageBox dialog *)
let twoBoxAdvanced
~parent ~title ~message ~longtext ~advLabel ~astock ~bstock =
let t =
GWindow.dialog ~parent ~border_width:6 ~modal:true ~no_separator:true
~allow_grow:false () in
t#vbox#set_spacing 12;
let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
ignore (GMisc.image ~stock:`DIALOG_QUESTION ~icon_size:`DIALOG
~yalign:0. ~packing:h1#pack ());
let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
ignore (GMisc.label
~markup:(primaryText title ^ "\n\n" ^ escapeMarkup message)
~selectable:true ~yalign:0. ~packing:v1#add ());
t#add_button_stock `CANCEL `NO;
let cmd () =
messageBox ~title:"Details" longtext
in
t#add_button advLabel `HELP;
t#add_button_stock `APPLY `YES;
t#set_default_response `NO;
let res = ref false in
let setRes signal =
match signal with
`YES -> res := true; t#destroy ()
| `NO -> res := false; t#destroy ()
| `HELP -> cmd ()
| _ -> ()
in
ignore (t#connect#response ~callback:setRes);
ignore (t#connect#destroy ~callback:GMain.Main.quit);
t#show();
GMain.Main.main();
!res
let summaryBox ~parent ~title ~message ~f =
let t =
GWindow.dialog ~parent ~border_width:6 ~modal:true ~no_separator:true
~allow_grow:false ~focus_on_map:false () in
t#vbox#set_spacing 12;
let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
ignore (GMisc.image ~stock:`DIALOG_INFO ~icon_size:`DIALOG
~yalign:0. ~packing:h1#pack ());
let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
ignore (GMisc.label
~markup:(primaryText title ^ "\n\n" ^ escapeMarkup message)
~selectable:true ~xalign:0. ~yalign:0. ~packing:v1#add ());
let exp = GBin.expander ~spacing:12 ~label:"Show details" ~packing:v1#add () in
let t_text =
new scrolled_text ~editable:false ~shadow_type:`IN
~width:60 ~height:10 ~packing:exp#add ()
in
f (t_text#text);
t#add_button_stock `OK `OK;
t#set_default_response `OK;
let setRes signal = t#destroy () in
ignore (t#connect#response ~callback:setRes);
ignore (t#connect#destroy ~callback:GMain.Main.quit);
t#show();
GMain.Main.main()
(**********************************************************************
TOP-LEVEL WINDOW
**********************************************************************)
let displayWaitMessage () =
make_busy (toplevelWindow ());
Trace.status (Uicommon.contactingServerMsg ())
(* ------ *)
type status = NoStatus | Done | Failed
let createToplevelWindow () =
let toplevelWindow =
GWindow.window ~kind:`TOPLEVEL ~position:`CENTER
~title:myNameCapitalized ()
in
setToplevelWindow toplevelWindow;
(* There is already a default icon under Windows, and transparent
icons are not supported by all version of Windows *)
if Util.osType <> `Win32 then toplevelWindow#set_icon (Some icon);
let toplevelVBox = GPack.vbox ~packing:toplevelWindow#add () in
(*******************************************************************
Statistic window
*******************************************************************)
let (statWin, startStats, stopStats) = statistics () in
(*******************************************************************
Groups of things that are sensitive to interaction at the same time
*******************************************************************)
let grAction = ref [] in
let grDiff = ref [] in
let grGo = ref [] in
let grRescan = ref [] in
let grDetail = ref [] in
let grAdd gr w = gr := w#misc::!gr in
let grSet gr st = Safelist.iter (fun x -> x#set_sensitive st) !gr in
let grDisactivateAll () =
grSet grAction false;
grSet grDiff false;
grSet grGo false;
grSet grRescan false;
grSet grDetail false
in
(*********************************************************************
Create the menu bar
*********************************************************************)
let topHBox = GPack.hbox ~packing:(toplevelVBox#pack ~expand:false) () in
let menuBar =
GMenu.menu_bar ~border_width:0
~packing:(topHBox#pack ~expand:true) () in
let menus = new gMenuFactory ~accel_modi:[] menuBar in
let accel_group = menus#accel_group in
toplevelWindow#add_accel_group accel_group;
let add_submenu ?(modi=[]) label =
let (menu, item) = menus#add_submenu label in
(new gMenuFactory ~accel_group:(menus#accel_group)
~accel_path:(menus#accel_path ^ label ^ "/")
~accel_modi:modi menu,
item)
in
let replace_submenu ?(modi=[]) label item =
let menu = menus#replace_submenu item in
new gMenuFactory ~accel_group:(menus#accel_group)
~accel_path:(menus#accel_path ^ label ^ "/")
~accel_modi:modi menu
in
let profileLabel =
GMisc.label ~text:"" ~packing:(topHBox#pack ~expand:false ~padding:2) () in
let displayNewProfileLabel () =
let p = match !Prefs.profileName with None -> "" | Some p -> p in
let label = Prefs.read Uicommon.profileLabel in
let s =
match p, label with
"", _ -> ""
| _, "" -> p
| "default", _ -> label
| _ -> Format.sprintf "%s (%s)" p label
in
toplevelWindow#set_title
(if s = "" then myNameCapitalized else
Format.sprintf "%s [%s]" myNameCapitalized s);
let s = if s="" then "No profile" else "Profile: " ^ s in
profileLabel#set_text (transcode s)
in
displayNewProfileLabel ();
(*********************************************************************
Create the menus
*********************************************************************)
let (fileMenu, _) = add_submenu "_Synchronization" in
let (actionMenu, actionItem) = add_submenu "_Actions" in
let (ignoreMenu, _) = add_submenu ~modi:[`SHIFT] "_Ignore" in
let (sortMenu, _) = add_submenu "S_ort" in
let (helpMenu, _) = add_submenu "_Help" in
(*********************************************************************
Action bar
*********************************************************************)
let actionBar =
let hb = GBin.handle_box ~packing:(toplevelVBox#pack ~expand:false) () in
GButton.toolbar ~style:`BOTH
(* 2003-0519 (stse): how to set space size in gtk 2.0? *)
(* Answer from Jacques Garrigue: this can only be done in
the user's.gtkrc, not programmatically *)
~orientation:`HORIZONTAL ~tooltips:true (* ~space_size:10 *)
~packing:(hb#add) () in
(*********************************************************************
Create the main window
*********************************************************************)
let mainWindowSW =
GBin.scrolled_window ~packing:(toplevelVBox#pack ~expand:true)
~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
in
let sizeMainWindow () =
let ctx = mainWindowSW#misc#pango_context in
let metrics = ctx#get_metrics () in
let h = GPango.to_pixels (metrics#ascent+metrics#descent) in
mainWindowSW#misc#set_size_request
~height:((h + 1) * (Prefs.read Uicommon.mainWindowHeight + 1) + 10) ()
in
let mainWindow =
GList.clist ~columns:5 ~titles_show:true
~selection_mode:`MULTIPLE ~packing:mainWindowSW#add ()
in
(*
let cols = new GTree.column_list in
let c_replica1 = cols#add Gobject.Data.string in
let c_action = cols#add Gobject.Data.gobject in
let c_replica2 = cols#add Gobject.Data.string in
let c_status = cols#add Gobject.Data.string in
let c_path = cols#add Gobject.Data.string in
let lst_store = GTree.list_store cols in
let lst =
GTree.view ~model:lst_store ~packing:(toplevelVBox#add)
~headers_clickable:false () in
let s = Uicommon.roots2string () in
ignore (lst#append_column
(GTree.view_column
~title:(" " ^ Unicode.protect (String.sub s 0 12) ^ " ")
~renderer:(GTree.cell_renderer_text [], ["text", c_replica1]) ()));
ignore (lst#append_column
(GTree.view_column ~title:" Action "
~renderer:(GTree.cell_renderer_pixbuf [], ["pixbuf", c_action]) ()));
ignore (lst#append_column
(GTree.view_column
~title:(" " ^ Unicode.protect (String.sub s 15 12) ^ " ")
~renderer:(GTree.cell_renderer_text [], ["text", c_replica2]) ()));
ignore (lst#append_column
(GTree.view_column ~title:" Status " ()));
ignore (lst#append_column
(GTree.view_column ~title:" Path "
~renderer:(GTree.cell_renderer_text [], ["text", c_path]) ()));
*)
(*
let status_width =
let font = mainWindow#misc#style#font in
4 + max (max (Gdk.Font.string_width font "working")
(Gdk.Font.string_width font "skipped"))
(Gdk.Font.string_width font " Action ")
in
*)
mainWindow#set_column ~justification:`CENTER 1;
mainWindow#set_column
~justification:`CENTER (*~auto_resize:false ~width:status_width*) 3;
let setMainWindowColumnHeaders s =
Array.iteri
(fun i data ->
mainWindow#set_column
~title_active:false ~auto_resize:true ~title:data i)
[| " " ^ Unicode.protect (String.sub s 0 12) ^ " "; " Action ";
" " ^ Unicode.protect (String.sub s 15 12) ^ " "; " Status ";
" Path" |];
sizeMainWindow ()
in
setMainWindowColumnHeaders " ";
(*********************************************************************
Create the details window
*********************************************************************)
let showDetCommand () =
let details =
match currentRow () with
None ->
None
| Some row ->
let path = Path.toString !theState.(row).ri.path1 in
match !theState.(row).whatHappened with
Some (Util.Failed _, Some det) ->
Some ("Merge execution details for file" ^
transcodeFilename path,
det)
| _ ->
match !theState.(row).ri.replicas with
Problem err ->
Some ("Errors for file " ^ transcodeFilename path, err)
| Different diff ->
let prefix s l =
Safelist.map (fun err -> Format.sprintf "%s%s\n" s err) l
in
let errors =
Safelist.append
(prefix "[root 1]: " diff.errors1)
(prefix "[root 2]: " diff.errors2)
in
let errors =
match !theState.(row).whatHappened with
Some (Util.Failed err, _) -> err :: errors
| _ -> errors
in
Some ("Errors for file " ^ transcodeFilename path,
String.concat "\n" errors)
in
match details with
None -> ((* Should not happen *))
| Some (title, details) -> messageBox ~title (transcode details)
in
let detailsWindowSW =
GBin.scrolled_window ~packing:(toplevelVBox#pack ~expand:false)
~shadow_type:`IN ~hpolicy:`NEVER ~vpolicy:`AUTOMATIC ()
in
let detailsWindow =
GText.view ~editable:false ~packing:detailsWindowSW#add ()
in
let detailsWindowPath = detailsWindow#buffer#create_tag [] in
let detailsWindowInfo =
detailsWindow#buffer#create_tag [`FONT_DESC (Lazy.force fontMonospace)] in
let detailsWindowError =
detailsWindow#buffer#create_tag [`WRAP_MODE `WORD] in
detailsWindow#misc#set_size_chars ~height:3 ~width:112 ();
detailsWindow#misc#set_can_focus false;
let updateButtons () =
if not !busy then
let actionPossible row =
let si = !theState.(row) in
match si.whatHappened, si.ri.replicas with
None, Different _ -> true
| _ -> false
in
match currentRow () with
None ->
grSet grAction (IntSet.exists actionPossible !current);
grSet grDiff false;
grSet grDetail false
| Some row ->
let details =
begin match !theState.(row).ri.replicas with
Different diff -> diff.errors1 <> [] || diff.errors2 <> []
| Problem _ -> true
end
||
begin match !theState.(row).whatHappened with
Some (Util.Failed _, _) -> true
| _ -> false
end
in
grSet grDetail details;
let activateAction = actionPossible row in
let activateDiff =
activateAction &&
match !theState.(row).ri.replicas with
Different {rc1 = {typ = `FILE}; rc2 = {typ = `FILE}} ->
true
| _ ->
false
in
grSet grAction activateAction;
grSet grDiff activateDiff
in
let makeRowVisible row =
if mainWindow#row_is_visible row <> `FULL then begin
let adj = mainWindow#vadjustment in
let upper = adj#upper and lower = adj#lower in
let v =
float row /. float (mainWindow#rows + 1) *. (upper-.lower) +. lower
in
adj#set_value (min v (upper -. adj#page_size));
end in
(*
let makeFirstUnfinishedVisible pRiInFocus =
let im = Array.length !theState in
let rec find i =
if i >= im then makeRowVisible im else
match pRiInFocus (!theState.(i).ri), !theState.(i).whatHappened with
true, None -> makeRowVisible i
| _ -> find (i+1) in
find 0
in
*)
let updateDetails () =
begin match currentRow () with
None ->
detailsWindow#buffer#set_text ""
| Some row ->
(* makeRowVisible row;*)
let (formated, details) =
match !theState.(row).whatHappened with
| Some(Util.Failed(s), _) ->
(false, s)
| None | Some(Util.Succeeded, _) ->
match !theState.(row).ri.replicas with
Problem _ ->
(false, Uicommon.details2string !theState.(row).ri " ")
| Different _ ->
(true, Uicommon.details2string !theState.(row).ri " ")
in
let path = Path.toString !theState.(row).ri.path1 in
detailsWindow#buffer#set_text "";
detailsWindow#buffer#insert ~tags:[detailsWindowPath]
(transcodeFilename path);
let len = String.length details in
let details =
if details.[len - 1] = '\n' then String.sub details 0 (len - 1)
else details
in
if details <> "" then
detailsWindow#buffer#insert
~tags:[if formated then detailsWindowInfo else detailsWindowError]
("\n" ^ transcode details)
end;
(* Display text *)
updateButtons () in
(*********************************************************************
Status window
*********************************************************************)
let statusHBox = GPack.hbox ~packing:(toplevelVBox#pack ~expand:false) () in
let progressBar =
GRange.progress_bar ~packing:(statusHBox#pack ~expand:false) () in
progressBar#misc#set_size_chars ~height:1 ~width:28 ();
progressBar#set_pulse_step 0.02;
let progressBarPulse = ref false in
let statusWindow =
GMisc.statusbar ~packing:(statusHBox#pack ~expand:true) () in
let statusContext = statusWindow#new_context ~name:"status" in
ignore (statusContext#push "");
let displayStatus m =
statusContext#pop ();
if !progressBarPulse then progressBar#pulse ();
ignore (statusContext#push (transcode m));
(* Force message to be displayed immediately *)
gtk_sync false
in
let formatStatus major minor = (Util.padto 30 (major ^ " ")) ^ minor in
(* Tell the Trace module about the status printer *)
Trace.messageDisplayer := displayStatus;
Trace.statusFormatter := formatStatus;
Trace.sendLogMsgsToStderr := false;
(*********************************************************************
Functions used to print in the main window
*********************************************************************)
let delayUpdates = ref false in
let hasFocus = ref false in
let select i scroll =
if !hasFocus then begin
(* If we have the focus, we move the focus row directely *)
if scroll then begin
let r = mainWindow#rows in
let p = if r < 2 then 0. else (float i +. 0.5) /. float (r - 1) in
mainWindow#scroll_vertical `JUMP (min p 1.)
end;
if IntSet.is_empty !current then mainWindow#select i 0
end else begin
(* If we don't have the focus, we just move the selection.
We delay updates to make sure not to change the button
states unnecessarily (which could result in a button
losing the focus). *)
delayUpdates := true;
mainWindow#unselect_all ();
mainWindow#select i 0;
delayUpdates := false;
if scroll then makeRowVisible i;
updateDetails ()
end
in
ignore (mainWindow#event#connect#focus_in ~callback:
(fun _ ->
hasFocus := true;
(* Adjust the focus row. We cannot do it immediately,
otherwise the focus row is not drawn correctly. *)
ignore (GMain.Idle.add (fun () ->
begin match currentRow () with
Some i -> select i false
| None -> ()
end;
false));
false));
ignore (mainWindow#event#connect#focus_out ~callback:
(fun _ -> hasFocus := false; false));
ignore (mainWindow#connect#select_row ~callback:
(fun ~row ~column ~event ->
current := IntSet.add row !current;
if not !delayUpdates then updateDetails ()));
ignore (mainWindow#connect#unselect_row ~callback:
(fun ~row ~column ~event ->
current := IntSet.remove row !current;
if not !delayUpdates then updateDetails ()));
let nextInteresting () =
let l = Array.length !theState in
let start = match currentRow () with Some i -> i + 1 | None -> 0 in
let rec loop i =
if i < l then
match !theState.(i).ri.replicas with
Different {direction = dir}
when not (Prefs.read Uicommon.auto) || isConflict dir ->
select i true
| _ ->
loop (i + 1) in
loop start in
let selectSomethingIfPossible () =
if IntSet.is_empty !current then nextInteresting () in
let columnsOf i =
let oldPath = if i = 0 then Path.empty else !theState.(i-1).ri.path1 in
let status =
match !theState.(i).ri.replicas with
Different {direction = Conflict _} | Problem _ ->
NoStatus
| _ ->
match !theState.(i).whatHappened with
None -> NoStatus
| Some (Util.Succeeded, _) -> Done
| Some (Util.Failed _, _) -> Failed
in
let (r1, action, r2, path) =
Uicommon.reconItem2stringList oldPath !theState.(i).ri in
(r1, action, r2, status, path)
in
let greenPixel = "00dd00" in
let redPixel = "ff2040" in
let lightbluePixel = "8888FF" in
let orangePixel = "ff9303" in
(*
let yellowPixel = "999900" in
let blackPixel = "000000" in
*)
let buildPixmap p =
GDraw.pixmap_from_xpm_d ~window:toplevelWindow ~data:p () in
let buildPixmaps f c1 =
(buildPixmap (f c1), buildPixmap (f lightbluePixel)) in
let doneIcon = buildPixmap Pixmaps.success in
let failedIcon = buildPixmap Pixmaps.failure in
let rightArrow = buildPixmaps Pixmaps.copyAB greenPixel in
let leftArrow = buildPixmaps Pixmaps.copyBA greenPixel in
let orangeRightArrow = buildPixmaps Pixmaps.copyAB orangePixel in
let orangeLeftArrow = buildPixmaps Pixmaps.copyBA orangePixel in
let ignoreAct = buildPixmaps Pixmaps.ignore redPixel in
let failedIcons = (failedIcon, failedIcon) in
let mergeLogo = buildPixmaps Pixmaps.mergeLogo greenPixel in
(*
let rightArrowBlack = buildPixmap (Pixmaps.copyAB blackPixel) in
let leftArrowBlack = buildPixmap (Pixmaps.copyBA blackPixel) in
let mergeLogoBlack = buildPixmap (Pixmaps.mergeLogo blackPixel) in
*)
let displayArrow i j action =
let changedFromDefault = match !theState.(j).ri.replicas with
Different diff -> diff.direction <> diff.default_direction
| _ -> false in
let sel pixmaps =
if changedFromDefault then snd pixmaps else fst pixmaps in
let pixmaps =
match action with
Uicommon.AError -> failedIcons
| Uicommon.ASkip _ -> ignoreAct
| Uicommon.ALtoR false -> rightArrow
| Uicommon.ALtoR true -> orangeRightArrow
| Uicommon.ARtoL false -> leftArrow
| Uicommon.ARtoL true -> orangeLeftArrow
| Uicommon.AMerge -> mergeLogo
in
mainWindow#set_cell ~pixmap:(sel pixmaps) i 1
in
let displayStatusIcon i status =
match status with
| Failed -> mainWindow#set_cell ~pixmap:failedIcon i 3
| Done -> mainWindow#set_cell ~pixmap:doneIcon i 3
| NoStatus -> mainWindow#set_cell ~text:" " i 3 in
let displayMain() =
(* The call to mainWindow#clear below side-effect current,
so we save the current value before we clear out the main window and
rebuild it. *)
let savedCurrent = currentRow () in
mainWindow#freeze ();
mainWindow#clear ();
for i = Array.length !theState - 1 downto 0 do
let (r1, action, r2, status, path) = columnsOf i in
(*
let row = lst_store#prepend () in
lst_store#set ~row ~column:c_replica1 r1;
lst_store#set ~row ~column:c_replica2 r2;
lst_store#set ~row ~column:c_status status;
lst_store#set ~row ~column:c_path path;
*)
ignore (mainWindow#prepend
[ r1; ""; r2; ""; transcodeFilename path ]);
displayArrow 0 i action;
displayStatusIcon i status
done;
debug (fun()-> Util.msg "reset current to %s\n"
(match savedCurrent with None->"None" | Some(i) -> string_of_int i));
begin match savedCurrent with
None -> selectSomethingIfPossible ()
| Some idx -> select idx true
end;
mainWindow#thaw ();
updateDetails (); (* Do we need this line? *)
in
let redisplay i =
let (r1, action, r2, status, path) = columnsOf i in
(*mainWindow#freeze ();*)
mainWindow#set_cell ~text:r1 i 0;
displayArrow i i action;
mainWindow#set_cell ~text:r2 i 2;
displayStatusIcon i status;
mainWindow#set_cell ~text:(transcodeFilename path) i 4;
if status = Failed then
mainWindow#set_cell
~text:(transcodeFilename path ^
" [failed: click on this line for details]") i 4;
(*mainWindow#thaw ();*)
if currentRow () = Some i then begin
updateDetails (); updateButtons ()
end
in
let fastRedisplay i =
let (r1, action, r2, status, path) = columnsOf i in
displayStatusIcon i status;
if status = Failed then
mainWindow#set_cell
~text:(transcodeFilename path ^
" [failed: click on this line for details]") i 4;
if currentRow () = Some i then updateDetails ();
in
let totalBytesToTransfer = ref Uutil.Filesize.zero in
let totalBytesTransferred = ref Uutil.Filesize.zero in
let t0 = ref 0. in
let t1 = ref 0. in
let lastFrac = ref 0. in
let oldWritten = ref 0. in
let writeRate = ref 0. in
let displayGlobalProgress v =
if v = 0. || abs_float (v -. !lastFrac) > 1. then begin
lastFrac := v;
progressBar#set_fraction (max 0. (min 1. (v /. 100.)))
end;
if v < 0.001 then
progressBar#set_text " "
else begin
let t = Unix.gettimeofday () in
let delta = t -. !t1 in
if delta >= 0.5 then begin
t1 := t;
let remTime =
if v >= 100. then "00:00 remaining" else
let t = truncate ((!t1 -. !t0) *. (100. -. v) /. v +. 0.5) in
Format.sprintf "%02d:%02d remaining" (t / 60) (t mod 60)
in
let written = !clientWritten +. !serverWritten in
let b = 0.64 ** delta in
writeRate :=
b *. !writeRate +.
(1. -. b) *. (written -. !oldWritten) /. delta;
oldWritten := written;
let rate = !writeRate (*!emitRate2 +. !receiveRate2*) in
let txt =
if rate > 99. then
Format.sprintf "%s (%s)" remTime (rate2str rate)
else
remTime
in
progressBar#set_text txt
end
end
in
let showGlobalProgress b =
(* Concatenate the new message *)
totalBytesTransferred := Uutil.Filesize.add !totalBytesTransferred b;
let v =
(Uutil.Filesize.percentageOfTotalSize
!totalBytesTransferred !totalBytesToTransfer)
in
displayGlobalProgress v
in
let root1IsLocal = ref true in
let root2IsLocal = ref true in
let initGlobalProgress b =
let (root1,root2) = Globals.roots () in
root1IsLocal := fst root1 = Local;
root2IsLocal := fst root2 = Local;
totalBytesToTransfer := b;
totalBytesTransferred := Uutil.Filesize.zero;
t0 := Unix.gettimeofday (); t1 := !t0;
writeRate := 0.; oldWritten := !clientWritten +. !serverWritten;
displayGlobalProgress 0.
in
let showProgress i bytes dbg =
let i = Uutil.File.toLine i in
let item = !theState.(i) in
item.bytesTransferred <- Uutil.Filesize.add item.bytesTransferred bytes;
let b = item.bytesTransferred in
let len = item.bytesToTransfer in
let newstatus =
if b = Uutil.Filesize.zero || len = Uutil.Filesize.zero then "start "
else if len = Uutil.Filesize.zero then
Printf.sprintf "%5s " (Uutil.Filesize.toString b)
else Util.percent2string (Uutil.Filesize.percentageOfTotalSize b len) in
let dbg = if Trace.enabled "progress" then dbg ^ "/" else "" in
let newstatus = dbg ^ newstatus in
let oldstatus = mainWindow#cell_text i 3 in
if oldstatus <> newstatus then mainWindow#set_cell ~text:newstatus i 3;
showGlobalProgress bytes;
gtk_sync false;
begin match item.ri.replicas with
Different diff ->
begin match diff.direction with
Replica1ToReplica2 ->
if !root2IsLocal then
clientWritten := !clientWritten +. Uutil.Filesize.toFloat bytes
else
serverWritten := !serverWritten +. Uutil.Filesize.toFloat bytes
| Replica2ToReplica1 ->
if !root1IsLocal then
clientWritten := !clientWritten +. Uutil.Filesize.toFloat bytes
else
serverWritten := !serverWritten +. Uutil.Filesize.toFloat bytes
| Conflict _ | Merge ->
(* Diff / merge *)
clientWritten := !clientWritten +. Uutil.Filesize.toFloat bytes
end
| _ ->
assert false
end
in
(* Install showProgress so that we get called back by low-level
file transfer stuff *)
Uutil.setProgressPrinter showProgress;
(* Apply new ignore patterns to the current state, expecting that the
number of reconitems will grow smaller. Adjust the display, being
careful to keep the cursor as near as possible to its position
before the new ignore patterns take effect. *)
let ignoreAndRedisplay () =
let lst = Array.to_list !theState in
(* FIX: we should actually test whether any prefix is now ignored *)
let keep sI = not (Globals.shouldIgnore sI.ri.path1) in
begin match currentRow () with
None ->
theState := Array.of_list (Safelist.filter keep lst);
current := IntSet.empty
| Some index ->
let i = ref index in
let l = ref [] in
Array.iteri
(fun j sI -> if keep sI then l := sI::!l
else if j < !i then decr i)
!theState;
theState := Array.of_list (Safelist.rev !l);
current :=
if !l = [] then IntSet.empty
else IntSet.singleton (min (!i) ((Array.length !theState) - 1))
end;
displayMain() in
let sortAndRedisplay () =
current := IntSet.empty;
let compareRIs = Sortri.compareReconItems() in
Array.stable_sort (fun si1 si2 -> compareRIs si1.ri si2.ri) !theState;
displayMain() in
(******************************************************************
Main detect-updates-and-reconcile logic
******************************************************************)
let commitUpdates () =
Trace.status "Updating synchronizer state";
let t = Trace.startTimer "Updating synchronizer state" in
gtk_sync true;
Update.commitUpdates();
Trace.showTimer t
in
let clearMainWindow () =
grDisactivateAll ();
make_busy toplevelWindow;
mainWindow#clear();
detailsWindow#buffer#set_text ""
in
let detectUpdatesAndReconcile () =
clearMainWindow ();
startStats ();
progressBarPulse := true;
sync_action := Some (fun () -> progressBar#pulse ());
let findUpdates () =
let t = Trace.startTimer "Checking for updates" in
Trace.status "Looking for changes";
let updates = Update.findUpdates ~wantWatcher:() !unsynchronizedPaths in
Trace.showTimer t;
updates in
let reconcile updates =
let t = Trace.startTimer "Reconciling" in
let reconRes = Recon.reconcileAll ~allowPartial:true updates in
Trace.showTimer t;
reconRes in
let (reconItemList, thereAreEqualUpdates, dangerousPaths) =
reconcile (findUpdates ()) in
if not !Update.foundArchives then commitUpdates ();
if reconItemList = [] then
if thereAreEqualUpdates then begin
if !Update.foundArchives then commitUpdates ();
Trace.status
"Replicas have been changed only in identical ways since last sync"
end else
Trace.status "Everything is up to date"
else
Trace.status "Check and/or adjust selected actions; then press Go";
theState :=
Array.of_list
(Safelist.map
(fun ri -> { ri = ri;
bytesTransferred = Uutil.Filesize.zero;
bytesToTransfer = Uutil.Filesize.zero;
whatHappened = None })
reconItemList);
unsynchronizedPaths :=
Some (List.map (fun ri -> ri.path1) reconItemList, []);
current := IntSet.empty;
displayMain();
progressBarPulse := false; sync_action := None; displayGlobalProgress 0.;
stopStats ();
grSet grGo (Array.length !theState > 0);
grSet grRescan true;
make_interactive toplevelWindow;
if Prefs.read Globals.confirmBigDeletes then begin
if dangerousPaths <> [] then begin
Prefs.set Globals.batch false;
Util.warn (Uicommon.dangerousPathMsg dangerousPaths)
end;
end;
in
(*********************************************************************
Help menu
*********************************************************************)
let addDocSection (shortname, (name, docstr)) =
if shortname = "about" then
ignore (helpMenu#add_image_item
~stock:`ABOUT ~callback:(fun () -> documentation shortname)
name)
else if shortname <> "" && name <> "" then
ignore (helpMenu#add_item
~callback:(fun () -> documentation shortname)
name) in
Safelist.iter addDocSection Strings.docs;
(*********************************************************************
Ignore menu
*********************************************************************)
let addRegExpByPath pathfunc =
Util.StringSet.iter (fun pat -> Uicommon.addIgnorePattern pat)
(IntSet.fold
(fun i s -> Util.StringSet.add (pathfunc !theState.(i).ri.path1) s)
!current Util.StringSet.empty);
ignoreAndRedisplay ()
in
grAdd grAction
(ignoreMenu#add_item ~key:GdkKeysyms._i
~callback:(fun () -> getLock (fun () ->
addRegExpByPath Uicommon.ignorePath))
"Permanently Ignore This _Path");
grAdd grAction
(ignoreMenu#add_item ~key:GdkKeysyms._E
~callback:(fun () -> getLock (fun () ->
addRegExpByPath Uicommon.ignoreExt))
"Permanently Ignore Files with this _Extension");
grAdd grAction
(ignoreMenu#add_item ~key:GdkKeysyms._N
~callback:(fun () -> getLock (fun () ->
addRegExpByPath Uicommon.ignoreName))
"Permanently Ignore Files with this _Name (in any Dir)");
(*
grAdd grRescan
(ignoreMenu#add_item ~callback:
(fun () -> getLock ignoreDialog) "Edit ignore patterns");
*)
(*********************************************************************
Sort menu
*********************************************************************)
grAdd grRescan
(sortMenu#add_item
~callback:(fun () -> getLock (fun () ->
Sortri.sortByName();
sortAndRedisplay()))
"Sort by _Name");
grAdd grRescan
(sortMenu#add_item
~callback:(fun () -> getLock (fun () ->
Sortri.sortBySize();
sortAndRedisplay()))
"Sort by _Size");
grAdd grRescan
(sortMenu#add_item
~callback:(fun () -> getLock (fun () ->
Sortri.sortNewFirst();
sortAndRedisplay()))
"Sort Ne_w Entries First (toggle)");
grAdd grRescan
(sortMenu#add_item
~callback:(fun () -> getLock (fun () ->
Sortri.restoreDefaultSettings();
sortAndRedisplay()))
"_Default Ordering");
(*********************************************************************
Main function : synchronize
*********************************************************************)
let synchronize () =
if Array.length !theState = 0 then
Trace.status "Nothing to synchronize"
else begin
grDisactivateAll ();
make_busy toplevelWindow;
Trace.status "Propagating changes";
Transport.logStart ();
let totalLength =
Array.fold_left
(fun l si ->
si.bytesTransferred <- Uutil.Filesize.zero;
let len =
if si.whatHappened = None then Common.riLength si.ri else
Uutil.Filesize.zero
in
si.bytesToTransfer <- len;
Uutil.Filesize.add l len)
Uutil.Filesize.zero !theState in
initGlobalProgress totalLength;
let t = Trace.startTimer "Propagating changes" in
let im = Array.length !theState in
let rec loop i actions pRiThisRound =
if i < im then begin
let theSI = !theState.(i) in
let textDetailed = ref None in
let action =
match theSI.whatHappened with
None ->
if not (pRiThisRound theSI.ri) then
return ()
else
catch (fun () ->
Transport.transportItem
theSI.ri (Uutil.File.ofLine i)
(fun title text ->
textDetailed := (Some text);
if Prefs.read Uicommon.confirmmerge then
twoBoxAdvanced
~parent:toplevelWindow
~title:title
~message:("Do you want to commit the changes to"
^ " the replicas ?")
~longtext:text
~advLabel:"View details..."
~astock:`YES
~bstock:`NO
else
true)
>>= (fun () ->
return Util.Succeeded))
(fun e ->
match e with
Util.Transient s ->
return (Util.Failed s)
| _ ->
fail e)
>>= (fun res ->
let rem =
Uutil.Filesize.sub
theSI.bytesToTransfer theSI.bytesTransferred
in
if rem <> Uutil.Filesize.zero then
showProgress (Uutil.File.ofLine i) rem "done";
theSI.whatHappened <- Some (res, !textDetailed);
fastRedisplay i;
(* JV (7/09): It does not seem that useful to me to scroll the display
to make the first unfinished item visible. The scrolling is way
too fast, and it makes it impossible to browse the list. *)
(*
sync_action :=
Some
(fun () ->
makeFirstUnfinishedVisible pRiThisRound;
sync_action := None);
*)
gtk_sync false;
return ())
| Some _ ->
return () (* Already processed this one (e.g. merged it) *)
in
loop (i + 1) (action :: actions) pRiThisRound
end else
actions
in
startStats ();
Lwt_unix.run
(let actions = loop 0 [] (fun ri -> not (Common.isDeletion ri)) in
Lwt_util.join actions);
Lwt_unix.run
(let actions = loop 0 [] Common.isDeletion in
Lwt_util.join actions);
Transport.logFinish ();
Trace.showTimer t;
commitUpdates ();
stopStats ();
let failureList =
Array.fold_right
(fun si l ->
match si.whatHappened with
Some (Util.Failed err, _) ->
(si, [err], "transport failure") :: l
| _ ->
l)
!theState []
in
let failureCount = List.length failureList in
let failures =
if failureCount = 0 then [] else
[Printf.sprintf "%d failure%s"
failureCount (if failureCount = 1 then "" else "s")]
in
let partialList =
Array.fold_right
(fun si l ->
match si.whatHappened with
Some (Util.Succeeded, _)
when partiallyProblematic si.ri &&
not (problematic si.ri) ->
let errs =
match si.ri.replicas with
Different diff -> diff.errors1 @ diff.errors2
| _ -> assert false
in
(si, errs,
"partial transfer (errors during update detection)") :: l
| _ ->
l)
!theState []
in
let partialCount = List.length partialList in
let partials =
if partialCount = 0 then [] else
[Printf.sprintf "%d partially transferred" partialCount]
in
let skippedList =
Array.fold_right
(fun si l ->
match si.ri.replicas with
Problem err ->
(si, [err], "error during update detection") :: l
| Different diff when isConflict diff.direction ->
(si, [],
if isConflict diff.default_direction then
"conflict"
else "skipped") :: l
| _ ->
l)
!theState []
in
let skippedCount = List.length skippedList in
let skipped =
if skippedCount = 0 then [] else
[Printf.sprintf "%d skipped" skippedCount]
in
unsynchronizedPaths :=
Some (List.map (fun (si, _, _) -> si.ri.path1)
(failureList @ partialList @ skippedList),
[]);
Trace.status
(Printf.sprintf "Synchronization complete %s"
(String.concat ", " (failures @ partials @ skipped)));
displayGlobalProgress 0.;
grSet grRescan true;
make_interactive toplevelWindow;
let totalCount = failureCount + partialCount + skippedCount in
if totalCount > 0 then begin
let format n item sing plur =
match n with
0 -> []
| 1 -> [Format.sprintf "one %s%s" item sing]
| n -> [Format.sprintf "%d %s%s" n item plur]
in
let infos =
format failureCount "failure" "" "s" @
format partialCount "partially transferred director" "y" "ies" @
format skippedCount "skipped item" "" "s"
in
let message =
(if failureCount = 0 then "The synchronization was successful.\n\n"
else "") ^
"The replicas are not fully synchronized.\n" ^
(if totalCount < 2 then "There was" else "There were") ^
begin match infos with
[] -> assert false
| [x] -> " " ^ x
| l -> ":\n - " ^ String.concat ";\n - " l
end ^
"."
in
summaryBox ~parent:toplevelWindow
~title:"Synchronization summary" ~message ~f:
(fun t ->
let bullet = "\xe2\x80\xa2 " in
let layout = t#misc#pango_context#create_layout in
Pango.Layout.set_text layout bullet;
let (n, _) = Pango.Layout.get_pixel_size layout in
let path =
t#buffer#create_tag [`FONT_DESC (Lazy.force fontBold)] in
let description =
t#buffer#create_tag [`FONT_DESC (Lazy.force fontItalic)] in
let errorFirstLine =
t#buffer#create_tag [`LEFT_MARGIN (n); `INDENT (- n)] in
let errorNextLines =
t#buffer#create_tag [`LEFT_MARGIN (2 * n)] in
List.iter
(fun (si, errs, desc) ->
t#buffer#insert ~tags:[path]
(transcodeFilename (Path.toString si.ri.path1));
t#buffer#insert ~tags:[description]
(" \xe2\x80\x94 " ^ desc ^ "\n");
List.iter
(fun err ->
let errl =
Str.split (Str.regexp_string "\n") (transcode err) in
match errl with
[] ->
()
| f :: rem ->
t#buffer#insert ~tags:[errorFirstLine]
(bullet ^ f ^ "\n");
List.iter
(fun n ->
t#buffer#insert ~tags:[errorNextLines]
(n ^ "\n"))
rem)
errs)
(failureList @ partialList @ skippedList))
end
end in
(*********************************************************************
Buttons for -->, M, <--, Skip
*********************************************************************)
let doActionOnRow f i =
let theSI = !theState.(i) in
begin match theSI.whatHappened, theSI.ri.replicas with
None, Different diff ->
f theSI.ri diff;
redisplay i
| _ ->
()
end
in
let updateCurrent () =
let n = mainWindow#rows in
(* This has quadratic complexity, thus we only do it when
the list is not too long... *)
if n < 300 then begin
current := IntSet.empty;
for i = 0 to n -1 do
if mainWindow#get_row_state i = `SELECTED then
current := IntSet.add i !current
done
end
in
let doAction f =
(* FIX: when the window does not have the focus, we are not notified
immediately from changes to the list of selected items. So, we
update our view of the current selection here. *)
updateCurrent ();
match currentRow () with
Some i ->
doActionOnRow f i;
nextInteresting ()
| None ->
(* FIX: this is quadratic when all items are selected.
We could trigger a redisplay instead, but it may be tricky
to preserve the set of selected rows, the focus row and the
scrollbar position.
The right fix is probably to move to a GTree.column_list. *)
let n = IntSet.cardinal !current in
if n > 0 then begin
if n > 20 then mainWindow#freeze ();
IntSet.iter (fun i -> doActionOnRow f i) !current;
if n > 20 then mainWindow#thaw ()
end
in
let leftAction _ =
doAction (fun _ diff -> diff.direction <- Replica2ToReplica1) in
let rightAction _ =
doAction (fun _ diff -> diff.direction <- Replica1ToReplica2) in
let questionAction _ = doAction (fun _ diff -> diff.direction <- Conflict "") in
let mergeAction _ = doAction (fun _ diff -> diff.direction <- Merge) in
(* actionBar#insert_space ();*)
grAdd grAction
(actionBar#insert_button
(* ~icon:((GMisc.pixmap rightArrowBlack ())#coerce)*)
~icon:((GMisc.image ~stock:`GO_FORWARD ())#coerce)
~text:"Left to Right"
~tooltip:"Propagate selected items\n\
from the left replica to the right one"
~callback:rightAction ());
(* actionBar#insert_space ();*)
grAdd grAction
(actionBar#insert_button ~text:"Skip"
~icon:((GMisc.image ~stock:`NO ())#coerce)
~tooltip:"Skip selected items"
~callback:questionAction ());
(* actionBar#insert_space ();*)
grAdd grAction
(actionBar#insert_button
(* ~icon:((GMisc.pixmap leftArrowBlack ())#coerce)*)
~icon:((GMisc.image ~stock:`GO_BACK ())#coerce)
~text:"Right to Left"
~tooltip:"Propagate selected items\n\
from the right replica to the left one"
~callback:leftAction ());
(* actionBar#insert_space ();*)
grAdd grAction
(actionBar#insert_button
(* ~icon:((GMisc.pixmap mergeLogoBlack())#coerce)*)
~icon:((GMisc.image ~stock:`ADD ())#coerce)
~text:"Merge"
~tooltip:"Merge selected files"
~callback:mergeAction ());
(*********************************************************************
Diff / merge buttons
*********************************************************************)
let diffCmd () =
match currentRow () with
Some i ->
getLock (fun () ->
let item = !theState.(i) in
let len =
match item.ri.replicas with
Problem _ ->
Uutil.Filesize.zero
| Different diff ->
snd (if !root1IsLocal then diff.rc2 else diff.rc1).size
in
item.bytesTransferred <- Uutil.Filesize.zero;
item.bytesToTransfer <- len;
initGlobalProgress len;
startStats ();
Uicommon.showDiffs item.ri
(fun title text ->
messageBox ~title:(transcode title) (transcode text))
Trace.status (Uutil.File.ofLine i);
stopStats ();
displayGlobalProgress 0.;
fastRedisplay i)
| None ->
() in
actionBar#insert_space ();
grAdd grDiff (actionBar#insert_button ~text:"Diff"
~icon:((GMisc.image ~stock:`DIALOG_INFO ())#coerce)
~tooltip:"Compare the two files at each replica"
~callback:diffCmd ());
(*********************************************************************
Detail button
*********************************************************************)
(* actionBar#insert_space ();*)
grAdd grDetail (actionBar#insert_button ~text:"Details"
~icon:((GMisc.image ~stock:`INFO ())#coerce)
~tooltip:"Show detailed information about\n\
an item, when available"
~callback:showDetCommand ());
(*********************************************************************
Quit button
*********************************************************************)
(* actionBar#insert_space ();
ignore (actionBar#insert_button ~text:"Quit"
~icon:((GMisc.image ~stock:`QUIT ())#coerce)
~tooltip:"Exit Unison"
~callback:safeExit ());
*)
(*********************************************************************
go button
*********************************************************************)
actionBar#insert_space ();
grAdd grGo
(actionBar#insert_button ~text:"Go"
(* tooltip:"Go with displayed actions" *)
~icon:((GMisc.image ~stock:`EXECUTE ())#coerce)
~tooltip:"Perform the synchronization"
~callback:(fun () ->
getLock synchronize) ());
(* Does not quite work: too slow, and Files.copy must be modifed to
support an interruption without error. *)
(*
ignore (actionBar#insert_button ~text:"Stop"
~icon:((GMisc.image ~stock:`STOP ())#coerce)
~tooltip:"Exit Unison"
~callback:Abort.all ());
*)
(*********************************************************************
Rescan button
*********************************************************************)
let updateFromProfile = ref (fun () -> ()) in
let loadProfile p reload =
debug (fun()-> Util.msg "Loading profile %s..." p);
Trace.status "Loading profile";
unsynchronizedPaths := None;
Uicommon.initPrefs p
(fun () -> if not reload then displayWaitMessage ())
getFirstRoot getSecondRoot termInteract;
!updateFromProfile ()
in
let reloadProfile () =
let n =
match !Prefs.profileName with
None -> assert false
| Some n -> n
in
clearMainWindow ();
if not (Prefs.profileUnchanged ()) then loadProfile n true
in
let detectCmd () =
getLock detectUpdatesAndReconcile;
updateDetails ();
if Prefs.read Globals.batch then begin
Prefs.set Globals.batch false; synchronize()
end
in
(* actionBar#insert_space ();*)
grAdd grRescan
(actionBar#insert_button ~text:"Rescan"
~icon:((GMisc.image ~stock:`REFRESH ())#coerce)
~tooltip:"Check for updates"
~callback: (fun () -> reloadProfile(); detectCmd()) ());
(*********************************************************************
Profile change button
*********************************************************************)
actionBar#insert_space ();
let profileChange _ =
match getProfile false with
None -> ()
| Some p -> clearMainWindow (); loadProfile p false; detectCmd ()
in
grAdd grRescan (actionBar#insert_button ~text:"Change Profile"
~icon:((GMisc.image ~stock:`OPEN ())#coerce)
~tooltip:"Select a different profile"
~callback:profileChange ());
(*********************************************************************
Keyboard commands
*********************************************************************)
ignore
(mainWindow#event#connect#key_press ~callback:
begin fun ev ->
let key = GdkEvent.Key.keyval ev in
if key = GdkKeysyms._Left then begin
leftAction (); GtkSignal.stop_emit (); true
end else if key = GdkKeysyms._Right then begin
rightAction (); GtkSignal.stop_emit (); true
end else
false
end);
(*********************************************************************
Action menu
*********************************************************************)
let buildActionMenu init =
let actionMenu = replace_submenu "_Actions" actionItem in
grAdd grRescan
(actionMenu#add_image_item
~callback:(fun _ -> mainWindow#select_all ())
~image:((GMisc.image ~stock:`SELECT_ALL ~icon_size:`MENU ())#coerce)
~modi:[`CONTROL] ~key:GdkKeysyms._A
"Select _All");
grAdd grRescan
(actionMenu#add_item
~callback:(fun _ -> mainWindow#unselect_all ())
~modi:[`SHIFT; `CONTROL] ~key:GdkKeysyms._A
"_Deselect All");
ignore (actionMenu#add_separator ());
let (loc1, loc2) =
if init then ("", "") else
let (root1,root2) = Globals.roots () in
(root2hostname root1, root2hostname root2)
in
let def_descr = "Left to Right" in
let descr =
if init || loc1 = loc2 then def_descr else
Printf.sprintf "from %s to %s" loc1 loc2 in
let left =
actionMenu#add_image_item ~key:GdkKeysyms._greater ~callback:rightAction
~image:((GMisc.image ~stock:`GO_FORWARD ~icon_size:`MENU ())#coerce)
~name:("Propagate " ^ def_descr) ("Propagate " ^ descr) in
grAdd grAction left;
left#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._greater;
left#add_accelerator ~group:accel_group GdkKeysyms._period;
let def_descl = "Right to Left" in
let descl =
if init || loc1 = loc2 then def_descl else
Printf.sprintf "from %s to %s"
(Unicode.protect loc2) (Unicode.protect loc1) in
let right =
actionMenu#add_image_item ~key:GdkKeysyms._less ~callback:leftAction
~image:((GMisc.image ~stock:`GO_BACK ~icon_size:`MENU ())#coerce)
~name:("Propagate " ^ def_descl) ("Propagate " ^ descl) in
grAdd grAction right;
right#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._less;
right#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._comma;
let skip =
actionMenu#add_image_item ~key:GdkKeysyms._slash ~callback:questionAction
~image:((GMisc.image ~stock:`NO ~icon_size:`MENU ())#coerce)
"Do _Not Propagate Changes" in
grAdd grAction skip;
skip#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._minus;
let merge =
actionMenu#add_image_item ~key:GdkKeysyms._m ~callback:mergeAction
~image:((GMisc.image ~stock:`ADD ~icon_size:`MENU ())#coerce)
"_Merge the Files" in
grAdd grAction merge;
(* merge#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._m; *)
(* Override actions *)
ignore (actionMenu#add_separator ());
grAdd grAction
(actionMenu#add_item
~callback:(fun () ->
doAction (fun ri _ ->
Recon.setDirection ri `Replica1ToReplica2 `Prefer))
"Resolve Conflicts in Favor of First Root");
grAdd grAction
(actionMenu#add_item
~callback:(fun () ->
doAction (fun ri _ ->
Recon.setDirection ri `Replica2ToReplica1 `Prefer))
"Resolve Conflicts in Favor of Second Root");
grAdd grAction
(actionMenu#add_item
~callback:(fun () ->
doAction (fun ri _ ->
Recon.setDirection ri `Newer `Prefer))
"Resolve Conflicts in Favor of Most Recently Modified");
grAdd grAction
(actionMenu#add_item
~callback:(fun () ->
doAction (fun ri _ ->
Recon.setDirection ri `Older `Prefer))
"Resolve Conflicts in Favor of Least Recently Modified");
ignore (actionMenu#add_separator ());
grAdd grAction
(actionMenu#add_item
~callback:(fun () ->
doAction (fun ri _ -> Recon.setDirection ri `Newer `Force))
"Force Newer Files to Replace Older Ones");
grAdd grAction
(actionMenu#add_item
~callback:(fun () ->
doAction (fun ri _ -> Recon.setDirection ri `Older `Force))
"Force Older Files to Replace Newer Ones");
ignore (actionMenu#add_separator ());
grAdd grAction
(actionMenu#add_item
~callback:(fun () ->
doAction (fun ri _ -> Recon.revertToDefaultDirection ri))
"_Revert to Unison's Recommendations");
grAdd grAction
(actionMenu#add_item
~callback:(fun () ->
doAction (fun ri _ -> Recon.setDirection ri `Merge `Force))
"Revert to the Merging Default, if Available");
(* Diff *)
ignore (actionMenu#add_separator ());
grAdd grDiff (actionMenu#add_image_item ~key:GdkKeysyms._d ~callback:diffCmd
~image:((GMisc.image ~stock:`DIALOG_INFO ~icon_size:`MENU ())#coerce)
"Show _Diffs");
(* Details *)
grAdd grDetail
(actionMenu#add_image_item ~key:GdkKeysyms._i ~callback:showDetCommand
~image:((GMisc.image ~stock:`INFO ~icon_size:`MENU ())#coerce)
"Detailed _Information")
in
buildActionMenu true;
(*********************************************************************
Synchronization menu
*********************************************************************)
grAdd grGo
(fileMenu#add_image_item ~key:GdkKeysyms._g
~image:(GMisc.image ~stock:`EXECUTE ~icon_size:`MENU () :> GObj.widget)
~callback:(fun () -> getLock synchronize)
"_Go");
grAdd grRescan
(fileMenu#add_image_item ~key:GdkKeysyms._r
~image:(GMisc.image ~stock:`REFRESH ~icon_size:`MENU () :> GObj.widget)
~callback:(fun () -> reloadProfile(); detectCmd())
"_Rescan");
grAdd grRescan
(fileMenu#add_item ~key:GdkKeysyms._a
~callback:(fun () ->
reloadProfile();
Prefs.set Globals.batch true;
detectCmd())
"_Detect Updates and Proceed (Without Waiting)");
grAdd grRescan
(fileMenu#add_item ~key:GdkKeysyms._f
~callback:(
fun () ->
let rec loop i acc =
if i >= Array.length (!theState) then acc else
let notok =
(match !theState.(i).whatHappened with
None-> true
| Some(Util.Failed _, _) -> true
| Some(Util.Succeeded, _) -> false)
|| match !theState.(i).ri.replicas with
Problem _ -> true
| Different diff -> isConflict diff.direction in
if notok then loop (i+1) (i::acc)
else loop (i+1) (acc) in
let failedindices = loop 0 [] in
let failedpaths =
Safelist.map (fun i -> !theState.(i).ri.path1) failedindices in
debug (fun()-> Util.msg "Rescaning with paths = %s\n"
(String.concat ", " (Safelist.map
(fun p -> "'"^(Path.toString p)^"'")
failedpaths)));
let paths = Prefs.read Globals.paths in
let confirmBigDeletes = Prefs.read Globals.confirmBigDeletes in
Prefs.set Globals.paths failedpaths;
Prefs.set Globals.confirmBigDeletes false;
(* Modifying global paths does not play well with filesystem
monitoring, so we disable it. *)
unsynchronizedPaths := None;
detectCmd();
Prefs.set Globals.paths paths;
Prefs.set Globals.confirmBigDeletes confirmBigDeletes;
unsynchronizedPaths := None)
"Re_check Unsynchronized Items");
ignore (fileMenu#add_separator ());
grAdd grRescan
(fileMenu#add_image_item ~key:GdkKeysyms._p
~callback:(fun _ ->
match getProfile false with
None -> ()
| Some(p) -> clearMainWindow (); loadProfile p false; detectCmd ())
~image:(GMisc.image ~stock:`OPEN ~icon_size:`MENU () :> GObj.widget)
"Change _Profile...");
let fastProf name key =
grAdd grRescan
(fileMenu#add_item ~key:key
~callback:(fun _ ->
if System.file_exists (Prefs.profilePathname name) then begin
Trace.status ("Loading profile " ^ name);
loadProfile name false; detectCmd ()
end else
Trace.status ("Profile " ^ name ^ " not found"))
("Select profile " ^ name)) in
let fastKeysyms =
[| GdkKeysyms._0; GdkKeysyms._1; GdkKeysyms._2; GdkKeysyms._3;
GdkKeysyms._4; GdkKeysyms._5; GdkKeysyms._6; GdkKeysyms._7;
GdkKeysyms._8; GdkKeysyms._9 |] in
Array.iteri
(fun i v -> match v with
None -> ()
| Some(profile, info) ->
fastProf profile fastKeysyms.(i))
profileKeymap;
ignore (fileMenu#add_separator ());
ignore (fileMenu#add_item
~callback:(fun _ -> statWin#show ()) "Show _Statistics");
ignore (fileMenu#add_separator ());
let quit =
fileMenu#add_image_item
~key:GdkKeysyms._q ~callback:safeExit
~image:((GMisc.image ~stock:`QUIT ~icon_size:`MENU ())#coerce)
"_Quit"
in
quit#add_accelerator ~group:accel_group ~modi:[`CONTROL] GdkKeysyms._q;
(*********************************************************************
Expert menu
*********************************************************************)
if Prefs.read Uicommon.expert then begin
let (expertMenu, _) = add_submenu "Expert" in
let addDebugToggle modname =
let cm =
expertMenu#add_check_item ~active:(Trace.enabled modname)
~callback:(fun b -> Trace.enable modname b)
("Debug '" ^ modname ^ "'") in
cm#set_show_toggle true in
addDebugToggle "all";
addDebugToggle "verbose";
addDebugToggle "update";
ignore (expertMenu#add_separator ());
ignore (expertMenu#add_item
~callback:(fun () ->
Printf.fprintf stderr "\nGC stats now:\n";
Gc.print_stat stderr;
Printf.fprintf stderr "\nAfter major collection:\n";
Gc.full_major(); Gc.print_stat stderr;
flush stderr)
"Show memory/GC stats")
end;
(*********************************************************************
Finish up
*********************************************************************)
grDisactivateAll ();
updateFromProfile :=
(fun () ->
displayNewProfileLabel ();
setMainWindowColumnHeaders (Uicommon.roots2string ());
buildActionMenu false);
ignore (toplevelWindow#event#connect#delete ~callback:
(fun _ -> safeExit (); true));
toplevelWindow#show ();
fun () ->
!updateFromProfile ();
mainWindow#misc#grab_focus ();
detectCmd ()
(*********************************************************************
STARTUP
*********************************************************************)
let start _ =
begin try
(* Initialize the GTK library *)
ignore (GMain.Main.init ());
Util.warnPrinter :=
Some (fun msg -> warnBox ~parent:(toplevelWindow ()) "Warning" msg);
GtkSignal.user_handler :=
(fun exn ->
match exn with
Util.Transient(s) | Util.Fatal(s) -> fatalError s
| exn -> fatalError (Uicommon.exn2string exn));
(* Ask the Remote module to call us back at regular intervals during
long network operations. *)
let rec tick () =
gtk_sync true;
Lwt_unix.sleep 0.05 >>= tick
in
ignore_result (tick ());
Os.createUnisonDir();
scanProfiles();
let detectCmd = createToplevelWindow() in
Uicommon.uiInit
fatalError
tryAgainOrQuit
displayWaitMessage
(fun () -> getProfile true)
getFirstRoot
getSecondRoot
termInteract;
detectCmd ();
(* Display the ui *)
(*JV: not useful, as Unison does not handle any signal
ignore (GMain.Timeout.add 500 (fun _ -> true));
(* Hack: this allows signals such as SIGINT to be
handled even when Gtk is waiting for events *)
*)
GMain.Main.main ()
with
Util.Transient(s) | Util.Fatal(s) -> fatalError s
| exn -> fatalError (Uicommon.exn2string exn)
end
end (* module Private *)
(*********************************************************************
UI SELECTION
*********************************************************************)
module Body : Uicommon.UI = struct
let start = function
Uicommon.Text -> Uitext.Body.start Uicommon.Text
| Uicommon.Graphic ->
let displayAvailable =
Util.osType = `Win32
||
try System.getenv "DISPLAY" <> "" with Not_found -> false
in
if displayAvailable then Private.start Uicommon.Graphic
else Uitext.Body.start Uicommon.Text
let defaultUi = Uicommon.Graphic
end (* module Body *)