Gentoo Websites Logo
Go to: Gentoo Home Documentation Forums Lists Bugs Planet Store Wiki Get Gentoo!
View | Details | Raw Unified | Return to bug 769341 | Differences between
and this patch

Collapse All | Expand All

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

Return to bug 769341