blob: 3ed93eb8c163a4c5c18e52777dd72db68940b6bb [file] [log] [blame]
Guido van Rossumac4f8d31995-08-29 23:46:35 +00001"""File selection dialog classes.
2
3Classes:
4
5- FileDialog
6- LoadFileDialog
7- SaveFileDialog
8
Georg Brandl2e081362008-05-17 19:04:04 +00009This 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.
Guido van Rossumac4f8d31995-08-29 23:46:35 +000013"""
Flavian Hautbois76b64512019-07-26 03:30:33 +020014__all__ = ["FileDialog", "LoadFileDialog", "SaveFileDialog",
15 "Open", "SaveAs", "Directory",
16 "askopenfilename", "asksaveasfilename", "askopenfilenames",
17 "askopenfile", "askopenfiles", "asksaveasfile", "askdirectory"]
Guido van Rossumac4f8d31995-08-29 23:46:35 +000018
Flavian Hautbois76b64512019-07-26 03:30:33 +020019import 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)
Georg Brandl14fc4272008-05-17 18:39:55 +000025from tkinter.dialog import Dialog
Georg Brandl2e081362008-05-17 19:04:04 +000026from tkinter import commondialog
Guido van Rossumac4f8d31995-08-29 23:46:35 +000027
Guido van Rossumac4f8d31995-08-29 23:46:35 +000028
Guido van Rossum0978f991996-05-28 23:14:36 +000029dialogstates = {}
30
31
Guido van Rossumac4f8d31995-08-29 23:46:35 +000032class FileDialog:
33
34 """Standard file selection dialog -- no checks on selected file.
35
36 Usage:
37
38 d = FileDialog(master)
Andrew M. Kuchling38752812003-01-21 14:19:21 +000039 fname = d.go(dir_or_file, pattern, default, key)
40 if fname is None: ...canceled...
Guido van Rossumc4570481998-03-20 20:45:49 +000041 else: ...open file...
Guido van Rossum0978f991996-05-28 23:14:36 +000042
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
Thomas Wouters7e474022000-07-16 12:04:32 +000050 kept even when the dialog is canceled. (All this emulates the
Guido van Rossum0978f991996-05-28 23:14:36 +000051 behavior of the Macintosh file selection dialogs.)
Guido van Rossumac4f8d31995-08-29 23:46:35 +000052
53 """
54
55 title = "File Selection Dialog"
56
Guido van Rossum0978f991996-05-28 23:14:36 +000057 def __init__(self, master, title=None):
Guido van Rossumc4570481998-03-20 20:45:49 +000058 if title is None: title = self.title
59 self.master = master
60 self.directory = None
Guido van Rossum0978f991996-05-28 23:14:36 +000061
Guido van Rossumc4570481998-03-20 20:45:49 +000062 self.top = Toplevel(master)
63 self.top.title(title)
64 self.top.iconname(title)
Guido van Rossum0978f991996-05-28 23:14:36 +000065
Guido van Rossumc4570481998-03-20 20:45:49 +000066 self.botframe = Frame(self.top)
67 self.botframe.pack(side=BOTTOM, fill=X)
Guido van Rossum0978f991996-05-28 23:14:36 +000068
Guido van Rossumc4570481998-03-20 20:45:49 +000069 self.selection = Entry(self.top)
70 self.selection.pack(side=BOTTOM, fill=X)
71 self.selection.bind('<Return>', self.ok_event)
Guido van Rossum0978f991996-05-28 23:14:36 +000072
Guido van Rossumc4570481998-03-20 20:45:49 +000073 self.filter = Entry(self.top)
74 self.filter.pack(side=TOP, fill=X)
75 self.filter.bind('<Return>', self.filter_command)
Guido van Rossum0978f991996-05-28 23:14:36 +000076
Guido van Rossumc4570481998-03-20 20:45:49 +000077 self.midframe = Frame(self.top)
78 self.midframe.pack(expand=YES, fill=BOTH)
Guido van Rossum0978f991996-05-28 23:14:36 +000079
Guido van Rossumc4570481998-03-20 20:45:49 +000080 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'))
Guido van Rossum0978f991996-05-28 23:14:36 +000090
Guido van Rossumc4570481998-03-20 20:45:49 +000091 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)
Guido van Rossum0978f991996-05-28 23:14:36 +0000101
Guido van Rossumc4570481998-03-20 20:45:49 +0000102 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000114
Guido van Rossumc4570481998-03-20 20:45:49 +0000115 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)
Guido van Rossum0978f991996-05-28 23:14:36 +0000119
120 def go(self, dir_or_file=os.curdir, pattern="*", default="", key=None):
Guido van Rossume014a132006-08-19 16:53:45 +0000121 if key and key in dialogstates:
Guido van Rossumc4570481998-03-20 20:45:49 +0000122 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()
Martin v. Löwisb217cd82004-08-03 18:36:25 +0000133 self.top.wait_visibility() # window needs to be visible for the grab
Guido van Rossumc4570481998-03-20 20:45:49 +0000134 self.top.grab_set()
135 self.how = None
136 self.master.mainloop() # Exited by self.quit(how)
Fred Drake073b8291998-05-06 17:28:23 +0000137 if key:
138 directory, pattern = self.get_filter()
139 if self.how:
140 directory = os.path.dirname(self.how)
141 dialogstates[key] = directory, pattern
Guido van Rossumc4570481998-03-20 20:45:49 +0000142 self.top.destroy()
143 return self.how
Guido van Rossum0978f991996-05-28 23:14:36 +0000144
145 def quit(self, how=None):
Guido van Rossumc4570481998-03-20 20:45:49 +0000146 self.how = how
147 self.master.quit() # Exit mainloop()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000148
149 def dirs_double_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000150 self.filter_command()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000151
152 def dirs_select_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000153 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000157
158 def files_double_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000159 self.ok_command()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000160
161 def files_select_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000162 file = self.files.get('active')
163 self.set_selection(file)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000164
165 def ok_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000166 self.ok_command()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000167
168 def ok_command(self):
Guido van Rossumc4570481998-03-20 20:45:49 +0000169 self.quit(self.get_selection())
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000170
171 def filter_command(self, event=None):
Guido van Rossumc4570481998-03-20 20:45:49 +0000172 dir, pat = self.get_filter()
173 try:
174 names = os.listdir(dir)
Andrew Svetlov786fbd82012-12-17 19:51:15 +0200175 except OSError:
Guido van Rossumc4570481998-03-20 20:45:49 +0000176 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000198
199 def get_filter(self):
Guido van Rossumc4570481998-03-20 20:45:49 +0000200 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000205
Guido van Rossum0978f991996-05-28 23:14:36 +0000206 def get_selection(self):
Guido van Rossumc4570481998-03-20 20:45:49 +0000207 file = self.selection.get()
208 file = os.path.expanduser(file)
209 return file
Guido van Rossum0978f991996-05-28 23:14:36 +0000210
211 def cancel_command(self, event=None):
Guido van Rossumc4570481998-03-20 20:45:49 +0000212 self.quit()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000213
214 def set_filter(self, dir, pat):
Guido van Rossumc4570481998-03-20 20:45:49 +0000215 if not os.path.isabs(dir):
216 try:
217 pwd = os.getcwd()
Andrew Svetlov786fbd82012-12-17 19:51:15 +0200218 except OSError:
Guido van Rossumc4570481998-03-20 20:45:49 +0000219 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 "*"))
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000225
226 def set_selection(self, file):
Guido van Rossumc4570481998-03-20 20:45:49 +0000227 self.selection.delete(0, END)
228 self.selection.insert(END, os.path.join(self.directory, file))
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000229
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):
Guido van Rossumc4570481998-03-20 20:45:49 +0000238 file = self.get_selection()
239 if not os.path.isfile(file):
240 self.master.bell()
241 else:
242 self.quit(file)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000243
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):
Guido van Rossumc4570481998-03-20 20:45:49 +0000252 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",
Walter Dörwald70a6b492004-02-12 17:35:32 +0000259 text="Overwrite existing file %r?" % (file,),
Guido van Rossumc4570481998-03-20 20:45:49 +0000260 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000271
272
Georg Brandl2e081362008-05-17 19:04:04 +0000273# 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
Georg Brandl2e081362008-05-17 19:04:04 +0000314 # 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:
Georg Brandl2e081362008-05-17 19:04:04 +0000340 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300349
Georg Brandl2e081362008-05-17 19:04:04 +0000350class 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.
Benjamin Petersonde342c42008-12-31 14:51:07 +0000357class Directory(commondialog.Dialog):
Georg Brandl2e081362008-05-17 19:04:04 +0000358 "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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300378
Georg Brandl2e081362008-05-17 19:04:04 +0000379def askopenfilename(**options):
380 "Ask for a filename to open"
381
382 return Open(**options).show()
383
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300384
Georg Brandl2e081362008-05-17 19:04:04 +0000385def asksaveasfilename(**options):
386 "Ask for a filename to save as"
387
388 return SaveAs(**options).show()
389
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300390
Georg Brandl2e081362008-05-17 19:04:04 +0000391def 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300402
Georg Brandl2e081362008-05-17 19:04:04 +0000403def 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300411
Georg Brandl2e081362008-05-17 19:04:04 +0000412def 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300437
Georg Brandl2e081362008-05-17 19:04:04 +0000438def askdirectory (**options):
439 "Ask for a directory, and return the file name"
440 return Directory(**options).show()
441
442
Georg Brandl2e081362008-05-17 19:04:04 +0000443# --------------------------------------------------------------------
444# test stuff
445
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000446def test():
447 """Simple test program."""
448 root = Tk()
449 root.withdraw()
450 fd = LoadFileDialog(root)
Guido van Rossum0978f991996-05-28 23:14:36 +0000451 loadfile = fd.go(key="test")
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000452 fd = SaveFileDialog(root)
Guido van Rossum0978f991996-05-28 23:14:36 +0000453 savefile = fd.go(key="test")
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000454 print(loadfile, savefile)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000455
Georg Brandl2e081362008-05-17 19:04:04 +0000456 # 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
Min ho Kim39d87b52019-08-31 06:21:19 +1000472 # dialog for opening files
Georg Brandl2e081362008-05-17 19:04:04 +0000473
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))
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000488
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300489
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000490if __name__ == '__main__':
491 test()