• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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            return True
387        except IOError as msg:
388            tkMessageBox.showerror("I/O Error", str(msg),
389                                   parent=self.text)
390            return False
391
392    def encode(self, chars):
393        if isinstance(chars, str):
394            # This is either plain ASCII, or Tk was returning mixed-encoding
395            # text to us. Don't try to guess further.
396            return chars
397        # See whether there is anything non-ASCII in it.
398        # If not, no need to figure out the encoding.
399        try:
400            return chars.encode('ascii')
401        except UnicodeError:
402            pass
403        # If there is an encoding declared, try this first.
404        try:
405            enc = coding_spec(chars)
406            failed = None
407        except LookupError as msg:
408            failed = msg
409            enc = None
410        if enc:
411            try:
412                return chars.encode(enc)
413            except UnicodeError:
414                failed = "Invalid encoding '%s'" % enc
415        if failed:
416            tkMessageBox.showerror(
417                "I/O Error",
418                "%s. Saving as UTF-8" % failed,
419                parent = self.text)
420        # If there was a UTF-8 signature, use that. This should not fail
421        if self.fileencoding == BOM_UTF8 or failed:
422            return BOM_UTF8 + chars.encode("utf-8")
423        # Try the original file encoding next, if any
424        if self.fileencoding:
425            try:
426                return chars.encode(self.fileencoding)
427            except UnicodeError:
428                tkMessageBox.showerror(
429                    "I/O Error",
430                    "Cannot save this as '%s' anymore. Saving as UTF-8" \
431                    % self.fileencoding,
432                    parent = self.text)
433                return BOM_UTF8 + chars.encode("utf-8")
434        # Nothing was declared, and we had not determined an encoding
435        # on loading. Recommend an encoding line.
436        config_encoding = idleConf.GetOption("main","EditorWindow",
437                                             "encoding")
438        if config_encoding == 'utf-8':
439            # User has requested that we save files as UTF-8
440            return BOM_UTF8 + chars.encode("utf-8")
441        ask_user = True
442        try:
443            chars = chars.encode(encoding)
444            enc = encoding
445            if config_encoding == 'locale':
446                ask_user = False
447        except UnicodeError:
448            chars = BOM_UTF8 + chars.encode("utf-8")
449            enc = "utf-8"
450        if not ask_user:
451            return chars
452        dialog = EncodingMessage(self.editwin.top, enc)
453        dialog.go()
454        if dialog.num == 1:
455            # User asked us to edit the file
456            encline = "# -*- coding: %s -*-\n" % enc
457            firstline = self.text.get("1.0", "2.0")
458            if firstline.startswith("#!"):
459                # Insert encoding after #! line
460                self.text.insert("2.0", encline)
461            else:
462                self.text.insert("1.0", encline)
463            return self.encode(self.text.get("1.0", "end-1c"))
464        return chars
465
466    def fixlastline(self):
467        c = self.text.get("end-2c")
468        if c != '\n':
469            self.text.insert("end-1c", "\n")
470
471    def print_window(self, event):
472        confirm = tkMessageBox.askokcancel(
473                  title="Print",
474                  message="Print to Default Printer",
475                  default=tkMessageBox.OK,
476                  parent=self.text)
477        if not confirm:
478            self.text.focus_set()
479            return "break"
480        tempfilename = None
481        saved = self.get_saved()
482        if saved:
483            filename = self.filename
484        # shell undo is reset after every prompt, looks saved, probably isn't
485        if not saved or filename is None:
486            (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_')
487            filename = tempfilename
488            os.close(tfd)
489            if not self.writefile(tempfilename):
490                os.unlink(tempfilename)
491                return "break"
492        platform = os.name
493        printPlatform = True
494        if platform == 'posix': #posix platform
495            command = idleConf.GetOption('main','General',
496                                         'print-command-posix')
497            command = command + " 2>&1"
498        elif platform == 'nt': #win32 platform
499            command = idleConf.GetOption('main','General','print-command-win')
500        else: #no printing for this platform
501            printPlatform = False
502        if printPlatform:  #we can try to print for this platform
503            command = command % pipes.quote(filename)
504            pipe = os.popen(command, "r")
505            # things can get ugly on NT if there is no printer available.
506            output = pipe.read().strip()
507            status = pipe.close()
508            if status:
509                output = "Printing failed (exit status 0x%x)\n" % \
510                         status + output
511            if output:
512                output = "Printing command: %s\n" % repr(command) + output
513                tkMessageBox.showerror("Print status", output, parent=self.text)
514        else:  #no printing for this platform
515            message = "Printing is not enabled for this platform: %s" % platform
516            tkMessageBox.showinfo("Print status", message, parent=self.text)
517        if tempfilename:
518            os.unlink(tempfilename)
519        return "break"
520
521    opendialog = None
522    savedialog = None
523
524    filetypes = [
525        ("Python files", "*.py *.pyw", "TEXT"),
526        ("Text files", "*.txt", "TEXT"),
527        ("All files", "*"),
528        ]
529
530    defaultextension = '.py' if sys.platform == 'darwin' else ''
531
532    def askopenfile(self):
533        dir, base = self.defaultfilename("open")
534        if not self.opendialog:
535            self.opendialog = tkFileDialog.Open(parent=self.text,
536                                                filetypes=self.filetypes)
537        filename = self.opendialog.show(initialdir=dir, initialfile=base)
538        if isinstance(filename, unicode):
539            filename = filename.encode(filesystemencoding)
540        return filename
541
542    def defaultfilename(self, mode="open"):
543        if self.filename:
544            return os.path.split(self.filename)
545        elif self.dirname:
546            return self.dirname, ""
547        else:
548            try:
549                pwd = os.getcwd()
550            except os.error:
551                pwd = ""
552            return pwd, ""
553
554    def asksavefile(self):
555        dir, base = self.defaultfilename("save")
556        if not self.savedialog:
557            self.savedialog = tkFileDialog.SaveAs(
558                    parent=self.text,
559                    filetypes=self.filetypes,
560                    defaultextension=self.defaultextension)
561        filename = self.savedialog.show(initialdir=dir, initialfile=base)
562        if isinstance(filename, unicode):
563            filename = filename.encode(filesystemencoding)
564        return filename
565
566    def updaterecentfileslist(self,filename):
567        "Update recent file list on all editor windows"
568        self.editwin.update_recent_files_list(filename)
569
570
571def _io_binding(parent):  # htest #
572    from Tkinter import Toplevel, Text
573
574    root = Toplevel(parent)
575    root.title("Test IOBinding")
576    width, height, x, y = list(map(int, re.split('[x+]', parent.geometry())))
577    root.geometry("+%d+%d"%(x, y + 150))
578    class MyEditWin:
579        def __init__(self, text):
580            self.text = text
581            self.flist = None
582            self.text.bind("<Control-o>", self.open)
583            self.text.bind('<Control-p>', self.printer)
584            self.text.bind("<Control-s>", self.save)
585            self.text.bind("<Alt-s>", self.saveas)
586            self.text.bind('<Control-c>', self.savecopy)
587        def get_saved(self): return 0
588        def set_saved(self, flag): pass
589        def reset_undo(self): pass
590        def update_recent_files_list(self, filename): pass
591        def open(self, event):
592            self.text.event_generate("<<open-window-from-file>>")
593        def printer(self, event):
594            self.text.event_generate("<<print-window>>")
595        def save(self, event):
596            self.text.event_generate("<<save-window>>")
597        def saveas(self, event):
598            self.text.event_generate("<<save-window-as-file>>")
599        def savecopy(self, event):
600            self.text.event_generate("<<save-copy-of-window-as-file>>")
601
602    text = Text(root)
603    text.pack()
604    text.focus_set()
605    editwin = MyEditWin(text)
606    IOBinding(editwin)
607
608if __name__ == "__main__":
609    from idlelib.idle_test.htest import run
610    run(_io_binding)
611