• 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
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# For the following classes and modules:
268#
269# options (all have default values):
270#
271# - defaultextension: added to filename if not explicitly given
272#
273# - filetypes: sequence of (label, pattern) tuples.  the same pattern
274#   may occur with several patterns.  use "*" as pattern to indicate
275#   all files.
276#
277# - initialdir: initial directory.  preserved by dialog instance.
278#
279# - initialfile: initial file (ignored by the open dialog).  preserved
280#   by dialog instance.
281#
282# - parent: which window to place the dialog on top of
283#
284# - title: dialog title
285#
286# - multiple: if true user may select more than one file
287#
288# options for the directory chooser:
289#
290# - initialdir, parent, title: see above
291#
292# - mustexist: if true, user must pick an existing directory
293#
294
295
296class _Dialog(commondialog.Dialog):
297
298    def _fixoptions(self):
299        try:
300            # make sure "filetypes" is a tuple
301            self.options["filetypes"] = tuple(self.options["filetypes"])
302        except KeyError:
303            pass
304
305    def _fixresult(self, widget, result):
306        if result:
307            # keep directory and filename until next time
308            # convert Tcl path objects to strings
309            try:
310                result = result.string
311            except AttributeError:
312                # it already is a string
313                pass
314            path, file = os.path.split(result)
315            self.options["initialdir"] = path
316            self.options["initialfile"] = file
317        self.filename = result # compatibility
318        return result
319
320
321#
322# file dialogs
323
324class Open(_Dialog):
325    "Ask for a filename to open"
326
327    command = "tk_getOpenFile"
328
329    def _fixresult(self, widget, result):
330        if isinstance(result, tuple):
331            # multiple results:
332            result = tuple([getattr(r, "string", r) for r in result])
333            if result:
334                path, file = os.path.split(result[0])
335                self.options["initialdir"] = path
336                # don't set initialfile or filename, as we have multiple of these
337            return result
338        if not widget.tk.wantobjects() and "multiple" in self.options:
339            # Need to split result explicitly
340            return self._fixresult(widget, widget.tk.splitlist(result))
341        return _Dialog._fixresult(self, widget, result)
342
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
372
373def askopenfilename(**options):
374    "Ask for a filename to open"
375
376    return Open(**options).show()
377
378
379def asksaveasfilename(**options):
380    "Ask for a filename to save as"
381
382    return SaveAs(**options).show()
383
384
385def askopenfilenames(**options):
386    """Ask for multiple filenames to open
387
388    Returns a list of filenames or empty list if
389    cancel button selected
390    """
391    options["multiple"]=1
392    return Open(**options).show()
393
394# FIXME: are the following  perhaps a bit too convenient?
395
396
397def askopenfile(mode = "r", **options):
398    "Ask for a filename to open, and returned the opened file"
399
400    filename = Open(**options).show()
401    if filename:
402        return open(filename, mode)
403    return None
404
405
406def askopenfiles(mode = "r", **options):
407    """Ask for multiple filenames and return the open file
408    objects
409
410    returns a list of open file objects or an empty list if
411    cancel selected
412    """
413
414    files = askopenfilenames(**options)
415    if files:
416        ofiles=[]
417        for filename in files:
418            ofiles.append(open(filename, mode))
419        files=ofiles
420    return files
421
422
423def asksaveasfile(mode = "w", **options):
424    "Ask for a filename to save as, and returned the opened file"
425
426    filename = SaveAs(**options).show()
427    if filename:
428        return open(filename, mode)
429    return None
430
431
432def askdirectory (**options):
433    "Ask for a directory, and return the file name"
434    return Directory(**options).show()
435
436
437# --------------------------------------------------------------------
438# test stuff
439
440def test():
441    """Simple test program."""
442    root = Tk()
443    root.withdraw()
444    fd = LoadFileDialog(root)
445    loadfile = fd.go(key="test")
446    fd = SaveFileDialog(root)
447    savefile = fd.go(key="test")
448    print(loadfile, savefile)
449
450    # Since the file name may contain non-ASCII characters, we need
451    # to find an encoding that likely supports the file name, and
452    # displays correctly on the terminal.
453
454    # Start off with UTF-8
455    enc = "utf-8"
456    import sys
457
458    # See whether CODESET is defined
459    try:
460        import locale
461        locale.setlocale(locale.LC_ALL,'')
462        enc = locale.nl_langinfo(locale.CODESET)
463    except (ImportError, AttributeError):
464        pass
465
466    # dialog for opening files
467
468    openfilename=askopenfilename(filetypes=[("all files", "*")])
469    try:
470        fp=open(openfilename,"r")
471        fp.close()
472    except:
473        print("Could not open File: ")
474        print(sys.exc_info()[1])
475
476    print("open", openfilename.encode(enc))
477
478    # dialog for saving files
479
480    saveasfilename=asksaveasfilename()
481    print("saveas", saveasfilename.encode(enc))
482
483
484if __name__ == '__main__':
485    test()
486