1import io 2import os 3import shlex 4import sys 5import tempfile 6import tokenize 7 8import tkinter.filedialog as tkFileDialog 9import tkinter.messagebox as tkMessageBox 10from tkinter.simpledialog import askstring 11 12import idlelib 13from idlelib.config import idleConf 14 15encoding = 'utf-8' 16if sys.platform == 'win32': 17 errors = 'surrogatepass' 18else: 19 errors = 'surrogateescape' 20 21 22 23class IOBinding: 24# One instance per editor Window so methods know which to save, close. 25# Open returns focus to self.editwin if aborted. 26# EditorWindow.open_module, others, belong here. 27 28 def __init__(self, editwin): 29 self.editwin = editwin 30 self.text = editwin.text 31 self.__id_open = self.text.bind("<<open-window-from-file>>", self.open) 32 self.__id_save = self.text.bind("<<save-window>>", self.save) 33 self.__id_saveas = self.text.bind("<<save-window-as-file>>", 34 self.save_as) 35 self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>", 36 self.save_a_copy) 37 self.fileencoding = 'utf-8' 38 self.__id_print = self.text.bind("<<print-window>>", self.print_window) 39 40 def close(self): 41 # Undo command bindings 42 self.text.unbind("<<open-window-from-file>>", self.__id_open) 43 self.text.unbind("<<save-window>>", self.__id_save) 44 self.text.unbind("<<save-window-as-file>>",self.__id_saveas) 45 self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy) 46 self.text.unbind("<<print-window>>", self.__id_print) 47 # Break cycles 48 self.editwin = None 49 self.text = None 50 self.filename_change_hook = None 51 52 def get_saved(self): 53 return self.editwin.get_saved() 54 55 def set_saved(self, flag): 56 self.editwin.set_saved(flag) 57 58 def reset_undo(self): 59 self.editwin.reset_undo() 60 61 filename_change_hook = None 62 63 def set_filename_change_hook(self, hook): 64 self.filename_change_hook = hook 65 66 filename = None 67 dirname = None 68 69 def set_filename(self, filename): 70 if filename and os.path.isdir(filename): 71 self.filename = None 72 self.dirname = filename 73 else: 74 self.filename = filename 75 self.dirname = None 76 self.set_saved(1) 77 if self.filename_change_hook: 78 self.filename_change_hook() 79 80 def open(self, event=None, editFile=None): 81 flist = self.editwin.flist 82 # Save in case parent window is closed (ie, during askopenfile()). 83 if flist: 84 if not editFile: 85 filename = self.askopenfile() 86 else: 87 filename=editFile 88 if filename: 89 # If editFile is valid and already open, flist.open will 90 # shift focus to its existing window. 91 # If the current window exists and is a fresh unnamed, 92 # unmodified editor window (not an interpreter shell), 93 # pass self.loadfile to flist.open so it will load the file 94 # in the current window (if the file is not already open) 95 # instead of a new window. 96 if (self.editwin and 97 not getattr(self.editwin, 'interp', None) and 98 not self.filename and 99 self.get_saved()): 100 flist.open(filename, self.loadfile) 101 else: 102 flist.open(filename) 103 else: 104 if self.text: 105 self.text.focus_set() 106 return "break" 107 108 # Code for use outside IDLE: 109 if self.get_saved(): 110 reply = self.maybesave() 111 if reply == "cancel": 112 self.text.focus_set() 113 return "break" 114 if not editFile: 115 filename = self.askopenfile() 116 else: 117 filename=editFile 118 if filename: 119 self.loadfile(filename) 120 else: 121 self.text.focus_set() 122 return "break" 123 124 eol_convention = os.linesep # default 125 126 def loadfile(self, filename): 127 try: 128 try: 129 with tokenize.open(filename) as f: 130 chars = f.read() 131 fileencoding = f.encoding 132 eol_convention = f.newlines 133 converted = False 134 except (UnicodeDecodeError, SyntaxError): 135 # Wait for the editor window to appear 136 self.editwin.text.update() 137 enc = askstring( 138 "Specify file encoding", 139 "The file's encoding is invalid for Python 3.x.\n" 140 "IDLE will convert it to UTF-8.\n" 141 "What is the current encoding of the file?", 142 initialvalue='utf-8', 143 parent=self.editwin.text) 144 with open(filename, encoding=enc) as f: 145 chars = f.read() 146 fileencoding = f.encoding 147 eol_convention = f.newlines 148 converted = True 149 except OSError as err: 150 tkMessageBox.showerror("I/O Error", str(err), parent=self.text) 151 return False 152 except UnicodeDecodeError: 153 tkMessageBox.showerror("Decoding Error", 154 "File %s\nFailed to Decode" % filename, 155 parent=self.text) 156 return False 157 158 self.text.delete("1.0", "end") 159 self.set_filename(None) 160 self.fileencoding = fileencoding 161 self.eol_convention = eol_convention 162 self.text.insert("1.0", chars) 163 self.reset_undo() 164 self.set_filename(filename) 165 if converted: 166 # We need to save the conversion results first 167 # before being able to execute the code 168 self.set_saved(False) 169 self.text.mark_set("insert", "1.0") 170 self.text.yview("insert") 171 self.updaterecentfileslist(filename) 172 return True 173 174 def maybesave(self): 175 if self.get_saved(): 176 return "yes" 177 message = "Do you want to save %s before closing?" % ( 178 self.filename or "this untitled document") 179 confirm = tkMessageBox.askyesnocancel( 180 title="Save On Close", 181 message=message, 182 default=tkMessageBox.YES, 183 parent=self.text) 184 if confirm: 185 reply = "yes" 186 self.save(None) 187 if not self.get_saved(): 188 reply = "cancel" 189 elif confirm is None: 190 reply = "cancel" 191 else: 192 reply = "no" 193 self.text.focus_set() 194 return reply 195 196 def save(self, event): 197 if not self.filename: 198 self.save_as(event) 199 else: 200 if self.writefile(self.filename): 201 self.set_saved(True) 202 try: 203 self.editwin.store_file_breaks() 204 except AttributeError: # may be a PyShell 205 pass 206 self.text.focus_set() 207 return "break" 208 209 def save_as(self, event): 210 filename = self.asksavefile() 211 if filename: 212 if self.writefile(filename): 213 self.set_filename(filename) 214 self.set_saved(1) 215 try: 216 self.editwin.store_file_breaks() 217 except AttributeError: 218 pass 219 self.text.focus_set() 220 self.updaterecentfileslist(filename) 221 return "break" 222 223 def save_a_copy(self, event): 224 filename = self.asksavefile() 225 if filename: 226 self.writefile(filename) 227 self.text.focus_set() 228 self.updaterecentfileslist(filename) 229 return "break" 230 231 def writefile(self, filename): 232 text = self.fixnewlines() 233 chars = self.encode(text) 234 try: 235 with open(filename, "wb") as f: 236 f.write(chars) 237 f.flush() 238 os.fsync(f.fileno()) 239 return True 240 except OSError as msg: 241 tkMessageBox.showerror("I/O Error", str(msg), 242 parent=self.text) 243 return False 244 245 def fixnewlines(self): 246 "Return text with final \n if needed and os eols." 247 if (self.text.get("end-2c") != '\n' 248 and not hasattr(self.editwin, "interp")): # Not shell. 249 self.text.insert("end-1c", "\n") 250 text = self.text.get("1.0", "end-1c") 251 if self.eol_convention != "\n": 252 text = text.replace("\n", self.eol_convention) 253 return text 254 255 def encode(self, chars): 256 if isinstance(chars, bytes): 257 # This is either plain ASCII, or Tk was returning mixed-encoding 258 # text to us. Don't try to guess further. 259 return chars 260 # Preserve a BOM that might have been present on opening 261 if self.fileencoding == 'utf-8-sig': 262 return chars.encode('utf-8-sig') 263 # See whether there is anything non-ASCII in it. 264 # If not, no need to figure out the encoding. 265 try: 266 return chars.encode('ascii') 267 except UnicodeEncodeError: 268 pass 269 # Check if there is an encoding declared 270 try: 271 encoded = chars.encode('ascii', 'replace') 272 enc, _ = tokenize.detect_encoding(io.BytesIO(encoded).readline) 273 return chars.encode(enc) 274 except SyntaxError as err: 275 failed = str(err) 276 except UnicodeEncodeError: 277 failed = "Invalid encoding '%s'" % enc 278 tkMessageBox.showerror( 279 "I/O Error", 280 "%s.\nSaving as UTF-8" % failed, 281 parent=self.text) 282 # Fallback: save as UTF-8, with BOM - ignoring the incorrect 283 # declared encoding 284 return chars.encode('utf-8-sig') 285 286 def print_window(self, event): 287 confirm = tkMessageBox.askokcancel( 288 title="Print", 289 message="Print to Default Printer", 290 default=tkMessageBox.OK, 291 parent=self.text) 292 if not confirm: 293 self.text.focus_set() 294 return "break" 295 tempfilename = None 296 saved = self.get_saved() 297 if saved: 298 filename = self.filename 299 # shell undo is reset after every prompt, looks saved, probably isn't 300 if not saved or filename is None: 301 (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_') 302 filename = tempfilename 303 os.close(tfd) 304 if not self.writefile(tempfilename): 305 os.unlink(tempfilename) 306 return "break" 307 platform = os.name 308 printPlatform = True 309 if platform == 'posix': #posix platform 310 command = idleConf.GetOption('main','General', 311 'print-command-posix') 312 command = command + " 2>&1" 313 elif platform == 'nt': #win32 platform 314 command = idleConf.GetOption('main','General','print-command-win') 315 else: #no printing for this platform 316 printPlatform = False 317 if printPlatform: #we can try to print for this platform 318 command = command % shlex.quote(filename) 319 pipe = os.popen(command, "r") 320 # things can get ugly on NT if there is no printer available. 321 output = pipe.read().strip() 322 status = pipe.close() 323 if status: 324 output = "Printing failed (exit status 0x%x)\n" % \ 325 status + output 326 if output: 327 output = "Printing command: %s\n" % repr(command) + output 328 tkMessageBox.showerror("Print status", output, parent=self.text) 329 else: #no printing for this platform 330 message = "Printing is not enabled for this platform: %s" % platform 331 tkMessageBox.showinfo("Print status", message, parent=self.text) 332 if tempfilename: 333 os.unlink(tempfilename) 334 return "break" 335 336 opendialog = None 337 savedialog = None 338 339 filetypes = ( 340 ("Python files", "*.py *.pyw", "TEXT"), 341 ("Text files", "*.txt", "TEXT"), 342 ("All files", "*"), 343 ) 344 345 defaultextension = '.py' if sys.platform == 'darwin' else '' 346 347 def askopenfile(self): 348 dir, base = self.defaultfilename("open") 349 if not self.opendialog: 350 self.opendialog = tkFileDialog.Open(parent=self.text, 351 filetypes=self.filetypes) 352 filename = self.opendialog.show(initialdir=dir, initialfile=base) 353 return filename 354 355 def defaultfilename(self, mode="open"): 356 if self.filename: 357 return os.path.split(self.filename) 358 elif self.dirname: 359 return self.dirname, "" 360 else: 361 try: 362 pwd = os.getcwd() 363 except OSError: 364 pwd = "" 365 return pwd, "" 366 367 def asksavefile(self): 368 dir, base = self.defaultfilename("save") 369 if not self.savedialog: 370 self.savedialog = tkFileDialog.SaveAs( 371 parent=self.text, 372 filetypes=self.filetypes, 373 defaultextension=self.defaultextension) 374 filename = self.savedialog.show(initialdir=dir, initialfile=base) 375 return filename 376 377 def updaterecentfileslist(self,filename): 378 "Update recent file list on all editor windows" 379 if self.editwin.flist: 380 self.editwin.update_recent_files_list(filename) 381 382def _io_binding(parent): # htest # 383 from tkinter import Toplevel, Text 384 385 root = Toplevel(parent) 386 root.title("Test IOBinding") 387 x, y = map(int, parent.geometry().split('+')[1:]) 388 root.geometry("+%d+%d" % (x, y + 175)) 389 class MyEditWin: 390 def __init__(self, text): 391 self.text = text 392 self.flist = None 393 self.text.bind("<Control-o>", self.open) 394 self.text.bind('<Control-p>', self.print) 395 self.text.bind("<Control-s>", self.save) 396 self.text.bind("<Alt-s>", self.saveas) 397 self.text.bind('<Control-c>', self.savecopy) 398 def get_saved(self): return 0 399 def set_saved(self, flag): pass 400 def reset_undo(self): pass 401 def open(self, event): 402 self.text.event_generate("<<open-window-from-file>>") 403 def print(self, event): 404 self.text.event_generate("<<print-window>>") 405 def save(self, event): 406 self.text.event_generate("<<save-window>>") 407 def saveas(self, event): 408 self.text.event_generate("<<save-window-as-file>>") 409 def savecopy(self, event): 410 self.text.event_generate("<<save-copy-of-window-as-file>>") 411 412 text = Text(root) 413 text.pack() 414 text.focus_set() 415 editwin = MyEditWin(text) 416 IOBinding(editwin) 417 418if __name__ == "__main__": 419 from unittest import main 420 main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False) 421 422 from idlelib.idle_test.htest import run 423 run(_io_binding) 424