1# changes by dscherer@cmu.edu 2# - IOBinding.open() replaces the current window with the opened file, 3# if the current window is both unmodified and unnamed 4# - IOBinding.loadfile() interprets Windows, UNIX, and Macintosh 5# end-of-line conventions, instead of relying on the standard library, 6# which will only understand the local convention. 7 8import codecs 9from codecs import BOM_UTF8 10import os 11import pipes 12import re 13import sys 14import tempfile 15 16from Tkinter import * 17import tkFileDialog 18import tkMessageBox 19from SimpleDialog import SimpleDialog 20 21from idlelib.configHandler import idleConf 22 23# Try setting the locale, so that we can find out 24# what encoding to use 25try: 26 import locale 27 locale.setlocale(locale.LC_CTYPE, "") 28except (ImportError, locale.Error): 29 pass 30 31# Encoding for file names 32filesystemencoding = sys.getfilesystemencoding() 33 34encoding = "ascii" 35if sys.platform == 'win32': 36 # On Windows, we could use "mbcs". However, to give the user 37 # a portable encoding name, we need to find the code page 38 try: 39 encoding = locale.getdefaultlocale()[1] 40 codecs.lookup(encoding) 41 except LookupError: 42 pass 43else: 44 try: 45 # Different things can fail here: the locale module may not be 46 # loaded, it may not offer nl_langinfo, or CODESET, or the 47 # resulting codeset may be unknown to Python. We ignore all 48 # these problems, falling back to ASCII 49 encoding = locale.nl_langinfo(locale.CODESET) 50 if encoding is None or encoding is '': 51 # situation occurs on Mac OS X 52 encoding = 'ascii' 53 codecs.lookup(encoding) 54 except (NameError, AttributeError, LookupError): 55 # Try getdefaultlocale well: it parses environment variables, 56 # which may give a clue. Unfortunately, getdefaultlocale has 57 # bugs that can cause ValueError. 58 try: 59 encoding = locale.getdefaultlocale()[1] 60 if encoding is None or encoding is '': 61 # situation occurs on Mac OS X 62 encoding = 'ascii' 63 codecs.lookup(encoding) 64 except (ValueError, LookupError): 65 pass 66 67encoding = encoding.lower() 68 69coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)') 70blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)') 71 72class EncodingMessage(SimpleDialog): 73 "Inform user that an encoding declaration is needed." 74 def __init__(self, master, enc): 75 self.should_edit = False 76 77 self.root = top = Toplevel(master) 78 top.bind("<Return>", self.return_event) 79 top.bind("<Escape>", self.do_ok) 80 top.protocol("WM_DELETE_WINDOW", self.wm_delete_window) 81 top.wm_title("I/O Warning") 82 top.wm_iconname("I/O Warning") 83 self.top = top 84 85 l1 = Label(top, 86 text="Non-ASCII found, yet no encoding declared. Add a line like") 87 l1.pack(side=TOP, anchor=W) 88 l2 = Entry(top, font="courier") 89 l2.insert(0, "# -*- coding: %s -*-" % enc) 90 # For some reason, the text is not selectable anymore if the 91 # widget is disabled. 92 # l2['state'] = DISABLED 93 l2.pack(side=TOP, anchor = W, fill=X) 94 l3 = Label(top, text="to your file\n" 95 "See Language Reference, 2.1.4 Encoding declarations.\n" 96 "Choose OK to save this file as %s\n" 97 "Edit your general options to silence this warning" % enc) 98 l3.pack(side=TOP, anchor = W) 99 100 buttons = Frame(top) 101 buttons.pack(side=TOP, fill=X) 102 # Both return and cancel mean the same thing: do nothing 103 self.default = self.cancel = 0 104 b1 = Button(buttons, text="Ok", default="active", 105 command=self.do_ok) 106 b1.pack(side=LEFT, fill=BOTH, expand=1) 107 b2 = Button(buttons, text="Edit my file", 108 command=self.do_edit) 109 b2.pack(side=LEFT, fill=BOTH, expand=1) 110 111 self._set_transient(master) 112 113 def do_ok(self): 114 self.done(0) 115 116 def do_edit(self): 117 self.done(1) 118 119def coding_spec(str): 120 """Return the encoding declaration according to PEP 263. 121 122 Raise LookupError if the encoding is declared but unknown. 123 """ 124 # Only consider the first two lines 125 lst = str.split("\n", 2)[:2] 126 for line in lst: 127 match = coding_re.match(line) 128 if match is not None: 129 break 130 if not blank_re.match(line): 131 return None 132 else: 133 return None 134 name = match.group(1) 135 # Check whether the encoding is known 136 import codecs 137 try: 138 codecs.lookup(name) 139 except LookupError: 140 # The standard encoding error does not indicate the encoding 141 raise LookupError, "Unknown encoding "+name 142 return name 143 144class IOBinding: 145 146 def __init__(self, editwin): 147 self.editwin = editwin 148 self.text = editwin.text 149 self.__id_open = self.text.bind("<<open-window-from-file>>", self.open) 150 self.__id_save = self.text.bind("<<save-window>>", self.save) 151 self.__id_saveas = self.text.bind("<<save-window-as-file>>", 152 self.save_as) 153 self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>", 154 self.save_a_copy) 155 self.fileencoding = None 156 self.__id_print = self.text.bind("<<print-window>>", self.print_window) 157 158 def close(self): 159 # Undo command bindings 160 self.text.unbind("<<open-window-from-file>>", self.__id_open) 161 self.text.unbind("<<save-window>>", self.__id_save) 162 self.text.unbind("<<save-window-as-file>>",self.__id_saveas) 163 self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy) 164 self.text.unbind("<<print-window>>", self.__id_print) 165 # Break cycles 166 self.editwin = None 167 self.text = None 168 self.filename_change_hook = None 169 170 def get_saved(self): 171 return self.editwin.get_saved() 172 173 def set_saved(self, flag): 174 self.editwin.set_saved(flag) 175 176 def reset_undo(self): 177 self.editwin.reset_undo() 178 179 filename_change_hook = None 180 181 def set_filename_change_hook(self, hook): 182 self.filename_change_hook = hook 183 184 filename = None 185 dirname = None 186 187 def set_filename(self, filename): 188 if filename and os.path.isdir(filename): 189 self.filename = None 190 self.dirname = filename 191 else: 192 self.filename = filename 193 self.dirname = None 194 self.set_saved(1) 195 if self.filename_change_hook: 196 self.filename_change_hook() 197 198 def open(self, event=None, editFile=None): 199 flist = self.editwin.flist 200 # Save in case parent window is closed (ie, during askopenfile()). 201 if flist: 202 if not editFile: 203 filename = self.askopenfile() 204 else: 205 filename=editFile 206 if filename: 207 # If editFile is valid and already open, flist.open will 208 # shift focus to its existing window. 209 # If the current window exists and is a fresh unnamed, 210 # unmodified editor window (not an interpreter shell), 211 # pass self.loadfile to flist.open so it will load the file 212 # in the current window (if the file is not already open) 213 # instead of a new window. 214 if (self.editwin and 215 not getattr(self.editwin, 'interp', None) and 216 not self.filename and 217 self.get_saved()): 218 flist.open(filename, self.loadfile) 219 else: 220 flist.open(filename) 221 else: 222 if self.text: 223 self.text.focus_set() 224 return "break" 225 226 # Code for use outside IDLE: 227 if self.get_saved(): 228 reply = self.maybesave() 229 if reply == "cancel": 230 self.text.focus_set() 231 return "break" 232 if not editFile: 233 filename = self.askopenfile() 234 else: 235 filename=editFile 236 if filename: 237 self.loadfile(filename) 238 else: 239 self.text.focus_set() 240 return "break" 241 242 eol = r"(\r\n)|\n|\r" # \r\n (Windows), \n (UNIX), or \r (Mac) 243 eol_re = re.compile(eol) 244 eol_convention = os.linesep # Default 245 246 def loadfile(self, filename): 247 try: 248 # open the file in binary mode so that we can handle 249 # end-of-line convention ourselves. 250 with open(filename, 'rb') as f: 251 chars = f.read() 252 except IOError as msg: 253 tkMessageBox.showerror("I/O Error", str(msg), parent=self.text) 254 return False 255 256 chars = self.decode(chars) 257 # We now convert all end-of-lines to '\n's 258 firsteol = self.eol_re.search(chars) 259 if firsteol: 260 self.eol_convention = firsteol.group(0) 261 if isinstance(self.eol_convention, unicode): 262 # Make sure it is an ASCII string 263 self.eol_convention = self.eol_convention.encode("ascii") 264 chars = self.eol_re.sub(r"\n", chars) 265 266 self.text.delete("1.0", "end") 267 self.set_filename(None) 268 self.text.insert("1.0", chars) 269 self.reset_undo() 270 self.set_filename(filename) 271 self.text.mark_set("insert", "1.0") 272 self.text.yview("insert") 273 self.updaterecentfileslist(filename) 274 return True 275 276 def decode(self, chars): 277 """Create a Unicode string 278 279 If that fails, let Tcl try its best 280 """ 281 # Check presence of a UTF-8 signature first 282 if chars.startswith(BOM_UTF8): 283 try: 284 chars = chars[3:].decode("utf-8") 285 except UnicodeError: 286 # has UTF-8 signature, but fails to decode... 287 return chars 288 else: 289 # Indicates that this file originally had a BOM 290 self.fileencoding = BOM_UTF8 291 return chars 292 # Next look for coding specification 293 try: 294 enc = coding_spec(chars) 295 except LookupError as name: 296 tkMessageBox.showerror( 297 title="Error loading the file", 298 message="The encoding '%s' is not known to this Python "\ 299 "installation. The file may not display correctly" % name, 300 parent = self.text) 301 enc = None 302 if enc: 303 try: 304 return unicode(chars, enc) 305 except UnicodeError: 306 pass 307 # If it is ASCII, we need not to record anything 308 try: 309 return unicode(chars, 'ascii') 310 except UnicodeError: 311 pass 312 # Finally, try the locale's encoding. This is deprecated; 313 # the user should declare a non-ASCII encoding 314 try: 315 chars = unicode(chars, encoding) 316 self.fileencoding = encoding 317 except UnicodeError: 318 pass 319 return chars 320 321 def maybesave(self): 322 if self.get_saved(): 323 return "yes" 324 message = "Do you want to save %s before closing?" % ( 325 self.filename or "this untitled document") 326 confirm = tkMessageBox.askyesnocancel( 327 title="Save On Close", 328 message=message, 329 default=tkMessageBox.YES, 330 parent=self.text) 331 if confirm: 332 reply = "yes" 333 self.save(None) 334 if not self.get_saved(): 335 reply = "cancel" 336 elif confirm is None: 337 reply = "cancel" 338 else: 339 reply = "no" 340 self.text.focus_set() 341 return reply 342 343 def save(self, event): 344 if not self.filename: 345 self.save_as(event) 346 else: 347 if self.writefile(self.filename): 348 self.set_saved(True) 349 try: 350 self.editwin.store_file_breaks() 351 except AttributeError: # may be a PyShell 352 pass 353 self.text.focus_set() 354 return "break" 355 356 def save_as(self, event): 357 filename = self.asksavefile() 358 if filename: 359 if self.writefile(filename): 360 self.set_filename(filename) 361 self.set_saved(1) 362 try: 363 self.editwin.store_file_breaks() 364 except AttributeError: 365 pass 366 self.text.focus_set() 367 self.updaterecentfileslist(filename) 368 return "break" 369 370 def save_a_copy(self, event): 371 filename = self.asksavefile() 372 if filename: 373 self.writefile(filename) 374 self.text.focus_set() 375 self.updaterecentfileslist(filename) 376 return "break" 377 378 def writefile(self, filename): 379 self.fixlastline() 380 chars = self.encode(self.text.get("1.0", "end-1c")) 381 if self.eol_convention != "\n": 382 chars = chars.replace("\n", self.eol_convention) 383 try: 384 with open(filename, "wb") as f: 385 f.write(chars) 386 return True 387 except IOError as msg: 388 tkMessageBox.showerror("I/O Error", str(msg), 389 parent=self.text) 390 return False 391 392 def encode(self, chars): 393 if isinstance(chars, str): 394 # This is either plain ASCII, or Tk was returning mixed-encoding 395 # text to us. Don't try to guess further. 396 return chars 397 # See whether there is anything non-ASCII in it. 398 # If not, no need to figure out the encoding. 399 try: 400 return chars.encode('ascii') 401 except UnicodeError: 402 pass 403 # If there is an encoding declared, try this first. 404 try: 405 enc = coding_spec(chars) 406 failed = None 407 except LookupError as msg: 408 failed = msg 409 enc = None 410 if enc: 411 try: 412 return chars.encode(enc) 413 except UnicodeError: 414 failed = "Invalid encoding '%s'" % enc 415 if failed: 416 tkMessageBox.showerror( 417 "I/O Error", 418 "%s. Saving as UTF-8" % failed, 419 parent = self.text) 420 # If there was a UTF-8 signature, use that. This should not fail 421 if self.fileencoding == BOM_UTF8 or failed: 422 return BOM_UTF8 + chars.encode("utf-8") 423 # Try the original file encoding next, if any 424 if self.fileencoding: 425 try: 426 return chars.encode(self.fileencoding) 427 except UnicodeError: 428 tkMessageBox.showerror( 429 "I/O Error", 430 "Cannot save this as '%s' anymore. Saving as UTF-8" \ 431 % self.fileencoding, 432 parent = self.text) 433 return BOM_UTF8 + chars.encode("utf-8") 434 # Nothing was declared, and we had not determined an encoding 435 # on loading. Recommend an encoding line. 436 config_encoding = idleConf.GetOption("main","EditorWindow", 437 "encoding") 438 if config_encoding == 'utf-8': 439 # User has requested that we save files as UTF-8 440 return BOM_UTF8 + chars.encode("utf-8") 441 ask_user = True 442 try: 443 chars = chars.encode(encoding) 444 enc = encoding 445 if config_encoding == 'locale': 446 ask_user = False 447 except UnicodeError: 448 chars = BOM_UTF8 + chars.encode("utf-8") 449 enc = "utf-8" 450 if not ask_user: 451 return chars 452 dialog = EncodingMessage(self.editwin.top, enc) 453 dialog.go() 454 if dialog.num == 1: 455 # User asked us to edit the file 456 encline = "# -*- coding: %s -*-\n" % enc 457 firstline = self.text.get("1.0", "2.0") 458 if firstline.startswith("#!"): 459 # Insert encoding after #! line 460 self.text.insert("2.0", encline) 461 else: 462 self.text.insert("1.0", encline) 463 return self.encode(self.text.get("1.0", "end-1c")) 464 return chars 465 466 def fixlastline(self): 467 c = self.text.get("end-2c") 468 if c != '\n': 469 self.text.insert("end-1c", "\n") 470 471 def print_window(self, event): 472 confirm = tkMessageBox.askokcancel( 473 title="Print", 474 message="Print to Default Printer", 475 default=tkMessageBox.OK, 476 parent=self.text) 477 if not confirm: 478 self.text.focus_set() 479 return "break" 480 tempfilename = None 481 saved = self.get_saved() 482 if saved: 483 filename = self.filename 484 # shell undo is reset after every prompt, looks saved, probably isn't 485 if not saved or filename is None: 486 (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_') 487 filename = tempfilename 488 os.close(tfd) 489 if not self.writefile(tempfilename): 490 os.unlink(tempfilename) 491 return "break" 492 platform = os.name 493 printPlatform = True 494 if platform == 'posix': #posix platform 495 command = idleConf.GetOption('main','General', 496 'print-command-posix') 497 command = command + " 2>&1" 498 elif platform == 'nt': #win32 platform 499 command = idleConf.GetOption('main','General','print-command-win') 500 else: #no printing for this platform 501 printPlatform = False 502 if printPlatform: #we can try to print for this platform 503 command = command % pipes.quote(filename) 504 pipe = os.popen(command, "r") 505 # things can get ugly on NT if there is no printer available. 506 output = pipe.read().strip() 507 status = pipe.close() 508 if status: 509 output = "Printing failed (exit status 0x%x)\n" % \ 510 status + output 511 if output: 512 output = "Printing command: %s\n" % repr(command) + output 513 tkMessageBox.showerror("Print status", output, parent=self.text) 514 else: #no printing for this platform 515 message = "Printing is not enabled for this platform: %s" % platform 516 tkMessageBox.showinfo("Print status", message, parent=self.text) 517 if tempfilename: 518 os.unlink(tempfilename) 519 return "break" 520 521 opendialog = None 522 savedialog = None 523 524 filetypes = [ 525 ("Python files", "*.py *.pyw", "TEXT"), 526 ("Text files", "*.txt", "TEXT"), 527 ("All files", "*"), 528 ] 529 530 defaultextension = '.py' if sys.platform == 'darwin' else '' 531 532 def askopenfile(self): 533 dir, base = self.defaultfilename("open") 534 if not self.opendialog: 535 self.opendialog = tkFileDialog.Open(parent=self.text, 536 filetypes=self.filetypes) 537 filename = self.opendialog.show(initialdir=dir, initialfile=base) 538 if isinstance(filename, unicode): 539 filename = filename.encode(filesystemencoding) 540 return filename 541 542 def defaultfilename(self, mode="open"): 543 if self.filename: 544 return os.path.split(self.filename) 545 elif self.dirname: 546 return self.dirname, "" 547 else: 548 try: 549 pwd = os.getcwd() 550 except os.error: 551 pwd = "" 552 return pwd, "" 553 554 def asksavefile(self): 555 dir, base = self.defaultfilename("save") 556 if not self.savedialog: 557 self.savedialog = tkFileDialog.SaveAs( 558 parent=self.text, 559 filetypes=self.filetypes, 560 defaultextension=self.defaultextension) 561 filename = self.savedialog.show(initialdir=dir, initialfile=base) 562 if isinstance(filename, unicode): 563 filename = filename.encode(filesystemencoding) 564 return filename 565 566 def updaterecentfileslist(self,filename): 567 "Update recent file list on all editor windows" 568 self.editwin.update_recent_files_list(filename) 569 570 571def _io_binding(parent): # htest # 572 from Tkinter import Toplevel, Text 573 574 root = Toplevel(parent) 575 root.title("Test IOBinding") 576 width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) 577 root.geometry("+%d+%d"%(x, y + 150)) 578 class MyEditWin: 579 def __init__(self, text): 580 self.text = text 581 self.flist = None 582 self.text.bind("<Control-o>", self.open) 583 self.text.bind('<Control-p>', self.printer) 584 self.text.bind("<Control-s>", self.save) 585 self.text.bind("<Alt-s>", self.saveas) 586 self.text.bind('<Control-c>', self.savecopy) 587 def get_saved(self): return 0 588 def set_saved(self, flag): pass 589 def reset_undo(self): pass 590 def update_recent_files_list(self, filename): pass 591 def open(self, event): 592 self.text.event_generate("<<open-window-from-file>>") 593 def printer(self, event): 594 self.text.event_generate("<<print-window>>") 595 def save(self, event): 596 self.text.event_generate("<<save-window>>") 597 def saveas(self, event): 598 self.text.event_generate("<<save-window-as-file>>") 599 def savecopy(self, event): 600 self.text.event_generate("<<save-copy-of-window-as-file>>") 601 602 text = Text(root) 603 text.pack() 604 text.focus_set() 605 editwin = MyEditWin(text) 606 IOBinding(editwin) 607 608if __name__ == "__main__": 609 from idlelib.idle_test.htest import run 610 run(_io_binding) 611