• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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