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 12from idlelib.config import idleConf 13from idlelib.util import py_extensions 14 15py_extensions = ' '.join("*"+ext for ext in py_extensions) 16encoding = 'utf-8' 17errors = 'surrogatepass' if sys.platform == 'win32' else 'surrogateescape' 18 19 20class IOBinding: 21# One instance per editor Window so methods know which to save, close. 22# Open returns focus to self.editwin if aborted. 23# EditorWindow.open_module, others, belong here. 24 25 def __init__(self, editwin): 26 self.editwin = editwin 27 self.text = editwin.text 28 self.__id_open = self.text.bind("<<open-window-from-file>>", self.open) 29 self.__id_save = self.text.bind("<<save-window>>", self.save) 30 self.__id_saveas = self.text.bind("<<save-window-as-file>>", 31 self.save_as) 32 self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>", 33 self.save_a_copy) 34 self.fileencoding = 'utf-8' 35 self.__id_print = self.text.bind("<<print-window>>", self.print_window) 36 37 def close(self): 38 # Undo command bindings 39 self.text.unbind("<<open-window-from-file>>", self.__id_open) 40 self.text.unbind("<<save-window>>", self.__id_save) 41 self.text.unbind("<<save-window-as-file>>",self.__id_saveas) 42 self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy) 43 self.text.unbind("<<print-window>>", self.__id_print) 44 # Break cycles 45 self.editwin = None 46 self.text = None 47 self.filename_change_hook = None 48 49 def get_saved(self): 50 return self.editwin.get_saved() 51 52 def set_saved(self, flag): 53 self.editwin.set_saved(flag) 54 55 def reset_undo(self): 56 self.editwin.reset_undo() 57 58 filename_change_hook = None 59 60 def set_filename_change_hook(self, hook): 61 self.filename_change_hook = hook 62 63 filename = None 64 dirname = None 65 66 def set_filename(self, filename): 67 if filename and os.path.isdir(filename): 68 self.filename = None 69 self.dirname = filename 70 else: 71 self.filename = filename 72 self.dirname = None 73 self.set_saved(1) 74 if self.filename_change_hook: 75 self.filename_change_hook() 76 77 def open(self, event=None, editFile=None): 78 flist = self.editwin.flist 79 # Save in case parent window is closed (ie, during askopenfile()). 80 if flist: 81 if not editFile: 82 filename = self.askopenfile() 83 else: 84 filename=editFile 85 if filename: 86 # If editFile is valid and already open, flist.open will 87 # shift focus to its existing window. 88 # If the current window exists and is a fresh unnamed, 89 # unmodified editor window (not an interpreter shell), 90 # pass self.loadfile to flist.open so it will load the file 91 # in the current window (if the file is not already open) 92 # instead of a new window. 93 if (self.editwin and 94 not getattr(self.editwin, 'interp', None) and 95 not self.filename and 96 self.get_saved()): 97 flist.open(filename, self.loadfile) 98 else: 99 flist.open(filename) 100 else: 101 if self.text: 102 self.text.focus_set() 103 return "break" 104 105 # Code for use outside IDLE: 106 if self.get_saved(): 107 reply = self.maybesave() 108 if reply == "cancel": 109 self.text.focus_set() 110 return "break" 111 if not editFile: 112 filename = self.askopenfile() 113 else: 114 filename=editFile 115 if filename: 116 self.loadfile(filename) 117 else: 118 self.text.focus_set() 119 return "break" 120 121 eol_convention = os.linesep # default 122 123 def loadfile(self, filename): 124 try: 125 try: 126 with tokenize.open(filename) as f: 127 chars = f.read() 128 fileencoding = f.encoding 129 eol_convention = f.newlines 130 converted = False 131 except (UnicodeDecodeError, SyntaxError): 132 # Wait for the editor window to appear 133 self.editwin.text.update() 134 enc = askstring( 135 "Specify file encoding", 136 "The file's encoding is invalid for Python 3.x.\n" 137 "IDLE will convert it to UTF-8.\n" 138 "What is the current encoding of the file?", 139 initialvalue='utf-8', 140 parent=self.editwin.text) 141 with open(filename, encoding=enc) as f: 142 chars = f.read() 143 fileencoding = f.encoding 144 eol_convention = f.newlines 145 converted = True 146 except OSError as err: 147 messagebox.showerror("I/O Error", str(err), parent=self.text) 148 return False 149 except UnicodeDecodeError: 150 messagebox.showerror("Decoding Error", 151 "File %s\nFailed to Decode" % filename, 152 parent=self.text) 153 return False 154 155 if not isinstance(eol_convention, str): 156 # If the file does not contain line separators, it is None. 157 # If the file contains mixed line separators, it is a tuple. 158 if eol_convention is not None: 159 messagebox.showwarning("Mixed Newlines", 160 "Mixed newlines detected.\n" 161 "The file will be changed on save.", 162 parent=self.text) 163 converted = True 164 eol_convention = os.linesep # default 165 166 self.text.delete("1.0", "end") 167 self.set_filename(None) 168 self.fileencoding = fileencoding 169 self.eol_convention = eol_convention 170 self.text.insert("1.0", chars) 171 self.reset_undo() 172 self.set_filename(filename) 173 if converted: 174 # We need to save the conversion results first 175 # before being able to execute the code 176 self.set_saved(False) 177 self.text.mark_set("insert", "1.0") 178 self.text.yview("insert") 179 self.updaterecentfileslist(filename) 180 return True 181 182 def maybesave(self): 183 if self.get_saved(): 184 return "yes" 185 message = "Do you want to save %s before closing?" % ( 186 self.filename or "this untitled document") 187 confirm = messagebox.askyesnocancel( 188 title="Save On Close", 189 message=message, 190 default=messagebox.YES, 191 parent=self.text) 192 if confirm: 193 reply = "yes" 194 self.save(None) 195 if not self.get_saved(): 196 reply = "cancel" 197 elif confirm is None: 198 reply = "cancel" 199 else: 200 reply = "no" 201 self.text.focus_set() 202 return reply 203 204 def save(self, event): 205 if not self.filename: 206 self.save_as(event) 207 else: 208 if self.writefile(self.filename): 209 self.set_saved(True) 210 try: 211 self.editwin.store_file_breaks() 212 except AttributeError: # may be a PyShell 213 pass 214 self.text.focus_set() 215 return "break" 216 217 def save_as(self, event): 218 filename = self.asksavefile() 219 if filename: 220 if self.writefile(filename): 221 self.set_filename(filename) 222 self.set_saved(1) 223 try: 224 self.editwin.store_file_breaks() 225 except AttributeError: 226 pass 227 self.text.focus_set() 228 self.updaterecentfileslist(filename) 229 return "break" 230 231 def save_a_copy(self, event): 232 filename = self.asksavefile() 233 if filename: 234 self.writefile(filename) 235 self.text.focus_set() 236 self.updaterecentfileslist(filename) 237 return "break" 238 239 def writefile(self, filename): 240 text = self.fixnewlines() 241 chars = self.encode(text) 242 try: 243 with open(filename, "wb") as f: 244 f.write(chars) 245 f.flush() 246 os.fsync(f.fileno()) 247 return True 248 except OSError as msg: 249 messagebox.showerror("I/O Error", str(msg), 250 parent=self.text) 251 return False 252 253 def fixnewlines(self): 254 """Return text with os eols. 255 256 Add prompts if shell else final \n if missing. 257 """ 258 259 if hasattr(self.editwin, "interp"): # Saving shell. 260 text = self.editwin.get_prompt_text('1.0', self.text.index('end-1c')) 261 else: 262 if self.text.get("end-2c") != '\n': 263 self.text.insert("end-1c", "\n") # Changes 'end-1c' value. 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