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