blob: 3f3f95b6b3df3cf21240357af066ee1a1c67320b [file] [log] [blame]
# 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 <<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 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("<<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_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()