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