summaryrefslogtreecommitdiffstats
path: root/otherlibs/labltk/browser/fileselect.ml
blob: 51d782b71b5ef823fb408de1418442743e76c8bf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
(*************************************************************************)
(*                                                                       *)
(*                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$ *)

(* file selection box *)

open StdLabels
open Useunix
open Str
open Filename

open Tk

(**** Memoized rexgexp *)

let (~!) = Jg_memo.fast ~f:Str.regexp

(************************************************************ Path name *)

(* Convert Windows-style directory separator '\' to caml-style '/' *)
let caml_dir path =
  if Sys.os_type = "Win32" then
    global_replace ~!"\\\\" "/" path
  else path

let parse_filter s = 
  let s = caml_dir s in
  (* replace // by / *)
  let s = global_replace ~!"/+" "/" s in
  (* replace /./ by / *)
  let s = global_replace ~!"/\\./" "/" s in
  (* replace hoge/../ by "" *)
  let s = global_replace
          ~!"\\([^/]\\|[^\\./][^/]\\|[^/][^\\./]\\|[^/][^/]+\\)/\\.\\./" "" s in
  (* replace hoge/..$ by *)
  let s = global_replace
          ~!"\\([^/]\\|[^\\./][^/]\\|[^/][^\\./]\\|[^/][^/]+\\)/\\.\\.$" "" s in
  (* replace ^/hoge/../ by / *)
  let s = global_replace ~!"^\\(/\\.\\.\\)+/" "/" s in
  if string_match ~!"^\\([^\\*?[]*[/:]\\)\\(.*\\)" s 0 then 
    let dirs = matched_group 1 s
    and ptrn = matched_group 2 s
    in
      dirs, ptrn
  else "", s

let concat dir name =
  let len = String.length dir in
  if len = 0 then name else
  if dir.[len-1] = '/' then dir ^ name
  else dir ^ "/" ^ name

let rec fixpoint ~f v =
  let v' = f v in
  if v = v' then v else fixpoint ~f v'

let unix_regexp s =
  let s = Str.global_replace ~!"[$^.+]" "\\\\\\0" s in
  let s = Str.global_replace ~!"\\*" ".*" s in
  let s = Str.global_replace ~!"\\?" ".?" s in
  let s =
    fixpoint s
      ~f:(Str.replace_first ~!"\\({.*\\),\\(.*}\\)" "\\1\\|\\2") in
  let s =
    Str.global_replace ~!"{\\(.*\\)}" "\\(\\1\\)" s in
  Str.regexp s

let exact_match ~pat s =
  Str.string_match pat s 0 && Str.match_end () = String.length s

let ls ~dir ~pattern =
  let files = get_files_in_directory dir in
  let regexp = unix_regexp pattern in
  List.filter files ~f:(exact_match ~pat:regexp)

(********************************************* Creation *)
let load_in_path = ref false

let search_in_path ~name = Misc.find_in_path !Config.load_path name

let f ~title ~action:proc ?(dir = Unix.getcwd ())
    ?filter:(deffilter ="*") ?file:(deffile ="")
    ?(multi=false) ?(sync=false) ?(usepath=true) () =

  let current_pattern = ref ""
  and current_dir = ref (caml_dir dir) in

  let may_prefix name =
    if Filename.is_relative name then concat !current_dir name else name in
  
  let tl = Jg_toplevel.titled title in
  Focus.set tl;

  let new_var () = Textvariable.create ~on:tl () in
  let filter_var = new_var ()
  and selection_var = new_var ()
  and sync_var = new_var () in
  Textvariable.set filter_var deffilter;

  let frm = Frame.create tl ~borderwidth:1 ~relief:`Raised in
    let df = Frame.create frm in
      let dfl = Frame.create df in
        let dfll = Label.create dfl ~text:"Directories" in
        let dflf, directory_listbox, directory_scrollbar =
            Jg_box.create_with_scrollbar dfl in
      let dfr = Frame.create df in
        let dfrl = Label.create dfr ~text:"Files" in
        let dfrf, filter_listbox, filter_scrollbar =
            Jg_box.create_with_scrollbar dfr in
  let cfrm = Frame.create tl ~borderwidth:1 ~relief:`Raised in

  let configure ~filter =
    let filter = may_prefix filter in
    let dir, pattern = parse_filter filter in
    let dir = if !load_in_path && usepath then "" else
              (current_dir := Filename.dirname dir; dir)
    and pattern = if pattern = "" then "*" else pattern in
      current_pattern := pattern;
    let filter =
        if !load_in_path && usepath then pattern else dir ^ pattern in
    let directories = get_directories_in_files ~path:dir 
          (get_files_in_directory dir) in
    let matched_files = (* get matched file by subshell call. *)
      if !load_in_path && usepath then
      List.fold_left !Config.load_path ~init:[] ~f:
      begin fun acc dir ->
        let files = ls ~dir ~pattern in
        Sort.merge (<) files
          (List.fold_left files ~init:acc
           ~f:(fun acc name -> List2.exclude name acc))
      end
      else
        List.fold_left directories ~init:(ls ~dir ~pattern)
          ~f:(fun acc dir -> List2.exclude dir acc)
    in
      Textvariable.set filter_var filter;
      Textvariable.set selection_var (dir ^ deffile); 
      Listbox.delete filter_listbox ~first:(`Num 0) ~last:`End;
      Listbox.insert filter_listbox ~index:`End ~texts:matched_files;
      Jg_box.recenter filter_listbox ~index:(`Num 0);
      if !load_in_path && usepath then
        Listbox.configure directory_listbox ~takefocus:false
      else
      begin
        Listbox.configure directory_listbox ~takefocus:true;
        Listbox.delete directory_listbox ~first:(`Num 0) ~last:`End;
        Listbox.insert directory_listbox ~index:`End ~texts:directories;
        Jg_box.recenter directory_listbox ~index:(`Num 0)
      end
  in
  
  let selected_files = ref [] in (* used for synchronous mode *)
  let activate l =
    Grab.release tl;
    destroy tl;
    let l =
      if !load_in_path && usepath then
        List.fold_right l ~init:[] ~f:
        begin fun name acc ->
          if not (Filename.is_implicit name) then
            may_prefix name :: acc
          else try search_in_path ~name :: acc with Not_found -> acc
        end
      else
        List.map l ~f:may_prefix
    in
    if sync then 
      begin
        selected_files := l;
        Textvariable.set sync_var "1"
      end
    else proc l 
  in
  
  (* entries *)
  let fl = Label.create frm ~text:"Filter" in
  let sl = Label.create frm ~text:"Selection" in
  let filter_entry = Jg_entry.create frm ~textvariable:filter_var
      ~command:(fun filter -> configure ~filter) in
  let selection_entry = Jg_entry.create frm ~textvariable:selection_var
      ~command:(fun file -> activate [file]) in

  (* and buttons *)
  let set_path = Button.create dfl ~text:"Path editor" ~command:
    begin fun () ->
      Setpath.add_update_hook (fun () -> configure ~filter:!current_pattern);
      let w = Setpath.f ~dir:!current_dir in
      Grab.set w;
      bind w ~events:[`Destroy] ~extend:true ~action:(fun _ -> Grab.set tl)
    end in
  let toggle_in_path = Checkbutton.create dfl ~text:"Use load path"
    ~command:
    begin fun () ->
      load_in_path := not !load_in_path;
      if !load_in_path then
        pack [set_path] ~side:`Bottom ~fill:`X ~expand:true
      else
        Pack.forget [set_path];
      configure ~filter:(Textvariable.get filter_var)
    end
  and okb = Button.create cfrm ~text:"Ok" ~command:
    begin fun () -> 
      let files = 
        List.map (Listbox.curselection filter_listbox) ~f:
        begin fun x ->
          !current_dir ^ Listbox.get filter_listbox ~index:x
        end
      in
      let files = if files = [] then [Textvariable.get selection_var] 
                                else files in
      activate [Textvariable.get selection_var]
    end
  and flb = Button.create cfrm ~text:"Filter"
      ~command:(fun () -> configure ~filter:(Textvariable.get filter_var))
  and ccb = Button.create cfrm ~text:"Cancel"
      ~command:(fun () -> activate []) in

  (* binding *)
  bind tl ~events:[`KeyPressDetail "Escape"] ~action:(fun _ -> activate []);
  Jg_box.add_completion filter_listbox
    ~action:(fun index -> activate [Listbox.get filter_listbox ~index]);
  if multi then Listbox.configure filter_listbox ~selectmode:`Multiple else
  bind filter_listbox ~events:[`ButtonPressDetail 1] ~fields:[`MouseY]
    ~action:(fun ev ->
      let name = Listbox.get filter_listbox
          ~index:(Listbox.nearest filter_listbox ~y:ev.ev_MouseY) in
      if !load_in_path && usepath then
        try Textvariable.set selection_var (search_in_path ~name)
        with Not_found -> ()
      else Textvariable.set selection_var (may_prefix name));

  Jg_box.add_completion directory_listbox ~action:
    begin fun index ->
      let filter =
        may_prefix (Listbox.get directory_listbox ~index) ^
        "/" ^ !current_pattern
      in configure ~filter
    end;

    pack [frm] ~fill:`Both ~expand:true;
    (* filter *)
    pack [fl] ~side:`Top ~anchor:`W;
    pack [filter_entry] ~side:`Top ~fill:`X;

    (* directory + files *)
    pack [df] ~side:`Top ~fill:`Both ~expand:true;
    (* directory *)
    pack [dfl] ~side:`Left ~fill:`Both ~expand:true;
    pack [dfll] ~side:`Top ~anchor:`W;
    if usepath then pack [toggle_in_path] ~side:`Bottom ~anchor:`W;
    pack [dflf] ~side:`Top ~fill:`Both ~expand:true;
    pack [directory_scrollbar] ~side:`Right ~fill:`Y;
    pack [directory_listbox] ~side:`Left ~fill:`Both ~expand:true;
    (* files *)
    pack [dfr] ~side:`Right ~fill:`Both ~expand:true;
    pack [dfrl] ~side:`Top ~anchor:`W;
    pack [dfrf] ~side:`Top ~fill:`Both ~expand:true;
    pack [filter_scrollbar] ~side:`Right ~fill:`Y; 
    pack [filter_listbox] ~side:`Left ~fill:`Both ~expand:true;

    (* selection *)
    pack [sl] ~before:df ~side:`Bottom ~anchor:`W;
    pack [selection_entry] ~before:sl ~side:`Bottom ~fill:`X;

    (* create OK, Filter and Cancel buttons *)
    pack [okb; flb; ccb] ~side:`Left ~fill:`X ~expand:true;
    pack [cfrm] ~before:frm ~side:`Bottom ~fill:`X;

  if !load_in_path && usepath then begin
    load_in_path := false;
    Checkbutton.invoke toggle_in_path;
    Checkbutton.select toggle_in_path
  end
  else configure ~filter:deffilter;

    Tkwait.visibility tl;
    Grab.set tl;

    if sync then
      begin
        Tkwait.variable sync_var;
        proc !selected_files
      end;
    ()