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