# 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 webbrowser 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 <> #$ win #$ unix #$ event <> #$ win #$ unix #$ event <> #$ event <> #$ unix #$ unix #$ win # Edit menu #$ event <> #$ win #$ unix #$ event <> #$ win #$ unix #$ event <> #$ win #$ unix #$ event <> #$ win #$ unix # Help menu #$ event <> #$ win #$ unix #$ event <> # Events without menu entries #$ event <> #$ win #$ event <> #$ win #$ unix #$ event <> #$ unix about_title = "About IDLEfork" about_text = """\ IDLEfork %s IDLE is an Integrated DeveLopment Environment for Python \ by Guido van Rossum. IDLEfork is an official experimental development version of IDLE. Succesful new features in IDLEfork will be mereged back in to stable IDLE. This version of IDLEfork is based on the work in stable IDLE version %s, \ IDLEfork 0.7.1 released by David Scherer, and the VPython idle fork. See README.txt for more details on IDLEfork. WARNING: IDLEfork is at this stage alpha quality software, expect things \ to be broken. """ % (idlever.IDLEFORK_VERSION, 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("<>", self.close_event) text.bind("<>", self.center_insert_event) text.bind("<>", self.help_dialog) text.bind("<>", self.good_advice) text.bind("<>", self.python_docs) text.bind("<>", self.about_dialog) text.bind("<>", self.open_module) text.bind("<>", lambda event: "break") text.bind("<>", self.select_all) text.bind("<>", self.remove_selection) text.bind("<3>", self.right_menu_event) if flist: flist.inversedict[self] = key if key: flist.dict[key] = self text.bind("<>", self.flist.new_callback) text.bind("<>", self.flist.close_all_callback) text.bind("<>", self.open_class_browser) text.bind("<>", 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('', self.set_line_and_column) self.text.bind('', 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", "<>"), ... ("Close", "<>"), # 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_url = "http://www.python.org/doc/current/" if sys.platform[:3] == "win": fn = os.path.dirname(__file__) fn = os.path.join(fn, os.pardir, os.pardir, "Doc", "index.html") fn = os.path.normpath(fn) if os.path.isfile(fn): help_url = fn del fn def python_docs(self, event=None): webbrowser.open(self.help_url) 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" * newtabwidth) 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()