1 """File selection dialog classes.
2
3 Classes:
4
5 - FileDialog
6 - LoadFileDialog
7 - SaveFileDialog
8
9 This module also presents tk common file dialogues, it provides interfaces
10 to the native file dialogues available in Tk 4.2 and newer, and the
11 directory dialogue available in Tk 8.3 and newer.
12 These interfaces were written by Fredrik Lundh, May 1997.
13 """
14 __all__ = ["FileDialog", "LoadFileDialog", "SaveFileDialog",
15 "Open", "SaveAs", "Directory",
16 "askopenfilename", "asksaveasfilename", "askopenfilenames",
17 "askopenfile", "askopenfiles", "asksaveasfile", "askdirectory"]
18
19 import fnmatch
20 import os
21 from tkinter import (
22 Frame, LEFT, YES, BOTTOM, Entry, TOP, Button, Tk, X,
23 Toplevel, RIGHT, Y, END, Listbox, BOTH, Scrollbar,
24 )
25 from tkinter.dialog import Dialog
26 from tkinter import commondialog
27 from tkinter.simpledialog import _setup_dialog
28
29
30 dialogstates = {}
31
32
33 class ESC[4;38;5;81mFileDialog:
34
35 """Standard file selection dialog -- no checks on selected file.
36
37 Usage:
38
39 d = FileDialog(master)
40 fname = d.go(dir_or_file, pattern, default, key)
41 if fname is None: ...canceled...
42 else: ...open file...
43
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
51 kept even when the dialog is canceled. (All this emulates the
52 behavior of the Macintosh file selection dialogs.)
53
54 """
55
56 title = "File Selection Dialog"
57
58 def __init__(self, master, title=None):
59 if title is None: title = self.title
60 self.master = master
61 self.directory = None
62
63 self.top = Toplevel(master)
64 self.top.title(title)
65 self.top.iconname(title)
66 _setup_dialog(self.top)
67
68 self.botframe = Frame(self.top)
69 self.botframe.pack(side=BOTTOM, fill=X)
70
71 self.selection = Entry(self.top)
72 self.selection.pack(side=BOTTOM, fill=X)
73 self.selection.bind('<Return>', self.ok_event)
74
75 self.filter = Entry(self.top)
76 self.filter.pack(side=TOP, fill=X)
77 self.filter.bind('<Return>', self.filter_command)
78
79 self.midframe = Frame(self.top)
80 self.midframe.pack(expand=YES, fill=BOTH)
81
82 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'))
92
93 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)
103
104 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)
116
117 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)
121
122 def go(self, dir_or_file=os.curdir, pattern="*", default="", key=None):
123 if key and key in dialogstates:
124 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()
135 self.top.wait_visibility() # window needs to be visible for the grab
136 self.top.grab_set()
137 self.how = None
138 self.master.mainloop() # Exited by self.quit(how)
139 if key:
140 directory, pattern = self.get_filter()
141 if self.how:
142 directory = os.path.dirname(self.how)
143 dialogstates[key] = directory, pattern
144 self.top.destroy()
145 return self.how
146
147 def quit(self, how=None):
148 self.how = how
149 self.master.quit() # Exit mainloop()
150
151 def dirs_double_event(self, event):
152 self.filter_command()
153
154 def dirs_select_event(self, event):
155 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)
159
160 def files_double_event(self, event):
161 self.ok_command()
162
163 def files_select_event(self, event):
164 file = self.files.get('active')
165 self.set_selection(file)
166
167 def ok_event(self, event):
168 self.ok_command()
169
170 def ok_command(self):
171 self.quit(self.get_selection())
172
173 def filter_command(self, event=None):
174 dir, pat = self.get_filter()
175 try:
176 names = os.listdir(dir)
177 except OSError:
178 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)
200
201 def get_filter(self):
202 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)
207
208 def get_selection(self):
209 file = self.selection.get()
210 file = os.path.expanduser(file)
211 return file
212
213 def cancel_command(self, event=None):
214 self.quit()
215
216 def set_filter(self, dir, pat):
217 if not os.path.isabs(dir):
218 try:
219 pwd = os.getcwd()
220 except OSError:
221 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 "*"))
227
228 def set_selection(self, file):
229 self.selection.delete(0, END)
230 self.selection.insert(END, os.path.join(self.directory, file))
231
232
233 class ESC[4;38;5;81mLoadFileDialog(ESC[4;38;5;149mFileDialog):
234
235 """File selection dialog which checks that the file exists."""
236
237 title = "Load File Selection Dialog"
238
239 def ok_command(self):
240 file = self.get_selection()
241 if not os.path.isfile(file):
242 self.master.bell()
243 else:
244 self.quit(file)
245
246
247 class ESC[4;38;5;81mSaveFileDialog(ESC[4;38;5;149mFileDialog):
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):
254 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",
261 text="Overwrite existing file %r?" % (file,),
262 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)
273
274
275 # 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
304 class ESC[4;38;5;81m_Dialog(ESC[4;38;5;149mcommondialogESC[4;38;5;149m.ESC[4;38;5;149mDialog):
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
316 # 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
332 class ESC[4;38;5;81mOpen(ESC[4;38;5;149m_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:
342 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
351
352 class ESC[4;38;5;81mSaveAs(ESC[4;38;5;149m_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.
359 class ESC[4;38;5;81mDirectory(ESC[4;38;5;149mcommondialogESC[4;38;5;149m.ESC[4;38;5;149mDialog):
360 "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
380
381 def askopenfilename(**options):
382 "Ask for a filename to open"
383
384 return Open(**options).show()
385
386
387 def asksaveasfilename(**options):
388 "Ask for a filename to save as"
389
390 return SaveAs(**options).show()
391
392
393 def 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
404
405 def 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
413
414 def 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
431 def 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
439
440 def askdirectory (**options):
441 "Ask for a directory, and return the file name"
442 return Directory(**options).show()
443
444
445 # --------------------------------------------------------------------
446 # test stuff
447
448 def test():
449 """Simple test program."""
450 root = Tk()
451 root.withdraw()
452 fd = LoadFileDialog(root)
453 loadfile = fd.go(key="test")
454 fd = SaveFileDialog(root)
455 savefile = fd.go(key="test")
456 print(loadfile, savefile)
457
458 # 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
474 # dialog for opening files
475
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))
490
491
492 if __name__ == '__main__':
493 test()