# - support partial or total redisplay
# - key bindings (instead of quick-n-dirty bindings on Canvas):
# - up/down arrow keys to move focus around
# - ditto for page up/down, home/end
# - left/right arrows to expand/collapse & move out/in
# - add icons for "file", "module", "class", "method"; better "python" icon
# - callback for selection???
# - multiple-item selection
# - redo geometry without magic numbers
# - keep track of object ids to allow more careful cleaning
# - optimize tree redraw after expand of subnode
from idlelib import ZoomHeight
from idlelib.configHandler import idleConf
# Look for Icons subdirectory in the same directory as this module
_icondir = os.path.join(os.path.dirname(__file__), ICONDIR)
if os.path.isdir(_icondir):
elif not os.path.isdir(ICONDIR):
raise RuntimeError, "can't find icon directory (%r)" % (ICONDIR,)
def listicons(icondir=ICONDIR):
"""Utility to display the available icons."""
list = glob.glob(os.path.join(icondir, "*.gif"))
name = os.path.splitext(os.path.basename(file))[0]
image = PhotoImage(file=file, master=root)
label = Label(root, image=image, bd=1, relief="raised")
label.grid(row=row, column=column)
label = Label(root, text=name)
label.grid(row=row+1, column=column)
def __init__(self, canvas, parent, item):
self.iconimages = {} # cache of PhotoImage instances for icons
for c in self.children[:]:
def geticonimage(self, name):
return self.iconimages[name]
file, ext = os.path.splitext(name)
fullname = os.path.join(ICONDIR, file + ext)
image = PhotoImage(master=self.canvas, file=fullname)
self.iconimages[name] = image
def select(self, event=None):
self.canvas.delete(self.image_id)
def deselect(self, event=None):
self.canvas.delete(self.image_id)
self.parent.deselectall()
for child in self.children:
def flip(self, event=None):
if self.state == 'expanded':
self.item.OnDoubleClick()
def expand(self, event=None):
if not self.item._IsExpandable():
if self.state != 'expanded':
def collapse(self, event=None):
if self.state != 'collapsed':
bottom = self.lastvisiblechild().y + 17
visible_top = self.canvas.canvasy(0)
visible_height = self.canvas.winfo_height()
visible_bottom = self.canvas.canvasy(visible_height)
if visible_top <= top and bottom <= visible_bottom:
x0, y0, x1, y1 = self.canvas._getints(self.canvas['scrollregion'])
if top >= visible_top and height <= visible_height:
fraction = top + height - visible_height
fraction = float(fraction) / y1
self.canvas.yview_moveto(fraction)
def lastvisiblechild(self):
if self.children and self.state == 'expanded':
return self.children[-1].lastvisiblechild()
oldcursor = self.canvas['cursor']
self.canvas['cursor'] = "watch"
self.canvas.delete(ALL) # XXX could be more subtle
x0, y0, x1, y1 = self.canvas.bbox(ALL)
self.canvas.configure(scrollregion=(0, 0, x1, y1))
self.canvas['cursor'] = oldcursor
# XXX This hard-codes too many geometry constants!
if self.state != 'expanded':
sublist = self.item._GetSubList()
# _IsExpandable() was mistaken; that's allowed
child = self.__class__(self.canvas, self, item)
self.children.append(child)
for child in self.children:
self.canvas.create_line(x+9, cy+7, cx, cy+7, fill="gray50")
if child.item._IsExpandable():
if child.state == 'expanded':
callback = child.collapse
image = self.geticonimage(iconname)
id = self.canvas.create_image(x+9, cylast+7, image=image)
# XXX This leaks bindings until canvas is deleted:
self.canvas.tag_bind(id, "<1>", callback)
self.canvas.tag_bind(id, "<Double-1>", lambda x: None)
id = self.canvas.create_line(x+9, y+10, x+9, cylast+7,
##stipple="gray50", # XXX Seems broken in Tk 8.0.x
self.canvas.tag_lower(id) # XXX .lower(id) before Python 1.5.2
imagename = (self.item.GetSelectedIconName() or
self.item.GetIconName() or
imagename = self.item.GetIconName() or "folder"
image = self.geticonimage(imagename)
id = self.canvas.create_image(self.x, self.y, anchor="nw", image=image)
self.canvas.tag_bind(id, "<1>", self.select)
self.canvas.tag_bind(id, "<Double-1>", self.flip)
labeltext = self.item.GetLabelText()
id = self.canvas.create_text(textx, texty, anchor="nw",
self.canvas.tag_bind(id, "<1>", self.select)
self.canvas.tag_bind(id, "<Double-1>", self.flip)
x0, y0, x1, y1 = self.canvas.bbox(id)
textx = max(x1, 200) + 10
text = self.item.GetText() or "<no text>"
# padding carefully selected (on Windows) to match Entry widget:
self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2)
theme = idleConf.CurrentTheme()
self.label.configure(idleConf.GetHighlight(theme, 'hilite'))
self.label.configure(idleConf.GetHighlight(theme, 'normal'))
id = self.canvas.create_window(textx, texty,
anchor="nw", window=self.label)
self.label.bind("<1>", self.select_or_edit)
self.label.bind("<Double-1>", self.flip)
def select_or_edit(self, event=None):
if self.selected and self.item.IsEditable():
def edit(self, event=None):
self.entry = Entry(self.label, bd=0, highlightthickness=1, width=0)
self.entry.insert(0, self.label['text'])
self.entry.selection_range(0, END)
self.entry.bind("<Return>", self.edit_finish)
self.entry.bind("<Escape>", self.edit_cancel)
def edit_finish(self, event=None):
if text and text != self.item.GetText():
text = self.item.GetText()
self.label['text'] = text
def edit_cancel(self, event=None):
"""Abstract class representing tree items.
Methods should typically be overridden, otherwise a default action
"""Constructor. Do whatever you need to do."""
"""Return text string to display."""
"""Return label text string to display in front of text (if any)."""
"""Do not override! Called by TreeNode."""
if self.expandable is None:
self.expandable = self.IsExpandable()
"""Return whether there are subitems."""
"""Do not override! Called by TreeNode."""
if not self.IsExpandable():
sublist = self.GetSubList()
"""Return whether the item's text may be edited."""
"""Change the item's text (if it is editable)."""
"""Return name of icon to be displayed normally."""
def GetSelectedIconName(self):
"""Return name of icon to be displayed when selected."""
"""Return list of items forming sublist."""
"""Called on a double-click on the item."""
class FileTreeItem(TreeItem):
"""Example TreeItem subclass -- browse the file system."""
def __init__(self, path):
return os.path.basename(self.path) or self.path
return os.path.basename(self.path) != ""
newpath = os.path.dirname(self.path)
newpath = os.path.join(newpath, text)
if os.path.dirname(newpath) != os.path.dirname(self.path):
os.rename(self.path, newpath)
if not self.IsExpandable():
return "python" # XXX wish there was a "file" icon
return os.path.isdir(self.path)
names = os.listdir(self.path)
names.sort(key = os.path.normcase)
item = FileTreeItem(os.path.join(self.path, name))
# A canvas widget with scroll bars and some useful bindings
def __init__(self, master, **opts):
if 'yscrollincrement' not in opts:
opts['yscrollincrement'] = 17
self.frame = Frame(master)
self.frame.rowconfigure(0, weight=1)
self.frame.columnconfigure(0, weight=1)
self.canvas = Canvas(self.frame, **opts)
self.canvas.grid(row=0, column=0, sticky="nsew")
self.vbar = Scrollbar(self.frame, name="vbar")
self.vbar.grid(row=0, column=1, sticky="nse")
self.hbar = Scrollbar(self.frame, name="hbar", orient="horizontal")
self.hbar.grid(row=1, column=0, sticky="ews")
self.canvas['yscrollcommand'] = self.vbar.set
self.vbar['command'] = self.canvas.yview
self.canvas['xscrollcommand'] = self.hbar.set
self.hbar['command'] = self.canvas.xview
self.canvas.bind("<Key-Prior>", self.page_up)
self.canvas.bind("<Key-Next>", self.page_down)
self.canvas.bind("<Key-Up>", self.unit_up)
self.canvas.bind("<Key-Down>", self.unit_down)
#if isinstance(master, Toplevel) or isinstance(master, Tk):
self.canvas.bind("<Alt-Key-2>", self.zoom_height)
def page_up(self, event):
self.canvas.yview_scroll(-1, "page")
def page_down(self, event):
self.canvas.yview_scroll(1, "page")
def unit_up(self, event):
self.canvas.yview_scroll(-1, "unit")
def unit_down(self, event):
self.canvas.yview_scroll(1, "unit")
def zoom_height(self, event):
ZoomHeight.zoom_height(self.master)
def _tree_widget(parent):
root.title("Test TreeWidget")
width, height, x, y = list(map(int, re.split('[x+]', parent.geometry())))
root.geometry("+%d+%d"%(x, y + 150))
sc = ScrolledCanvas(root, bg="white", highlightthickness=0, takefocus=1)
sc.frame.pack(expand=1, fill="both", side=LEFT)
item = FileTreeItem(os.getcwd())
node = TreeNode(sc.canvas, None, item)
if __name__ == '__main__':
from idlelib.idle_test.htest import run