(* $Id: tiBase.ml,v 1.18 2001/09/17 13:09:32 garrigue Exp $ *)

open StdLabels
open Gaux
open Gtk
open GObj
open GContainer

open Utils
open Common
open Property

(* possible children; used to make the menus *)
let widget_add_list =
  [ "vbox"; "hbox"; "vbutton_box"; "hbutton_box"; "fixed";
    "frame"; "aspect_frame"; "handle_box"; "event_box";
    "hseparator"; "vseparator"; "statusbar"; "label"; "notebook";
    "color_selection";
    "button";
    "toggle_button"; "check_button"; "radio_button"; "scrolled_window";

    "entry"; "spin_button"; "combo"; "clist"; "toolbar"]


(*********** selection ***********)

let selection = ref ""
let window_selection = ref ""


(**************** signals class ***************)

class tiwidget_signals ~signals =
  let name_changed : string signal = signals in
  object
    val after = false
    method after = {< after = true >}
    method name_changed = name_changed#connect ~after
  end


(************* class type ***************)
(* the ti<gtkwidget> classes encapsulate the corresponding gtk
   widget which will be in the gtk-window and a tree item
   labelled with the name of the widget which will be in the
   tree-window.
   all these classes have the same following interface *)

class virtual tiwidget0 = object
  method virtual widget : GObj.widget
  method virtual connect_event : GObj.event_signals
  method virtual parent : tiwidget0 option
  method virtual set_parent : tiwidget0 -> unit
  method virtual base : GObj.widget
  method virtual tree_item : GTree2.tree_item
  method virtual tree : GTree2.tree
  method virtual children : (tiwidget0 * Gtk.Tags.pack_type) list
  method virtual name : string
  method virtual proplist : (string * prop) list
  method virtual add_to_proplist : (string * prop) list -> unit
  method virtual change_name_in_proplist : string -> string -> unit
  method virtual set_property : string -> string -> unit
  method virtual forall :  callback:(tiwidget0 -> unit) -> unit
  method virtual remove : tiwidget0 -> unit
(*  method virtual add_child_with_name : string -> string -> pos:int -> tiwidget0 *)
  method virtual add_children : ?pos:int -> yywidget_tree -> unit
  method virtual add_children_wo_undo : ?pos:int -> yywidget_tree -> string
  method virtual remove_me  : unit -> unit
  method virtual remove_me_without_undo  : unit -> unit
  method virtual emit_code : Format.formatter -> char list -> unit
  method virtual emit_init_code : Format.formatter -> packing:string -> unit
  method virtual emit_method_code : Format.formatter -> unit
  method virtual emit_initializer_code : Format.formatter -> unit
  method virtual save : Format.formatter -> unit
  method virtual copy : unit -> unit
  method virtual connect : tiwidget_signals
  method virtual disconnect : GtkSignal.id -> bool
  method virtual child_up : tiwidget0 -> unit
  method virtual up : unit -> unit
  method virtual child_down : tiwidget0 -> unit
  method virtual down : unit -> unit
  method virtual next : tiwidget0
  method virtual next_child : tiwidget0 -> tiwidget0
  method virtual last : tiwidget0
  method virtual prev : tiwidget0
  method virtual set_full_menu : bool -> unit
end

class virtual window_and_tree0 = object
  method virtual tiwin : tiwidget0
(*  method virtual tree_window : window *)
  method virtual change_selected : tiwidget0 -> unit
  method virtual remove_sel : tiwidget0 -> unit
  method virtual add_param : char
  method virtual remove_param : char -> unit
(*  method virtual emit : unit -> unit *)
end

(* forward declaration of function new_widget *)
let new_tiwidget :
    (classe:string -> ?pos:int -> name:string ->parent_tree:GTree2.tree ->
      ?insert_evbox:bool -> ?listprop:(string * string) list -> window_and_tree0 -> tiwidget0) ref =
  ref (fun ~classe ?pos ~name ~parent_tree ?insert_evbox ?listprop w -> failwith "new_tiwidget")


