1 import io
2 import os
3 import shlex
4 import sys
5 import tempfile
6 import tokenize
7
8 from tkinter import filedialog
9 from tkinter import messagebox
10 from tkinter.simpledialog import askstring
11
12 from idlelib.config import idleConf
13 from idlelib.util import py_extensions
14
15 py_extensions = ' '.join("*"+ext for ext in py_extensions)
16 encoding = 'utf-8'
17 errors = 'surrogatepass' if sys.platform == 'win32' else 'surrogateescape'
18
19
20 class ESC[4;38;5;81mIOBinding:
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
396
397 def _io_binding(parent): # htest #
398 from tkinter import Toplevel, Text
399
400 top = Toplevel(parent)
401 top.title("Test IOBinding")
402 x, y = map(int, parent.geometry().split('+')[1:])
403 top.geometry("+%d+%d" % (x, y + 175))
404
405 class ESC[4;38;5;81mMyEditWin:
406 def __init__(self, text):
407 self.text = text
408 self.flist = None
409 self.text.bind("<Control-o>", self.open)
410 self.text.bind('<Control-p>', self.print)
411 self.text.bind("<Control-s>", self.save)
412 self.text.bind("<Alt-s>", self.saveas)
413 self.text.bind('<Control-c>', self.savecopy)
414 def get_saved(self): return 0
415 def set_saved(self, flag): pass
416 def reset_undo(self): pass
417 def open(self, event):
418 self.text.event_generate("<<open-window-from-file>>")
419 def print(self, event):
420 self.text.event_generate("<<print-window>>")
421 def save(self, event):
422 self.text.event_generate("<<save-window>>")
423 def saveas(self, event):
424 self.text.event_generate("<<save-window-as-file>>")
425 def savecopy(self, event):
426 self.text.event_generate("<<save-copy-of-window-as-file>>")
427
428 text = Text(top)
429 text.pack()
430 text.focus_set()
431 editwin = MyEditWin(text)
432 IOBinding(editwin)
433
434
435 if __name__ == "__main__":
436 from unittest import main
437 main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False)
438
439 from idlelib.idle_test.htest import run
440 run(_io_binding)