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 15from tkinter import * 16from tkinter.dialog import Dialog 17from tkinter import commondialog 18 19import os 20import fnmatch 21 22 23dialogstates = {} 24 25 26class FileDialog: 27 28 """Standard file selection dialog -- no checks on selected file. 29 30 Usage: 31 32 d = FileDialog(master) 33 fname = d.go(dir_or_file, pattern, default, key) 34 if fname is None: ...canceled... 35 else: ...open file... 36 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 44 kept even when the dialog is canceled. (All this emulates the 45 behavior of the Macintosh file selection dialogs.) 46 47 """ 48 49 title = "File Selection Dialog" 50 51 def __init__(self, master, title=None): 52 if title is None: title = self.title 53 self.master = master 54 self.directory = None 55 56 self.top = Toplevel(master) 57 self.top.title(title) 58 self.top.iconname(title) 59 60 self.botframe = Frame(self.top) 61 self.botframe.pack(side=BOTTOM, fill=X) 62 63 self.selection = Entry(self.top) 64 self.selection.pack(side=BOTTOM, fill=X) 65 self.selection.bind('<Return>', self.ok_event) 66 67 self.filter = Entry(self.top) 68 self.filter.pack(side=TOP, fill=X) 69 self.filter.bind('<Return>', self.filter_command) 70 71 self.midframe = Frame(self.top) 72 self.midframe.pack(expand=YES, fill=BOTH) 73 74 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')) 84 85 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) 95 96 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) 108 109 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) 113 114 def go(self, dir_or_file=os.curdir, pattern="*", default="", key=None): 115 if key and key in dialogstates: 116 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() 127 self.top.wait_visibility() # window needs to be visible for the grab 128 self.top.grab_set() 129 self.how = None 130 self.master.mainloop() # Exited by self.quit(how) 131 if key: 132 directory, pattern = self.get_filter() 133 if self.how: 134 directory = os.path.dirname(self.how) 135 dialogstates[key] = directory, pattern 136 self.top.destroy() 137 return self.how 138 139 def quit(self, how=None): 140 self.how = how 141 self.master.quit() # Exit mainloop() 142 143 def dirs_double_event(self, event): 144 self.filter_command() 145 146 def dirs_select_event(self, event): 147 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) 151 152 def files_double_event(self, event): 153 self.ok_command() 154 155 def files_select_event(self, event): 156 file = self.files.get('active') 157 self.set_selection(file) 158 159 def ok_event(self, event): 160 self.ok_command() 161 162 def ok_command(self): 163 self.quit(self.get_selection()) 164 165 def filter_command(self, event=None): 166 dir, pat = self.get_filter() 167 try: 168 names = os.listdir(dir) 169 except OSError: 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) 192 193 def get_filter(self): 194 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) 199 200 def get_selection(self): 201 file = self.selection.get() 202 file = os.path.expanduser(file) 203 return file 204 205 def cancel_command(self, event=None): 206 self.quit() 207 208 def set_filter(self, dir, pat): 209 if not os.path.isabs(dir): 210 try: 211 pwd = os.getcwd() 212 except OSError: 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 "*")) 219 220 def set_selection(self, file): 221 self.selection.delete(0, END) 222 self.selection.insert(END, os.path.join(self.directory, file)) 223 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): 232 file = self.get_selection() 233 if not os.path.isfile(file): 234 self.master.bell() 235 else: 236 self.quit(file) 237 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): 246 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", 253 text="Overwrite existing file %r?" % (file,), 254 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) 265 266 267# For the following classes and modules: 268# 269# options (all have default values): 270# 271# - defaultextension: added to filename if not explicitly given 272# 273# - filetypes: sequence of (label, pattern) tuples. the same pattern 274# may occur with several patterns. use "*" as pattern to indicate 275# all files. 276# 277# - initialdir: initial directory. preserved by dialog instance. 278# 279# - initialfile: initial file (ignored by the open dialog). preserved 280# by dialog instance. 281# 282# - parent: which window to place the dialog on top of 283# 284# - title: dialog title 285# 286# - multiple: if true user may select more than one file 287# 288# options for the directory chooser: 289# 290# - initialdir, parent, title: see above 291# 292# - mustexist: if true, user must pick an existing directory 293# 294 295 296class _Dialog(commondialog.Dialog): 297 298 def _fixoptions(self): 299 try: 300 # make sure "filetypes" is a tuple 301 self.options["filetypes"] = tuple(self.options["filetypes"]) 302 except KeyError: 303 pass 304 305 def _fixresult(self, widget, result): 306 if result: 307 # keep directory and filename until next time 308 # convert Tcl path objects to strings 309 try: 310 result = result.string 311 except AttributeError: 312 # it already is a string 313 pass 314 path, file = os.path.split(result) 315 self.options["initialdir"] = path 316 self.options["initialfile"] = file 317 self.filename = result # compatibility 318 return result 319 320 321# 322# file dialogs 323 324class Open(_Dialog): 325 "Ask for a filename to open" 326 327 command = "tk_getOpenFile" 328 329 def _fixresult(self, widget, result): 330 if isinstance(result, tuple): 331 # multiple results: 332 result = tuple([getattr(r, "string", r) for r in result]) 333 if result: 334 path, file = os.path.split(result[0]) 335 self.options["initialdir"] = path 336 # don't set initialfile or filename, as we have multiple of these 337 return result 338 if not widget.tk.wantobjects() and "multiple" in self.options: 339 # Need to split result explicitly 340 return self._fixresult(widget, widget.tk.splitlist(result)) 341 return _Dialog._fixresult(self, widget, result) 342 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. 351class Directory(commondialog.Dialog): 352 "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 372 373def askopenfilename(**options): 374 "Ask for a filename to open" 375 376 return Open(**options).show() 377 378 379def asksaveasfilename(**options): 380 "Ask for a filename to save as" 381 382 return SaveAs(**options).show() 383 384 385def askopenfilenames(**options): 386 """Ask for multiple filenames to open 387 388 Returns a list of filenames or empty list if 389 cancel button selected 390 """ 391 options["multiple"]=1 392 return Open(**options).show() 393 394# FIXME: are the following perhaps a bit too convenient? 395 396 397def askopenfile(mode = "r", **options): 398 "Ask for a filename to open, and returned the opened file" 399 400 filename = Open(**options).show() 401 if filename: 402 return open(filename, mode) 403 return None 404 405 406def askopenfiles(mode = "r", **options): 407 """Ask for multiple filenames and return the open file 408 objects 409 410 returns a list of open file objects or an empty list if 411 cancel selected 412 """ 413 414 files = askopenfilenames(**options) 415 if files: 416 ofiles=[] 417 for filename in files: 418 ofiles.append(open(filename, mode)) 419 files=ofiles 420 return files 421 422 423def asksaveasfile(mode = "w", **options): 424 "Ask for a filename to save as, and returned the opened file" 425 426 filename = SaveAs(**options).show() 427 if filename: 428 return open(filename, mode) 429 return None 430 431 432def askdirectory (**options): 433 "Ask for a directory, and return the file name" 434 return Directory(**options).show() 435 436 437# -------------------------------------------------------------------- 438# test stuff 439 440def test(): 441 """Simple test program.""" 442 root = Tk() 443 root.withdraw() 444 fd = LoadFileDialog(root) 445 loadfile = fd.go(key="test") 446 fd = SaveFileDialog(root) 447 savefile = fd.go(key="test") 448 print(loadfile, savefile) 449 450 # Since the file name may contain non-ASCII characters, we need 451 # to find an encoding that likely supports the file name, and 452 # displays correctly on the terminal. 453 454 # Start off with UTF-8 455 enc = "utf-8" 456 import sys 457 458 # See whether CODESET is defined 459 try: 460 import locale 461 locale.setlocale(locale.LC_ALL,'') 462 enc = locale.nl_langinfo(locale.CODESET) 463 except (ImportError, AttributeError): 464 pass 465 466 # dialog for opening files 467 468 openfilename=askopenfilename(filetypes=[("all files", "*")]) 469 try: 470 fp=open(openfilename,"r") 471 fp.close() 472 except: 473 print("Could not open File: ") 474 print(sys.exc_info()[1]) 475 476 print("open", openfilename.encode(enc)) 477 478 # dialog for saving files 479 480 saveasfilename=asksaveasfilename() 481 print("saveas", saveasfilename.encode(enc)) 482 483 484if __name__ == '__main__': 485 test() 486