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 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 # convert Tcl path objects to strings 310 try: 311 result = result.string 312 except AttributeError: 313 # it already is a string 314 pass 315 path, file = os.path.split(result) 316 self.options["initialdir"] = path 317 self.options["initialfile"] = file 318 self.filename = result # compatibility 319 return result 320 321 322# 323# file dialogs 324 325class Open(_Dialog): 326 "Ask for a filename to open" 327 328 command = "tk_getOpenFile" 329 330 def _fixresult(self, widget, result): 331 if isinstance(result, tuple): 332 # multiple results: 333 result = tuple([getattr(r, "string", r) for r in result]) 334 if result: 335 path, file = os.path.split(result[0]) 336 self.options["initialdir"] = path 337 # don't set initialfile or filename, as we have multiple of these 338 return result 339 if not widget.tk.wantobjects() and "multiple" in self.options: 340 # Need to split result explicitly 341 return self._fixresult(widget, widget.tk.splitlist(result)) 342 return _Dialog._fixresult(self, widget, result) 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 372def askopenfilename(**options): 373 "Ask for a filename to open" 374 375 return Open(**options).show() 376 377def asksaveasfilename(**options): 378 "Ask for a filename to save as" 379 380 return SaveAs(**options).show() 381 382def askopenfilenames(**options): 383 """Ask for multiple filenames to open 384 385 Returns a list of filenames or empty list if 386 cancel button selected 387 """ 388 options["multiple"]=1 389 return Open(**options).show() 390 391# FIXME: are the following perhaps a bit too convenient? 392 393def askopenfile(mode = "r", **options): 394 "Ask for a filename to open, and returned the opened file" 395 396 filename = Open(**options).show() 397 if filename: 398 return open(filename, mode) 399 return None 400 401def askopenfiles(mode = "r", **options): 402 """Ask for multiple filenames and return the open file 403 objects 404 405 returns a list of open file objects or an empty list if 406 cancel selected 407 """ 408 409 files = askopenfilenames(**options) 410 if files: 411 ofiles=[] 412 for filename in files: 413 ofiles.append(open(filename, mode)) 414 files=ofiles 415 return files 416 417 418def asksaveasfile(mode = "w", **options): 419 "Ask for a filename to save as, and returned the opened file" 420 421 filename = SaveAs(**options).show() 422 if filename: 423 return open(filename, mode) 424 return None 425 426def askdirectory (**options): 427 "Ask for a directory, and return the file name" 428 return Directory(**options).show() 429 430 431 432# -------------------------------------------------------------------- 433# test stuff 434 435def test(): 436 """Simple test program.""" 437 root = Tk() 438 root.withdraw() 439 fd = LoadFileDialog(root) 440 loadfile = fd.go(key="test") 441 fd = SaveFileDialog(root) 442 savefile = fd.go(key="test") 443 print(loadfile, savefile) 444 445 # Since the file name may contain non-ASCII characters, we need 446 # to find an encoding that likely supports the file name, and 447 # displays correctly on the terminal. 448 449 # Start off with UTF-8 450 enc = "utf-8" 451 import sys 452 453 # See whether CODESET is defined 454 try: 455 import locale 456 locale.setlocale(locale.LC_ALL,'') 457 enc = locale.nl_langinfo(locale.CODESET) 458 except (ImportError, AttributeError): 459 pass 460 461 # dialog for openening files 462 463 openfilename=askopenfilename(filetypes=[("all files", "*")]) 464 try: 465 fp=open(openfilename,"r") 466 fp.close() 467 except: 468 print("Could not open File: ") 469 print(sys.exc_info()[1]) 470 471 print("open", openfilename.encode(enc)) 472 473 # dialog for saving files 474 475 saveasfilename=asksaveasfilename() 476 print("saveas", saveasfilename.encode(enc)) 477 478if __name__ == '__main__': 479 test() 480