let widget_map = Hashtbl.create 17

(* list of names of radio_buttons (for groups) *)
let radio_button_pool = ref []


(************* window creation class *************)
(* an instance of this class is created for each window opened
   in radtest. It contains the tree window and the gtk window (tiwin) *)

class window_and_tree ~name =
  let tree_window = GWindow.window ~show:true ~title:(name ^ "-Tree") () in
  let vbox = GPack.vbox ~spacing:2 ~packing:tree_window#add () in
  let root_tree = GTree2.tree ~packing:vbox#pack ~selection_mode:`EXTENDED () in
  let project_tree_item = GTree2.tree_item () in
  let label = GMisc.label ~text:name ~xalign:0. ~yalign:0.5
      ~packing:project_tree_item#add () in

  object(self)

    inherit window_and_tree0

(* the params of the window class; because the class clist needs a param
   I suppose there will be no more than 26 clists in a single window    *)
    val param_list = Array.create 26 false

    method add_param =
      let i = ref 0 in
      while param_list.(!i) do incr i done;
      param_list.(!i) <- true;
      char_of_int (97 + !i)

    method remove_param c =
      param_list.(int_of_char c - 97) <- false

    method private param_list =
      let r = ref [] in
      for i = 25 downto 0 do
	if Array.unsafe_get param_list i then r := (char_of_int (i+97)) :: !r
      done;
      !r

(* I use magic here because the real initialization is done
   below in the initializer part. It can't be done here because
   of the call to self *)
    val mutable tiwin = (Obj.magic 0 : tiwidget0)

    method tiwin = tiwin
    method tree_window = tree_window

    method project_tree_item = project_tree_item

(* the selected item in this window *)
    val mutable selected = (None : tiwidget0 option)

(* what to do when a new item is selected.
   this method is passed to all the tiwidgets (by the select_fun
   parameter) and they will call it when they are clicked on;
   she is also called when changing the selection the arrow keys
   (see in the initializer part) *)
    method change_selected sel =
      match selected with
      |	None ->
	  selected <- Some sel;
	  sel#tree_item#misc#set_state `SELECTED;
	  sel#base#misc#set_state `SELECTED;
	  Propwin.show sel
      |	Some old_sel ->
	  if sel = old_sel then begin
	    selected <- None;
	    sel#base#misc#set_state `NORMAL;
	    sel#tree_item#misc#set_state `NORMAL
	  end else begin
	    old_sel#tree_item#misc#set_state `NORMAL;
	    old_sel#base#misc#set_state `NORMAL;
	    selected <- Some sel;
	    sel#tree_item#misc#set_state `SELECTED;
	    sel#base#misc#set_state `SELECTED;
	    Propwin.show sel
	  end

(* the tiwidget tiw is being removed; if it was selected,
   put the selection to None *)
    method remove_sel tiw =
      match selected with
      |	Some sel when sel = tiw -> selected <- None
      |	_ -> ()

