Below is the file 'ui.ml' from this revision. You can also download the file.

open Viz_misc
open Viz_types

let valid_utf8 = Glib.Utf8.validate

let wrap_in_scroll_window packing =
  let sw = GBin.scrolled_window
      ~hpolicy:`AUTOMATIC
      ~vpolicy:`AUTOMATIC ~packing () in
  sw#add

let error_notice ~parent message =
  let d = GWindow.message_dialog
      ~message ~message_type:`ERROR
      ~buttons:GWindow.Buttons.close
      ~parent ~destroy_with_parent:true ~show:true () in
  ignore (d#connect#after#close d#destroy) ;
  ignore (d#connect#response (fun _ -> d#destroy ()))

let error_notice_f ~parent fmt =
  Printf.kprintf (error_notice ~parent) fmt

let with_grab f =
  let w = Viz_gmisc.invisible_new () in
  GtkMain.Grab.add w ;
  try
    let r = f () in
    GtkMain.Grab.remove w ; GtkBase.Object.destroy w ; r
  with exn ->
    GtkMain.Grab.remove w ; GtkBase.Object.destroy w ; raise exn

let pump () =
  while Glib.Main.iteration false do () done

let fold_in_loop ?(granularity=10) f init l =
  with_grab (fun () ->
    let i = ref 0 in
    List.fold_left
      (fun acc e ->
	incr i ;
	if !i mod granularity = 0
	then pump () ;
	f acc e)
      init
      l)

let add_label ~text ~packing =
  ignore (GMisc.label ~text ~packing ())

module Busy = struct
  let busy_cursor   = lazy (Gdk.Cursor.create `WATCH)
  let normal_cursor = lazy (Gdk.Cursor.create `LEFT_PTR)

  let set_busy_cursor w busy =
    Gdk.Window.set_cursor w#misc#window
      (Lazy.force
	 (if busy then busy_cursor else normal_cursor))

  type t = {
      widget : GObj.widget ;
      mutable depth : int ;
      mutable timer_id : Glib.Timeout.id option ;
    }

  let make w =
    { widget = w#coerce ; depth = 0 ; timer_id = None }

  let start b =
    b.depth <- b.depth + 1 ;
    match b.timer_id with
    | None when b.depth = 1 ->
	let id =
	  Glib.Timeout.add 500
	    (fun () ->
	      b.timer_id <- None ;
	      set_busy_cursor b.widget true ;
	      false) in
	b.timer_id <- Some id
    | _ -> ()

  let stop b =
    match b.timer_id with
    | None when b.depth = 1 ->
	set_busy_cursor b.widget false ;
	b.depth <- 0
    | Some id when b.depth = 1 ->
	Glib.Timeout.remove id ;
	b.timer_id <- None ;
	b.depth <- 0
    | _ when b.depth > 1 ->
	b.depth <- b.depth - 1
    | _ -> ()
end


let category title packing =
  let vb = GPack.vbox ~packing () in
  let _ =
    GMisc.label
      ~markup:(Printf.sprintf "<b>%s</b>" (Glib.Markup.escape_text title))
      ~xalign:0.
      ~packing:(vb#pack ~expand:false) () in
  let al = GBin.alignment ~border_width:8 ~packing:(vb#pack ~expand:true) () in
  al#misc#set_property "left-padding" (`INT 16) ;
  (GPack.vbox ~packing:al#add ())#pack


let make_factory () =
  let set = GtkStock.Icon_factory.lookup_default "gtk-execute" in
  let add id label =
    GtkStock.Item.add
      { GtkStock.stock_id = id ; label = label ;
	modifier = [] ; keyval = 0 } ;
    ignore (GtkStock.make_icon_factory ~icons:[ `STOCK id, set ] ()) in
  add "mviz-query" "_Query" ;
  add "mviz-view"  "_View"

let _ = make_factory ()


class status_bar ~packing =
  let status = GMisc.statusbar ~packing () in
  let progress = GRange.progress_bar () in
  let _ =
    (* work around some limitations in the GtkStatusBar mapping *)
    let status_w = status#as_widget in
    let b = GtkPack.Box.cast status_w in
    GtkPack.Box.pack_end b progress#as_widget false false 0 ;
    Gobject.Property.set_dyn status_w "has-resize-grip" (`BOOL false) in
  fun description ->
  object (self)
    val ctx = status#new_context description

    method push msg =
      ignore (ctx#push msg)

    method pop =
      ctx#pop

    val mutable total = 0.
    val mutable count = 0
    val mutable last_fraction = 0.

    method progress_start msg tot =
      progress#set_fraction 0. ;
      total <- float tot ;
      count <- 0 ;
      last_fraction <- 0. ;
      ignore (ctx#push msg)

    method progress nb =
      count <- count + nb ;
      let f = float count /. total in
      if f -. last_fraction >= 0.02 then begin
	last_fraction <- f ;
	progress#set_fraction f
      end

    method progress_end () =
      progress#set_fraction 0. ;
      ctx#pop ()

    method with_status : 'a. string -> (unit -> 'a) -> 'a =
      fun msg f ->
	self#push msg ;
	try let res = f () in self#pop () ; res
	with exn -> self#pop () ; raise exn
  end




module Prefs = struct
  let make ctrl =
    let prefs = ref ctrl#get_prefs in
    let w = GWindow.dialog
	~title:"Monotone-viz Preferences"
	~parent:ctrl#get_toplevel
	~destroy_with_parent:true
	~allow_grow:false
	~border_width:8 () in
    begin
      let packing = category "Ancestry Graph Layout" w#vbox#pack in
      let button =
	GButton.check_button
	  ~label:"left-to-right _layout"
	  ~use_mnemonic:true
	  ~active:!prefs.Viz_style.lr_layout
	  ~packing () in
      ignore (button#connect#toggled (fun () ->
	prefs := { !prefs with Viz_style.lr_layout = not !prefs.Viz_style.lr_layout }))
    end ;
    begin
      let packing = category "Autocolouring" w#vbox#pack in
      ignore
	(List.fold_left
	   (fun group (label, autocolor_style) ->
	     let b = GButton.radio_button
		 ?group ~label
		 ~active:(!prefs.Viz_style.autocolor = autocolor_style)
		 ~packing () in
	     ignore (b#connect#toggled (fun () ->
	       if b#active then
		 prefs := { !prefs with Viz_style.autocolor = autocolor_style })) ;
	     if group = None then Some b#group else group)
	   None
	   [ "no automatic coloring", NONE ;
	     "color by author",       BY_AUTHOR_HASH ;
	     "color by branch",       BY_BRANCH_HASH ] )
    end ;
    begin
      let packing = category "External Programs" w#vbox#pack in
      let tb = GPack.table ~columns:2 ~rows:2 ~packing () in
      begin
	let _ = GMisc.label ~text:"monotone: " ~xalign:1.
	    ~packing:(tb#attach ~left:1 ~top:1) () in
	let e = GEdit.entry ~text:(!prefs.Viz_style.monotone_path)
	    ~packing:(tb#attach ~left:2 ~top:1 ~expand:`X) () in
	ignore (e#connect#changed (fun () ->
	  prefs := { !prefs with Viz_style.monotone_path = e#text }))
      end ;
      begin
	let _ = GMisc.label ~text:"dot: " ~xalign:1.
	    ~packing:(tb#attach ~left:1 ~top:2) () in
	let e = GEdit.entry ~text:(!prefs.Viz_style.dot_path)
	    ~packing:(tb#attach ~left:2 ~top:2 ~expand:`X) () in
	ignore (e#connect#changed (fun () ->
	  prefs := { !prefs with Viz_style.dot_path = e#text }))
      end ;
    end ;

    w#add_button_stock `CLOSE `CLOSE ;
    w#add_button_stock `SAVE  `SAVE ;
    w#add_button_stock `APPLY `APPLY ;
    ignore (w#connect#after#close w#misc#hide) ;
    ignore (w#event#connect#delete (fun _ -> w#misc#hide () ; true)) ;
    ignore (w#connect#response (function
      | `APPLY ->
	  ctrl#set_prefs !prefs
      | `SAVE ->
	  Viz_style.save !prefs
      | `CLOSE | `DELETE_EVENT ->
	  w#misc#hide ())) ;
    w

  let update_prefs ctrl old_prefs p =
    let need_layout =
      old_prefs.Viz_style.font <> p.Viz_style.font ||
      old_prefs.Viz_style.lr_layout <> p.Viz_style.lr_layout in
    let need_redraw =
      old_prefs.Viz_style.autocolor <> p.Viz_style.autocolor ||
      old_prefs.Viz_style.style <> p.Viz_style.style in
    if need_layout
    then ctrl#re_layout ()
    else if need_redraw
    then ctrl#redraw ()

  let show ctrl =
    let p = lazy (make ctrl) in
    fun () -> (Lazy.force p)#present ()

end

module About = struct

  let authors  = ["Olivier Andrieu <oandrieu@gmail.com>"]
  let comments = "Lets you visualize ancestry graphs from the Revision Control System monotone"
  let copyright = "Copyright © 2004-2007 Olivier Andrieu"
  let license   = "\
monotone-viz is free software; you can redistribute it
and/or modify it under the terms of the GNU General
Public License as published by the Free Software Foundation;
either version 2 of the License, or (at your option)
any later version."

  let name = "monotone-viz"
  let version = Version.version
  let website = "http://oandrieu.nerim.net/monotone-viz/"
  let website_label = "monotone-viz website"

  let make ctrl =
    let d =
      GWindow.about_dialog
	~authors ~comments ~copyright
	~license ~logo:(Lazy.force Icon.monotone)
	~name ~version ~website ~website_label
	~parent:ctrl#get_toplevel () in
    ignore (d#event#connect#delete (fun _ -> d#misc#hide () ; true)) ;
    ignore (d#connect#response (fun _ -> d#misc#hide ())) ;
    d

  let show ctrl =
    let d = lazy (make ctrl) in
    fun () -> (Lazy.force d)#present ()
end

module Open = struct
  type t = [`OPEN|`CLOSE|`DELETE_EVENT] GWindow.file_chooser_dialog
  let make ctrl =
    let dialog = GWindow.file_chooser_dialog
	~action:`OPEN ~parent:ctrl#get_toplevel
	~destroy_with_parent:true
	~title:"Open a Monotone database" () in
    dialog#add_button_stock `CLOSE `CLOSE ;
    dialog#add_select_button_stock `OPEN `OPEN ;
    ignore (dialog#connect#after#close (fun () -> dialog#response `CLOSE)) ;
    dialog

  let show dialog =
    let resp =
      match dialog#run () with
      | `CLOSE | `DELETE_EVENT -> None
      | `OPEN  -> dialog#filename in
    dialog#misc#hide () ;
    resp
end

(*
module LockedDB = struct

  let message ctrl =
    let db_fname =
      Database.get_filename
	(some ctrl#get_db) in
    Printf.sprintf
      "<big>Database <tt>%s</tt> is currently in use by another process.</big>"
      (Glib.Markup.escape_text db_fname)

  let show ctrl =
    (* for some reason GtkMessageDialog looks ugly here, so I rool my own GtkDialog *)
    let dialog =
      GWindow.dialog
	~no_separator:true
	~parent:ctrl#get_toplevel
	~destroy_with_parent:true
	~title:"Monotone-viz: database locked"
	~modal:true () in
    begin
      let vbox = dialog#vbox in
      vbox#set_border_width 12 ;
      let hbox = GPack.hbox ~spacing:12 ~border_width:12 ~packing:vbox#pack () in
      ignore (GMisc.image
		~stock:`DIALOG_WARNING
		~icon_size:`DIALOG
		~yalign:0.
		~packing:hbox#pack
		()) ;
      ignore (GMisc.label
		~markup:(message ctrl)
		~line_wrap:true ~selectable:true
		~xalign:0. ~yalign:0.
		~packing:(hbox#pack ~expand:true)
		())
    end ;
    begin
      ignore (dialog#connect#close (fun () -> dialog#response `CANCEL)) ;
      dialog#add_button_stock `CANCEL `CANCEL ;
      dialog#add_button "Retry" `RETRY
    end ;
    let resp =
      match dialog#run () with
      | `CANCEL | `DELETE_EVENT -> `FAIL
      | `RETRY -> `RETRY in
    dialog#destroy () ;
    resp

end
*)



let ui_info = "\
  <ui>\
    <toolbar>\
      <toolitem action='New'/>\
      <toolitem action='Refresh'/>\
      <toolitem action='Zoom_in'/>\
      <toolitem action='Zoom_out'/>\
      <toolitem action='Query'/>\
    </toolbar>\
    <popup>\
      <menuitem action='Certs'/>\
      <menuitem action='Diff_one'/>\
      <menu     action='Diff_many'/>\
      <menuitem action='Diff_other'/>\
      <menuitem action='Copy_revision'/>\
      <menuitem action='Copy_manifest'/>\
    </popup>\
    <popup name='popup_cert'>\
      <menuitem action='Copy_cert'/>\
    </popup>\
    <menubar>\
      <menu action='FileMenu'>\
        <menuitem action='Open'/>\
        <menuitem action='Close'/>\
        <menuitem action='New'/>\
        <menuitem action='Prefs'/>\
        <menuitem action='Quit'/>\
      </menu>\
      <menu action='ViewMenu'>\
        <menuitem action='Refresh'/>\
        <menuitem action='Zoom_in'/>\
        <menuitem action='Zoom_out'/>\
        <menuitem action='Query'/>\
      </menu>\
      <menu action='HelpMenu'>\
        <menuitem action='About'/>\
      </menu>\
    </menubar>\
    <accelerator action='FindEntry'/>\
  </ui>"

type popup_data = {
    menu            : GMenu.menu ;
    diff_many       : GMenu.menu_item ;
    mutable popup_id : string ;
    group           : GAction.action_group ;
    mutable signals : (unit Gobject.obj * GtkSignal.id) list ;
    menu_cert       : GMenu.menu ;
    clipboard1       : GData.clipboard ;
    clipboard2       : GData.clipboard ;
  }

type manager = {
    manager      : GAction.ui_manager ;
    db_group     : GAction.action_group ;
    view_group   : GAction.action_group ;
    popup_data   : popup_data Lazy.t ;
  }

let make_groups () =
  let add = GAction.add_action in
  let g_main = GAction.action_group ~name:"main" () in
  GAction.add_actions g_main [
  add "FileMenu" ~label:"_File" ;
  add "Open"  ~stock:`OPEN  ~tooltip:"Open a database" ;
  add "Quit"  ~stock:`QUIT  ~tooltip:"Exit" ;
  add "Prefs" ~stock:`PREFERENCES ~tooltip:"Edit Preferences" ;
  add "FindEntry" ~accel:"<Ctrl>l" ;
  add "HelpMenu" ~label:"_Help" ;
  add "About" ~stock:`ABOUT ] ;
  let g_db = GAction.action_group ~name:"db" () in
  GAction.add_actions g_db [
  add "Close" ~stock:`CLOSE ~tooltip:"Close the database" ;
  add "New"   ~stock:`NEW   ~label:"New view" ~tooltip:"View a monotone ancestry graph" ] ;
  g_db#set_sensitive false ;
  let g_popup = GAction.action_group ~name:"popup" () in
  GAction.add_actions g_popup [
  add "Certs" ~label:"Display certs" ;
  add "Diff_one"   ~label:"Diff with ancestor" ;
  add "Diff_many"  ~label:"Diff with ancestor" ;
  add "Diff_other" ~label:"Diff with selected node" ;
  add "Copy_cert"     ~stock:`COPY ~label:"Copy the cert value to the clipboard" ;
  add "Copy_revision" ~stock:`COPY ~label:"Copy revision id to the clipboard" ;
  add "Copy_manifest" ~stock:`COPY ~label:"Copy manifest id to the clipboard" ] ;
  let g_view = GAction.action_group ~name:"view" () in
  GAction.add_actions g_view [
  add "ViewMenu" ~label:"_View" ;
  add "Refresh"  ~stock:`REFRESH  ~tooltip:"Redraw"   ~accel:"<Ctrl>R" ;
  add "Zoom_in"  ~stock:`ZOOM_IN  ~tooltip:"Zoom in"  ~accel:"<Ctrl>plus" ;
  add "Zoom_out" ~stock:`ZOOM_OUT ~tooltip:"Zoom out" ~accel:"<Ctrl>minus" ;
  add "Query"    ~stock:`FIND     ~tooltip:"Search the database" ] ;
  g_view#set_sensitive false ;
  (g_main, g_db, g_popup, g_view)

let get_obj m name =
  (m#get_widget name)#as_widget

let make_popup_data m g =
  { menu       = new GMenu.menu (GtkMenu.Menu.cast (get_obj m "/popup")) ;
    diff_many  = new GMenu.menu_item (GtkMenu.MenuItem.cast (get_obj m "/popup/Diff_many")) ;
    group      = g ;
    signals    = [] ;
    popup_id   = "" ;
    menu_cert  = new GMenu.menu (GtkMenu.Menu.cast (get_obj m "/popup_cert")) ;
    clipboard1  = GData.clipboard Gdk.Atom.clipboard ;
    clipboard2  = GData.clipboard Gdk.Atom.primary
  }

let make () =
  let m = GAction.ui_manager () in
  let (g_main, g_db, g_popup, g_view) = make_groups () in
  m#insert_action_group g_main  1 ;
  m#insert_action_group g_db    2 ;
  m#insert_action_group g_popup 3 ;
  m#insert_action_group g_view  4 ;
  ignore (m#add_ui_from_string ui_info) ;
  let menubar = m#get_widget "/menubar" in
  let toolbar = m#get_widget "/toolbar" in
  { manager = m ;
    db_group = g_db ; view_group = g_view ;
    popup_data = lazy (make_popup_data m g_popup)
  }, menubar, toolbar

let get_popup_data { popup_data = p } = Lazy.force p

let reset_popup_menu p id =
  List.iter
    (fun (o, id) -> GtkSignal.disconnect o id)
    p.signals ;
  p.signals <- [] ;
  p.diff_many#remove_submenu () ;
  p.popup_id <- id

let popup_cert m button =
  let p = get_popup_data m in
  let time = GtkMain.Main.get_current_event_time () in
  p.menu_cert#popup ~button ~time


let set_clipboard m data =
  let p = get_popup_data m in
  p.clipboard1#set_text data ;
  p.clipboard2#set_text data

let popup m ctrl ~popup_id button =
  let p = get_popup_data m in
  reset_popup_menu p popup_id ;
  let remember_signal o callback =
    p.signals <- (Gobject.coerce o#as_action, o#connect#activate ~callback) :: p.signals in

  (* setup the copy entries *)
  begin
    let copy_revision = p.group#get_action "Copy_revision" in
    let copy_manifest = p.group#get_action "Copy_manifest" in
    let data = Monotone.get_revision (some ctrl#get_mtn) popup_id in
    remember_signal copy_revision
      (fun () -> set_clipboard m data.revision_id) ;
    remember_signal copy_manifest
      (fun () -> set_clipboard m data.manifest_id)
  end ;

  (* Setup the "diff with other entry" *)
  begin
    let diff_other = p.group#get_action "Diff_other" in
    match ctrl#get_selected_node with
    | Some id when id <> popup_id ->
	diff_other#set_sensitive true ;
	remember_signal diff_other
	  (fun () -> ctrl#show_diff id popup_id)
    | _ ->
	diff_other#set_sensitive false
  end ;

  (* Setup the "diff with ancestor(s)" entry *)
  begin
    let diff_one = p.group#get_action "Diff_one" in
    match Agraph.get_ancestors (some ctrl#get_agraph) popup_id with
    | [] ->
	p.diff_many#misc#hide () ;
	diff_one#set_visible true;
	diff_one#set_sensitive false ;
    | [ ancestor_id ] ->
	p.diff_many#misc#hide () ;
	remember_signal diff_one
	  (fun () -> ctrl#show_diff ancestor_id popup_id) ;
	diff_one#set_visible true ;
	diff_one#set_sensitive true
    | a ->
	diff_one#set_visible false ;
	let submenu = GMenu.menu ~packing:p.diff_many#set_submenu () in
	List.iter
          (fun (ancestor_id as label) ->
            let i = GMenu.menu_item ~label ~packing:submenu#append () in
	    ignore (i#connect#activate
		      (fun () -> ctrl#show_diff ancestor_id popup_id)))
	  a ;
	p.diff_many#misc#show ()
  end ;

  (* popup the menu *)
  let time = GtkMain.Main.get_current_event_time () in
  p.menu#popup ~button ~time




let automate_cb auto o =
  begin
    match o with
    | `OUTPUT msg ->
	Printf.eprintf "### automate: output '%s'\n%!" (String.escaped msg) ;
	let message = Printf.sprintf "<tt><b>interface_version</b>: %s</tt>" msg in
	let d =
	  GWindow.message_dialog
	    ~message ~use_markup:true
	    ~message_type:`INFO
	    ~buttons:GWindow.Buttons.close () in
	ignore (d#run ()) ;
	d#destroy ()
    | `SYNTAX_ERROR msg ->
	Printf.eprintf "### automate: syntax error '%s'\n%!" msg
    | `ERROR msg ->
	Printf.eprintf "### automate: error '%s'\n%!" msg
  end ;
  ignore (Glib.Timeout.add 5000 (fun () -> Automate.exit auto ; false))













let setup ({ manager = ui } as m) ctrl =
  ctrl#get_toplevel#add_accel_group
    m.manager#get_accel_group ;

  let action_connect name callback =
    ignore ((ui#get_action name)#connect#activate ~callback) in

  action_connect "/menubar/FileMenu/Close"    ctrl#close_db ;
  action_connect "/menubar/FileMenu/Open"     ctrl#show_open ;
  action_connect "/menubar/FileMenu/Quit"     GMain.quit ;
  action_connect "/menubar/ViewMenu/Zoom_in"  ctrl#zoom_in ;
  action_connect "/menubar/ViewMenu/Zoom_out" ctrl#zoom_out ;
  action_connect "/menubar/ViewMenu/Refresh"  ctrl#reload ;
  action_connect "/menubar/FileMenu/Prefs"    ctrl#show_prefs ;
  action_connect "/menubar/ViewMenu/Query"    ctrl#show_search ;
  action_connect "/menubar/FileMenu/New"      ctrl#show_view ;
  action_connect "/menubar/HelpMenu/About"    (About.show ctrl) ;
  action_connect "/popup/Certs"
    (fun () -> ctrl#display_certs (get_popup_data m).popup_id) ;
  action_connect "/popup_cert/Copy_cert"
    (fun () ->
      may
	(set_clipboard m)
	ctrl#get_current_cert_value) ;
  action_connect "/FindEntry" ctrl#focus_find_entry


let open_db m ctrl =
  m.db_group#set_sensitive true

let close_db m ctrl =
  m.db_group#set_sensitive false

let clear m =
  m.view_group#set_sensitive false

let update_begin m =
  m.view_group#set_sensitive true