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 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 messagebox.showerror("I/O Error", str(err), parent=self.text) 151 return False 152 except UnicodeDecodeError: 153 messagebox.showerror("Decoding Error", 154 "File %s\nFailed to Decode" % filename, 155 parent=self.text) 156 return False 157 158 if not isinstance(eol_convention, str): 159 # If the file does not contain line separators, it is None. 160 # If the file contains mixed line separators, it is a tuple. 161 if eol_convention is not None: 162 messagebox.showwarning("Mixed Newlines", 163 "Mixed newlines detected.\n" 164 "The file will be changed on save.", 165 parent=self.text) 166 converted = True 167 eol_convention = os.linesep # default 168 169 self.text.delete("1.0", "end") 170 self.set_filename(None) 171 self.fileencoding = fileencoding 172 self.eol_convention = eol_convention 173 self.text.insert("1.0", chars) 174 self.reset_undo() 175 self.set_filename(filename) 176 if converted: 177 # We need to save the conversion results first 178 # before being able to execute the code 179 self.set_saved(False) 180 self.text.mark_set("insert", "1.0") 181 self.text.yview("insert") 182 self.updaterecentfileslist(filename) 183 return True 184 185 def maybesave(self): 186 if self.get_saved(): 187 return "yes" 188 message = "Do you want to save %s before closing?" % ( 189 self.filename or "this untitled document") 190 confirm = messagebox.askyesnocancel( 191 title="Save On Close", 192 message=message, 193 default=messagebox.YES, 194 parent=self.text) 195 if confirm: 196 reply = "yes" 197 self.save(None) 198 if not self.get_saved(): 199 reply = "cancel" 200 elif confirm is None: 201 reply = "cancel" 202 else: 203 reply = "no" 204 self.text.focus_set() 205 return reply 206 207 def save(self, event): 208 if not self.filename: 209 self.save_as(event) 210 else: 211 if self.writefile(self.filename): 212 self.set_saved(True) 213 try: 214 self.editwin.store_file_breaks() 215 except AttributeError: # may be a PyShell 216 pass 217 self.text.focus_set() 218 return "break" 219 220 def save_as(self, event): 221 filename = self.asksavefile() 222 if filename: 223 if self.writefile(filename): 224 self.set_filename(filename) 225 self.set_saved(1) 226 try: 227 self.editwin.store_file_breaks() 228 except AttributeError: 229 pass 230 self.text.focus_set() 231 self.updaterecentfileslist(filename) 232 return "break" 233 234 def save_a_copy(self, event): 235 filename = self.asksavefile() 236 if filename: 237 self.writefile(filename) 238 self.text.focus_set() 239 self.updaterecentfileslist(filename) 240 return "break" 241 242 def writefile(self, filename): 243 text = self.fixnewlines() 244 chars = self.encode(text) 245 try: 246 with open(filename, "wb") as f: 247 f.write(chars) 248 f.flush() 249 os.fsync(f.fileno()) 250 return True 251 except OSError as msg: 252 messagebox.showerror("I/O Error", str(msg), 253 parent=self.text) 254 return False 255 256 def fixnewlines(self): 257 "Return text with final \n if needed and os eols." 258 if (self.text.get("end-2c") != '\n' 259 and not hasattr(self.editwin, "interp")): # Not shell. 260 self.text.insert("end-1c", "\n") 261 text = self.text.get("1.0", "end-1c") 262 if self.eol_convention != "\n": 263 text = text.replace("\n", self.eol_convention) 264 return text 265 266 def encode(self, chars): 267 if isinstance(chars, bytes): 268 # This is either plain ASCII, or Tk was returning mixed-encoding 269 # text to us. Don't try to guess further. 270 return chars 271 # Preserve a BOM that might have been present on opening 272 if self.fileencoding == 'utf-8-sig': 273 return chars.encode('utf-8-sig') 274 # See whether there is anything non-ASCII in it. 275 # If not, no need to figure out the encoding. 276 try: 277 return chars.encode('ascii') 278 except UnicodeEncodeError: 279 pass 280 # Check if there is an encoding declared 281 try: 282 encoded = chars.encode('ascii', 'replace') 283 enc, _ = tokenize.detect_encoding(io.BytesIO(encoded).readline) 284 return chars.encode(enc) 285 except SyntaxError as err: 286 failed = str(err) 287 except UnicodeEncodeError: 288 failed = "Invalid encoding '%s'" % enc 289 messagebox.showerror( 290 "I/O Error", 291 "%s.\nSaving as UTF-8" % failed, 292 parent=self.text) 293 # Fallback: save as UTF-8, with BOM - ignoring the incorrect 294 # declared encoding 295 return chars.encode('utf-8-sig') 296 297 def print_window(self, event): 298 confirm = messagebox.askokcancel( 299 title="Print", 300 message="Print to Default Printer", 301 default=messagebox.OK, 302 parent=self.text) 303 if not confirm: 304 self.text.focus_set() 305 return "break" 306 tempfilename = None 307 saved = self.get_saved() 308 if saved: 309 filename = self.filename 310 # shell undo is reset after every prompt, looks saved, probably isn't 311 if not saved or filename is None: 312 (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_') 313 filename = tempfilename 314 os.close(tfd) 315 if not self.writefile(tempfilename): 316 os.unlink(tempfilename) 317 return "break" 318 platform = os.name 319 printPlatform = True 320 if platform == 'posix': #posix platform 321 command = idleConf.GetOption('main','General', 322 'print-command-posix') 323 command = command + " 2>&1" 324 elif platform == 'nt': #win32 platform 325 command = idleConf.GetOption('main','General','print-command-win') 326 else: #no printing for this platform 327 printPlatform = False 328 if printPlatform: #we can try to print for this platform 329 command = command % shlex.quote(filename) 330 pipe = os.popen(command, "r") 331 # things can get ugly on NT if there is no printer available. 332 output = pipe.read().strip() 333 status = pipe.close() 334 if status: 335 output = "Printing failed (exit status 0x%x)\n" % \ 336 status + output 337 if output: 338 output = "Printing command: %s\n" % repr(command) + output 339 messagebox.showerror("Print status", output, parent=self.text) 340 else: #no printing for this platform 341 message = "Printing is not enabled for this platform: %s" % platform 342 messagebox.showinfo("Print status", message, parent=self.text) 343 if tempfilename: 344 os.unlink(tempfilename) 345 return "break" 346 347 opendialog = None 348 savedialog = None 349 350 filetypes = ( 351 ("Python files", "*.py *.pyw", "TEXT"), 352 ("Text files", "*.txt", "TEXT"), 353 ("All files", "*"), 354 ) 355 356 defaultextension = '.py' if sys.platform == 'darwin' else '' 357 358 def askopenfile(self): 359 dir, base = self.defaultfilename("open") 360 if not self.opendialog: 361 self.opendialog = filedialog.Open(parent=self.text, 362 filetypes=self.filetypes) 363 filename = self.opendialog.show(initialdir=dir, initialfile=base) 364 return filename 365 366 def defaultfilename(self, mode="open"): 367 if self.filename: 368 return os.path.split(self.filename) 369 elif self.dirname: 370 return self.dirname, "" 371 else: 372 try: 373 pwd = os.getcwd() 374 except OSError: 375 pwd = "" 376 return pwd, "" 377 378 def asksavefile(self): 379 dir, base = self.defaultfilename("save") 380 if not self.savedialog: 381 self.savedialog = filedialog.SaveAs( 382 parent=self.text, 383 filetypes=self.filetypes, 384 defaultextension=self.defaultextension) 385 filename = self.savedialog.show(initialdir=dir, initialfile=base) 386 return filename 387 388 def updaterecentfileslist(self,filename): 389 "Update recent file list on all editor windows" 390 if self.editwin.flist: 391 self.editwin.update_recent_files_list(filename) 392 393def _io_binding(parent): # htest # 394 from tkinter import Toplevel, Text 395 396 root = Toplevel(parent) 397 root.title("Test IOBinding") 398 x, y = map(int, parent.geometry().split('+')[1:]) 399 root.geometry("+%d+%d" % (x, y + 175)) 400 class MyEditWin: 401 def __init__(self, text): 402 self.text = text 403 self.flist = None 404 self.text.bind("<Control-o>", self.open) 405 self.text.bind('<Control-p>', self.print) 406 self.text.bind("<Control-s>", self.save) 407 self.text.bind("<Alt-s>", self.saveas) 408 self.text.bind('<Control-c>', self.savecopy) 409 def get_saved(self): return 0 410 def set_saved(self, flag): pass 411 def reset_undo(self): pass 412 def open(self, event): 413 self.text.event_generate("<<open-window-from-file>>") 414 def print(self, event): 415 self.text.event_generate("<<print-window>>") 416 def save(self, event): 417 self.text.event_generate("<<save-window>>") 418 def saveas(self, event): 419 self.text.event_generate("<<save-window-as-file>>") 420 def savecopy(self, event): 421 self.text.event_generate("<<save-copy-of-window-as-file>>") 422 423 text = Text(root) 424 text.pack() 425 text.focus_set() 426 editwin = MyEditWin(text) 427 IOBinding(editwin) 428 429if __name__ == "__main__": 430 from unittest import main 431 main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False) 432 433 from idlelib.idle_test.htest import run 434 run(_io_binding) 435