| import sys |
| import os |
| 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 <<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 |
| """ % idlever.IDLE_VERSION |
| |
| def _find_module(fullname, path=None): |
| """Version of imp.find_module() that handles hierarchical module names""" |
| |
| file = None |
| for tgt in fullname.split('.'): |
| if file is not None: |
| file.close() # close intermediate files |
| (file, filename, descr) = imp.find_module(tgt, path) |
| if descr[2] == imp.PY_SOURCE: |
| break # find but not load the source file |
| module = imp.load_module(tgt, file, filename, descr) |
| path = module.__path__ |
| return file, filename, descr |
| |
| 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 = {} |
| runnable = False # Shell window cannot Import Module or Run Script |
| |
| 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("<<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 = self.text.index(INSERT).split('.') |
| 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"), |
| ("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 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, "pythlp.chm") |
| fn = os.path.normpath(fn) |
| if os.path.isfile(fn): |
| help_url = fn |
| else: |
| 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): |
| os.startfile(self.help_url) |
| else: |
| 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 = name.strip() |
| 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 = name.strip() |
| if not name: |
| return |
| # XXX Ought to insert current file's directory in front of path |
| try: |
| (f, file, (suffix, mode, type)) = _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 True |
| base, ext = os.path.splitext(os.path.basename(filename)) |
| if os.path.normcase(ext) in (".py", ".pyw"): |
| return True |
| try: |
| f = open(filename) |
| line = f.readline() |
| f.close() |
| except IOError: |
| return False |
| return line.startswith('#!') and 'python' in line |
| |
| 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 = vevent.replace("-", "_") |
| 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 = s.find('_') |
| 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: m.group().upper(), 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("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() |