1"""File selection dialog classes. 2 3Classes: 4 5- FileDialog 6- LoadFileDialog 7- SaveFileDialog 8 9This 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. 13""" 14__all__ = ["FileDialog", "LoadFileDialog", "SaveFileDialog", 15 "Open", "SaveAs", "Directory", 16 "askopenfilename", "asksaveasfilename", "askopenfilenames", 17 "askopenfile", "askopenfiles", "asksaveasfile", "askdirectory"] 18 19import 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) 25from tkinter.dialog import Dialog 26from tkinter import commondialog 27 28 29dialogstates = {} 30 31 32class FileDialog: 33 34 """Standard file selection dialog -- no checks on selected file. 35 36 Usage: 37 38 d = FileDialog(master) 39 fname = d.go(dir_or_file, pattern, default, key) 40 if fname is None: ...canceled... 41 else: ...open file... 42 43 All arguments to go() are optional. 44 45 The 'key' argument specifies a key in the global dictionary 46 'dialogstates', which keeps track of the values for the directory 47 and pattern arguments, overriding the values passed in (it does 48 not keep track of the default argument!). If no key is specified, 49 the dialog keeps no memory of previous state. Note that memory is 50 kept even when the dialog is canceled. (All this emulates the 51 behavior of the Macintosh file selection dialogs.) 52 53 """ 54 55 title = "File Selection Dialog" 56 57 def __init__(self, master, title=None): 58 if title is None: title = self.title 59 self.master = master 60 self.directory = None 61 62 self.top = Toplevel(master) 63 self.top.title(title) 64 self.top.iconname(title) 65 66 self.botframe = Frame(self.top) 67 self.botframe.pack(side=BOTTOM, fill=X) 68 69 self.selection = Entry(self.top) 70 self.selection.pack(side=BOTTOM, fill=X) 71 self.selection.bind('<Return>', self.ok_event) 72 73 self.filter = Entry(self.top) 74 self.filter.pack(side=TOP, fill=X) 75 self.filter.bind('<Return>', self.filter_command) 76 77 self.midframe = Frame(self.top) 78 self.midframe.pack(expand=YES, fill=BOTH) 79 80 self.filesbar = Scrollbar(self.midframe) 81 self.filesbar.pack(side=RIGHT, fill=Y) 82 self.files = Listbox(self.midframe, exportselection=0, 83 yscrollcommand=(self.filesbar, 'set')) 84 self.files.pack(side=RIGHT, expand=YES, fill=BOTH) 85 btags = self.files.bindtags() 86 self.files.bindtags(btags[1:] + btags[:1]) 87 self.files.bind('<ButtonRelease-1>', self.files_select_event) 88 self.files.bind('<Double-ButtonRelease-1>', self.files_double_event) 89 self.filesbar.config(command=(self.files, 'yview')) 90 91 self.dirsbar = Scrollbar(self.midframe) 92 self.dirsbar.pack(side=LEFT, fill=Y) 93 self.dirs = Listbox(self.midframe, exportselection=0, 94 yscrollcommand=(self.dirsbar, 'set')) 95 self.dirs.pack(side=LEFT, expand=YES, fill=BOTH) 96 self.dirsbar.config(command=(self.dirs, 'yview')) 97 btags = self.dirs.bindtags() 98 self.dirs.bindtags(btags[1:] + btags[:1]) 99 self.dirs.bind('<ButtonRelease-1>', self.dirs_select_event) 100 self.dirs.bind('<Double-ButtonRelease-1>', self.dirs_double_event) 101 102 self.ok_button = Button(self.botframe, 103 text="OK", 104 command=self.ok_command) 105 self.ok_button.pack(side=LEFT) 106 self.filter_button = Button(self.botframe, 107 text="Filter", 108 command=self.filter_command) 109 self.filter_button.pack(side=LEFT, expand=YES) 110 self.cancel_button = Button(self.botframe, 111 text="Cancel", 112 command=self.cancel_command) 113 self.cancel_button.pack(side=RIGHT) 114 115 self.top.protocol('WM_DELETE_WINDOW', self.cancel_command) 116 # XXX Are the following okay for a general audience? 117 self.top.bind('<Alt-w>', self.cancel_command) 118 self.top.bind('<Alt-W>', self.cancel_command) 119 120 def go(self, dir_or_file=os.curdir, pattern="*", default="", key=None): 121 if key and key in dialogstates: 122 self.directory, pattern = dialogstates[key] 123 else: 124 dir_or_file = os.path.expanduser(dir_or_file) 125 if os.path.isdir(dir_or_file): 126 self.directory = dir_or_file 127 else: 128 self.directory, default = os.path.split(dir_or_file) 129 self.set_filter(self.directory, pattern) 130 self.set_selection(default) 131 self.filter_command() 132 self.selection.focus_set() 133 self.top.wait_visibility() # window needs to be visible for the grab 134 self.top.grab_set() 135 self.how = None 136 self.master.mainloop() # Exited by self.quit(how) 137 if key: 138 directory, pattern = self.get_filter() 139 if self.how: 140 directory = os.path.dirname(self.how) 141 dialogstates[key] = directory, pattern 142 self.top.destroy() 143 return self.how 144 145 def quit(self, how=None): 146 self.how = how 147 self.master.quit() # Exit mainloop() 148 149 def dirs_double_event(self, event): 150 self.filter_command() 151 152 def dirs_select_event(self, event): 153 dir, pat = self.get_filter() 154 subdir = self.dirs.get('active') 155 dir = os.path.normpath(os.path.join(self.directory, subdir)) 156 self.set_filter(dir, pat) 157 158 def files_double_event(self, event): 159 self.ok_command() 160 161 def files_select_event(self, event): 162 file = self.files.get('active') 163 self.set_selection(file) 164 165 def ok_event(self, event): 166 self.ok_command() 167 168 def ok_command(self): 169 self.quit(self.get_selection()) 170 171 def filter_command(self, event=None): 172 dir, pat = self.get_filter() 173 try: 174 names = os.listdir(dir) 175 except OSError: 176 self.master.bell() 177 return 178 self.directory = dir 179 self.set_filter(dir, pat) 180 names.sort() 181 subdirs = [os.pardir] 182 matchingfiles = [] 183 for name in names: 184 fullname = os.path.join(dir, name) 185 if os.path.isdir(fullname): 186 subdirs.append(name) 187 elif fnmatch.fnmatch(name, pat): 188 matchingfiles.append(name) 189 self.dirs.delete(0, END) 190 for name in subdirs: 191 self.dirs.insert(END, name) 192 self.files.delete(0, END) 193 for name in matchingfiles: 194 self.files.insert(END, name) 195 head, tail = os.path.split(self.get_selection()) 196 if tail == os.curdir: tail = '' 197 self.set_selection(tail) 198 199 def get_filter(self): 200 filter = self.filter.get() 201 filter = os.path.expanduser(filter) 202 if filter[-1:] == os.sep or os.path.isdir(filter): 203 filter = os.path.join(filter, "*") 204 return os.path.split(filter) 205 206 def get_selection(self): 207 file = self.selection.get() 208 file = os.path.expanduser(file) 209 return file 210 211 def cancel_command(self, event=None): 212 self.quit() 213 214 def set_filter(self, dir, pat): 215 if not os.path.isabs(dir): 216 try: 217 pwd = os.getcwd() 218 except OSError: 219 pwd = None 220 if pwd: 221 dir = os.path.join(pwd, dir) 222 dir = os.path.normpath(dir) 223 self.filter.delete(0, END) 224 self.filter.insert(END, os.path.join(dir or os.curdir, pat or "*")) 225 226 def set_selection(self, file): 227 self.selection.delete(0, END) 228 self.selection.insert(END, os.path.join(self.directory, file)) 229 230 231class LoadFileDialog(FileDialog): 232 233 """File selection dialog which checks that the file exists.""" 234 235 title = "Load File Selection Dialog" 236 237 def ok_command(self): 238 file = self.get_selection() 239 if not os.path.isfile(file): 240 self.master.bell() 241 else: 242 self.quit(file) 243 244 245class SaveFileDialog(FileDialog): 246 247 """File selection dialog which checks that the file may be created.""" 248 249 title = "Save File Selection Dialog" 250 251 def ok_command(self): 252 file = self.get_selection() 253 if os.path.exists(file): 254 if os.path.isdir(file): 255 self.master.bell() 256 return 257 d = Dialog(self.top, 258 title="Overwrite Existing File Question", 259 text="Overwrite existing file %r?" % (file,), 260 bitmap='questhead', 261 default=1, 262 strings=("Yes", "Cancel")) 263 if d.num != 0: 264 return 265 else: 266 head, tail = os.path.split(file) 267 if not os.path.isdir(head): 268 self.master.bell() 269 return 270 self.quit(file) 271 272 273# For the following classes and modules: 274# 275# options (all have default values): 276# 277# - defaultextension: added to filename if not explicitly given 278# 279# - filetypes: sequence of (label, pattern) tuples. the same pattern 280# may occur with several patterns. use "*" as pattern to indicate 281# all files. 282# 283# - initialdir: initial directory. preserved by dialog instance. 284# 285# - initialfile: initial file (ignored by the open dialog). preserved 286# by dialog instance. 287# 288# - parent: which window to place the dialog on top of 289# 290# - title: dialog title 291# 292# - multiple: if true user may select more than one file 293# 294# options for the directory chooser: 295# 296# - initialdir, parent, title: see above 297# 298# - mustexist: if true, user must pick an existing directory 299# 300 301 302class _Dialog(commondialog.Dialog): 303 304 def _fixoptions(self): 305 try: 306 # make sure "filetypes" is a tuple 307 self.options["filetypes"] = tuple(self.options["filetypes"]) 308 except KeyError: 309 pass 310 311 def _fixresult(self, widget, result): 312 if result: 313 # keep directory and filename until next time 314 # convert Tcl path objects to strings 315 try: 316 result = result.string 317 except AttributeError: 318 # it already is a string 319 pass 320 path, file = os.path.split(result) 321 self.options["initialdir"] = path 322 self.options["initialfile"] = file 323 self.filename = result # compatibility 324 return result 325 326 327# 328# file dialogs 329 330class Open(_Dialog): 331 "Ask for a filename to open" 332 333 command = "tk_getOpenFile" 334 335 def _fixresult(self, widget, result): 336 if isinstance(result, tuple): 337 # multiple results: 338 result = tuple([getattr(r, "string", r) for r in result]) 339 if result: 340 path, file = os.path.split(result[0]) 341 self.options["initialdir"] = path 342 # don't set initialfile or filename, as we have multiple of these 343 return result 344 if not widget.tk.wantobjects() and "multiple" in self.options: 345 # Need to split result explicitly 346 return self._fixresult(widget, widget.tk.splitlist(result)) 347 return _Dialog._fixresult(self, widget, result) 348 349 350class SaveAs(_Dialog): 351 "Ask for a filename to save as" 352 353 command = "tk_getSaveFile" 354 355 356# the directory dialog has its own _fix routines. 357class Directory(commondialog.Dialog): 358 "Ask for a directory" 359 360 command = "tk_chooseDirectory" 361 362 def _fixresult(self, widget, result): 363 if result: 364 # convert Tcl path objects to strings 365 try: 366 result = result.string 367 except AttributeError: 368 # it already is a string 369 pass 370 # keep directory until next time 371 self.options["initialdir"] = result 372 self.directory = result # compatibility 373 return result 374 375# 376# convenience stuff 377 378 379def askopenfilename(**options): 380 "Ask for a filename to open" 381 382 return Open(**options).show() 383 384 385def asksaveasfilename(**options): 386 "Ask for a filename to save as" 387 388 return SaveAs(**options).show() 389 390 391def askopenfilenames(**options): 392 """Ask for multiple filenames to open 393 394 Returns a list of filenames or empty list if 395 cancel button selected 396 """ 397 options["multiple"]=1 398 return Open(**options).show() 399 400# FIXME: are the following perhaps a bit too convenient? 401 402 403def askopenfile(mode = "r", **options): 404 "Ask for a filename to open, and returned the opened file" 405 406 filename = Open(**options).show() 407 if filename: 408 return open(filename, mode) 409 return None 410 411 412def askopenfiles(mode = "r", **options): 413 """Ask for multiple filenames and return the open file 414 objects 415 416 returns a list of open file objects or an empty list if 417 cancel selected 418 """ 419 420 files = askopenfilenames(**options) 421 if files: 422 ofiles=[] 423 for filename in files: 424 ofiles.append(open(filename, mode)) 425 files=ofiles 426 return files 427 428 429def asksaveasfile(mode = "w", **options): 430 "Ask for a filename to save as, and returned the opened file" 431 432 filename = SaveAs(**options).show() 433 if filename: 434 return open(filename, mode) 435 return None 436 437 438def askdirectory (**options): 439 "Ask for a directory, and return the file name" 440 return Directory(**options).show() 441 442 443# -------------------------------------------------------------------- 444# test stuff 445 446def test(): 447 """Simple test program.""" 448 root = Tk() 449 root.withdraw() 450 fd = LoadFileDialog(root) 451 loadfile = fd.go(key="test") 452 fd = SaveFileDialog(root) 453 savefile = fd.go(key="test") 454 print(loadfile, savefile) 455 456 # Since the file name may contain non-ASCII characters, we need 457 # to find an encoding that likely supports the file name, and 458 # displays correctly on the terminal. 459 460 # Start off with UTF-8 461 enc = "utf-8" 462 import sys 463 464 # See whether CODESET is defined 465 try: 466 import locale 467 locale.setlocale(locale.LC_ALL,'') 468 enc = locale.nl_langinfo(locale.CODESET) 469 except (ImportError, AttributeError): 470 pass 471 472 # dialog for opening files 473 474 openfilename=askopenfilename(filetypes=[("all files", "*")]) 475 try: 476 fp=open(openfilename,"r") 477 fp.close() 478 except: 479 print("Could not open File: ") 480 print(sys.exc_info()[1]) 481 482 print("open", openfilename.encode(enc)) 483 484 # dialog for saving files 485 486 saveasfilename=asksaveasfilename() 487 print("saveas", saveasfilename.encode(enc)) 488 489 490if __name__ == '__main__': 491 test() 492