(**************************************************************************)
(*                                                                        *)
(*  This file is part of Frama-C.                                         *)
(*                                                                        *)
(*  Copyright (C) 2007-2020                                               *)
(*    CEA (Commissariat à l'énergie atomique et aux énergies              *)
(*         alternatives)                                                  *)
(*                                                                        *)
(*  you can redistribute it and/or modify it under the terms of the GNU   *)
(*  Lesser General Public License as published by the Free Software       *)
(*  Foundation, version 2.1.                                              *)
(*                                                                        *)
(*  It is distributed in the hope that it will be useful,                 *)
(*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *)
(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *)
(*  GNU Lesser General Public License for more details.                   *)
(*                                                                        *)
(*  See the GNU Lesser General Public License version 2.1                 *)
(*  for more details (enclosed in the file licenses/LGPLv2.1).            *)
(*                                                                        *)
(**************************************************************************)

(** Generic Gtk helpers. *)

let () =
  begin
    Wutil.share := Fc_config.datadir;
    Wutil.flush := (fun msg -> Gui_parameters.warning "%s" msg);
  end

let framac_logo, framac_icon =
  try
    let img ext =
      Some (GdkPixbuf.from_file (Fc_config.datadir ^ "/frama-c." ^ ext))
    in
    img "png", img "ico"
  with
  | Glib.GError _ ->
    Gui_parameters.warning
      "Frama-C icon/logo not found. Is FRAMAC_SHARE correctly set?";
    None, None
  | GdkPixbuf.GdkPixbufError (_, errmsg) ->
    Gui_parameters.warning
      "Could not load Frama-C icon/logo: %s" errmsg;
    None, None

module Configuration = struct
  include Cilconfig
  let configuration_file () =
    Gui_parameters.Config.get_file ~mode:`Create_path "frama-c-gui.config"
  let load () = loadConfiguration (configuration_file ())
  let save () = saveConfiguration (configuration_file ())
  let reset () = Extlib.safe_remove (configuration_file () :> string);
    loadConfiguration (configuration_file ())
  let () = Cmdline.at_normal_exit save

  let set = setConfiguration
  let find = findConfiguration

  let use_int = useConfigurationInt
  let set_int key s = set key (ConfInt s)
  let find_int ?default key =
    try findConfigurationInt key
    with Not_found -> match default with
      | None -> raise Not_found
      | Some v ->
        set key (ConfInt v);
        v

  let use_float = useConfigurationFloat
  let set_float key f = set key (ConfFloat f)
  let find_float ?default key =
    try findConfigurationFloat key
    with Not_found -> match default with
      | None -> raise Not_found
      | Some v ->
        set key (ConfFloat v);
        v

  let use_bool = useConfigurationBool
  let set_bool key b = set key (ConfBool b)
  let find_bool ?default key =
    try findConfigurationBool key
    with Not_found -> match default with
      | None -> raise Not_found
      | Some v ->
        set key (ConfBool v);
        v

  let use_string = useConfigurationString
  let set_string key s = set key (ConfString s)
  let find_string ?default s =
    try findConfigurationString s
    with Not_found -> match default with
      | None -> raise Not_found
      | Some v ->
        set s (ConfString v);
        v

  let set_list key l = set key (ConfList l)
  let use_list = useConfigurationList
  let find_list = findConfigurationList

  class type ['a] selector =
    object
      method set : 'a -> unit
      method connect : ('a -> unit) -> unit
    end

  let config_string ~key ~default widget =
    let init = find_string ~default key in
    widget#set init ;
    widget#connect (set_string key)

  let config_int ~key ~default widget =
    let init = find_int ~default key in
    widget#set init ;
    widget#connect (set_int key)

  let config_bool ~key ~default widget =
    let init = find_bool ~default key in
    widget#set init ;
    widget#connect (set_bool key)

  let config_float ~key ~default widget =
    let init = find_float ~default key in
    widget#set init ;
    widget#connect (set_float key)

  let config_values ~key ~default ~values widget =
    begin
      let of_string s = fst (List.find (fun e -> snd e = s) values) in
      let to_string v = snd (List.find (fun e -> fst e = v) values) in
      let init =
        try of_string (find_string key)
        with Not_found -> default
      in
      widget#set init ;
      widget#connect
        (fun v ->
           try set_string key (to_string v)
           with Not_found -> ())
    end

end

module Icon = struct

  type kind = Frama_C | Unmark | Fold | Unfold
            | Custom of string
            | Feedback of Property_status.Feedback.t

  module F = Property_status.Feedback

  type theme_file =
    | ThemeSpecific of string
    | ThemeGeneric of string

  let builtins =
    [(Frama_C,                      ThemeGeneric "frama-c.ico");
     (Unmark,                       ThemeGeneric "unmark.png");
     (Fold,                         ThemeSpecific "fold.png");
     (Unfold,                       ThemeSpecific "unfold.png");
     (Feedback F.Never_tried,       ThemeSpecific "never_tried.png");
     (Feedback F.Unknown,           ThemeSpecific "unknown.png");
     (Feedback F.Valid,             ThemeSpecific "surely_valid.png");
     (Feedback F.Invalid,           ThemeSpecific "surely_invalid.png");
     (Feedback F.Considered_valid,  ThemeSpecific "considered_valid.png");
     (Feedback F.Valid_under_hyp,   ThemeSpecific "valid_under_hyp.png");
     (Feedback F.Invalid_under_hyp, ThemeSpecific "invalid_under_hyp.png");
     (Feedback F.Invalid_but_dead,  ThemeSpecific "invalid_but_dead.png");
     (Feedback F.Unknown_but_dead,  ThemeSpecific "unknown_but_dead.png");
     (Feedback F.Valid_but_dead,    ThemeSpecific "valid_but_dead.png");
     (Feedback F.Inconsistent,      ThemeSpecific "inconsistent.png");
    ]

  let theme_directory () =
    Configuration.find_string ~default:"default" "theme"

  let get_file_in_theme = function
    | ThemeSpecific x -> "theme/" ^ (theme_directory ()) ^ "/" ^ x
    | ThemeGeneric x -> x

  type icon = Filename of theme_file | Pixbuf of GdkPixbuf.pixbuf

  let h = Hashtbl.create 7

  let clear () =
    List.iter
      (fun (k,f) -> Hashtbl.replace h k (Filename f))
      builtins

  let () = clear ()

  let get k =
    try match Hashtbl.find h k with
      | Filename f' ->
        let f = get_file_in_theme f' in
        let p = Widget.shared_icon f in
        Hashtbl.replace h k (Pixbuf p); p
      | Pixbuf p -> p
    with Not_found -> assert false

  let default = Widget.default_icon

  let register ~name ~file =
    Hashtbl.replace h (Custom name) (Filename (ThemeGeneric file))
end

let apply_tag b tag pb pe =
  let b = (b:>GText.buffer) in
  let start = b#get_iter (`OFFSET pb) in
  let stop = b#get_iter (`OFFSET pe) in
  b#apply_tag ~start ~stop tag

