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