blob: d9d3436145c9c9cdff78d7a62c0874bc59f094dd [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"""
14
Georg Brandl14fc4272008-05-17 18:39:55 +000015from tkinter import *
16from tkinter.dialog import Dialog
Georg Brandl2e081362008-05-17 19:04:04 +000017from tkinter import commondialog
Guido van Rossumac4f8d31995-08-29 23:46:35 +000018
Guido van Rossumac4f8d31995-08-29 23:46:35 +000019import os
20import fnmatch
21
22
Guido van Rossum0978f991996-05-28 23:14:36 +000023dialogstates = {}
24
25
Guido van Rossumac4f8d31995-08-29 23:46:35 +000026class FileDialog:
27
28 """Standard file selection dialog -- no checks on selected file.
29
30 Usage:
31
32 d = FileDialog(master)
Andrew M. Kuchling38752812003-01-21 14:19:21 +000033 fname = d.go(dir_or_file, pattern, default, key)
34 if fname is None: ...canceled...
Guido van Rossumc4570481998-03-20 20:45:49 +000035 else: ...open file...
Guido van Rossum0978f991996-05-28 23:14:36 +000036
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
Thomas Wouters7e474022000-07-16 12:04:32 +000044 kept even when the dialog is canceled. (All this emulates the
Guido van Rossum0978f991996-05-28 23:14:36 +000045 behavior of the Macintosh file selection dialogs.)
Guido van Rossumac4f8d31995-08-29 23:46:35 +000046
47 """
48
49 title = "File Selection Dialog"
50
Guido van Rossum0978f991996-05-28 23:14:36 +000051 def __init__(self, master, title=None):
Guido van Rossumc4570481998-03-20 20:45:49 +000052 if title is None: title = self.title
53 self.master = master
54 self.directory = None
Guido van Rossum0978f991996-05-28 23:14:36 +000055
Guido van Rossumc4570481998-03-20 20:45:49 +000056 self.top = Toplevel(master)
57 self.top.title(title)
58 self.top.iconname(title)
Guido van Rossum0978f991996-05-28 23:14:36 +000059
Guido van Rossumc4570481998-03-20 20:45:49 +000060 self.botframe = Frame(self.top)
61 self.botframe.pack(side=BOTTOM, fill=X)
Guido van Rossum0978f991996-05-28 23:14:36 +000062
Guido van Rossumc4570481998-03-20 20:45:49 +000063 self.selection = Entry(self.top)
64 self.selection.pack(side=BOTTOM, fill=X)
65 self.selection.bind('<Return>', self.ok_event)
Guido van Rossum0978f991996-05-28 23:14:36 +000066
Guido van Rossumc4570481998-03-20 20:45:49 +000067 self.filter = Entry(self.top)
68 self.filter.pack(side=TOP, fill=X)
69 self.filter.bind('<Return>', self.filter_command)
Guido van Rossum0978f991996-05-28 23:14:36 +000070
Guido van Rossumc4570481998-03-20 20:45:49 +000071 self.midframe = Frame(self.top)
72 self.midframe.pack(expand=YES, fill=BOTH)
Guido van Rossum0978f991996-05-28 23:14:36 +000073
Guido van Rossumc4570481998-03-20 20:45:49 +000074 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'))
Guido van Rossum0978f991996-05-28 23:14:36 +000084
Guido van Rossumc4570481998-03-20 20:45:49 +000085 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)
Guido van Rossum0978f991996-05-28 23:14:36 +000095
Guido van Rossumc4570481998-03-20 20:45:49 +000096 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000108
Guido van Rossumc4570481998-03-20 20:45:49 +0000109 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)
Guido van Rossum0978f991996-05-28 23:14:36 +0000113
114 def go(self, dir_or_file=os.curdir, pattern="*", default="", key=None):
Guido van Rossume014a132006-08-19 16:53:45 +0000115 if key and key in dialogstates:
Guido van Rossumc4570481998-03-20 20:45:49 +0000116 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()
Martin v. Löwisb217cd82004-08-03 18:36:25 +0000127 self.top.wait_visibility() # window needs to be visible for the grab
Guido van Rossumc4570481998-03-20 20:45:49 +0000128 self.top.grab_set()
129 self.how = None
130 self.master.mainloop() # Exited by self.quit(how)
Fred Drake073b8291998-05-06 17:28:23 +0000131 if key:
132 directory, pattern = self.get_filter()
133 if self.how:
134 directory = os.path.dirname(self.how)
135 dialogstates[key] = directory, pattern
Guido van Rossumc4570481998-03-20 20:45:49 +0000136 self.top.destroy()
137 return self.how
Guido van Rossum0978f991996-05-28 23:14:36 +0000138
139 def quit(self, how=None):
Guido van Rossumc4570481998-03-20 20:45:49 +0000140 self.how = how
141 self.master.quit() # Exit mainloop()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000142
143 def dirs_double_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000144 self.filter_command()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000145
146 def dirs_select_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000147 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000151
152 def files_double_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000153 self.ok_command()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000154
155 def files_select_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000156 file = self.files.get('active')
157 self.set_selection(file)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000158
159 def ok_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +0000160 self.ok_command()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000161
162 def ok_command(self):
Guido van Rossumc4570481998-03-20 20:45:49 +0000163 self.quit(self.get_selection())
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000164
165 def filter_command(self, event=None):
Guido van Rossumc4570481998-03-20 20:45:49 +0000166 dir, pat = self.get_filter()
167 try:
168 names = os.listdir(dir)
Andrew Svetlov786fbd82012-12-17 19:51:15 +0200169 except OSError:
Guido van Rossumc4570481998-03-20 20:45:49 +0000170 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000192
193 def get_filter(self):
Guido van Rossumc4570481998-03-20 20:45:49 +0000194 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000199
Guido van Rossum0978f991996-05-28 23:14:36 +0000200 def get_selection(self):
Guido van Rossumc4570481998-03-20 20:45:49 +0000201 file = self.selection.get()
202 file = os.path.expanduser(file)
203 return file
Guido van Rossum0978f991996-05-28 23:14:36 +0000204
205 def cancel_command(self, event=None):
Guido van Rossumc4570481998-03-20 20:45:49 +0000206 self.quit()
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000207
208 def set_filter(self, dir, pat):
Guido van Rossumc4570481998-03-20 20:45:49 +0000209 if not os.path.isabs(dir):
210 try:
211 pwd = os.getcwd()
Andrew Svetlov786fbd82012-12-17 19:51:15 +0200212 except OSError:
Guido van Rossumc4570481998-03-20 20:45:49 +0000213 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 "*"))
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000219
220 def set_selection(self, file):
Guido van Rossumc4570481998-03-20 20:45:49 +0000221 self.selection.delete(0, END)
222 self.selection.insert(END, os.path.join(self.directory, file))
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000223
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):
Guido van Rossumc4570481998-03-20 20:45:49 +0000232 file = self.get_selection()
233 if not os.path.isfile(file):
234 self.master.bell()
235 else:
236 self.quit(file)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000237
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):
Guido van Rossumc4570481998-03-20 20:45:49 +0000246 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",
Walter Dörwald70a6b492004-02-12 17:35:32 +0000253 text="Overwrite existing file %r?" % (file,),
Guido van Rossumc4570481998-03-20 20:45:49 +0000254 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)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000265
266
Georg Brandl2e081362008-05-17 19:04:04 +0000267# 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
Georg Brandl2e081362008-05-17 19:04:04 +0000308 # 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:
Georg Brandl2e081362008-05-17 19:04:04 +0000334 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300343
Georg Brandl2e081362008-05-17 19:04:04 +0000344class 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.
Benjamin Petersonde342c42008-12-31 14:51:07 +0000351class Directory(commondialog.Dialog):
Georg Brandl2e081362008-05-17 19:04:04 +0000352 "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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300372
Georg Brandl2e081362008-05-17 19:04:04 +0000373def askopenfilename(**options):
374 "Ask for a filename to open"
375
376 return Open(**options).show()
377
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300378
Georg Brandl2e081362008-05-17 19:04:04 +0000379def asksaveasfilename(**options):
380 "Ask for a filename to save as"
381
382 return SaveAs(**options).show()
383
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300384
Georg Brandl2e081362008-05-17 19:04:04 +0000385def 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300396
Georg Brandl2e081362008-05-17 19:04:04 +0000397def 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300405
Georg Brandl2e081362008-05-17 19:04:04 +0000406def 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300431
Georg Brandl2e081362008-05-17 19:04:04 +0000432def askdirectory (**options):
433 "Ask for a directory, and return the file name"
434 return Directory(**options).show()
435
436
Georg Brandl2e081362008-05-17 19:04:04 +0000437# --------------------------------------------------------------------
438# test stuff
439
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000440def test():
441 """Simple test program."""
442 root = Tk()
443 root.withdraw()
444 fd = LoadFileDialog(root)
Guido van Rossum0978f991996-05-28 23:14:36 +0000445 loadfile = fd.go(key="test")
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000446 fd = SaveFileDialog(root)
Guido van Rossum0978f991996-05-28 23:14:36 +0000447 savefile = fd.go(key="test")
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000448 print(loadfile, savefile)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000449
Georg Brandl2e081362008-05-17 19:04:04 +0000450 # 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 openening 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))
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000482
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300483
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000484if __name__ == '__main__':
485 test()