blob: 600d0bd49fe2cf6095511a889db6477ad3bfa321 [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
Serhiy Storchaka3bb3fb32021-04-25 13:07:58 +030027from tkinter.simpledialog import _setup_dialog
Guido van Rossumac4f8d31995-08-29 23:46:35 +000028
Guido van Rossumac4f8d31995-08-29 23:46:35 +000029
Guido van Rossum0978f991996-05-28 23:14:36 +000030dialogstates = {}
31
32
Guido van Rossumac4f8d31995-08-29 23:46:35 +000033class FileDialog:
34
35 """Standard file selection dialog -- no checks on selected file.
36
37 Usage:
38
39 d = FileDialog(master)
Andrew M. Kuchling38752812003-01-21 14:19:21 +000040 fname = d.go(dir_or_file, pattern, default, key)
41 if fname is None: ...canceled...
Guido van Rossumc4570481998-03-20 20:45:49 +000042 else: ...open file...
Guido van Rossum0978f991996-05-28 23:14:36 +000043
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
Thomas Wouters7e474022000-07-16 12:04:32 +000051 kept even when the dialog is canceled. (All this emulates the
Guido van Rossum0978f991996-05-28 23:14:36 +000052 behavior of the Macintosh file selection dialogs.)
Guido van Rossumac4f8d31995-08-29 23:46:35 +000053
54 """
55
56 title = "File Selection Dialog"
57
Guido van Rossum0978f991996-05-28 23:14:36 +000058 def __init__(self, master, title=None):
Guido van Rossumc4570481998-03-20 20:45:49 +000059 if title is None: title = self.title
60 self.master = master
61 self.directory = None
Guido van Rossum0978f991996-05-28 23:14:36 +000062
Guido van Rossumc4570481998-03-20 20:45:49 +000063 self.top = Toplevel(master)
64 self.top.title(title)
65 self.top.iconname(title)
Serhiy Storchaka3bb3fb32021-04-25 13:07:58 +030066 _setup_dialog(self.top)
Guido van Rossum0978f991996-05-28 23:14:36 +000067
Guido van Rossumc4570481998-03-20 20:45:49 +000068 self.botframe = Frame(self.top)
69 self.botframe.pack(side=BOTTOM, fill=X)
Guido van Rossum0978f991996-05-28 23:14:36 +000070
Guido van Rossumc4570481998-03-20 20:45:49 +000071 self.selection = Entry(self.top)
72 self.selection.pack(side=BOTTOM, fill=X)
73 self.selection.bind('<Return>', self.ok_event)
Guido van Rossum0978f991996-05-28 23:14:36 +000074
Guido van Rossumc4570481998-03-20 20:45:49 +000075 self.filter = Entry(self.top)
76 self.filter.pack(side=TOP, fill=X)
77 self.filter.bind('<Return>', self.filter_command)
Guido van Rossum0978f991996-05-28 23:14:36 +000078
Guido van Rossumc4570481998-03-20 20:45:49 +000079 self.midframe = Frame(self.top)
80 self.midframe.pack(expand=YES, fill=BOTH)
Guido van Rossum0978f991996-05-28 23:14:36 +000081
Guido van Rossumc4570481998-03-20 20:45:49 +000082 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'))
Guido van Rossum0978f991996-05-28 23:14:36 +000092
Guido van Rossumc4570481998-03-20 20:45:49 +000093 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)
Guido van Rossum0978f991996-05-28 23:14:36 +0000103
Guido van Rossumc4570481998-03-20 20:45:49 +0000104 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000116
Guido van Rossumc4570481998-03-20 20:45:49 +0000117 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)
Guido van Rossum0978f991996-05-28 23:14:36 +0000121
122 def go(self, dir_or_file=os.curdir, pattern="*", default="", key=None):
Guido van Rossume014a132006-08-19 16:53:45 +0000123 if key and key in dialogstates:
Guido van Rossumc4570481998-03-20 20:45:49 +0000124 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()
Martin v. Löwisb217cd82004-08-03 18:36:25 +0000135 self.top.wait_visibility() # window needs to be visible for the grab
Guido van Rossumc4570481998-03-20 20:45:49 +0000136 self.top.grab_set()
137 self.how = None
138 self.master.mainloop() # Exited by self.quit(how)
Fred Drake073b8291998-05-06 17:28:23 +0000139 if key:
140 directory, pattern = self.get_filter()
141 if self.how:
142 directory = os.path.dirname(self.how)
143 dialogstates[key] = directory, pattern
Guido van Rossumc4570481998-03-20 20:45:49 +0000144 self.top.destroy()
145 return self.how
Guido van Rossum0978f991996-05-28 23:14:36 +0000146
147 def quit(self, how=None):
Guido van Rossumc4570481998-03-20 20:45:49 +0000148 self.how = how
149 self.master.quit() # Exit mainloop()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000150
151 def dirs_double_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000152 self.filter_command()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000153
154 def dirs_select_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000155 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000159
160 def files_double_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000161 self.ok_command()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000162
163 def files_select_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000164 file = self.files.get('active')
165 self.set_selection(file)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000166
167 def ok_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000168 self.ok_command()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000169
170 def ok_command(self):
Guido van Rossumc4570481998-03-20 20:45:49 +0000171 self.quit(self.get_selection())
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000172
173 def filter_command(self, event=None):
Guido van Rossumc4570481998-03-20 20:45:49 +0000174 dir, pat = self.get_filter()
175 try:
176 names = os.listdir(dir)
Andrew Svetlov786fbd82012-12-17 19:51:15 +0200177 except OSError:
Guido van Rossumc4570481998-03-20 20:45:49 +0000178 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000200
201 def get_filter(self):
Guido van Rossumc4570481998-03-20 20:45:49 +0000202 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000207
Guido van Rossum0978f991996-05-28 23:14:36 +0000208 def get_selection(self):
Guido van Rossumc4570481998-03-20 20:45:49 +0000209 file = self.selection.get()
210 file = os.path.expanduser(file)
211 return file
Guido van Rossum0978f991996-05-28 23:14:36 +0000212
213 def cancel_command(self, event=None):
Guido van Rossumc4570481998-03-20 20:45:49 +0000214 self.quit()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000215
216 def set_filter(self, dir, pat):
Guido van Rossumc4570481998-03-20 20:45:49 +0000217 if not os.path.isabs(dir):
218 try:
219 pwd = os.getcwd()
Andrew Svetlov786fbd82012-12-17 19:51:15 +0200220 except OSError:
Guido van Rossumc4570481998-03-20 20:45:49 +0000221 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 "*"))
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000227
228 def set_selection(self, file):
Guido van Rossumc4570481998-03-20 20:45:49 +0000229 self.selection.delete(0, END)
230 self.selection.insert(END, os.path.join(self.directory, file))
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000231
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):
Guido van Rossumc4570481998-03-20 20:45:49 +0000240 file = self.get_selection()
241 if not os.path.isfile(file):
242 self.master.bell()
243 else:
244 self.quit(file)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000245
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):
Guido van Rossumc4570481998-03-20 20:45:49 +0000254 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",
Walter Dörwald70a6b492004-02-12 17:35:32 +0000261 text="Overwrite existing file %r?" % (file,),
Guido van Rossumc4570481998-03-20 20:45:49 +0000262 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000273
274
Georg Brandl2e081362008-05-17 19:04:04 +0000275# 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
Georg Brandl2e081362008-05-17 19:04:04 +0000316 # 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:
Georg Brandl2e081362008-05-17 19:04:04 +0000342 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300351
Georg Brandl2e081362008-05-17 19:04:04 +0000352class 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.
Benjamin Petersonde342c42008-12-31 14:51:07 +0000359class Directory(commondialog.Dialog):
Georg Brandl2e081362008-05-17 19:04:04 +0000360 "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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300380
Georg Brandl2e081362008-05-17 19:04:04 +0000381def askopenfilename(**options):
382 "Ask for a filename to open"
383
384 return Open(**options).show()
385
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300386
Georg Brandl2e081362008-05-17 19:04:04 +0000387def asksaveasfilename(**options):
388 "Ask for a filename to save as"
389
390 return SaveAs(**options).show()
391
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300392
Georg Brandl2e081362008-05-17 19:04:04 +0000393def 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300404
Georg Brandl2e081362008-05-17 19:04:04 +0000405def 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300413
Georg Brandl2e081362008-05-17 19:04:04 +0000414def 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300439
Georg Brandl2e081362008-05-17 19:04:04 +0000440def askdirectory (**options):
441 "Ask for a directory, and return the file name"
442 return Directory(**options).show()
443
444
Georg Brandl2e081362008-05-17 19:04:04 +0000445# --------------------------------------------------------------------
446# test stuff
447
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000448def test():
449 """Simple test program."""
450 root = Tk()
451 root.withdraw()
452 fd = LoadFileDialog(root)
Guido van Rossum0978f991996-05-28 23:14:36 +0000453 loadfile = fd.go(key="test")
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000454 fd = SaveFileDialog(root)
Guido van Rossum0978f991996-05-28 23:14:36 +0000455 savefile = fd.go(key="test")
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000456 print(loadfile, savefile)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000457
Georg Brandl2e081362008-05-17 19:04:04 +0000458 # 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
Min ho Kim39d87b52019-08-31 06:21:19 +1000474 # dialog for opening files
Georg Brandl2e081362008-05-17 19:04:04 +0000475
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))
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000490
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300491
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000492if __name__ == '__main__':
493 test()