diff options
Diffstat (limited to 'Lib/idlelib/EditorWindow.py')
-rw-r--r-- | Lib/idlelib/EditorWindow.py | 749 |
1 files changed, 749 insertions, 0 deletions
diff --git a/Lib/idlelib/EditorWindow.py b/Lib/idlelib/EditorWindow.py new file mode 100644 index 0000000..9f76ef7 --- /dev/null +++ b/Lib/idlelib/EditorWindow.py @@ -0,0 +1,749 @@ +# changes by dscherer@cmu.edu +# - created format and run menus +# - added silly advice dialog (apologies to Douglas Adams) +# - made Python Documentation work on Windows (requires win32api to +# do a ShellExecute(); other ways of starting a web browser are awkward) + +import sys +import os +import string +import re +import imp +from Tkinter import * +import tkSimpleDialog +import tkMessageBox +import idlever +import WindowList +from IdleConf import idleconf + +# The default tab setting for a Text widget, in average-width characters. +TK_TABWIDTH_DEFAULT = 8 + +# File menu + +#$ event <<open-module>> +#$ win <Alt-m> +#$ unix <Control-x><Control-m> + +#$ event <<open-class-browser>> +#$ win <Alt-c> +#$ unix <Control-x><Control-b> + +#$ event <<open-path-browser>> + +#$ event <<close-window>> +#$ unix <Control-x><Control-0> +#$ unix <Control-x><Key-0> +#$ win <Alt-F4> + +# Edit menu + +#$ event <<Copy>> +#$ win <Control-c> +#$ unix <Alt-w> + +#$ event <<Cut>> +#$ win <Control-x> +#$ unix <Control-w> + +#$ event <<Paste>> +#$ win <Control-v> +#$ unix <Control-y> + +#$ event <<select-all>> +#$ win <Alt-a> +#$ unix <Alt-a> + +# Help menu + +#$ event <<help>> +#$ win <F1> +#$ unix <F1> + +#$ event <<about-idle>> + +# Events without menu entries + +#$ event <<remove-selection>> +#$ win <Escape> + +#$ event <<center-insert>> +#$ win <Control-l> +#$ unix <Control-l> + +#$ event <<do-nothing>> +#$ unix <Control-x> + + +about_title = "About IDLE" +about_text = """\ +IDLE %s + +An Integrated DeveLopment Environment for Python + +by Guido van Rossum + +This version of IDLE has been modified by David Scherer + (dscherer@cmu.edu). See readme.txt for details. +""" % idlever.IDLE_VERSION + +class EditorWindow: + + from Percolator import Percolator + from ColorDelegator import ColorDelegator + from UndoDelegator import UndoDelegator + from IOBinding import IOBinding + import Bindings + from Tkinter import Toplevel + from MultiStatusBar import MultiStatusBar + + about_title = about_title + about_text = about_text + + vars = {} + + def __init__(self, flist=None, filename=None, key=None, root=None): + edconf = idleconf.getsection('EditorWindow') + coconf = idleconf.getsection('Colors') + self.flist = flist + root = root or flist.root + self.root = root + if flist: + self.vars = flist.vars + self.menubar = Menu(root) + self.top = top = self.Toplevel(root, menu=self.menubar) + self.vbar = vbar = Scrollbar(top, name='vbar') + self.text_frame = text_frame = Frame(top) + self.text = text = Text(text_frame, name='text', padx=5, + foreground=coconf.getdef('normal-foreground'), + background=coconf.getdef('normal-background'), + highlightcolor=coconf.getdef('hilite-foreground'), + highlightbackground=coconf.getdef('hilite-background'), + insertbackground=coconf.getdef('cursor-background'), + width=edconf.getint('width'), + height=edconf.getint('height'), + wrap="none") + + self.createmenubar() + self.apply_bindings() + + self.top.protocol("WM_DELETE_WINDOW", self.close) + self.top.bind("<<close-window>>", self.close_event) + text.bind("<<center-insert>>", self.center_insert_event) + text.bind("<<help>>", self.help_dialog) + text.bind("<<good-advice>>", self.good_advice) + text.bind("<<python-docs>>", self.python_docs) + text.bind("<<about-idle>>", self.about_dialog) + text.bind("<<open-module>>", self.open_module) + text.bind("<<do-nothing>>", lambda event: "break") + text.bind("<<select-all>>", self.select_all) + text.bind("<<remove-selection>>", self.remove_selection) + text.bind("<3>", self.right_menu_event) + if flist: + flist.inversedict[self] = key + if key: + flist.dict[key] = self + text.bind("<<open-new-window>>", self.flist.new_callback) + text.bind("<<close-all-windows>>", self.flist.close_all_callback) + text.bind("<<open-class-browser>>", self.open_class_browser) + text.bind("<<open-path-browser>>", self.open_path_browser) + + vbar['command'] = text.yview + vbar.pack(side=RIGHT, fill=Y) + + text['yscrollcommand'] = vbar.set + text['font'] = edconf.get('font-name'), edconf.get('font-size') + text_frame.pack(side=LEFT, fill=BOTH, expand=1) + text.pack(side=TOP, fill=BOTH, expand=1) + text.focus_set() + + self.per = per = self.Percolator(text) + if self.ispythonsource(filename): + self.color = color = self.ColorDelegator(); per.insertfilter(color) + ##print "Initial colorizer" + else: + ##print "No initial colorizer" + self.color = None + self.undo = undo = self.UndoDelegator(); per.insertfilter(undo) + self.io = io = self.IOBinding(self) + + text.undo_block_start = undo.undo_block_start + text.undo_block_stop = undo.undo_block_stop + undo.set_saved_change_hook(self.saved_change_hook) + io.set_filename_change_hook(self.filename_change_hook) + + if filename: + if os.path.exists(filename): + io.loadfile(filename) + else: + io.set_filename(filename) + + self.saved_change_hook() + + self.load_extensions() + + menu = self.menudict.get('windows') + if menu: + end = menu.index("end") + if end is None: + end = -1 + if end >= 0: + menu.add_separator() + end = end + 1 + self.wmenu_end = end + WindowList.register_callback(self.postwindowsmenu) + + # Some abstractions so IDLE extensions are cross-IDE + self.askyesno = tkMessageBox.askyesno + self.askinteger = tkSimpleDialog.askinteger + self.showerror = tkMessageBox.showerror + + if self.extensions.has_key('AutoIndent'): + self.extensions['AutoIndent'].set_indentation_params( + self.ispythonsource(filename)) + self.set_status_bar() + + def set_status_bar(self): + self.status_bar = self.MultiStatusBar(self.text_frame) + self.status_bar.set_label('column', 'Col: ?', side=RIGHT) + self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) + self.status_bar.pack(side=BOTTOM, fill=X) + self.text.bind('<KeyRelease>', self.set_line_and_column) + self.text.bind('<ButtonRelease>', self.set_line_and_column) + self.text.after_idle(self.set_line_and_column) + + def set_line_and_column(self, event=None): + line, column = string.split(self.text.index(INSERT), '.') + self.status_bar.set_label('column', 'Col: %s' % column) + self.status_bar.set_label('line', 'Ln: %s' % line) + + def wakeup(self): + if self.top.wm_state() == "iconic": + self.top.wm_deiconify() + else: + self.top.tkraise() + self.text.focus_set() + + menu_specs = [ + ("file", "_File"), + ("edit", "_Edit"), + ("format", "F_ormat"), + ("run", "_Run"), + ("windows", "_Windows"), + ("help", "_Help"), + ] + + def createmenubar(self): + mbar = self.menubar + self.menudict = menudict = {} + for name, label in self.menu_specs: + underline, label = prepstr(label) + menudict[name] = menu = Menu(mbar, name=name) + mbar.add_cascade(label=label, menu=menu, underline=underline) + self.fill_menus() + + def postwindowsmenu(self): + # Only called when Windows menu exists + # XXX Actually, this Just-In-Time updating interferes badly + # XXX with the tear-off feature. It would be better to update + # XXX all Windows menus whenever the list of windows changes. + menu = self.menudict['windows'] + end = menu.index("end") + if end is None: + end = -1 + if end > self.wmenu_end: + menu.delete(self.wmenu_end+1, end) + WindowList.add_windows_to_menu(menu) + + rmenu = None + + def right_menu_event(self, event): + self.text.tag_remove("sel", "1.0", "end") + self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) + if not self.rmenu: + self.make_rmenu() + rmenu = self.rmenu + self.event = event + iswin = sys.platform[:3] == 'win' + if iswin: + self.text.config(cursor="arrow") + rmenu.tk_popup(event.x_root, event.y_root) + if iswin: + self.text.config(cursor="ibeam") + + rmenu_specs = [ + # ("Label", "<<virtual-event>>"), ... + ("Close", "<<close-window>>"), # Example + ] + + def make_rmenu(self): + rmenu = Menu(self.text, tearoff=0) + for label, eventname in self.rmenu_specs: + def command(text=self.text, eventname=eventname): + text.event_generate(eventname) + rmenu.add_command(label=label, command=command) + self.rmenu = rmenu + + def about_dialog(self, event=None): + tkMessageBox.showinfo(self.about_title, self.about_text, + master=self.text) + + helpfile = "help.txt" + + def good_advice(self, event=None): + tkMessageBox.showinfo('Advice', "Don't Panic!", master=self.text) + + def help_dialog(self, event=None): + try: + helpfile = os.path.join(os.path.dirname(__file__), self.helpfile) + except NameError: + helpfile = self.helpfile + if self.flist: + self.flist.open(helpfile) + else: + self.io.loadfile(helpfile) + + help_viewer = "netscape -remote 'openurl(%(url)s)' 2>/dev/null || " \ + "netscape %(url)s &" + help_url = "http://www.python.org/doc/current/" + + def python_docs(self, event=None): + if sys.platform=='win32': + try: + import win32api + import ExecBinding + doc = os.path.join( os.path.dirname( ExecBinding.pyth_exe ), "doc", "index.html" ) + win32api.ShellExecute(0, None, doc, None, sys.path[0], 1) + except: + pass + else: + cmd = self.help_viewer % {"url": self.help_url} + os.system(cmd) + + def select_all(self, event=None): + self.text.tag_add("sel", "1.0", "end-1c") + self.text.mark_set("insert", "1.0") + self.text.see("insert") + return "break" + + def remove_selection(self, event=None): + self.text.tag_remove("sel", "1.0", "end") + self.text.see("insert") + + def open_module(self, event=None): + # XXX Shouldn't this be in IOBinding or in FileList? + try: + name = self.text.get("sel.first", "sel.last") + except TclError: + name = "" + else: + name = string.strip(name) + if not name: + name = tkSimpleDialog.askstring("Module", + "Enter the name of a Python module\n" + "to search on sys.path and open:", + parent=self.text) + if name: + name = string.strip(name) + if not name: + return + # XXX Ought to support package syntax + # XXX Ought to insert current file's directory in front of path + try: + (f, file, (suffix, mode, type)) = imp.find_module(name) + except (NameError, ImportError), msg: + tkMessageBox.showerror("Import error", str(msg), parent=self.text) + return + if type != imp.PY_SOURCE: + tkMessageBox.showerror("Unsupported type", + "%s is not a source module" % name, parent=self.text) + return + if f: + f.close() + if self.flist: + self.flist.open(file) + else: + self.io.loadfile(file) + + def open_class_browser(self, event=None): + filename = self.io.filename + if not filename: + tkMessageBox.showerror( + "No filename", + "This buffer has no associated filename", + master=self.text) + self.text.focus_set() + return None + head, tail = os.path.split(filename) + base, ext = os.path.splitext(tail) + import ClassBrowser + ClassBrowser.ClassBrowser(self.flist, base, [head]) + + def open_path_browser(self, event=None): + import PathBrowser + PathBrowser.PathBrowser(self.flist) + + def gotoline(self, lineno): + if lineno is not None and lineno > 0: + self.text.mark_set("insert", "%d.0" % lineno) + self.text.tag_remove("sel", "1.0", "end") + self.text.tag_add("sel", "insert", "insert +1l") + self.center() + + def ispythonsource(self, filename): + if not filename: + return 1 + base, ext = os.path.splitext(os.path.basename(filename)) + if os.path.normcase(ext) in (".py", ".pyw"): + return 1 + try: + f = open(filename) + line = f.readline() + f.close() + except IOError: + return 0 + return line[:2] == '#!' and string.find(line, 'python') >= 0 + + def close_hook(self): + if self.flist: + self.flist.close_edit(self) + + def set_close_hook(self, close_hook): + self.close_hook = close_hook + + def filename_change_hook(self): + if self.flist: + self.flist.filename_changed_edit(self) + self.saved_change_hook() + if self.ispythonsource(self.io.filename): + self.addcolorizer() + else: + self.rmcolorizer() + + def addcolorizer(self): + if self.color: + return + ##print "Add colorizer" + self.per.removefilter(self.undo) + self.color = self.ColorDelegator() + self.per.insertfilter(self.color) + self.per.insertfilter(self.undo) + + def rmcolorizer(self): + if not self.color: + return + ##print "Remove colorizer" + self.per.removefilter(self.undo) + self.per.removefilter(self.color) + self.color = None + self.per.insertfilter(self.undo) + + def saved_change_hook(self): + short = self.short_title() + long = self.long_title() + if short and long: + title = short + " - " + long + elif short: + title = short + elif long: + title = long + else: + title = "Untitled" + icon = short or long or title + if not self.get_saved(): + title = "*%s*" % title + icon = "*%s" % icon + self.top.wm_title(title) + self.top.wm_iconname(icon) + + def get_saved(self): + return self.undo.get_saved() + + def set_saved(self, flag): + self.undo.set_saved(flag) + + def reset_undo(self): + self.undo.reset_undo() + + def short_title(self): + filename = self.io.filename + if filename: + filename = os.path.basename(filename) + return filename + + def long_title(self): + return self.io.filename or "" + + def center_insert_event(self, event): + self.center() + + def center(self, mark="insert"): + text = self.text + top, bot = self.getwindowlines() + lineno = self.getlineno(mark) + height = bot - top + newtop = max(1, lineno - height/2) + text.yview(float(newtop)) + + def getwindowlines(self): + text = self.text + top = self.getlineno("@0,0") + bot = self.getlineno("@0,65535") + if top == bot and text.winfo_height() == 1: + # Geometry manager hasn't run yet + height = int(text['height']) + bot = top + height - 1 + return top, bot + + def getlineno(self, mark="insert"): + text = self.text + return int(float(text.index(mark))) + + def close_event(self, event): + self.close() + + def maybesave(self): + if self.io: + return self.io.maybesave() + + def close(self): + self.top.wm_deiconify() + self.top.tkraise() + reply = self.maybesave() + if reply != "cancel": + self._close() + return reply + + def _close(self): + WindowList.unregister_callback(self.postwindowsmenu) + if self.close_hook: + self.close_hook() + self.flist = None + colorizing = 0 + self.unload_extensions() + self.io.close(); self.io = None + self.undo = None # XXX + if self.color: + colorizing = self.color.colorizing + doh = colorizing and self.top + self.color.close(doh) # Cancel colorization + self.text = None + self.vars = None + self.per.close(); self.per = None + if not colorizing: + self.top.destroy() + + def load_extensions(self): + self.extensions = {} + self.load_standard_extensions() + + def unload_extensions(self): + for ins in self.extensions.values(): + if hasattr(ins, "close"): + ins.close() + self.extensions = {} + + def load_standard_extensions(self): + for name in self.get_standard_extension_names(): + try: + self.load_extension(name) + except: + print "Failed to load extension", `name` + import traceback + traceback.print_exc() + + def get_standard_extension_names(self): + return idleconf.getextensions() + + def load_extension(self, name): + mod = __import__(name, globals(), locals(), []) + cls = getattr(mod, name) + ins = cls(self) + self.extensions[name] = ins + kdnames = ["keydefs"] + if sys.platform == 'win32': + kdnames.append("windows_keydefs") + elif sys.platform == 'mac': + kdnames.append("mac_keydefs") + else: + kdnames.append("unix_keydefs") + keydefs = {} + for kdname in kdnames: + if hasattr(ins, kdname): + keydefs.update(getattr(ins, kdname)) + if keydefs: + self.apply_bindings(keydefs) + for vevent in keydefs.keys(): + methodname = string.replace(vevent, "-", "_") + while methodname[:1] == '<': + methodname = methodname[1:] + while methodname[-1:] == '>': + methodname = methodname[:-1] + methodname = methodname + "_event" + if hasattr(ins, methodname): + self.text.bind(vevent, getattr(ins, methodname)) + if hasattr(ins, "menudefs"): + self.fill_menus(ins.menudefs, keydefs) + return ins + + def apply_bindings(self, keydefs=None): + if keydefs is None: + keydefs = self.Bindings.default_keydefs + text = self.text + text.keydefs = keydefs + for event, keylist in keydefs.items(): + if keylist: + apply(text.event_add, (event,) + tuple(keylist)) + + def fill_menus(self, defs=None, keydefs=None): + # Fill the menus. Menus that are absent or None in + # self.menudict are ignored. + if defs is None: + defs = self.Bindings.menudefs + if keydefs is None: + keydefs = self.Bindings.default_keydefs + menudict = self.menudict + text = self.text + for mname, itemlist in defs: + menu = menudict.get(mname) + if not menu: + continue + for item in itemlist: + if not item: + menu.add_separator() + else: + label, event = item + checkbutton = (label[:1] == '!') + if checkbutton: + label = label[1:] + underline, label = prepstr(label) + accelerator = get_accelerator(keydefs, event) + def command(text=text, event=event): + text.event_generate(event) + if checkbutton: + var = self.getrawvar(event, BooleanVar) + menu.add_checkbutton(label=label, underline=underline, + command=command, accelerator=accelerator, + variable=var) + else: + menu.add_command(label=label, underline=underline, + command=command, accelerator=accelerator) + + def getvar(self, name): + var = self.getrawvar(name) + if var: + return var.get() + + def setvar(self, name, value, vartype=None): + var = self.getrawvar(name, vartype) + if var: + var.set(value) + + def getrawvar(self, name, vartype=None): + var = self.vars.get(name) + if not var and vartype: + self.vars[name] = var = vartype(self.text) + return var + + # Tk implementations of "virtual text methods" -- each platform + # reusing IDLE's support code needs to define these for its GUI's + # flavor of widget. + + # Is character at text_index in a Python string? Return 0 for + # "guaranteed no", true for anything else. This info is expensive + # to compute ab initio, but is probably already known by the + # platform's colorizer. + + def is_char_in_string(self, text_index): + if self.color: + # Return true iff colorizer hasn't (re)gotten this far + # yet, or the character is tagged as being in a string + return self.text.tag_prevrange("TODO", text_index) or \ + "STRING" in self.text.tag_names(text_index) + else: + # The colorizer is missing: assume the worst + return 1 + + # If a selection is defined in the text widget, return (start, + # end) as Tkinter text indices, otherwise return (None, None) + def get_selection_indices(self): + try: + first = self.text.index("sel.first") + last = self.text.index("sel.last") + return first, last + except TclError: + return None, None + + # Return the text widget's current view of what a tab stop means + # (equivalent width in spaces). + + def get_tabwidth(self): + current = self.text['tabs'] or TK_TABWIDTH_DEFAULT + return int(current) + + # Set the text widget's current view of what a tab stop means. + + def set_tabwidth(self, newtabwidth): + text = self.text + if self.get_tabwidth() != newtabwidth: + pixels = text.tk.call("font", "measure", text["font"], + "-displayof", text.master, + "n" * newtabwith) + text.configure(tabs=pixels) + +def prepstr(s): + # Helper to extract the underscore from a string, e.g. + # prepstr("Co_py") returns (2, "Copy"). + i = string.find(s, '_') + if i >= 0: + s = s[:i] + s[i+1:] + return i, s + + +keynames = { + 'bracketleft': '[', + 'bracketright': ']', + 'slash': '/', +} + +def get_accelerator(keydefs, event): + keylist = keydefs.get(event) + if not keylist: + return "" + s = keylist[0] + s = re.sub(r"-[a-z]\b", lambda m: string.upper(m.group()), s) + s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s) + s = re.sub("Key-", "", s) + s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu + s = re.sub("Control-", "Ctrl-", s) + s = re.sub("-", "+", s) + s = re.sub("><", " ", s) + s = re.sub("<", "", s) + s = re.sub(">", "", s) + return s + + +def fixwordbreaks(root): + # Make sure that Tk's double-click and next/previous word + # operations use our definition of a word (i.e. an identifier) + tk = root.tk + tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded + tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]') + tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]') + + +def test(): + root = Tk() + fixwordbreaks(root) + root.withdraw() + if sys.argv[1:]: + filename = sys.argv[1] + else: + filename = None + edit = EditorWindow(root=root, filename=filename) + edit.set_close_hook(root.quit) + root.mainloop() + root.destroy() + +if __name__ == '__main__': + test() |