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 27from tkinter.simpledialog import _setup_dialog 28 29 30dialogstates = {} 31 32 33class FileDialog: 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 233class LoadFileDialog(FileDialog): 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 247class SaveFileDialog(FileDialog): 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 304class _Dialog(commondialog.Dialog): 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 332class Open(_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 352class SaveAs(_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. 359class Directory(commondialog.Dialog): 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 381def askopenfilename(**options): 382 "Ask for a filename to open" 383 384 return Open(**options).show() 385 386 387def asksaveasfilename(**options): 388 "Ask for a filename to save as" 389 390 return SaveAs(**options).show() 391 392 393def 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 405def 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 414def 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 431def 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 440def askdirectory (**options): 441 "Ask for a directory, and return the file name" 442 return Directory(**options).show() 443 444 445# -------------------------------------------------------------------- 446# test stuff 447 448def 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 492if __name__ == '__main__': 493 test() 494