#!/usr/bin/env python
#
# $Id: TreeNavigator.py,v 1.3 2001/11/03 11:05:22 doughellmann Exp $
#
# Copyright 2001 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""A Pmw style widget for navigating the contents of a tree data structure.
"""
__rcs_info__ = {
#
# Creation Information
#
'module_name' : '$RCSfile: TreeNavigator.py,v $',
'rcs_id' : '$Id: TreeNavigator.py,v 1.3 2001/11/03 11:05:22 doughellmann Exp $',
'creator' : 'Doug Hellmann <doug@hellfly.net>',
'project' : 'PmwContribD',
'created' : 'Sat, 05-May-2001 13:39:44 EDT',
#
# Current Information
#
'author' : '$Author: doughellmann $',
'version' : '$Revision: 1.3 $',
'date' : '$Date: 2001/11/03 11:05:22 $',
}
#
# Import system modules
#
import Tkinter, Canvas
import Pmw
import sys, os, string, math
#
# Import Local modules
#
import NavigableTree
#
# Module
#
class TreeNavigator(Pmw.LabeledWidget):
"""A Pmw style widget for navigating the contents of a tree data structure.
The ideas for the implementation of this widget are based on the
Hierarchical browser discussed in _Effective Tcl/Tk Programming_
by Mark Harrison and Michael McLennan.
"""
root_mark_name = 'root:start'
def __init__(self, parent=None, **kw):
"""Create a TreeNavigator widget.
Create a new instance with our parent widget and the treedata
to be displayed.
Options
'activebackground' -- Background color for active nodes.
'autoexpand' -- Boolean indicating whether or not to
atuomatically expand nodes in the tree.
'command' -- Callback to be executed when nodes are
selected.
'doubleclickcommand' -- Callback to be executed when nodes
are double clicked.
'entercommand' -- Callback to be executed when pointer
enters node.
'iconheight' -- Height to use for all icons.
'iconwidth' -- Width to use for all icons.
'indent' -- Amount of space to indent each child node level.
'ipadx' -- Internal padding (X).
'ipady' -- Internal padding (Y).
'leavecommand' -- Callback to be executed when mouse pointer
leaves the node.
'selectedbackground' -- Background color to use to show a
node when it was selected by the user.
'treedata' -- The NavigableTree instance to be displayed.
"""
optiondefs = (
('activebackground', 'lightslateblue', Pmw.INITOPT),
('autoexpand', None, Pmw.INITOPT),
('command', None, Pmw.INITOPT),
('doubleclickcommand', None, Pmw.INITOPT),
('entercommand', None, Pmw.INITOPT),
('iconheight', 20, Pmw.INITOPT),
('iconwidth', 30, Pmw.INITOPT),
('indent', 20, Pmw.INITOPT),
('ipadx', 0, Pmw.INITOPT),
('ipady', 0, Pmw.INITOPT),
('leavecommand', None, Pmw.INITOPT),
('selectedbackground', 'CadetBlue', Pmw.INITOPT),
('treedata', None, self._reset_data),
)
self.defineoptions(kw, optiondefs)
# Initialize the base class
Pmw.LabeledWidget.__init__(self, parent=parent)
self.selected = None
self._reset_data()
# Create our interface
self._createInterior()
# Check for initialization options
# for this class
self.initialiseoptions(TreeNavigator)
return
def is_selected(self, node):
"Determine if the given node is currently selected."
if self.selected == node:
return 1
return 0
def _reset_data(self):
"Update the tree data and redraw."
# Store parameters
self.treedata = self['treedata']
self.treelookup = {}
self.iconlookup = {}
self.canvaslookup = {}
self.select_node(None)
try:
#print 'deleting current tree data'
self.tree.delete( '0.0', 'end' )
except AttributeError:
# 'tree' component hasn't been initialized.
pass
else:
# insert base data
#print 'inserting new data (%s) into tree' % self.treedata
self.insert_node(self.treedata, insert_expanded=self['autoexpand'])
return
def getcurselection(self):
'Returns the current selection of the TreeNavigator.'
return self.selected
def deselect_node(self, node):
"Remove selection from a node."
self.highlight_node(node, color=self.tree.component('text')['background'])
return
def setcurselection(self, new_selection):
"Set the current selection."
if self.selected:
self.deselect_node(self.selected)
self.selected = new_selection
return
def reset_selection(self, parent):
"Reset the selection."
sel = self.getcurselection()
if not sel:
return None
if len(parent.tag_name) < len(sel.tag_name):
# the current selection could possibly be
# a child of the parent node that is being
# collapsed
if sel.tag_name[:len(parent.tag_name)] == parent.tag_name:
# it is
self.select_node(parent)
else:
# it is not
pass
else:
# if the parent name is longer
# than the current selection, it can't
# be the parent of the current selection
pass
return
def build_mark_name(self, levels=()):
"""Create a name for the mark representing the position of the given levels.
Given a tuple of level indeces, create a name of the form
'root-1-1-...' to be used as the unique marker name for the
node.
"""
mn = 'root'
for level in levels:
mn = mn + '-%d' % level
return mn
def highlight_node(self, node, color='white'):
"""Draw the node in our highlight color.
"""
#print 'highlighting %s' % node_name
self.tree.component('text').tag_configure(node.tag_name, background=color)
return
def insert_node(self, node, levels=(), counter=1, insert_expanded=0):
"""Insert a node in the tree.
Given a node, the level tuple indicating where it goes, and the counter
indicating its sibling order, insert a representation of the node at
the appropriate place. The node's parent or older sibling (smaller counter)
needs to be in place already.
If insert_expanded is true, the children of the node are also inserted.
"""
if not node: return None
# Get some names and other local variables
mark_name = self.build_mark_name( levels + (counter,) )
highlight_tag_name = '%s:highlight' % mark_name
node_end = '%s:end' % mark_name
# compute the level of indention
indent = '\t' * len(levels)
# mark this node as closed
node.navigator_state = 'closed'
# Fix the insertion point
self.tree.mark_set('pos', '%s:start' % mark_name)
# Create the canvas to hold the icon for the node
canvas = Tkinter.Canvas(self.tree.component('text'),
width=self['iconwidth'],
height=self['iconheight'],
background=self.tree.component('text')['background'],
bd=0,
#outline=self.tree.component('text')['background'],
)
self.canvaslookup[mark_name] = canvas
# Insert the indent text
self.tree.insert('pos', indent, highlight_tag_name)
# Insert the icon (start with inserting the canvas in our text widget, then
# draw the icon)
self.tree.window_create('pos', window=canvas, padx=0, pady=0)
if insert_expanded and node.has_children():
icon_state='open'
else:
icon_state='closed'
icon = node.create_icon(
canvas,
command=lambda ignore, event, x=node, s=self: s.select_node_icon(x),
state=icon_state)
self.iconlookup[mark_name] = icon
icon.bind('<Enter>', lambda event, x=node, s=self: s.enter_node(x))
icon.bind('<Leave>', lambda event, x=node, s=self: s.leave_node(x))
# Insert the node text
self.tree.insert('pos', ' %s ' % node, mark_name)
self.tree.insert('pos', '\n', highlight_tag_name)
# Store the node in a quick-lookup for later
self.treelookup[mark_name] = node
node.tag_name = mark_name
# Bind events to highlight this node when we
# enter it with the mouse or select it when
# we click on it
self.tree.tag_bind(highlight_tag_name,
'<Enter>',
lambda event, x=node, s=self: s.enter_node(x))
self.tree.tag_bind(highlight_tag_name,
'<Leave>',
lambda event, x=node, s=self: s.leave_node(x))
self.tree.tag_bind(mark_name,
'<Enter>',
lambda event, x=node, s=self: s.enter_node(x))
self.tree.tag_bind(mark_name,
'<Leave>',
lambda event, x=node, s=self: s.leave_node(x))
self.tree.tag_bind(mark_name,
'<ButtonPress-1>',
lambda event, x=node, s=self: s.select_node(x))
# Add a new mark to indicate the beginning of the interior of this node
interior = '%s:interior' % mark_name
self.tree.mark_set(interior, 'pos')
self.tree.mark_gravity(interior, 'left')
# Insert some debugging information
#self.tree.insert('pos', '%s{ %s\n' % (indent, mark_name))
# Add a new mark to indicate the beginning of the subnode
subnode = '%s-1' % mark_name
subnode_start = '%s:start' % subnode
self.tree.mark_set(subnode_start, 'pos')
self.tree.mark_gravity(subnode_start, 'left')
# Add a new mark to indicate the end of this node
self.tree.mark_set(node_end, 'pos')
self.tree.mark_gravity(node_end, 'left')
self.tree.insert('pos', ' ')
self.tree.mark_gravity(node_end, 'right')
self.tree.insert('pos', ' ')
# Insert some debugging information
#self.tree.insert('pos', '%s%s }\n' % (indent, mark_name))
# Add a new mark to indicate the beginning of our next sibling
sibling_start = '%s:start' % self.build_mark_name(levels + (counter+1,))
self.tree.mark_set(sibling_start, 'pos')
self.tree.mark_gravity(sibling_start, 'left')
# Add our children nodes
if insert_expanded and node.has_children():
self.expand_node(node, insert_expanded=1)
return
def enter_node(self, node):
"""Called when the mouse enters the node area.
"""
#print 'entering: %s' % node_name
self.highlight_node(node, color=self['activebackground'])
if self['entercommand']:
self['entercommand'](node)
return
def leave_node(self, node):
"""Called when the mouse leaves the node area.
"""
#print 'leaving: %s' % node.name
if self.is_selected(node):
self.highlight_node(node, color=self['selectedbackground'])
else:
self.highlight_node(node, color=self.tree.component('text')['background'])
if self['leavecommand']:
self['leavecommand'](node)
return
def select_node_icon(self, node):
"Update the icon for the node because it is selected."
self.expand_node(node)
#self.select_node(node)
return
def select_node(self, node):
"""Called when a node is selected by clicking on the text.
"""
self.setcurselection(node)
if node:
#print 'selected %s' % node.name
self.highlight_node(node, color=self['selectedbackground'])
if self['command']:
self['command'](node)
return
def expand_node(self, node, insert_expanded=0):
"""Either expand or collapse the node.
"""
# Figure out where we are
#print 'Node is a ', type(node), node.__class__.__name__
#print dir(node)
name_parts = string.split(node.tag_name, '-')
levels = tuple(map(string.atoi, name_parts[1:]))
# Handle the change in state
if node.navigator_state == 'closed':
# Open a closed node
child_counter = 1
node.navigator_state = 'open'
for child in node.children():
self.insert_node(child, levels=levels, counter=child_counter,
insert_expanded=insert_expanded)
child_counter = child_counter + 1
elif node.navigator_state == 'open':
# Close an opened node
node.navigator_state = 'closed'
mark_name = node.tag_name
start_mark = '%s:interior' % mark_name
end_mark = '%s:end' % mark_name
self.tree.delete( start_mark, end_mark )
self.reset_selection(node)
else:
# Future states ?
pass
return
def _createInterior(self):
"""Create the interior components of the widget.
"""
interior = self.interior()
self.tree = self.createcomponent('tree', (), None,
Pmw.ScrolledText,
(interior,),
)
self.tree.component('text').configure(bg='white',
takefocus=0,
wrap='none',
#cursor='center_ptr',
cursor='left_ptr',
)
# Remove the Text class bindings from the text widget
tags = list(self.tree.component('text').bindtags())
tags.remove('Text')
self.tree.component('text').bindtags(tags)
# Set the tab stops for the text widget
tabs = ''
for i in range(1, 20):
tabs = '%s %d' % (tabs, i*self['indent'])
self.tree.component('text').configure(tabs=tabs)
# Initialize the contents of the tree
self.tree.delete('1.0', 'end')
self.tree.mark_set(self.root_mark_name, '1.0')
self.tree.mark_gravity(self.root_mark_name, 'left')
root1name = 'root-1:start'
self.tree.mark_set(root1name, '1.0')
self.tree.mark_gravity(root1name, 'left')
# insert base data
#self.insert_node(self.treedata, insert_expanded=self['autoexpand'])
self.tree.pack(side=Tkinter.TOP,
expand=Tkinter.YES,
fill=Tkinter.BOTH,
)
return
if __name__ == '__main__':
import GuiAppD
class TNTest(GuiAppD.GuiAppD):
appname = 'Test the TreeNavigator widget'
usebuttonbox=1
def createInterface(self):
data = NavigableTree.create_from_tuples(
('data', 'nodedata', [ ('sub1', 'sub1 data',
[ ('sub11', 'sub11 data', [])
]
),
('sub2', 'sub2 data',
[ ('sub21', 'sub21 data',
[ ('sub211', 'sub211 data', [])
]
),
('sub22', 'sub22 data', [] ),
('sub23', 'sub23 data', [] ),
]
),
('sub3', 'sub3 data', []),
]
)
)
self.navigator = TreeNavigator(self.interior(), treedata=data,
labelpos = 'n',
label_text='TreeNavigator',
autoexpand=1,
command=self.update_label)
self.navigator.pack(side=Tkinter.TOP,
expand=Tkinter.YES,
fill=Tkinter.BOTH)
#self.buttonAdd('Change data', 'Change tree data to a different tree', command=self.do_it)
def do_it(self):
self.navigator.configure(treedata=NavigableTree.create_from_tuples(
('root', 'rootdata', [ ('datanode', 'this is the data', []),
('done', 'this is the last node', []),
]
)
)
)
def update_label(self, node):
if node:
pathstr = ''
for n in node.getpath():
if pathstr:
pathstr='%s->%s' % (pathstr, n)
else:
pathstr = '%s' % n
self.navigator.configure(label_text='Selected: %s (%s)' % (node,pathstr))
#print 'updating label for %s' % node
else:
try:
self.navigator.configure(label_text='No item selected')
except AttributeError:
# 'navigator' component not completely constructed
pass
TNTest().run()
|