(* emits the code corresponding to this window *)
    method emit c = tiwin#emit_code c self#param_list;

    method delete () =
      tiwin#remove_me_without_undo ();
      tree_window#destroy ();

    initializer
      tiwin <- !new_tiwidget ~classe:"window" ~name ~parent_tree:root_tree
	  (self : #window_and_tree0 :> window_and_tree0);

      tiwin#connect#name_changed ~callback:
	  (fun n -> label#set_text n; tree_window#set_title (n ^ "-Tree"));

      Propwin.show tiwin;

      tree_window#event#connect#key_press ~callback:
	begin fun ev ->
	  let state = GdkEvent.Key.state ev in
	  let keyval = GdkEvent.Key.keyval ev in
	  if keyval = GdkKeysyms._Up then begin
	    match selected with
	    | None -> ()
	    | Some t -> 
		if List.mem `CONTROL state then t#up ()
		else try
		  self#change_selected t#prev
		with Not_found -> ()
	  end
	  else if keyval = GdkKeysyms._Down then begin
	    match selected with
	    | None -> ()
	    | Some t -> 
		if List.mem `CONTROL state then t#down ()
		else try
		  self#change_selected t#next
		with Not_found -> ()
	  end;
	  GtkSignal.stop_emit ();
          true
	end;
      ()
  end



(***************** class implementation *****************)
(* this is the base class of the ti<gtkwidget> hierarchy.
   all these classes will inherit from tiwidget, but without
   adding new methods. In this way all the classes have the
   same interface and we can use them in lists, pass them to
   functions without caring on the type.
   All methods needed by any of the classes are defined in
   tiwidget but if a method is not pertinent in tiwidget
   it has for implementation:
      failwith "<name of the method>"
   the real implementation of the method is done in the
   class (or classes) in which it is needed (or sometimes
   in tiwidget anyway).
   Additionally, to workaround some problem with recursive types
   the type of the (public) methods of tiwidget is defined in
   tiwidget0 of which tiwidget inherits.
   The parent_tree parameter is the tree in which the
   tiwidget#tree_item will be inserted at position :pos.
*)

class virtual tiwidget ~name ~parent_tree:(parent_tree : GTree2.tree) ~pos
    ~widget ?(insert_evbox=true) (parent_window : window_and_tree0) =
object(self)

  inherit tiwidget0
  inherit has_ml_signals

  val evbox =
    if insert_evbox then
      let ev = GBin.event_box () in ev#add widget#coerce; Some ev
    else None

(* used only for windows delete_event *)
  method connect_event = failwith "tiwidget::connect_event"

  val widget = widget#coerce
  method widget = widget

  val mutable parent = None
  method set_parent p = parent <- Some p
  method parent =  parent
  method private sure_parent =
    match parent with
    | None -> failwith "sure_parent"
    | Some p -> p

  method base =
    match evbox with
    | None -> widget#coerce
    | Some ev -> ev#coerce

(* this is the name used in new_tiwidget for the creation
   of an object of this class *)
  val mutable classe = ""

  val tree_item = GTree2.tree_item ()
  method tree_item = tree_item

  val mutable stree = GTree2.tree ()
  method tree = stree

  val label = GMisc.label ~text:name ~xalign:0. ~yalign:0.5 ()

  val mutable name : string = name
  method name = name

(* this is the complete name for the creation of the widget
   in lablgtk e.g. GPack.vbox; used in emit_init_code *)
  method private class_name = ""

  val mutable proplist : (string * prop) list = []
  method proplist = proplist
  method private get_mandatory_props = []

  method add_to_proplist plist = proplist <- proplist @ plist

(* for children of a box *)
  method change_name_in_proplist : string -> string -> unit =
    fun _ _ -> ()
  method set_property name value_string = try
    (List.assoc name proplist)#set value_string
  with Not_found -> Printf.printf "Property not_found %s, %s\n" name value_string;
    flush stdout

  method private get_property name =
    (List.assoc name proplist)#get


(* the proplist with some items removed e.g. the expand... in a box
   used for saving and emitting code *)
  method private emit_clean_proplist =
    List.fold_left ~f:(fun l p -> List.remove_assoc p l)
      ~init:proplist
      ([ "name"; "expand"; "fill"; "padding" ] @ self#get_mandatory_props)
(*  method private emit_clean_proplist plist =
    List.fold_left ~init:plist ~f:
      (fun pl propname -> List.remove_assoc propname pl)
	[ "name"; "expand"; "fill"; "padding" ]
*)

  method private save_clean_proplist =
    List.fold_left ~f:(fun l p -> List.remove_assoc p l)
      ~init:proplist ("name" :: self#get_mandatory_props)
(*  method private save_clean_proplist =
    List.remove_assoc "name" proplist *)

  val mutable children : (tiwidget0 * Gtk.Tags.pack_type) list = []
  method children = children
  method forall =
    fun ~callback -> List.iter (List.map children ~f:fst) ~f:callback

(* encapsulate container#add and container#remove 
   they are here because they depend on the type of the widget:
   e.g.: gtkbin->add scrolled_window->add_with_viewport box->pack *)
  method private add = failwith (name ^ "::add")
  method remove = failwith (name ^ "::remove")


(* removes self from his parent;
   will be different for a window *)
  method remove_me () =
    let sref = ref "" in
    self#save_to_string sref;
    let pos = list_pos ~item:(self : #tiwidget0 :> tiwidget0)
	(List.map self#sure_parent#children ~f:fst) in
    let lexbuf = Lexing.from_string !sref in
    let node = Load_parser.widget Load_lexer.token lexbuf in
    add_undo (Add (self#sure_parent#name, node, pos));
    self#remove_me_without_undo ()

  method remove_me_without_undo () =
(* it should be enough to only recursively remove the children from the
   name_list and do the tip#remove and tip#tree#remove
   only for self *)
    self#forall ~callback:(fun tiw -> tiw#remove_me_without_undo ());
    parent_window#remove_sel (self : #tiwidget0 :> tiwidget0);
    match parent with
    | None -> failwith "remove without parent"
    | Some (tip : #tiwidget0) ->
	tip#tree#remove tree_item;
	tip#remove (self : #tiwidget0 :> tiwidget0);
	name_list := list_remove !name_list ~f:(fun n -> n=name);
	Hashtbl.remove widget_map name;
	Propwin.remove name

(* used for undo *)
  method private remove_child_by_name name () =
    let child = fst (List.find children
	~f:(fun (ch, _) -> ch#name = name)) in
    child#remove_me ()

(* for most widgets we make a child with new_tiwidget and then add it
   to self; for toolbars we use toolbar#insert_button...     *)
      method private make_child = !new_tiwidget

(* adds a child and shows his properties;
   used when adding a child by the menu or DnD *)
  method private add_child classe ?name ?(undo = true) ?(affich = true) ?(pos = -1) ?(listprop = []) () =
    let name = match name with
    | None -> make_new_name classe
    | Some n -> n in
    let child = self#make_child ~classe ~name ~parent_tree:stree parent_window ~pos ~listprop in
    child#set_parent (self : #tiwidget0 :> tiwidget0);
    self#add child ~pos;
    if affich then Propwin.show child;
    if undo then add_undo (Remove name);
    child


(* adds the subtree saved in the Node *)
  method add_children ?(pos = -1) node =
    let child_name = self#add_children_wo_undo node ~pos in
    add_undo (Remove child_name)

  method add_children_wo_undo ?(pos = -1) (Node (child, children)) =
    let classe, name, property_list = child in
    let rname = change_name name in
    let tc = self#add_child classe ~name:rname ~undo:false ~affich:false ~pos ~listprop:property_list () in
    List.iter (List.rev children)
      ~f:(fun c -> tc#add_children_wo_undo c; ());
    List.iter property_list ~f:(fun (n,v) -> tc#set_property n v);
    rname

(* only a tiwindow can emit code *)
  method emit_code = failwith "emit_code"

(* some methods for emitting code *)
(* this one calculates the expand, fill and padding parameters
   of a box child *)
  method private get_packing packing =
    let aux name =
      let prop  = List.assoc name proplist in
      if prop#modified then " ~" ^ name ^ ":" ^ prop#code else ""
    in
    let efp = try
      (aux "expand") ^ (aux "fill") ^ (aux "padding")
    with Not_found -> "" in
    if efp = "" then ("~packing:" ^ packing)
    else ("~packing:(" ^ packing ^ efp ^ ")")

(* this one emits the declaration code of the widget *)
  method emit_init_code formatter ~packing =
    Format.fprintf formatter "@ @[<hv 2>let %s =@ @[<hov 2>%s"
      name self#class_name;
    List.iter self#get_mandatory_props
      ~f:begin fun name ->
	Format.fprintf formatter "@ ~%s:%s" name
	  (List.assoc name proplist)#code
      end;
    let packing = self#get_packing packing in
    if packing <> "" then Format.fprintf formatter "@ %s" packing;
    self#emit_prop_code formatter;
    Format.fprintf formatter "@ ()@ in@]@]"

(* this one emits the properties which do not have their
   default value; used by emit_init_code *)
  method private emit_prop_code formatter =
    let mandatory = self#get_mandatory_props in
    List.iter self#emit_clean_proplist ~f:
      begin  fun (name, prop) ->
	if List.mem name mandatory then () else
	if prop#modified then
	  Format.fprintf formatter "@ ~%s:%s" prop#name prop#code
      end

(* this one emits the method returning this widget *)
  method emit_method_code formatter =
    Format.fprintf formatter "@ method %s = %s" name name;

(* emits the code in the initializer part for this widget *)
  method emit_initializer_code _ = ()

(* for saving the project to a file. Used also by copy and cut *)
  method private save_start formatter =
    Format.fprintf formatter "@\n@[<2><%s name=%s>" classe name;
    List.iter
      ~f:(fun p -> Format.fprintf formatter 
	  "@\n%s=\"%s\"" p (List.assoc p proplist)#get)
      self#get_mandatory_props
      

  method private save_end formatter =
    Format.fprintf formatter "@]@\n</%s>" classe

  method save formatter =
    self#save_start formatter;
    List.iter self#save_clean_proplist ~f:
      (fun (name, prop) ->
	if prop#modified then
	  Format.fprintf formatter "@\n%s=%s" name prop#save_code);
    self#forall ~callback:(fun w -> w#save formatter);
    self#save_end formatter


  method private save_to_string string_ref =
    let b = Buffer.create 80 in
    let f = Format.formatter_of_buffer b in
    self#save f;
    Format.pp_print_flush f ();
    string_ref := Buffer.contents b

  method private copy_to_sel selection = self#save_to_string selection

  method copy () = self#copy_to_sel selection

  method private cut () =
    self#copy ();
    self#remove_me ()

  method private paste () =
    let lexbuf = Lexing.from_string !selection in
    let node = Load_parser.widget Load_lexer.token lexbuf in
    self#add_children node
    

(* ML signal used when the name of the widget is changed *)
  val name_changed : string signal = new signal
  method connect = new tiwidget_signals ~signals:name_changed
  method private call_name_changed = name_changed#call


(* this is necessary because gtk_tree#remove deletes the tree
   when removing the last item  *)
(* suppressed this in gtktree2 
  method new_tree () =
    stree <- GTree2.tree;
    tree_item#set_subtree stree;
    tree_item#expand ()
*)

(* when full_menu is true we use the menu else the restricted menu *)
  val mutable full_menu  = true
  method set_full_menu b = full_menu <- b

(* the menu for this widget 
   This menu is recalculated when one clicks on the 3rd button.
   There is nothing to do e.g. when the name of the widget changes,
   it will change in the menu the next time. *)
  method private menu ~time = self#restricted_menu ~time

(* the restricted menu for this widget 
   used for containers when they are full *)
  method private restricted_menu ~time =
    let menu = GMenu.menu () in
    let mi_remove = GMenu.menu_item ~packing:menu#append	~label:"remove" ()
    and mi_cut  = GMenu.menu_item ~packing:menu#append ~label:"Cut" ()
    and mi_copy = GMenu.menu_item ~packing:menu#append ~label:"Copy" () in
    mi_remove#connect#activate ~callback:self#remove_me;
    mi_copy#connect#activate ~callback:self#copy;
    mi_cut#connect#activate ~callback:self#cut;
    menu#popup ~button:3 ~time

(* changes all that depends on the name *)
  method private set_new_name new_name =
    if test_unique new_name then begin
      Hashtbl.remove widget_map name;
      Hashtbl.add widget_map new_name (self : #tiwidget0 :> tiwidget0);
      if (classe = "radio_button") then begin
	radio_button_pool := new_name ::
	  (list_remove !radio_button_pool ~f:(fun x -> x = name));
	List.iter
	  ~f:(fun x -> Propwin.update (Hashtbl.find widget_map x) false)
	  !radio_button_pool
      end;
      label#set_text new_name;
      let old_name = name in
      name <- new_name;
(*      Propwin.change_name old_name new_name; *)
      name_list :=
	new_name :: (list_remove !name_list ~f:(fun n -> n=old_name));
      begin match self#parent with
      | None -> ()
      | Some p -> p#change_name_in_proplist old_name new_name
      end;
      self#call_name_changed new_name;
      true
    end
    else begin
      message_name ();
      Propwin.update self true;
      false
    end


(* moves the present tiw up in his parents' children list *)
(* does something only when the parent is a box *)
  method child_up = fun _ -> ()

  method up () = match parent with
  | None -> ()
  | Some t -> t#child_up (self : #tiwidget0 :> tiwidget0)

  method child_down = fun _ -> ()

  method down () = match parent with
  | None -> ()
  | Some t -> t#child_down (self : #tiwidget0 :> tiwidget0)


(* get the next tiwidget in the tree (used with Down arrow) *)
  method next =
    if children <> [] then fst (List.hd children)
    else begin
      match parent with
      |	None -> raise Not_found
      |	Some p -> p#next_child (self : #tiwidget0 :> tiwidget0)
    end

  method next_child child =
    let _, tl = cut_list ~item:child (List.map ~f:fst children) in
    match tl with
    | ch :: next :: _ -> next
    | ch :: [] -> begin
	match parent with
	| None -> raise Not_found
	| Some p -> p#next_child (self : #tiwidget0 :> tiwidget0)
    end
    | _ -> failwith "next_child"

(* get the last child of the last child ... of our last child.
   Used by prev. *)
  method last =
    if children = [] then (self : #tiwidget0 :> tiwidget0)
    else (fst (List.hd (List.rev children)))#last

(* get the previous tiwidget in the tree (used with Up arrow) *)
  method prev =
    match parent with
    | None -> raise Not_found
    | Some p ->
	let hd, _ = cut_list ~item:(self : #tiwidget0 :> tiwidget0)
	    (List.map ~f:fst p#children) in
	match hd with
	| [] -> p
	| h :: _ -> h#last

  initializer
    Hashtbl.add widget_map name (self : #tiwidget0 :> tiwidget0);
    name_list := name :: !name_list;
    parent_tree#insert tree_item ~pos;
    tree_item#set_subtree stree;
    tree_item#add label#coerce;
    tree_item#expand ();

    proplist <-  proplist @
      [ "name",
        new prop_string ~name:"name" ~init:name ~set:self#set_new_name; 
        "width", new prop_int ~name:"width" ~init:"-2"
	  ~set:(fun v -> widget#misc#set_geometry ~width:v (); true);
        "height", new prop_int ~name:"height" ~init:"-2"
	  ~set:(fun v -> widget#misc#set_geometry ~height:v (); true) ];

    self#add_signal name_changed;

    tree_item#event#connect#button_press ~callback:
      (fun ev -> match GdkEvent.get_type ev with
      | `BUTTON_PRESS ->
	  if GdkEvent.Button.button ev = 1 then begin
	    parent_window#change_selected
	      (self : #tiwidget0 :> tiwidget0);
	  end
	  else if GdkEvent.Button.button ev = 3 then begin
	    if full_menu
	    then self#menu ~time:(GdkEvent.Button.time ev)
	    else self#restricted_menu ~time:(GdkEvent.Button.time ev);
	  end;
	  GtkSignal.stop_emit ();
          true
      | _ -> false);
    ()
end

