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