blob: 5ebf7089fb9abede84b41c504f4d95cac0923de0 [file] [log] [blame]
xf.li86118912025-03-19 20:07:27 -07001import 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)