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