1# changes by dscherer@cmu.edu
2#   - IOBinding.open() replaces the current window with the opened file,
3#     if the current window is both unmodified and unnamed
4#   - IOBinding.loadfile() interprets Windows, UNIX, and Macintosh
5#     end-of-line conventions, instead of relying on the standard library,
6#     which will only understand the local convention.
7
8import codecs
9from codecs import BOM_UTF8
10import os
11import pipes
12import re
13import sys
14import tempfile
15
16from Tkinter import *
17import tkFileDialog
18import tkMessageBox
19from SimpleDialog import SimpleDialog
20
21from idlelib.configHandler import idleConf
22
23# Try setting the locale, so that we can find out
24# what encoding to use
25try:
26    import locale
27    locale.setlocale(locale.LC_CTYPE, "")
28except (ImportError, locale.Error):
29    pass
30
31# Encoding for file names
32filesystemencoding = sys.getfilesystemencoding()
33
34encoding = "ascii"
35if sys.platform == 'win32':
36    # On Windows, we could use "mbcs". However, to give the user
37    # a portable encoding name, we need to find the code page
38    try:
39        encoding = locale.getdefaultlocale()[1]
40        codecs.lookup(encoding)
41    except LookupError:
42        pass
43else:
44    try:
45        # Different things can fail here: the locale module may not be
46        # loaded, it may not offer nl_langinfo, or CODESET, or the
47        # resulting codeset may be unknown to Python. We ignore all
48        # these problems, falling back to ASCII
49        encoding = locale.nl_langinfo(locale.CODESET)
50        if encoding is None or encoding is '':
51            # situation occurs on Mac OS X
52            encoding = 'ascii'
53        codecs.lookup(encoding)
54    except (NameError, AttributeError, LookupError):
55        # Try getdefaultlocale well: it parses environment variables,
56        # which may give a clue. Unfortunately, getdefaultlocale has
57        # bugs that can cause ValueError.
58        try:
59            encoding = locale.getdefaultlocale()[1]
60            if encoding is None or encoding is '':
61                # situation occurs on Mac OS X
62                encoding = 'ascii'
63            codecs.lookup(encoding)
64        except (ValueError, LookupError):
65            pass
66
67encoding = encoding.lower()
68
69coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)')
70blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)')
71
72class EncodingMessage(SimpleDialog):
73    "Inform user that an encoding declaration is needed."
74    def __init__(self, master, enc):
75        self.should_edit = False
76
77        self.root = top = Toplevel(master)
78        top.bind("<Return>", self.return_event)
79        top.bind("<Escape>", self.do_ok)
80        top.protocol("WM_DELETE_WINDOW", self.wm_delete_window)
81        top.wm_title("I/O Warning")
82        top.wm_iconname("I/O Warning")
83        self.top = top
84
85        l1 = Label(top,
86            text="Non-ASCII found, yet no encoding declared. Add a line like")
87        l1.pack(side=TOP, anchor=W)
88        l2 = Entry(top, font="courier")
89        l2.insert(0, "# -*- coding: %s -*-" % enc)
90        # For some reason, the text is not selectable anymore if the
91        # widget is disabled.
92        # l2['state'] = DISABLED
93        l2.pack(side=TOP, anchor = W, fill=X)
94        l3 = Label(top, text="to your file\n"
95                   "See Language Reference, 2.1.4 Encoding declarations.\n"
96                   "Choose OK to save this file as %s\n"
97                   "Edit your general options to silence this warning" % enc)
98        l3.pack(side=TOP, anchor = W)
99
100        buttons = Frame(top)
101        buttons.pack(side=TOP, fill=X)
102        # Both return and cancel mean the same thing: do nothing
103        self.default = self.cancel = 0
104        b1 = Button(buttons, text="Ok", default="active",
105                    command=self.do_ok)
106        b1.pack(side=LEFT, fill=BOTH, expand=1)
107        b2 = Button(buttons, text="Edit my file",
108                    command=self.do_edit)
109        b2.pack(side=LEFT, fill=BOTH, expand=1)
110
111        self._set_transient(master)
112
113    def do_ok(self):
114        self.done(0)
115
116    def do_edit(self):
117        self.done(1)
118
119def coding_spec(str):
120    """Return the encoding declaration according to PEP 263.
121
122    Raise LookupError if the encoding is declared but unknown.
123    """
124    # Only consider the first two lines
125    lst = str.split("\n", 2)[:2]
126    for line in lst:
127        match = coding_re.match(line)
128        if match is not None:
129            break
130        if not blank_re.match(line):
131            return None
132    else:
133        return None
134    name = match.group(1)
135    # Check whether the encoding is known
136    import codecs
137    try:
138        codecs.lookup(name)
139    except LookupError:
140        # The standard encoding error does not indicate the encoding
141        raise LookupError, "Unknown encoding "+name
142    return name
143
144class IOBinding:
145
146    def __init__(self, editwin):
147        self.editwin = editwin
148        self.text = editwin.text
149        self.__id_open = self.text.bind("<<open-window-from-file>>", self.open)
150        self.__id_save = self.text.bind("<<save-window>>", self.save)
151        self.__id_saveas = self.text.bind("<<save-window-as-file>>",
152                                          self.save_as)
153        self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
154                                            self.save_a_copy)
155        self.fileencoding = None
156        self.__id_print = self.text.bind("<<print-window>>", self.print_window)
157
158    def close(self):
159        # Undo command bindings
160        self.text.unbind("<<open-window-from-file>>", self.__id_open)
161        self.text.unbind("<<save-window>>", self.__id_save)
162        self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
163        self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
164        self.text.unbind("<<print-window>>", self.__id_print)
165        # Break cycles
166        self.editwin = None
167        self.text = None
168        self.filename_change_hook = None
169
170    def get_saved(self):
171        return self.editwin.get_saved()
172
173    def set_saved(self, flag):
174        self.editwin.set_saved(flag)
175
176    def reset_undo(self):
177        self.editwin.reset_undo()
178
179    filename_change_hook = None
180
181    def set_filename_change_hook(self, hook):
182        self.filename_change_hook = hook
183
184    filename = None
185    dirname = None
186
187    def set_filename(self, filename):
188        if filename and os.path.isdir(filename):
189            self.filename = None
190            self.dirname = filename
191        else:
192            self.filename = filename
193            self.dirname = None
194            self.set_saved(1)
195            if self.filename_change_hook:
196                self.filename_change_hook()
197
198    def open(self, event=None, editFile=None):
199        flist = self.editwin.flist
200        # Save in case parent window is closed (ie, during askopenfile()).
201        if flist:
202            if not editFile:
203                filename = self.askopenfile()
204            else:
205                filename=editFile
206            if filename:
207                # If editFile is valid and already open, flist.open will
208                # shift focus to its existing window.
209                # If the current window exists and is a fresh unnamed,
210                # unmodified editor window (not an interpreter shell),
211                # pass self.loadfile to flist.open so it will load the file
212                # in the current window (if the file is not already open)
213                # instead of a new window.
214                if (self.editwin and
215                        not getattr(self.editwin, 'interp', None) and
216                        not self.filename and
217                        self.get_saved()):
218                    flist.open(filename, self.loadfile)
219                else:
220                    flist.open(filename)
221            else:
222                if self.text:
223                    self.text.focus_set()
224            return "break"
225
226        # Code for use outside IDLE:
227        if self.get_saved():
228            reply = self.maybesave()
229            if reply == "cancel":
230                self.text.focus_set()
231                return "break"
232        if not editFile:
233            filename = self.askopenfile()
234        else:
235            filename=editFile
236        if filename:
237            self.loadfile(filename)
238        else:
239            self.text.focus_set()
240        return "break"
241
242    eol = r"(\r\n)|\n|\r"  # \r\n (Windows), \n (UNIX), or \r (Mac)
243    eol_re = re.compile(eol)
244    eol_convention = os.linesep # Default
245
246    def loadfile(self, filename):
247        try:
248            # open the file in binary mode so that we can handle
249            #   end-of-line convention ourselves.
250            with open(filename, 'rb') as f:
251                chars = f.read()
252        except IOError as msg:
253            tkMessageBox.showerror("I/O Error", str(msg), parent=self.text)
254            return False
255
256        chars = self.decode(chars)
257        # We now convert all end-of-lines to '\n's
258        firsteol = self.eol_re.search(chars)
259        if firsteol:
260            self.eol_convention = firsteol.group(0)
261            if isinstance(self.eol_convention, unicode):
262                # Make sure it is an ASCII string
263                self.eol_convention = self.eol_convention.encode("ascii")
264            chars = self.eol_re.sub(r"\n", chars)
265
266        self.text.delete("1.0", "end")
267        self.set_filename(None)
268        self.text.insert("1.0", chars)
269        self.reset_undo()
270        self.set_filename(filename)
271        self.text.mark_set("insert", "1.0")
272        self.text.yview("insert")
273        self.updaterecentfileslist(filename)
274        return True
275
276    def decode(self, chars):
277        """Create a Unicode string
278
279        If that fails, let Tcl try its best
280        """
281        # Check presence of a UTF-8 signature first
282        if chars.startswith(BOM_UTF8):
283            try:
284                chars = chars[3:].decode("utf-8")
285            except UnicodeError:
286                # has UTF-8 signature, but fails to decode...
287                return chars
288            else:
289                # Indicates that this file originally had a BOM
290                self.fileencoding = BOM_UTF8
291                return chars
292        # Next look for coding specification
293        try:
294            enc = coding_spec(chars)
295        except LookupError as name:
296            tkMessageBox.showerror(
297                title="Error loading the file",
298                message="The encoding '%s' is not known to this Python "\
299                "installation. The file may not display correctly" % name,
300                parent = self.text)
301            enc = None
302        if enc:
303            try:
304                return unicode(chars, enc)
305            except UnicodeError:
306                pass
307        # If it is ASCII, we need not to record anything
308        try:
309            return unicode(chars, 'ascii')
310        except UnicodeError:
311            pass
312        # Finally, try the locale's encoding. This is deprecated;
313        # the user should declare a non-ASCII encoding
314        try:
315            chars = unicode(chars, encoding)
316            self.fileencoding = encoding
317        except UnicodeError:
318            pass
319        return chars
320
321    def maybesave(self):
322        if self.get_saved():
323            return "yes"
324        message = "Do you want to save %s before closing?" % (
325            self.filename or "this untitled document")
326        confirm = tkMessageBox.askyesnocancel(
327                  title="Save On Close",
328                  message=message,
329                  default=tkMessageBox.YES,
330                  parent=self.text)
331        if confirm:
332            reply = "yes"
333            self.save(None)
334            if not self.get_saved():
335                reply = "cancel"
336        elif confirm is None:
337            reply = "cancel"
338        else:
339            reply = "no"
340        self.text.focus_set()
341        return reply
342
343    def save(self, event):
344        if not self.filename:
345            self.save_as(event)
346        else:
347            if self.writefile(self.filename):
348                self.set_saved(True)
349                try:
350                    self.editwin.store_file_breaks()
351                except AttributeError:  # may be a PyShell
352                    pass
353        self.text.focus_set()
354        return "break"
355
356    def save_as(self, event):
357        filename = self.asksavefile()
358        if filename:
359            if self.writefile(filename):
360                self.set_filename(filename)
361                self.set_saved(1)
362                try:
363                    self.editwin.store_file_breaks()
364                except AttributeError:
365                    pass
366        self.text.focus_set()
367        self.updaterecentfileslist(filename)
368        return "break"
369
370    def save_a_copy(self, event):
371        filename = self.asksavefile()
372        if filename:
373            self.writefile(filename)
374        self.text.focus_set()
375        self.updaterecentfileslist(filename)
376        return "break"
377
378    def writefile(self, filename):
379        self.fixlastline()
380        chars = self.encode(self.text.get("1.0", "end-1c"))
381        if self.eol_convention != "\n":
382            chars = chars.replace("\n", self.eol_convention)
383        try:
384            with open(filename, "wb") as f:
385                f.write(chars)
386                f.flush()
387                os.fsync(f.fileno())
388            return True
389        except IOError as msg:
390            tkMessageBox.showerror("I/O Error", str(msg),
391                                   parent=self.text)
392            return False
393
394    def encode(self, chars):
395        if isinstance(chars, str):
396            # This is either plain ASCII, or Tk was returning mixed-encoding
397            # text to us. Don't try to guess further.
398            return chars
399        # See whether there is anything non-ASCII in it.
400        # If not, no need to figure out the encoding.
401        try:
402            return chars.encode('ascii')
403        except UnicodeError:
404            pass
405        # If there is an encoding declared, try this first.
406        try:
407            enc = coding_spec(chars)
408            failed = None
409        except LookupError as msg:
410            failed = msg
411            enc = None
412        if enc:
413            try:
414                return chars.encode(enc)
415            except UnicodeError:
416                failed = "Invalid encoding '%s'" % enc
417        if failed:
418            tkMessageBox.showerror(
419                "I/O Error",
420                "%s. Saving as UTF-8" % failed,
421                parent = self.text)
422        # If there was a UTF-8 signature, use that. This should not fail
423        if self.fileencoding == BOM_UTF8 or failed:
424            return BOM_UTF8 + chars.encode("utf-8")
425        # Try the original file encoding next, if any
426        if self.fileencoding:
427            try:
428                return chars.encode(self.fileencoding)
429            except UnicodeError:
430                tkMessageBox.showerror(
431                    "I/O Error",
432                    "Cannot save this as '%s' anymore. Saving as UTF-8" \
433                    % self.fileencoding,
434                    parent = self.text)
435                return BOM_UTF8 + chars.encode("utf-8")
436        # Nothing was declared, and we had not determined an encoding
437        # on loading. Recommend an encoding line.
438        config_encoding = idleConf.GetOption("main","EditorWindow",
439                                             "encoding")
440        if config_encoding == 'utf-8':
441            # User has requested that we save files as UTF-8
442            return BOM_UTF8 + chars.encode("utf-8")
443        ask_user = True
444        try:
445            chars = chars.encode(encoding)
446            enc = encoding
447            if config_encoding == 'locale':
448                ask_user = False
449        except UnicodeError:
450            chars = BOM_UTF8 + chars.encode("utf-8")
451            enc = "utf-8"
452        if not ask_user:
453            return chars
454        dialog = EncodingMessage(self.editwin.top, enc)
455        dialog.go()
456        if dialog.num == 1:
457            # User asked us to edit the file
458            encline = "# -*- coding: %s -*-\n" % enc
459            firstline = self.text.get("1.0", "2.0")
460            if firstline.startswith("#!"):
461                # Insert encoding after #! line
462                self.text.insert("2.0", encline)
463            else:
464                self.text.insert("1.0", encline)
465            return self.encode(self.text.get("1.0", "end-1c"))
466        return chars
467
468    def fixlastline(self):
469        c = self.text.get("end-2c")
470        if c != '\n':
471            self.text.insert("end-1c", "\n")
472
473    def print_window(self, event):
474        confirm = tkMessageBox.askokcancel(
475                  title="Print",
476                  message="Print to Default Printer",
477                  default=tkMessageBox.OK,
478                  parent=self.text)
479        if not confirm:
480            self.text.focus_set()
481            return "break"
482        tempfilename = None
483        saved = self.get_saved()
484        if saved:
485            filename = self.filename
486        # shell undo is reset after every prompt, looks saved, probably isn't
487        if not saved or filename is None:
488            (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_')
489            filename = tempfilename
490            os.close(tfd)
491            if not self.writefile(tempfilename):
492                os.unlink(tempfilename)
493                return "break"
494        platform = os.name
495        printPlatform = True
496        if platform == 'posix': #posix platform
497            command = idleConf.GetOption('main','General',
498                                         'print-command-posix')
499            command = command + " 2>&1"
500        elif platform == 'nt': #win32 platform
501            command = idleConf.GetOption('main','General','print-command-win')
502        else: #no printing for this platform
503            printPlatform = False
504        if printPlatform:  #we can try to print for this platform
505            command = command % pipes.quote(filename)
506            pipe = os.popen(command, "r")
507            # things can get ugly on NT if there is no printer available.
508            output = pipe.read().strip()
509            status = pipe.close()
510            if status:
511                output = "Printing failed (exit status 0x%x)\n" % \
512                         status + output
513            if output:
514                output = "Printing command: %s\n" % repr(command) + output
515                tkMessageBox.showerror("Print status", output, parent=self.text)
516        else:  #no printing for this platform
517            message = "Printing is not enabled for this platform: %s" % platform
518            tkMessageBox.showinfo("Print status", message, parent=self.text)
519        if tempfilename:
520            os.unlink(tempfilename)
521        return "break"
522
523    opendialog = None
524    savedialog = None
525
526    filetypes = [
527        ("Python files", "*.py *.pyw", "TEXT"),
528        ("Text files", "*.txt", "TEXT"),
529        ("All files", "*"),
530        ]
531
532    defaultextension = '.py' if sys.platform == 'darwin' else ''
533
534    def askopenfile(self):
535        dir, base = self.defaultfilename("open")
536        if not self.opendialog:
537            self.opendialog = tkFileDialog.Open(parent=self.text,
538                                                filetypes=self.filetypes)
539        filename = self.opendialog.show(initialdir=dir, initialfile=base)
540        if isinstance(filename, unicode):
541            filename = filename.encode(filesystemencoding)
542        return filename
543
544    def defaultfilename(self, mode="open"):
545        if self.filename:
546            return os.path.split(self.filename)
547        elif self.dirname:
548            return self.dirname, ""
549        else:
550            try:
551                pwd = os.getcwd()
552            except os.error:
553                pwd = ""
554            return pwd, ""
555
556    def asksavefile(self):
557        dir, base = self.defaultfilename("save")
558        if not self.savedialog:
559            self.savedialog = tkFileDialog.SaveAs(
560                    parent=self.text,
561                    filetypes=self.filetypes,
562                    defaultextension=self.defaultextension)
563        filename = self.savedialog.show(initialdir=dir, initialfile=base)
564        if isinstance(filename, unicode):
565            filename = filename.encode(filesystemencoding)
566        return filename
567
568    def updaterecentfileslist(self,filename):
569        "Update recent file list on all editor windows"
570        self.editwin.update_recent_files_list(filename)
571
572
573def _io_binding(parent):  # htest #
574    from Tkinter import Toplevel, Text
575
576    root = Toplevel(parent)
577    root.title("Test IOBinding")
578    width, height, x, y = list(map(int, re.split('[x+]', parent.geometry())))
579    root.geometry("+%d+%d"%(x, y + 150))
580    class MyEditWin:
581        def __init__(self, text):
582            self.text = text
583            self.flist = None
584            self.text.bind("<Control-o>", self.open)
585            self.text.bind('<Control-p>', self.printer)
586            self.text.bind("<Control-s>", self.save)
587            self.text.bind("<Alt-s>", self.saveas)
588            self.text.bind('<Control-c>', self.savecopy)
589        def get_saved(self): return 0
590        def set_saved(self, flag): pass
591        def reset_undo(self): pass
592        def update_recent_files_list(self, filename): pass
593        def open(self, event):
594            self.text.event_generate("<<open-window-from-file>>")
595        def printer(self, event):
596            self.text.event_generate("<<print-window>>")
597        def save(self, event):
598            self.text.event_generate("<<save-window>>")
599        def saveas(self, event):
600            self.text.event_generate("<<save-window-as-file>>")
601        def savecopy(self, event):
602            self.text.event_generate("<<save-copy-of-window-as-file>>")
603
604    text = Text(root)
605    text.pack()
606    text.focus_set()
607    editwin = MyEditWin(text)
608    IOBinding(editwin)
609
610if __name__ == "__main__":
611    from idlelib.idle_test.htest import run
612    run(_io_binding)
613