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
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)
435