(***********************************************************************) (* *) (* MLTk, Tcl/Tk interface of Objective Caml *) (* *) (* Francois Rouaix, Francois Pessaux, Jun Furuse and Pierre Weis *) (* projet Cristal, INRIA Rocquencourt *) (* Jacques Garrigue, Kyoto University RIMS *) (* *) (* Copyright 2002 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 found in the Objective Caml source tree. *) (* *) (***********************************************************************) (* $Id$ *) (* file selection box *) (* This file selecter works only under the OS with the full unix support. For the portability, Tk.getOpenFile and Tk.getSaveFile are recommended. *) open StdLabels open UnixLabels open Str open Filename open Tk open Widget exception Not_selected (********************************************************** Search directory *) (* Default is curdir *) let global_dir = ref (getcwd ()) (***************************************************** Some widgets creation *) (* from frx_listbox.ml *) let scroll_link sb lb = Listbox.configure lb ~yscrollcommand: (Scrollbar.set sb); Scrollbar.configure sb ~command: (Listbox.yview lb) (* focus when enter binding *) let bind_enter_focus w = bind w ~events:[`Enter] ~action:(fun _ -> Focus.set w);; let myentry_create p ~variable = let w = Entry.create p ~relief: `Sunken ~textvariable: variable in bind_enter_focus w; w (************************************************************* Subshell call *) let subshell cmd = let r,w = pipe () in match fork () with 0 -> close r; dup2 ~src:w ~dst:stdout; execv ~prog:"/bin/sh" ~args:[| "/bin/sh"; "-c"; cmd |]; exit 127 | id -> close w; let rc = in_channel_of_descr r in let rec it l = match try Some(input_line rc) with _ -> None with Some x -> it (x::l) | None -> List.rev l in let answer = it [] in close_in rc; (* because of finalize_channel *) let p, st = waitpid ~mode:[] id in answer (***************************************************************** Path name *) (* find directory name which doesn't contain "?*[" *) let dirget = regexp "^\\([^\\*?[]*/\\)\\(.*\\)" let parse_filter src = (* replace // by / *) let s = global_replace (regexp "/+") "/" src in (* replace /./ by / *) let s = global_replace (regexp "/\\./") "/" s in (* replace ????/../ by "" *) let s = global_replace (regexp "\\([^/]\\|[^\\./][^/]\\|[^/][^\\./]\\|[^/][^/]+\\)/\\.\\./") "" s in (* replace ????/..$ by "" *) let s = global_replace (regexp "\\([^/]\\|[^\\./][^/]\\|[^/][^\\./]\\|[^/][^/]+\\)/\\.\\.$") "" s in (* replace ^/../../ by / *) let s = global_replace (regexp "^\\(/\\.\\.\\)+/") "/" s in if string_match dirget s 0 then let dirs = matched_group 1 s and ptrn = matched_group 2 s in dirs, ptrn else "", s let ls dir pattern = subshell ("cd " ^ dir ^ ";/bin/ls -ad " ^ pattern ^" 2>/dev/null") (*************************************************************** File System *) let get_files_in_directory dir = let dirh = opendir dir in let rec get_them l = match try Some(Unix.readdir dirh) with _ -> None with | None -> Unix.closedir dirh; l | Some x -> get_them (x::l) in List.sort ~cmp:compare (get_them []) let rec get_directories_in_files path = List.filter ~f:(fun x -> try (stat (path ^ x)).st_kind = S_DIR with _ -> false) let remove_directories path = List.filter ~f:(fun x -> try (stat (path ^ x)).st_kind <> S_DIR with _ -> false) (************************* a nice interface to listbox - from frx_listbox.ml *) let add_completion lb action = let prefx = ref "" (* current match prefix *) and maxi = ref 0 (* maximum index (doesn'y matter actually) *) and current = ref 0 (* current position *) and lastevent = ref 0 in let rec move_forward () = if Listbox.get lb ~index:(`Num !current) < !prefx then if !current < !maxi then begin incr current; move_forward() end and recenter () = let element = `Num !current in (* Clean the selection *) Listbox.selection_clear lb ~first:(`Num 0) ~last:`End; (* Set it to our unique element *) Listbox.selection_set lb ~first:element ~last:element; (* Activate it, to keep consistent with Up/Down. You have to be in Extended or Browse mode *) Listbox.activate lb ~index:element; Listbox.selection_anchor lb ~index:element; Listbox.see lb ~index:element in let complete time s = if time - !lastevent < 500 then (* sorry, hard coded limit *) prefx := !prefx ^ s else begin (* reset *) current := 0; prefx := s end; lastevent := time; move_forward(); recenter() in bind lb ~events:[`KeyPress] ~fields:[`Char; `Time] (* consider only keys producing characters. The callback is called if you press Shift. *) ~action:(fun ev -> if ev.ev_Char <> "" then complete ev.ev_Time ev.ev_Char); (* Key specific bindings override KeyPress *) bind lb ~events:[`KeyPressDetail "Return"] ~action; (* Finally, we have to set focus, otherwise events dont get through *) Focus.set lb; recenter() (* so that first item is selected *); (* returns init_completion function *) (fun lb -> prefx := ""; maxi := Listbox.size lb - 1; current := 0) (****************************************************************** Creation *) let f ~title ~action:proc ~filter:deffilter ~file:deffile ~multi ~sync = (* Ah ! Now I regret about the names of the widgets... *) let current_pattern = ref "" and current_dir = ref "" in (* init_completions *) let filter_init_completion = ref (fun _ -> ()) and directory_init_completion = ref (fun _ -> ()) in let tl = Toplevel.create default_toplevel in Focus.set tl; Wm.title_set tl title; let filter_var = Textvariable.create ~on:tl () (* new_temporary *) and selection_var = Textvariable.create ~on:tl () and sync_var = Textvariable.create ~on:tl () in let frm' = Frame.create tl ~borderwidth: 1 ~relief: `Raised in let frm = Frame.create frm' ~borderwidth: 8 in let fl = Label.create frm ~text: "Filter" in let df = Frame.create frm in let dfl = Frame.create df in let dfll = Label.create dfl ~text: "Directories" in let dflf = Frame.create dfl in let directory_listbox = Listbox.create dflf ~relief: `Sunken and directory_scrollbar = Scrollbar.create dflf in scroll_link directory_scrollbar directory_listbox; let dfr = Frame.create df in let dfrl = Label.create dfr ~text: "Files" in let dfrf = Frame.create dfr in let filter_listbox = Listbox.create dfrf ~relief: `Sunken in let filter_scrollbar = Scrollbar.create dfrf in scroll_link filter_scrollbar filter_listbox; let sl = Label.create frm ~text: "Selection" in let filter_entry = myentry_create frm ~variable: filter_var in let selection_entry = myentry_create frm ~variable: selection_var in let cfrm' = Frame.create tl ~borderwidth: 1 ~relief: `Raised in let cfrm = Frame.create cfrm' ~borderwidth: 8 in let dumf = Frame.create cfrm in let dumf2 = Frame.create cfrm in let configure filter = (* OLDER let curdir = getcwd () in *) (* Printf.eprintf "CURDIR %s\n" curdir; *) let filter = if string_match (regexp "^/.*") filter 0 then filter else if filter = "" then !global_dir ^ "/*" else !global_dir ^ "/" ^ filter in (* Printf.eprintf "FILTER %s\n" filter; *) let dirname, patternname = parse_filter filter in (* Printf.eprintf "DIRNAME %s PATTERNNAME %s\n" dirname patternname; *) current_dir := dirname; global_dir := dirname; let patternname = if patternname = "" then "*" else patternname in current_pattern := patternname; let filter = dirname ^ patternname in (* Printf.eprintf "FILTER : %s\n\n" filter; *) (* flush Pervasives.stderr; *) try let directories = get_directories_in_files dirname (get_files_in_directory dirname) in (* get matched file by subshell call. *) let matched_files = remove_directories dirname (ls dirname patternname) in Textvariable.set filter_var filter; Textvariable.set selection_var (dirname ^ deffile); Listbox.delete directory_listbox ~first:(`Num 0) ~last:`End; Listbox.insert directory_listbox ~index:`End ~texts:directories; Listbox.delete filter_listbox ~first:(`Num 0) ~last:`End; Listbox.insert filter_listbox ~index:`End ~texts:matched_files; !directory_init_completion directory_listbox; !filter_init_completion filter_listbox with Unix_error (ENOENT,_,_) -> (* Directory is not found (maybe) *) Bell.ring () in let selected_files = ref [] in (* used for synchronous mode *) let activate l () = Grab.release tl; destroy tl; if sync then begin selected_files := l; Textvariable.set sync_var "1" end else begin proc l; break () end in (* and buttons *) let okb = Button.create cfrm ~text: "OK" ~command: begin fun () -> let files = List.map (Listbox.curselection filter_listbox) ~f:(fun x -> !current_dir ^ (Listbox.get filter_listbox ~index:x)) in let files = if files = [] then [Textvariable.get selection_var] else files in activate files () end in let flb = Button.create cfrm ~text: "Filter" ~command: (fun () -> configure (Textvariable.get filter_var)) in let ccb = Button.create cfrm ~text: "Cancel" ~command: (fun () -> activate [] ()) in (* binding *) bind selection_entry ~events:[`KeyPressDetail "Return"] ~breakable:true ~action:(fun _ -> activate [Textvariable.get selection_var] ()); bind filter_entry ~events:[`KeyPressDetail "Return"] ~action:(fun _ -> configure (Textvariable.get filter_var)); let action _ = let files = List.map (Listbox.curselection filter_listbox) ~f:(fun x -> !current_dir ^ (Listbox.get filter_listbox ~index:x)) in activate files () in bind filter_listbox ~events:[`Modified([`Double], `ButtonPressDetail 1)] ~breakable:true ~action; if multi then Listbox.configure filter_listbox ~selectmode: `Multiple; filter_init_completion := add_completion filter_listbox action; let action _ = try configure (!current_dir ^ ((function [x] -> Listbox.get directory_listbox ~index:x | _ -> (* you must choose at least one directory. *) Bell.ring (); raise Not_selected) (Listbox.curselection directory_listbox)) ^ "/" ^ !current_pattern) with _ -> () in bind directory_listbox ~events:[`Modified([`Double], `ButtonPressDetail 1)] ~breakable:true ~action; Listbox.configure directory_listbox ~selectmode: `Browse; directory_init_completion := add_completion directory_listbox action; pack [frm'; frm] ~fill: `X; (* filter *) pack [fl] ~side: `Top ~anchor: `W; pack [filter_entry] ~side: `Top ~fill: `X; (* directory + files *) pack [df] ~side: `Top ~fill: `X ~ipadx: 8; (* directory *) pack [dfl] ~side: `Left; pack [dfll] ~side: `Top ~anchor: `W; pack [dflf] ~side: `Top; pack [coe directory_listbox; coe directory_scrollbar] ~side: `Left ~fill: `Y; (* files *) pack [dfr] ~side: `Right; pack [dfrl] ~side: `Top ~anchor: `W; pack [dfrf] ~side: `Top; pack [coe filter_listbox; coe filter_scrollbar] ~side: `Left ~fill: `Y; (* selection *) pack [sl] ~side: `Top ~anchor: `W; pack [selection_entry] ~side: `Top ~fill: `X; (* create OK, Filter and Cancel buttons *) pack [cfrm'] ~fill: `X; pack [cfrm] ~fill: `X; pack [okb] ~side: `Left; pack [dumf] ~side: `Left ~expand: true; pack [flb] ~side: `Left; pack [dumf2] ~side: `Left ~expand: true; pack [ccb] ~side: `Left; configure deffilter; Tkwait.visibility tl; Grab.set tl; if sync then begin Tkwait.variable sync_var; proc !selected_files end; ()