(*************************************************************************)
(*                                                                       *)
(*                Objective Caml LablTk library                          *)
(*                                                                       *)
(*            Jacques Garrigue, Kyoto University RIMS                    *)
(*                                                                       *)
(*   Copyright 1999 Institut National de Recherche en Informatique et    *)
(*   en Automatique and Kyoto University.  All rights reserved.          *)
(*   This file is distributed under the terms of the GNU Library         *)
(*   General Public License, with the special exception on linking       *)
(*   described in file ../../../LICENSE.                                 *)
(*                                                                       *)
(*************************************************************************)

(* $Id: jg_multibox.ml 4144 2001-12-07 13:41:02Z xleroy $ *)

open StdLabels

let rec gen_list ~f:f ~len =
  if len = 0 then [] else f () :: gen_list ~f:f ~len:(len - 1)

let rec make_list ~len ~fill =
  if len = 0 then [] else fill :: make_list ~len:(len - 1) ~fill

(* By column version
let rec firsts ~len l =
  if len = 0 then ([],l) else
  match l with
    a::l ->
      let (f,l) = firsts l len:(len - 1) in
      (a::f,l)
  | [] ->
      (l,[])

let rec split ~len = function
    [] -> []
  | l ->
      let (f,r) = firsts l ~len in
      let ret = split ~len r in
      f :: ret

let extend l ~len ~fill =
  if List.length l >= len then l
  else l @ make_list ~fill len:(len - List.length l)
*)

(* By row version *)

let rec first l ~len =
  if len = 0 then [], l else
  match l with
    [] -> make_list ~len ~fill:"", []
  | a::l ->
      let (l',r) = first ~len:(len - 1) l in a::l',r

let rec split l ~len =
  if l = [] then make_list ~len ~fill:[] else
  let (cars,r) = first l ~len in
  let cdrs = split r ~len in
  List.map2 cars cdrs ~f:(fun a l -> a::l)
  

open Tk

class c ~cols ~texts ?maxheight ?width parent = object (self)
  val parent' = coe parent
  val length = List.length texts
  val boxes =
    let height = (List.length texts - 1) / cols + 1 in
    let height =
      match maxheight with None -> height
      | Some max -> min max height
    in
    gen_list ~len:cols ~f:
      begin fun () ->
        Listbox.create parent ~height ?width
          ~highlightthickness:0
          ~borderwidth:1
      end
  val mutable current = 0
  method cols = cols
  method texts = texts
  method parent = parent'
  method boxes = boxes
  method current = current
  method recenter ?(aligntop=false) n =
    current <-
       if n < 0 then 0 else
       if n < length then n else length - 1;
    (* Activate it, to keep consistent with Up/Down.
       You have to be in Extended or Browse mode *)
    let box = List.nth boxes (current mod cols)
    and index = `Num (current / cols) in
    List.iter boxes ~f:
      begin fun box ->
        Listbox.selection_clear box ~first:(`Num 0) ~last:`End;
        Listbox.selection_anchor box ~index;
        Listbox.activate box ~index
      end;
    Focus.set box;
    if aligntop then Listbox.yview_index box ~index
    else Listbox.see box ~index;
    let (first,last) = Listbox.yview_get box in
    List.iter boxes ~f:(Listbox.yview ~scroll:(`Moveto first))
  method init =
    let textl = split ~len:cols texts in
    List.iter2 boxes textl ~f:
      begin fun box texts ->
        Jg_bind.enter_focus box;
        Listbox.insert box ~texts ~index:`End
      end;
    pack boxes ~side:`Left ~expand:true ~fill:`Both;
    self#bind_mouse ~events:[`ButtonPressDetail 1]
      ~action:(fun _ ~index:n -> self#recenter n; break ());
    let current_height () =
      let (top,bottom) = Listbox.yview_get (List.hd boxes) in
      truncate ((bottom -. top) *. float (Listbox.size (List.hd boxes))
                  +. 0.99)
    in
    List.iter
      [ "Right", (fun n -> n+1);
        "Left", (fun n -> n-1);
        "Up", (fun n -> n-cols);
        "Down", (fun n -> n+cols);
        "Prior", (fun n -> n - current_height () * cols);
        "Next", (fun n -> n + current_height () * cols);
        "Home", (fun _ -> 0);
        "End", (fun _ -> List.length texts) ]
      ~f:begin fun (key,f) ->
        self#bind_kbd ~events:[`KeyPressDetail key]
          ~action:(fun _ ~index:n -> self#recenter (f n); break ())
      end;
    self#recenter 0
  method bind_mouse ~events ~action =
    let i = ref 0 in
    List.iter boxes ~f:
      begin fun box ->
        let b = !i in
        bind box ~events ~breakable:true ~fields:[`MouseX;`MouseY]
          ~action:(fun ev ->
            let `Num n = Listbox.nearest box ~y:ev.ev_MouseY
            in action ev ~index:(n * cols + b));
        incr i
      end
  method bind_kbd ~events ~action =
    let i = ref 0 in
    List.iter boxes ~f:
      begin fun box ->
        let b = !i in
        bind box ~events ~breakable:true ~fields:[`Char]
          ~action:(fun ev ->
            let `Num n = Listbox.index box ~index:`Active in
            action ev ~index:(n * cols + b));
        incr i
      end
end

let add_scrollbar (box : c) =
  let boxes = box#boxes in
  let sb =
    Scrollbar.create (box#parent)
      ~command:(fun ~scroll -> List.iter boxes ~f:(Listbox.yview ~scroll)) in
  List.iter boxes
    ~f:(fun lb -> Listbox.configure lb ~yscrollcommand:(Scrollbar.set sb));
  pack [sb] ~before:(List.hd boxes) ~side:`Right ~fill:`Y;
  sb

let add_completion ?action ?wait (box : c) =
  let comp = new Jg_completion.timed (box#texts) ?wait in
  box#bind_kbd ~events:[`KeyPress]
    ~action:(fun ev ~index -> 
      (* consider only keys producing characters. The callback is called
       * even if you press Shift. *)
      if ev.ev_Char <> "" then
        box#recenter (comp#add ev.ev_Char) ~aligntop:true);
  match action with
    Some action ->
      box#bind_kbd ~events:[`KeyPressDetail "space"]
        ~action:(fun ev ~index -> action (box#current));
      box#bind_kbd ~events:[`KeyPressDetail "Return"]
        ~action:(fun ev ~index -> action (box#current));
      box#bind_mouse ~events:[`ButtonPressDetail 1]
        ~action:(fun ev ~index ->
          box#recenter index; action (box#current); break ())
  | None -> ()
