1"""File selection dialog classes.
2
3Classes:
4
5- FileDialog
6- LoadFileDialog
7- SaveFileDialog
8
9This module also presents tk common file dialogues, it provides interfaces
10to the native file dialogues available in Tk 4.2 and newer, and the
11directory dialogue available in Tk 8.3 and newer.
12These interfaces were written by Fredrik Lundh, May 1997.
13"""
14
15from tkinter import *
16from tkinter.dialog import Dialog
17from tkinter import commondialog
18
19import os
20import fnmatch
21
22
23dialogstates = {}
24
25
26class FileDialog:
27
28    """Standard file selection dialog -- no checks on selected file.
29
30    Usage:
31
32        d = FileDialog(master)
33        fname = d.go(dir_or_file, pattern, default, key)
34        if fname is None: ...canceled...
35        else: ...open file...
36
37    All arguments to go() are optional.
38
39    The 'key' argument specifies a key in the global dictionary
40    'dialogstates', which keeps track of the values for the directory
41    and pattern arguments, overriding the values passed in (it does
42    not keep track of the default argument!).  If no key is specified,
43    the dialog keeps no memory of previous state.  Note that memory is
44    kept even when the dialog is canceled.  (All this emulates the
45    behavior of the Macintosh file selection dialogs.)
46
47    """
48
49    title = "File Selection Dialog"
50
51    def __init__(self, master, title=None):
52        if title is None: title = self.title
53        self.master = master
54        self.directory = None
55
56        self.top = Toplevel(master)
57        self.top.title(title)
58        self.top.iconname(title)
59
60        self.botframe = Frame(self.top)
61        self.botframe.pack(side=BOTTOM, fill=X)
62
63        self.selection = Entry(self.top)
64        self.selection.pack(side=BOTTOM, fill=X)
65        self.selection.bind('<Return>', self.ok_event)
66
67        self.filter = Entry(self.top)
68        self.filter.pack(side=TOP, fill=X)
69        self.filter.bind('<Return>', self.filter_command)
70
71        self.midframe = Frame(self.top)
72        self.midframe.pack(expand=YES, fill=BOTH)
73
74        self.filesbar = Scrollbar(self.midframe)
75        self.filesbar.pack(side=RIGHT, fill=Y)
76        self.files = Listbox(self.midframe, exportselection=0,
77                             yscrollcommand=(self.filesbar, 'set'))
78        self.files.pack(side=RIGHT, expand=YES, fill=BOTH)
79        btags = self.files.bindtags()
80        self.files.bindtags(btags[1:] + btags[:1])
81        self.files.bind('<ButtonRelease-1>', self.files_select_event)
82        self.files.bind('<Double-ButtonRelease-1>', self.files_double_event)
83        self.filesbar.config(command=(self.files, 'yview'))
84
85        self.dirsbar = Scrollbar(self.midframe)
86        self.dirsbar.pack(side=LEFT, fill=Y)
87        self.dirs = Listbox(self.midframe, exportselection=0,
88                            yscrollcommand=(self.dirsbar, 'set'))
89        self.dirs.pack(side=LEFT, expand=YES, fill=BOTH)
90        self.dirsbar.config(command=(self.dirs, 'yview'))
91        btags = self.dirs.bindtags()
92        self.dirs.bindtags(btags[1:] + btags[:1])
93        self.dirs.bind('<ButtonRelease-1>', self.dirs_select_event)
94        self.dirs.bind('<Double-ButtonRelease-1>', self.dirs_double_event)
95
96        self.ok_button = Button(self.botframe,
97                                 text="OK",
98                                 command=self.ok_command)
99        self.ok_button.pack(side=LEFT)
100        self.filter_button = Button(self.botframe,
101                                    text="Filter",
102                                    command=self.filter_command)
103        self.filter_button.pack(side=LEFT, expand=YES)
104        self.cancel_button = Button(self.botframe,
105                                    text="Cancel",
106                                    command=self.cancel_command)
107        self.cancel_button.pack(side=RIGHT)
108
109        self.top.protocol('WM_DELETE_WINDOW', self.cancel_command)
110        # XXX Are the following okay for a general audience?
111        self.top.bind('<Alt-w>', self.cancel_command)
112        self.top.bind('<Alt-W>', self.cancel_command)
113
114    def go(self, dir_or_file=os.curdir, pattern="*", default="", key=None):
115        if key and key in dialogstates:
116            self.directory, pattern = dialogstates[key]
117        else:
118            dir_or_file = os.path.expanduser(dir_or_file)
119            if os.path.isdir(dir_or_file):
120                self.directory = dir_or_file
121            else:
122                self.directory, default = os.path.split(dir_or_file)
123        self.set_filter(self.directory, pattern)
124        self.set_selection(default)
125        self.filter_command()
126        self.selection.focus_set()
127        self.top.wait_visibility() # window needs to be visible for the grab
128        self.top.grab_set()
129        self.how = None
130        self.master.mainloop()          # Exited by self.quit(how)
131        if key:
132            directory, pattern = self.get_filter()
133            if self.how:
134                directory = os.path.dirname(self.how)
135            dialogstates[key] = directory, pattern
136        self.top.destroy()
137        return self.how
138
139    def quit(self, how=None):
140        self.how = how
141        self.master.quit()              # Exit mainloop()
142
143    def dirs_double_event(self, event):
144        self.filter_command()
145
146    def dirs_select_event(self, event):
147        dir, pat = self.get_filter()
148        subdir = self.dirs.get('active')
149        dir = os.path.normpath(os.path.join(self.directory, subdir))
150        self.set_filter(dir, pat)
151
152    def files_double_event(self, event):
153        self.ok_command()
154
155    def files_select_event(self, event):
156        file = self.files.get('active')
157        self.set_selection(file)
158
159    def ok_event(self, event):
160        self.ok_command()
161
162    def ok_command(self):
163        self.quit(self.get_selection())
164
165    def filter_command(self, event=None):
166        dir, pat = self.get_filter()
167        try:
168            names = os.listdir(dir)
169        except OSError:
170            self.master.bell()
171            return
172        self.directory = dir
173        self.set_filter(dir, pat)
174        names.sort()
175        subdirs = [os.pardir]
176        matchingfiles = []
177        for name in names:
178            fullname = os.path.join(dir, name)
179            if os.path.isdir(fullname):
180                subdirs.append(name)
181            elif fnmatch.fnmatch(name, pat):
182                matchingfiles.append(name)
183        self.dirs.delete(0, END)
184        for name in subdirs:
185            self.dirs.insert(END, name)
186        self.files.delete(0, END)
187        for name in matchingfiles:
188            self.files.insert(END, name)
189        head, tail = os.path.split(self.get_selection())
190        if tail == os.curdir: tail = ''
191        self.set_selection(tail)
192
193    def get_filter(self):
194        filter = self.filter.get()
195        filter = os.path.expanduser(filter)
196        if filter[-1:] == os.sep or os.path.isdir(filter):
197            filter = os.path.join(filter, "*")
198        return os.path.split(filter)
199
200    def get_selection(self):
201        file = self.selection.get()
202        file = os.path.expanduser(file)
203        return file
204
205    def cancel_command(self, event=None):
206        self.quit()
207
208    def set_filter(self, dir, pat):
209        if not os.path.isabs(dir):
210            try:
211                pwd = os.getcwd()
212            except OSError:
213                pwd = None
214            if pwd:
215                dir = os.path.join(pwd, dir)
216                dir = os.path.normpath(dir)
217        self.filter.delete(0, END)
218        self.filter.insert(END, os.path.join(dir or os.curdir, pat or "*"))
219
220    def set_selection(self, file):
221        self.selection.delete(0, END)
222        self.selection.insert(END, os.path.join(self.directory, file))
223
224
225class LoadFileDialog(FileDialog):
226
227    """File selection dialog which checks that the file exists."""
228
229    title = "Load File Selection Dialog"
230
231    def ok_command(self):
232        file = self.get_selection()
233        if not os.path.isfile(file):
234            self.master.bell()
235        else:
236            self.quit(file)
237
238
239class SaveFileDialog(FileDialog):
240
241    """File selection dialog which checks that the file may be created."""
242
243    title = "Save File Selection Dialog"
244
245    def ok_command(self):
246        file = self.get_selection()
247        if os.path.exists(file):
248            if os.path.isdir(file):
249                self.master.bell()
250                return
251            d = Dialog(self.top,
252                       title="Overwrite Existing File Question",
253                       text="Overwrite existing file %r?" % (file,),
254                       bitmap='questhead',
255                       default=1,
256                       strings=("Yes", "Cancel"))
257            if d.num != 0:
258                return
259        else:
260            head, tail = os.path.split(file)
261            if not os.path.isdir(head):
262                self.master.bell()
263                return
264        self.quit(file)
265
266
267
268# For the following classes and modules:
269#
270# options (all have default values):
271#
272# - defaultextension: added to filename if not explicitly given
273#
274# - filetypes: sequence of (label, pattern) tuples.  the same pattern
275#   may occur with several patterns.  use "*" as pattern to indicate
276#   all files.
277#
278# - initialdir: initial directory.  preserved by dialog instance.
279#
280# - initialfile: initial file (ignored by the open dialog).  preserved
281#   by dialog instance.
282#
283# - parent: which window to place the dialog on top of
284#
285# - title: dialog title
286#
287# - multiple: if true user may select more than one file
288#
289# options for the directory chooser:
290#
291# - initialdir, parent, title: see above
292#
293# - mustexist: if true, user must pick an existing directory
294#
295
296
297class _Dialog(commondialog.Dialog):
298
299    def _fixoptions(self):
300        try:
301            # make sure "filetypes" is a tuple
302            self.options["filetypes"] = tuple(self.options["filetypes"])
303        except KeyError:
304            pass
305
306    def _fixresult(self, widget, result):
307        if result:
308            # keep directory and filename until next time
309            # convert Tcl path objects to strings
310            try:
311                result = result.string
312            except AttributeError:
313                # it already is a string
314                pass
315            path, file = os.path.split(result)
316            self.options["initialdir"] = path
317            self.options["initialfile"] = file
318        self.filename = result # compatibility
319        return result
320
321
322#
323# file dialogs
324
325class Open(_Dialog):
326    "Ask for a filename to open"
327
328    command = "tk_getOpenFile"
329
330    def _fixresult(self, widget, result):
331        if isinstance(result, tuple):
332            # multiple results:
333            result = tuple([getattr(r, "string", r) for r in result])
334            if result:
335                path, file = os.path.split(result[0])
336                self.options["initialdir"] = path
337                # don't set initialfile or filename, as we have multiple of these
338            return result
339        if not widget.tk.wantobjects() and "multiple" in self.options:
340            # Need to split result explicitly
341            return self._fixresult(widget, widget.tk.splitlist(result))
342        return _Dialog._fixresult(self, widget, result)
343
344class SaveAs(_Dialog):
345    "Ask for a filename to save as"
346
347    command = "tk_getSaveFile"
348
349
350# the directory dialog has its own _fix routines.
351class Directory(commondialog.Dialog):
352    "Ask for a directory"
353
354    command = "tk_chooseDirectory"
355
356    def _fixresult(self, widget, result):
357        if result:
358            # convert Tcl path objects to strings
359            try:
360                result = result.string
361            except AttributeError:
362                # it already is a string
363                pass
364            # keep directory until next time
365            self.options["initialdir"] = result
366        self.directory = result # compatibility
367        return result
368
369#
370# convenience stuff
371
372def askopenfilename(**options):
373    "Ask for a filename to open"
374
375    return Open(**options).show()
376
377def asksaveasfilename(**options):
378    "Ask for a filename to save as"
379
380    return SaveAs(**options).show()
381
382def askopenfilenames(**options):
383    """Ask for multiple filenames to open
384
385    Returns a list of filenames or empty list if
386    cancel button selected
387    """
388    options["multiple"]=1
389    return Open(**options).show()
390
391# FIXME: are the following  perhaps a bit too convenient?
392
393def askopenfile(mode = "r", **options):
394    "Ask for a filename to open, and returned the opened file"
395
396    filename = Open(**options).show()
397    if filename:
398        return open(filename, mode)
399    return None
400
401def askopenfiles(mode = "r", **options):
402    """Ask for multiple filenames and return the open file
403    objects
404
405    returns a list of open file objects or an empty list if
406    cancel selected
407    """
408
409    files = askopenfilenames(**options)
410    if files:
411        ofiles=[]
412        for filename in files:
413            ofiles.append(open(filename, mode))
414        files=ofiles
415    return files
416
417
418def asksaveasfile(mode = "w", **options):
419    "Ask for a filename to save as, and returned the opened file"
420
421    filename = SaveAs(**options).show()
422    if filename:
423        return open(filename, mode)
424    return None
425
426def askdirectory (**options):
427    "Ask for a directory, and return the file name"
428    return Directory(**options).show()
429
430
431
432# --------------------------------------------------------------------
433# test stuff
434
435def test():
436    """Simple test program."""
437    root = Tk()
438    root.withdraw()
439    fd = LoadFileDialog(root)
440    loadfile = fd.go(key="test")
441    fd = SaveFileDialog(root)
442    savefile = fd.go(key="test")
443    print(loadfile, savefile)
444
445    # Since the file name may contain non-ASCII characters, we need
446    # to find an encoding that likely supports the file name, and
447    # displays correctly on the terminal.
448
449    # Start off with UTF-8
450    enc = "utf-8"
451    import sys
452
453    # See whether CODESET is defined
454    try:
455        import locale
456        locale.setlocale(locale.LC_ALL,'')
457        enc = locale.nl_langinfo(locale.CODESET)
458    except (ImportError, AttributeError):
459        pass
460
461    # dialog for openening files
462
463    openfilename=askopenfilename(filetypes=[("all files", "*")])
464    try:
465        fp=open(openfilename,"r")
466        fp.close()
467    except:
468        print("Could not open File: ")
469        print(sys.exc_info()[1])
470
471    print("open", openfilename.encode(enc))
472
473    # dialog for saving files
474
475    saveasfilename=asksaveasfilename()
476    print("saveas", saveasfilename.encode(enc))
477
478if __name__ == '__main__':
479    test()
480