blob: 98d2d5c320561be2e62f33f7977a42115f28b240 [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)
169 except os.error:
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)
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()
212 except os.error:
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 "*"))
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
309 import os
310 # convert Tcl path objects to strings
311 try:
312 result = result.string
313 except AttributeError:
314 # it already is a string
315 pass
316 path, file = os.path.split(result)
317 self.options["initialdir"] = path
318 self.options["initialfile"] = file
319 self.filename = result # compatibility
320 return result
321
322
323#
324# file dialogs
325
326class Open(_Dialog):
327 "Ask for a filename to open"
328
329 command = "tk_getOpenFile"
330
331 def _fixresult(self, widget, result):
332 if isinstance(result, tuple):
333 # multiple results:
334 result = tuple([getattr(r, "string", r) for r in result])
335 if result:
336 import os
337 path, file = os.path.split(result[0])
338 self.options["initialdir"] = path
339 # don't set initialfile or filename, as we have multiple of these
340 return result
341 if not widget.tk.wantobjects() and "multiple" in self.options:
342 # Need to split result explicitly
343 return self._fixresult(widget, widget.tk.splitlist(result))
344 return _Dialog._fixresult(self, widget, result)
345
346class SaveAs(_Dialog):
347 "Ask for a filename to save as"
348
349 command = "tk_getSaveFile"
350
351
352# the directory dialog has its own _fix routines.
Benjamin Petersonde342c42008-12-31 14:51:07 +0000353class Directory(commondialog.Dialog):
Georg Brandl2e081362008-05-17 19:04:04 +0000354 "Ask for a directory"
355
356 command = "tk_chooseDirectory"
357
358 def _fixresult(self, widget, result):
359 if result:
360 # convert Tcl path objects to strings
361 try:
362 result = result.string
363 except AttributeError:
364 # it already is a string
365 pass
366 # keep directory until next time
367 self.options["initialdir"] = result
368 self.directory = result # compatibility
369 return result
370
371#
372# convenience stuff
373
374def askopenfilename(**options):
375 "Ask for a filename to open"
376
377 return Open(**options).show()
378
379def asksaveasfilename(**options):
380 "Ask for a filename to save as"
381
382 return SaveAs(**options).show()
383
384def askopenfilenames(**options):
385 """Ask for multiple filenames to open
386
387 Returns a list of filenames or empty list if
388 cancel button selected
389 """
390 options["multiple"]=1
391 return Open(**options).show()
392
393# FIXME: are the following perhaps a bit too convenient?
394
395def askopenfile(mode = "r", **options):
396 "Ask for a filename to open, and returned the opened file"
397
398 filename = Open(**options).show()
399 if filename:
400 return open(filename, mode)
401 return None
402
403def askopenfiles(mode = "r", **options):
404 """Ask for multiple filenames and return the open file
405 objects
406
407 returns a list of open file objects or an empty list if
408 cancel selected
409 """
410
411 files = askopenfilenames(**options)
412 if files:
413 ofiles=[]
414 for filename in files:
415 ofiles.append(open(filename, mode))
416 files=ofiles
417 return files
418
419
420def asksaveasfile(mode = "w", **options):
421 "Ask for a filename to save as, and returned the opened file"
422
423 filename = SaveAs(**options).show()
424 if filename:
425 return open(filename, mode)
426 return None
427
428def askdirectory (**options):
429 "Ask for a directory, and return the file name"
430 return Directory(**options).show()
431
432
433
434# --------------------------------------------------------------------
435# test stuff
436
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000437def test():
438 """Simple test program."""
439 root = Tk()
440 root.withdraw()
441 fd = LoadFileDialog(root)
Guido van Rossum0978f991996-05-28 23:14:36 +0000442 loadfile = fd.go(key="test")
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000443 fd = SaveFileDialog(root)
Guido van Rossum0978f991996-05-28 23:14:36 +0000444 savefile = fd.go(key="test")
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000445 print(loadfile, savefile)
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000446
Georg Brandl2e081362008-05-17 19:04:04 +0000447 # Since the file name may contain non-ASCII characters, we need
448 # to find an encoding that likely supports the file name, and
449 # displays correctly on the terminal.
450
451 # Start off with UTF-8
452 enc = "utf-8"
453 import sys
454
455 # See whether CODESET is defined
456 try:
457 import locale
458 locale.setlocale(locale.LC_ALL,'')
459 enc = locale.nl_langinfo(locale.CODESET)
460 except (ImportError, AttributeError):
461 pass
462
463 # dialog for openening files
464
465 openfilename=askopenfilename(filetypes=[("all files", "*")])
466 try:
467 fp=open(openfilename,"r")
468 fp.close()
469 except:
470 print("Could not open File: ")
471 print(sys.exc_info()[1])
472
473 print("open", openfilename.encode(enc))
474
475 # dialog for saving files
476
477 saveasfilename=asksaveasfilename()
478 print("saveas", saveasfilename.encode(enc))
Guido van Rossumac4f8d31995-08-29 23:46:35 +0000479
480if __name__ == '__main__':
481 test()