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