let remove_tag b tag pb pe =
  let b = (b:>GText.buffer) in
  let start = b#get_iter (`OFFSET pb) in
  let stop = b#get_iter (`OFFSET pe) in
  b#remove_tag ~start ~stop tag

let cleanup_tag b tag =
  let b = (b:>GText.buffer) in
  b#remove_tag tag ~start:b#start_iter ~stop:b#end_iter

(* This table shall not be projectified: it contains trans-project
   informations *)
module IntHashtbl =
  Hashtbl.Make(struct
    type t = int
    let hash = Hashtbl.hash
    let equal : int -> int -> bool = (=)
  end)

let tag_names = IntHashtbl.create 17
let cleanup_all_tags b =
  let b = (b:>GText.buffer) in
  let start = b#start_iter in
  let stop = b#end_iter in
  try
    let tags = IntHashtbl.find tag_names (Oo.id b) in
    Datatype.String.Set.iter (fun s -> b#remove_tag_by_name s ~start ~stop) tags
  with Not_found -> ()

let make_tag (buffer:< tag_table : Gtk.text_tag_table;
              create_tag : ?name:string -> GText.tag_property list -> GText.tag ; .. >)
    ~name l
  =
  match GtkText.TagTable.lookup buffer#tag_table name with
  | None ->
    let oid = Oo.id buffer in
    let old_set =
      try IntHashtbl.find tag_names oid
      with Not_found -> Datatype.String.Set.empty
    in
    IntHashtbl.replace tag_names oid (Datatype.String.Set.add name old_set);
    buffer#create_tag ~name l
  | Some t -> new GText.tag t

let expand_to_path (treeview:GTree.view) path = treeview#expand_to_path path

let make_formatter ?(flush= fun () -> ()) t =
  let t = (t:>GText.buffer) in
  let fmt_emit s start length =
    let subs = String.sub s start length in
    t#insert ~iter:t#end_iter subs
  in
  Format.make_formatter fmt_emit flush

let redirect fmt (t:#GText.buffer) =
  let fmt_emit s start length =
    let subs = String.sub s start length in
    t#insert subs
  in
  let fmt_flush () = () in
  Format.pp_set_formatter_output_functions
    fmt
    fmt_emit fmt_flush

let gui_unlocked = ref false
module Lock = struct
  let last = ref (fun _ -> ())
  module H = Hook.Build(struct type t = bool end)
  let extend is_last f = if is_last then last := f else H.extend f
  let apply b = H.apply b; !last b
end
module Unlock = struct
  let first = ref (fun () -> ())
  module H = Hook.Make(struct end)
  let extend is_first f = if is_first then first := f else H.extend f
  let apply () = !first (); H.apply ()
end
let register_locking_machinery ?(lock_last=false) ~lock ~unlock () =
  if lock_last then begin
    Lock.extend true lock;
    Unlock.extend true unlock
  end else begin
    Lock.extend false lock;
    Unlock.extend false unlock
  end

let log_redirector ?(flush=fun () -> ()) emit_string =
  let output s offset length = emit_string (String.sub s offset length) in
  Log.set_output ~isatty:false output flush

let make_string_list ~packing =
  let (model,column) =
    GTree.store_of_list Gobject.Data.string [] in
  let insert s =
    let row = model#append () in
    model#set ~row ~column s
  in
  let get_all () =
    let l = ref [] in
    model#foreach (fun _ row ->
        l := model#get ~row ~column ::!l ;
        false);
    !l
  in
  let view = GTree.view ~model ~reorderable:true ~packing () in
  let view_column = GTree.view_column ~title:"Source file(s)" () in
  let str_renderer = GTree.cell_renderer_text [] in
  view_column#pack str_renderer;
  view_column#add_attribute str_renderer "text" column;
  let _ = view#append_column view_column in
  let remove_selected () =
    let path_list = view#selection#get_selected_rows in
    let row_refs = List.map model#get_row_reference path_list in
    List.iter (fun rr -> ignore (model#remove rr#iter)) row_refs
  in
  insert,remove_selected, get_all

let model_of_list conv l =
  let cols = new GTree.column_list in
  let column = cols#add conv in
  let model = GTree.list_store cols in
  List.iter
    (fun data ->
       let row = model#append () in
       model#set ~row ~column data)
    l ;
  (model, column)

let string_selector completions packing =
  let (model, col) = model_of_list Gobject.Data.string completions in
  let entry = GEdit.entry ~packing () in
  let c = GEdit.entry_completion ~model ~entry () in
  c#set_text_column col ;
  entry
(*  (GEdit.combo ~popdown_strings:completions ~packing ())#entry *)

let mk_label ?(use_markup=false) ?xalign (container:GPack.box) label =
  let l =
    GMisc.label ~xpad:3 ~line_wrap:true ?xalign
      ~packing:(container#pack ~expand:true ~fill:true)
  in
  if use_markup then l ~markup:label () else l ~text:label ()

type 'a chooser =
  GPack.box -> string -> (unit -> 'a) -> ('a -> unit) -> (unit -> unit)

(* ------------------------------------------------------------------------ *)
(* --- Bundle of fields                                                 --- *)
(* ------------------------------------------------------------------------ *)

let do_tooltip ?tooltip obj = match tooltip with
  | None -> ()
  | Some text -> obj#coerce#misc#set_tooltip_text text

let on_bool ?tooltip ?use_markup (container:GPack.box) label get set =
  let result = ref (get ()) in
  let container = GPack.hbox ~packing:container#pack () in
  do_tooltip ?tooltip container;
  let button =
    GButton.check_button ~packing:container#pack ~active:!result ()
  in
  ignore (mk_label ?use_markup container ~xalign:0. label);
  ignore (button#connect#toggled ~callback:(fun () -> set button#active));
  let update () = button#set_active (get()) in
  (fun () -> update ())

let range_selector
    ?tooltip ?use_markup (container:GPack.box) ~label ~lower ~upper
    set get =
  let container = GPack.hbox ~packing:container#pack () in
  do_tooltip ?tooltip container;
  let x =
    GEdit.spin_button ~digits:0 ~packing:(container#pack ~padding:10) ()
  in
  x#adjustment#set_bounds
    ~lower:(float lower) ~upper:(float upper) ~step_incr:1. ();
  x#adjustment#set_value (float (get ()));
  ignore
    (x#connect#value_changed
       ~callback:
         (fun () -> set x#value_as_int));
  ignore (mk_label ?use_markup ~xalign:0. container label);
  (fun () -> x#adjustment#set_value (float (get ())))

let on_int
    ?tooltip ?use_markup ?(lower=0) ?(upper=max_int)
    ?(sensitive=(fun () -> true)) ?width
    (container:GPack.box) label get set =
  let container = GPack.hbox ~packing:container#pack () in
  do_tooltip ?tooltip container;
  let non_fixed = width=None in
  let spin = GEdit.spin_button ~digits:0
      ?width ~packing:(container#pack ~expand:non_fixed ~fill:non_fixed) ()
  in
  spin#adjustment#set_bounds
    ~lower:(float lower) ~upper:(float upper) ~step_incr:1. ();
  spin#adjustment#set_value (float (get()));
  ignore
    (spin#connect#value_changed
       ~callback:
         (fun () -> set spin#value_as_int));
  let label = mk_label ?use_markup ~xalign:0. container label in
  (fun () ->
     label#misc#set_sensitive (sensitive ());
     spin#misc#set_sensitive (sensitive ());
     spin#adjustment#set_value (float (get())))

let on_string ?tooltip ?use_markup ?(validator=(fun _ -> true)) ?width
    (container:GPack.box) label get set =
  let container = GPack.hbox ~packing:container#pack () in
  do_tooltip ?tooltip container;
  let entry = GEdit.entry ~packing:container#pack ~text:(get()) ?width () in
  let callback _ =
    let text = entry#text in
    if validator text then set text
    else entry#set_text (get ());
    false
  in
  ignore (entry#event#connect#focus_out ~callback);
  ignore (entry#connect#activate ~callback:(fun () -> ignore (callback ())));
  ignore (mk_label ?use_markup ~xalign:0. container label);
  (fun () ->
     if not (Gobject.Property.get entry#as_widget GtkBase.Widget.P.has_focus)
     then entry#set_text (get ()))

let on_string_set ?tooltip ?use_markup ?width (container:GPack.box) label get set =
  let container = GPack.hbox ~packing:container#pack () in
  do_tooltip ?tooltip container;
  let entry = GEdit.entry ~packing:container#pack ~text:(get()) ?width () in
  let callback _ = set entry#text; false in
  ignore (entry#event#connect#focus_out ~callback);
  ignore (entry#connect#activate ~callback:(fun () -> ignore (callback ())));
  ignore (mk_label ?use_markup ~xalign:0. container (label ^ " (list)"));
  (fun () ->
     if not (Gobject.Property.get entry#as_widget GtkBase.Widget.P.has_focus)
     then entry#set_text (get()))

let on_string_completion
    ?tooltip ?use_markup ?(validator=(fun _ -> true)) completions
    (container:GPack.box) label get set =
  let box = GPack.hbox ~packing:container#pack () in
  do_tooltip ?tooltip box;
  let entry = string_selector completions box#pack in
  ignore (mk_label ?use_markup ~xalign:0. box label);
  let () = entry#set_text (get()) in
  let callback _ =
    let text =  entry#text in
    if validator text then set text else entry#set_text (get());
    false
  in
  ignore (entry#event#connect#focus_out ~callback);
  ignore (entry#connect#activate ~callback:(fun () -> ignore (callback ())));
  (fun () -> entry#set_text (get()))

let on_combo
    values ?tooltip ?(use_markup=false) ?width (container:GPack.box)
    label get set =
  let rec select i (x:string) = function
    | [] -> (-1)
    | y::ys -> if x=y then i else select (succ i) x ys
  in
  let container = GPack.hbox ~packing:container#pack () in
  do_tooltip ?tooltip container;
  let non_fixed = width=None in
  let combo_box, (_model, column) =
    GEdit.combo_box_text
      ~strings:values ?width
      ~wrap_width:3 ~use_markup
      ~packing:(container#pack ~expand:non_fixed ~fill:non_fixed)
      ()
  in
  let callback () =
    match combo_box#active_iter with
    | None -> ()
    | Some row -> set (combo_box#model#get ~row ~column)
  in
  let update () =
    let result = ref (get ()) in
    let k = select 0 !result values in
    if k >= 0 then combo_box#set_active k
  in
  ignore (combo_box#connect#changed callback) ;
  ignore (mk_label ~use_markup ~xalign:0. container label) ;
  (fun () -> update ())

(* ------------------------------------------------------------------------ *)
(* --- Misc                                                             --- *)
(* ------------------------------------------------------------------------ *)

let save_paned_ratio key (paned:GPack.paned) =
  let ratio = Wutil.get_pane_ratio paned in
  Configuration.set key (Configuration.ConfFloat ratio)

let place_paned = Wutil.set_pane_ratio

let old_gtk_compat f x = try f x with Not_found -> ()

let trace_event (w:GObj.event_ops) =
  let string_of_event x =
    match GdkEvent.get_type x with
    | `NOTHING -> "nothing"
    | `DELETE -> "delete"
    | `DESTROY -> "destroy"
    | `EXPOSE -> "expose"
    | `MOTION_NOTIFY -> "motion-notify"
    | `BUTTON_PRESS -> "button-press"
    | `TWO_BUTTON_PRESS -> "2 button-press"
    | `THREE_BUTTON_PRESS -> "3 button-press"
    | `BUTTON_RELEASE -> "button-release"
    | `KEY_PRESS -> "key-press"
    | `KEY_RELEASE  -> "key-release"
    | `ENTER_NOTIFY  -> "enter-notfiy"
    | `LEAVE_NOTIFY -> "leave-notify"
    | `FOCUS_CHANGE  -> "focus-change"
    | `CONFIGURE -> "configure"
    | `MAP -> "map"
    | `UNMAP -> "unmap"
    | `PROPERTY_NOTIFY -> "property-notify"
    | `SELECTION_CLEAR -> "selection-clear"
    | `SELECTION_REQUEST -> "selection-request"
    | `SELECTION_NOTIFY -> "selection-notify"
    | `PROXIMITY_IN -> "proximity-in"
    | `PROXIMITY_OUT -> "proximiy-out"
    | `DRAG_ENTER -> "drag-enter"
    | `DRAG_LEAVE -> "drag-leave"
    | `DRAG_MOTION -> "drag-motion"
    | `DRAG_STATUS -> "drag-status"
    | `DROP_START -> "drop-start"
    | `DROP_FINISHED -> "drop-finish"
    | `CLIENT_EVENT -> "client-event"
    | `VISIBILITY_NOTIFY -> "visibility-notify"
    (*GTK3 Event does not exist anymore *)
    (*    | `NO_EXPOSE-> "no-expose" *)
    | `SCROLL -> "scroll"
    | `WINDOW_STATE -> "window-state"
    | `SETTING -> "setting"
    (*GTK3: leave room for more events. *)
    | _ -> "unknown-gtk3-event"
  in
  ignore (w#connect#any
            ~callback:(fun e ->
                Format.eprintf "TRACING event: %s@." (string_of_event e);
                false))

module MAKE_CUSTOM_LIST(A:sig type t end) =
struct
  type custom_list =
    {finfo: A.t;
     fidx: int (* invariant: root.(fidx)==myself *) }

  module H = Hashtbl

  let inbound i a = i>=0 && i<Array.length a

  (** The custom model itself *)
  class custom_list_class column_list =
    object (self)
      inherit
        [custom_list,custom_list,unit,unit] GTree.custom_tree_model column_list

      method custom_encode_iter cr = cr, (), ()
      method custom_decode_iter cr () () = cr

      val mutable last_idx = 0
      val mutable roots : (int,custom_list) H.t = H.create 19
      method private find_opt i =
        try Some (H.find roots i) with Not_found -> None
      method! custom_flags = [`LIST_ONLY]
      method custom_get_iter (path:Gtk.tree_path) : custom_list option =
        let indices: int array  = GTree.Path.get_indices path in
        match indices with
        | [||] ->
          None
        | [|i|] -> self#find_opt i
        | _ -> failwith "Invalid Path of depth > 1 in a list"

      method custom_get_path (row:custom_list) : Gtk.tree_path =
        GTree.Path.create [row.fidx]

      method custom_value (_t:Gobject.g_type) (_row:custom_list) ~column:_ =
        assert false

      method custom_iter_next (row:custom_list) : custom_list option =
        let nidx = succ row.fidx in
        self#find_opt nidx

      method custom_iter_children (rowopt:custom_list option):custom_list option =
        match rowopt with
        | None -> self#find_opt 0
        | Some _ -> None

      method custom_iter_has_child (_:custom_list) : bool = false

      method custom_iter_n_children (rowopt:custom_list option) : int =
        match rowopt with
        | None -> H.length roots
        | Some _ -> assert false

      method custom_iter_nth_child (rowopt:custom_list option) (n:int)
        : custom_list option =
        match rowopt with
        | None -> self#find_opt n
        | _ -> None

      method custom_iter_parent (_:custom_list) : custom_list option = None

      method insert (t:A.t) =
        let e = {finfo=t; fidx= last_idx } in
        self#custom_row_inserted (GTree.Path.create [last_idx]) e;
        H.add roots last_idx e;
        last_idx <- last_idx+1;

      method clear () =
        for i=last_idx-1 downto 0 do
          self#custom_row_deleted (GTree.Path.create [i]);
        done;
        last_idx <- 0;
        H.clear roots;
    end

  let custom_list () =
    new custom_list_class (new GTree.column_list)

  let make_view_column model renderer properties ~title =
    let m_renderer renderer (lmodel:GTree.model) iter =
      let (path:Gtk.tree_path) = lmodel#get_path iter  in
      let props = match model#custom_get_iter path with
        | Some {finfo=v} -> properties v
        | None -> []
      in
      renderer#set_properties props
    in
    let cview = GTree.view_column ~title ~renderer:(renderer,[]) () in
    cview#set_cell_data_func renderer (m_renderer renderer);
    cview
end

(* NOTE: this code has been copied from lablgtk's gToolbox.ml to
   allow binding the behavior of "keypad enter" to the "return" key *)
let input_widget ~parent ~widget ~event ~get_text ~bind_ok ~expand
    ~title ?(ok="Ok") ?(cancel="Cancel") message =
  let retour = ref None in
  let window = GWindow.dialog ~parent ~title ~modal:false () in
  ignore (window#connect#destroy ~callback: GMain.Main.quit);
  let main_box = window#vbox in
  let hbox_boutons = window#action_area in
  let vbox_saisie = GPack.vbox ~packing: (main_box#pack ~expand: true) () in
  ignore (GMisc.label ~text:message ~packing:(vbox_saisie#pack ~padding:3) ());
  vbox_saisie#pack widget ~expand ~padding: 3;
  let wb_ok = GButton.button ~label: ok
      ~packing: (hbox_boutons#pack ~expand: true ~padding: 3) () in
  wb_ok#grab_default ();
  let wb_cancel = GButton.button ~label: cancel
      ~packing: (hbox_boutons#pack ~expand: true ~padding: 3) () in
  let f_ok () =
    retour := Some (get_text ()) ;
    window#destroy ()
  in
  let f_cancel () =
    retour := None;
    window#destroy ()
  in
  ignore (wb_ok#connect#clicked f_ok);
  ignore (wb_cancel#connect#clicked f_cancel);
  (* the enter key is linked to the ok action *)
  (* the escape key is linked to the cancel action *)
  ignore (event#connect#key_press ~callback:
            begin fun ev ->
              if (GdkEvent.Key.keyval ev = GdkKeysyms._Return ||
                  GdkEvent.Key.keyval ev = GdkKeysyms._KP_Enter) && bind_ok
              then f_ok ();
              if GdkEvent.Key.keyval ev = GdkKeysyms._Escape then f_cancel ();
              false
            end);
  widget#misc#grab_focus ();
  window#show ();
  GMain.Main.main ();
  !retour

(* NOTE: this code has been copied from lablgtk's gToolbox.ml to
   allow binding the behavior of "keypad enter" to the "return" key *)
let input_string ~parent ~title ?ok ?cancel ?(text="") message =
  let we_chaine = GEdit.entry ~text () in
  if text <> "" then
    we_chaine#select_region 0 (we_chaine#text_length);
  input_widget ~parent ~widget:we_chaine#coerce ~event:we_chaine#event
    ~get_text:(fun () -> we_chaine#text) ~bind_ok:true
    ~expand: false
    ~title ?ok ?cancel message

(* ************************************************************************** *)
(** {2 Error manager} *)
(* ************************************************************************** *)

(**  A utility class to catch exceptions and report proper error messages. *)
class type host = object
  method error:
    'a. ?parent:GWindow.window_skel -> ?reset:bool -> ('a, Format.formatter, unit) format -> 'a
  method full_protect :
    'a. cancelable:bool -> ?parent:GWindow.window_skel -> (unit -> 'a) ->
    'a option
  method protect :
    cancelable:bool -> ?parent:GWindow.window_skel -> (unit -> unit) -> unit
  method private set_reset: (unit -> unit) -> unit
end

class error_manager ?reset (o_parent:GWindow.window_skel) : host =
  object (self: #host)

    val mutable f_reset = match reset with
      | None -> fun () -> ()
      | Some f -> f

    method private set_reset f = f_reset <- f

    method private error_string ?parent ~reset message =
      let w = GWindow.message_dialog
          ~message
          ~message_type:`ERROR
          ~parent:(Extlib.opt_conv o_parent parent)
          ~buttons:GWindow.Buttons.ok
          ~title:"Error"
          ~position:`CENTER_ALWAYS
          ~modal:true
          ()
      in
      ignore (w#connect#response (fun _ -> w#destroy ()));
      ignore (w#run ());
      if reset then f_reset ()

    method error ?parent ?(reset=false) fmt =
      let b = Buffer.create 80 in
      let bfmt = Format.formatter_of_buffer b in
      Format.kfprintf
        (function fmt ->
           Format.pp_print_flush fmt ();
           let content = Buffer.contents b in
           self#error_string ?parent ~reset content)
        bfmt
        fmt

    method private display_toplevel_error ?parent ~cancelable e =
      Cmdline.error_occurred e;
      if cancelable then Project.Undo.restore ();
      self#error ?parent ~reset:true "%s" (Cmdline.protect e);

    method protect ~cancelable ?(parent:GWindow.window_skel option) f =
      ignore (self#full_protect ~cancelable ?parent f)

    method full_protect ~cancelable ?(parent:GWindow.window_skel option) f =
      let cancelable = cancelable && Gui_parameters.Undo.get () in
      try
        if cancelable then Project.Undo.breakpoint ();
        let old_gui_unlocked = !gui_unlocked in
        let res =
          Extlib.try_finally
            ~finally:(fun () -> if old_gui_unlocked then begin
                Unlock.apply ();
                gui_unlocked := true
              end)
            (fun () ->
               if old_gui_unlocked then begin
                 Lock.apply cancelable;
                 gui_unlocked := false;
               end;
               f ())
            ()
        in
        if cancelable then Project.Undo.clear_breakpoint ();
        Some res
      with
      | Cmdline.Exit ->
        if cancelable then Project.Undo.clear_breakpoint ();
        None
      | Sys.Break | Db.Cancel ->
        if cancelable then Project.Undo.restore ();
        self#error ?parent ~reset:true
          "Stopping current computation on user request.";
        None
      | Globals.No_such_entry_point msg ->
        (try Gui_parameters.abort "%s" msg
         with
         | Log.AbortError _ as e ->
           self#display_toplevel_error ?parent ~cancelable e;
           None)
      | e when Cmdline.catch_at_toplevel e ->
        self#display_toplevel_error ?parent ~cancelable e;
        None
      | e ->
        if Gui_parameters.debug_atleast 1 then begin
          Cmdline.error_occurred e;
          raise e
        end else begin
          self#display_toplevel_error ?parent ~cancelable e;
          None
        end

  end

let make_text_page ?pos (notebook:GPack.notebook) title =
  let make_tab_label (notebook:GPack.notebook) =
    let flash_title = Format.sprintf "<i>%s</i>" title in
    let tab_label = GMisc.label ~markup:title () in
    let sw = GBin.scrolled_window
        ~vpolicy:`AUTOMATIC
        ~hpolicy:`AUTOMATIC
        ~packing:
          (fun w -> ignore (notebook#insert_page ?pos ~tab_label:tab_label#coerce w))
        ()
    in
    let flash b =
      tab_label#set_text (if b then flash_title else title);
      ignore(tab_label#set_use_markup true)
    in
    flash, sw
  in
  let flash,sw = make_tab_label notebook in
  let flash_ref = ref flash in
  let w = GText.view ~packing:sw#add () in
  let _ = w#set_editable false in
  let _ = w#misc#connect#map (fun () -> !flash_ref false) in
  let _ = w#event#connect#focus_in (fun _ -> !flash_ref false; false) in
  let _ = w#buffer#connect#changed (fun () -> !flash_ref true)
  in
  let reparent_page (notebook:GPack.notebook) =
    let flash, sw = make_tab_label notebook in
    flash_ref := flash;
    w#misc#reparent sw#coerce
  in
  reparent_page, w

(* Converts the 'editor' string saved in the GUI configuration to the actual
   command to be executed *)
let prepare_editor_cmd s line filename =
  let s = Str.global_replace (Str.regexp "%s") filename s in
  let s = Str.global_replace (Str.regexp "%d") (string_of_int line) s in
  (* always start in background, otherwise will freeze the GUI *)
  s ^ " &"

let open_in_external_viewer ?(line=1) (file : Datatype.Filepath.t) =
  let filename = Format.asprintf "%S" (file :> string) in
  let editor = Configuration.find_string ~default:"emacs +%d %s" "editor" in
  if editor = "" then
    Gui_parameters.feedback "no external viewer configured in Preferences"
  else
    let cmd = prepare_editor_cmd editor line filename in
    Gui_parameters.feedback "opening external viewer, running command: %s" cmd;
    ignore (Sys.command cmd)

exception Too_many_events
let refresh_gui () =
  let counter = ref 0 in
  try
    while Glib.Main.iteration false do
      if !counter >= 10 then raise Too_many_events
      else incr counter
    done
  with Too_many_events -> ()

(* ************************************************************************* *)
(** {2 Source File Chooser} *)
(* ************************************************************************* *)

class type source_files_chooser_host = object
  inherit host
  method main_window: GWindow.window_skel
  method reset: unit -> unit
end

let accepted_source_files () =
  let f = GFile.filter ~name:"Source files" () in
  List.iter (fun s -> f#add_pattern ("*" ^ s)) (File.get_suffixes ());
  f

let all_files () =
  let f = GFile.filter ~name:"All files" () in
  f#add_pattern "*.*";
  f

let source_files_chooser (main_ui: source_files_chooser_host) defaults f =
  let dialog = GWindow.dialog
      ~width:800
      ~height:400
      ~modal:true
      ~title:"Select C source files"
      ~parent:main_ui#main_window
      ~destroy_with_parent:true
      ()
  in
  dialog#add_button_stock `CANCEL `CANCEL ;
  dialog#add_button_stock `OK `OPEN;
  let hbox = GPack.box `HORIZONTAL ~packing:dialog#vbox#add () in
  let filechooser = GFile.chooser_widget
      ~action:`OPEN
      ~packing:(hbox#pack ~expand:true ~fill:true)
      ()
  in
  Configuration.use_string "last_opened_dir"
    (fun s -> ignore (filechooser#set_current_folder s));
  filechooser#set_select_multiple true;
  filechooser#add_filter (accepted_source_files ());
  filechooser#add_filter (all_files ());
  let bbox =
    GPack.button_box
      ~layout:`START
      `VERTICAL ~packing:(hbox#pack ~expand:false ~fill:false) () in
  let add_button = GButton.button ~stock:`ADD ~packing:bbox#add () in
  let remove_button = GButton.button ~stock:`REMOVE ~packing:bbox#add () in
  let w =
    GBin.scrolled_window
      ~vpolicy:`AUTOMATIC
      ~hpolicy:`AUTOMATIC
      ~packing:(hbox#pack ~expand:true ~fill:true)
      ()
  in
  let add,remove,get_all = make_string_list ~packing:w#add in
  let add_selected_files () =
    let f = filechooser#get_filenames in
    List.iter add f
  in
  List.iter add defaults;
  ignore (add_button#connect#pressed ~callback:add_selected_files);
  ignore (remove_button#connect#pressed ~callback:remove);
  ignore (filechooser#connect#file_activated ~callback:add_selected_files);
  let response r = (match r with
      | `OPEN ->
        main_ui#protect
          ~cancelable:true
          ~parent:(dialog :> GWindow.window_skel)
          (fun () -> f (get_all ()))
      | `DELETE_EVENT | `CANCEL ->
        ());
    Extlib.may (fun f ->
        Configuration.set "last_opened_dir"
          (Configuration.ConfString f)) filechooser#current_folder;
    dialog#destroy ()
  in
  let (_:GtkSignal.id) = dialog#connect#response ~callback:response in
  dialog#show ();
  ()

let default_dir = ref ""

let select_file ?title ?(dir=default_dir) ?(filename="") () =
  let filename =
    if Filename.is_relative filename then
      if !dir <> "" then !dir ^ "/" ^ filename
      else ""
    else begin
      dir:= Filename.dirname filename;
      filename
    end
  in
  let dialog: GWindow.Buttons.file_selection GWindow.file_chooser_dialog =
    GWindow.file_chooser_dialog
      ~action:`OPEN
      ?title
      ~modal:true
      ()
  in
  ignore (dialog#set_filename filename);
  let result = ref None in
  let action r =
    (match r with
     | `OK ->
       let file = dialog#filename in
       (match file with
        | None -> ()
        | Some file ->
          dir := Filename.dirname file;
          result := Some file)
     | _ -> ());
    dialog#destroy ()
  in
  dialog#add_select_button "Open" `OK;
  dialog#add_button "Cancel" `CANCEL;
  dialog#show ();
  action (dialog#run ());
  !result

let spawn_command ?(timeout=0) ?stdout ?stderr s args f =
  let check_result = Command.command_async s ?stdout ?stderr args in
  let has_timeout = timeout > 0 in
  let hang_on = float_of_int timeout in
  let starting_time = if has_timeout then Unix.time () else 0. in
  let for_idle () =
    match check_result () with
    | Command.Not_ready kill ->
      if has_timeout && Unix.time () -. starting_time >= hang_on then
        begin
          kill ();
          f (Unix.WSIGNALED Sys.sigalrm);
          false
        end
      else true
    | Command.Result p -> f p; false
  in
  let prio = Glib.int_of_priority `LOW in
  ignore (Glib.Idle.add ~prio for_idle)

let image_menu_item ~(image:GObj.widget) ~text ~packing =
  let mi = GMenu.menu_item () in
  let box =
    GPack.hbox ~spacing:2 ~border_width:0 ~packing:mi#add ()
  in
  box#add image;
  box#add (GMisc.label ~justify:`LEFT ~xalign:0. ~xpad:0 ~text ())#coerce;
  packing mi;
  mi

(*
Local Variables:
compile-command: "make -C ../../.."
End:
*)
