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