You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1130 lines
37 KiB
Python

5 years ago
# Natural Language Toolkit: Graphical Representations for Trees
#
# Copyright (C) 2001-2019 NLTK Project
# Author: Edward Loper <edloper@gmail.com>
# URL: <http://nltk.org/>
# For license information, see LICENSE.TXT
"""
Graphically display a Tree.
"""
from six.moves.tkinter import IntVar, Menu, Tk
from nltk.util import in_idle
from nltk.tree import Tree
from nltk.draw.util import (
CanvasFrame,
CanvasWidget,
BoxWidget,
TextWidget,
ParenWidget,
OvalWidget,
)
##//////////////////////////////////////////////////////
## Tree Segment
##//////////////////////////////////////////////////////
class TreeSegmentWidget(CanvasWidget):
"""
A canvas widget that displays a single segment of a hierarchical
tree. Each ``TreeSegmentWidget`` connects a single "node widget"
to a sequence of zero or more "subtree widgets". By default, the
bottom of the node is connected to the top of each subtree by a
single line. However, if the ``roof`` attribute is set, then a
single triangular "roof" will connect the node to all of its
children.
Attributes:
- ``roof``: What sort of connection to draw between the node and
its subtrees. If ``roof`` is true, draw a single triangular
"roof" over the subtrees. If ``roof`` is false, draw a line
between each subtree and the node. Default value is false.
- ``xspace``: The amount of horizontal space to leave between
subtrees when managing this widget. Default value is 10.
- ``yspace``: The amount of space to place between the node and
its children when managing this widget. Default value is 15.
- ``color``: The color of the lines connecting the node to its
subtrees; and of the outline of the triangular roof. Default
value is ``'#006060'``.
- ``fill``: The fill color for the triangular roof. Default
value is ``''`` (no fill).
- ``width``: The width of the lines connecting the node to its
subtrees; and of the outline of the triangular roof. Default
value is 1.
- ``orientation``: Determines whether the tree branches downwards
or rightwards. Possible values are ``'horizontal'`` and
``'vertical'``. The default value is ``'vertical'`` (i.e.,
branch downwards).
- ``draggable``: whether the widget can be dragged by the user.
"""
def __init__(self, canvas, label, subtrees, **attribs):
"""
:type node:
:type subtrees: list(CanvasWidgetI)
"""
self._label = label
self._subtrees = subtrees
# Attributes
self._horizontal = 0
self._roof = 0
self._xspace = 10
self._yspace = 15
self._ordered = False
# Create canvas objects.
self._lines = [canvas.create_line(0, 0, 0, 0, fill='#006060') for c in subtrees]
self._polygon = canvas.create_polygon(
0, 0, fill='', state='hidden', outline='#006060'
)
# Register child widgets (label + subtrees)
self._add_child_widget(label)
for subtree in subtrees:
self._add_child_widget(subtree)
# Are we currently managing?
self._managing = False
CanvasWidget.__init__(self, canvas, **attribs)
def __setitem__(self, attr, value):
canvas = self.canvas()
if attr == 'roof':
self._roof = value
if self._roof:
for l in self._lines:
canvas.itemconfig(l, state='hidden')
canvas.itemconfig(self._polygon, state='normal')
else:
for l in self._lines:
canvas.itemconfig(l, state='normal')
canvas.itemconfig(self._polygon, state='hidden')
elif attr == 'orientation':
if value == 'horizontal':
self._horizontal = 1
elif value == 'vertical':
self._horizontal = 0
else:
raise ValueError('orientation must be horizontal or vertical')
elif attr == 'color':
for l in self._lines:
canvas.itemconfig(l, fill=value)
canvas.itemconfig(self._polygon, outline=value)
elif isinstance(attr, tuple) and attr[0] == 'color':
# Set the color of an individual line.
l = self._lines[int(attr[1])]
canvas.itemconfig(l, fill=value)
elif attr == 'fill':
canvas.itemconfig(self._polygon, fill=value)
elif attr == 'width':
canvas.itemconfig(self._polygon, {attr: value})
for l in self._lines:
canvas.itemconfig(l, {attr: value})
elif attr in ('xspace', 'yspace'):
if attr == 'xspace':
self._xspace = value
elif attr == 'yspace':
self._yspace = value
self.update(self._label)
elif attr == 'ordered':
self._ordered = value
else:
CanvasWidget.__setitem__(self, attr, value)
def __getitem__(self, attr):
if attr == 'roof':
return self._roof
elif attr == 'width':
return self.canvas().itemcget(self._polygon, attr)
elif attr == 'color':
return self.canvas().itemcget(self._polygon, 'outline')
elif isinstance(attr, tuple) and attr[0] == 'color':
l = self._lines[int(attr[1])]
return self.canvas().itemcget(l, 'fill')
elif attr == 'xspace':
return self._xspace
elif attr == 'yspace':
return self._yspace
elif attr == 'orientation':
if self._horizontal:
return 'horizontal'
else:
return 'vertical'
elif attr == 'ordered':
return self._ordered
else:
return CanvasWidget.__getitem__(self, attr)
def label(self):
return self._label
def subtrees(self):
return self._subtrees[:]
def set_label(self, label):
"""
Set the node label to ``label``.
"""
self._remove_child_widget(self._label)
self._add_child_widget(label)
self._label = label
self.update(self._label)
def replace_child(self, oldchild, newchild):
"""
Replace the child ``oldchild`` with ``newchild``.
"""
index = self._subtrees.index(oldchild)
self._subtrees[index] = newchild
self._remove_child_widget(oldchild)
self._add_child_widget(newchild)
self.update(newchild)
def remove_child(self, child):
index = self._subtrees.index(child)
del self._subtrees[index]
self._remove_child_widget(child)
self.canvas().delete(self._lines.pop())
self.update(self._label)
def insert_child(self, index, child):
canvas = self.canvas()
self._subtrees.insert(index, child)
self._add_child_widget(child)
self._lines.append(canvas.create_line(0, 0, 0, 0, fill='#006060'))
self.update(self._label)
# but.. lines???
def _tags(self):
if self._roof:
return [self._polygon]
else:
return self._lines
def _subtree_top(self, child):
if isinstance(child, TreeSegmentWidget):
bbox = child.label().bbox()
else:
bbox = child.bbox()
if self._horizontal:
return (bbox[0], (bbox[1] + bbox[3]) / 2.0)
else:
return ((bbox[0] + bbox[2]) / 2.0, bbox[1])
def _node_bottom(self):
bbox = self._label.bbox()
if self._horizontal:
return (bbox[2], (bbox[1] + bbox[3]) / 2.0)
else:
return ((bbox[0] + bbox[2]) / 2.0, bbox[3])
def _update(self, child):
if len(self._subtrees) == 0:
return
if self._label.bbox() is None:
return # [XX] ???
# Which lines need to be redrawn?
if child is self._label:
need_update = self._subtrees
else:
need_update = [child]
if self._ordered and not self._managing:
need_update = self._maintain_order(child)
# Update the polygon.
(nodex, nodey) = self._node_bottom()
(xmin, ymin, xmax, ymax) = self._subtrees[0].bbox()
for subtree in self._subtrees[1:]:
bbox = subtree.bbox()
xmin = min(xmin, bbox[0])
ymin = min(ymin, bbox[1])
xmax = max(xmax, bbox[2])
ymax = max(ymax, bbox[3])
if self._horizontal:
self.canvas().coords(
self._polygon, nodex, nodey, xmin, ymin, xmin, ymax, nodex, nodey
)
else:
self.canvas().coords(
self._polygon, nodex, nodey, xmin, ymin, xmax, ymin, nodex, nodey
)
# Redraw all lines that need it.
for subtree in need_update:
(nodex, nodey) = self._node_bottom()
line = self._lines[self._subtrees.index(subtree)]
(subtreex, subtreey) = self._subtree_top(subtree)
self.canvas().coords(line, nodex, nodey, subtreex, subtreey)
def _maintain_order(self, child):
if self._horizontal:
return self._maintain_order_horizontal(child)
else:
return self._maintain_order_vertical(child)
def _maintain_order_vertical(self, child):
(left, top, right, bot) = child.bbox()
if child is self._label:
# Check all the leaves
for subtree in self._subtrees:
(x1, y1, x2, y2) = subtree.bbox()
if bot + self._yspace > y1:
subtree.move(0, bot + self._yspace - y1)
return self._subtrees
else:
moved = [child]
index = self._subtrees.index(child)
# Check leaves to our right.
x = right + self._xspace
for i in range(index + 1, len(self._subtrees)):
(x1, y1, x2, y2) = self._subtrees[i].bbox()
if x > x1:
self._subtrees[i].move(x - x1, 0)
x += x2 - x1 + self._xspace
moved.append(self._subtrees[i])
# Check leaves to our left.
x = left - self._xspace
for i in range(index - 1, -1, -1):
(x1, y1, x2, y2) = self._subtrees[i].bbox()
if x < x2:
self._subtrees[i].move(x - x2, 0)
x -= x2 - x1 + self._xspace
moved.append(self._subtrees[i])
# Check the node
(x1, y1, x2, y2) = self._label.bbox()
if y2 > top - self._yspace:
self._label.move(0, top - self._yspace - y2)
moved = self._subtrees
# Return a list of the nodes we moved
return moved
def _maintain_order_horizontal(self, child):
(left, top, right, bot) = child.bbox()
if child is self._label:
# Check all the leaves
for subtree in self._subtrees:
(x1, y1, x2, y2) = subtree.bbox()
if right + self._xspace > x1:
subtree.move(right + self._xspace - x1)
return self._subtrees
else:
moved = [child]
index = self._subtrees.index(child)
# Check leaves below us.
y = bot + self._yspace
for i in range(index + 1, len(self._subtrees)):
(x1, y1, x2, y2) = self._subtrees[i].bbox()
if y > y1:
self._subtrees[i].move(0, y - y1)
y += y2 - y1 + self._yspace
moved.append(self._subtrees[i])
# Check leaves above us
y = top - self._yspace
for i in range(index - 1, -1, -1):
(x1, y1, x2, y2) = self._subtrees[i].bbox()
if y < y2:
self._subtrees[i].move(0, y - y2)
y -= y2 - y1 + self._yspace
moved.append(self._subtrees[i])
# Check the node
(x1, y1, x2, y2) = self._label.bbox()
if x2 > left - self._xspace:
self._label.move(left - self._xspace - x2, 0)
moved = self._subtrees
# Return a list of the nodes we moved
return moved
def _manage_horizontal(self):
(nodex, nodey) = self._node_bottom()
# Put the subtrees in a line.
y = 20
for subtree in self._subtrees:
subtree_bbox = subtree.bbox()
dx = nodex - subtree_bbox[0] + self._xspace
dy = y - subtree_bbox[1]
subtree.move(dx, dy)
y += subtree_bbox[3] - subtree_bbox[1] + self._yspace
# Find the center of their tops.
center = 0.0
for subtree in self._subtrees:
center += self._subtree_top(subtree)[1]
center /= len(self._subtrees)
# Center the subtrees with the node.
for subtree in self._subtrees:
subtree.move(0, nodey - center)
def _manage_vertical(self):
(nodex, nodey) = self._node_bottom()
# Put the subtrees in a line.
x = 0
for subtree in self._subtrees:
subtree_bbox = subtree.bbox()
dy = nodey - subtree_bbox[1] + self._yspace
dx = x - subtree_bbox[0]
subtree.move(dx, dy)
x += subtree_bbox[2] - subtree_bbox[0] + self._xspace
# Find the center of their tops.
center = 0.0
for subtree in self._subtrees:
center += self._subtree_top(subtree)[0] / len(self._subtrees)
# Center the subtrees with the node.
for subtree in self._subtrees:
subtree.move(nodex - center, 0)
def _manage(self):
self._managing = True
(nodex, nodey) = self._node_bottom()
if len(self._subtrees) == 0:
return
if self._horizontal:
self._manage_horizontal()
else:
self._manage_vertical()
# Update lines to subtrees.
for subtree in self._subtrees:
self._update(subtree)
self._managing = False
def __repr__(self):
return '[TreeSeg %s: %s]' % (self._label, self._subtrees)
def _tree_to_treeseg(
canvas,
t,
make_node,
make_leaf,
tree_attribs,
node_attribs,
leaf_attribs,
loc_attribs,
):
if isinstance(t, Tree):
label = make_node(canvas, t.label(), **node_attribs)
subtrees = [
_tree_to_treeseg(
canvas,
child,
make_node,
make_leaf,
tree_attribs,
node_attribs,
leaf_attribs,
loc_attribs,
)
for child in t
]
return TreeSegmentWidget(canvas, label, subtrees, **tree_attribs)
else:
return make_leaf(canvas, t, **leaf_attribs)
def tree_to_treesegment(
canvas, t, make_node=TextWidget, make_leaf=TextWidget, **attribs
):
"""
Convert a Tree into a ``TreeSegmentWidget``.
:param make_node: A ``CanvasWidget`` constructor or a function that
creates ``CanvasWidgets``. ``make_node`` is used to convert
the Tree's nodes into ``CanvasWidgets``. If no constructor
is specified, then ``TextWidget`` will be used.
:param make_leaf: A ``CanvasWidget`` constructor or a function that
creates ``CanvasWidgets``. ``make_leaf`` is used to convert
the Tree's leafs into ``CanvasWidgets``. If no constructor
is specified, then ``TextWidget`` will be used.
:param attribs: Attributes for the canvas widgets that make up the
returned ``TreeSegmentWidget``. Any attribute beginning with
``'tree_'`` will be passed to all ``TreeSegmentWidgets`` (with
the ``'tree_'`` prefix removed. Any attribute beginning with
``'node_'`` will be passed to all nodes. Any attribute
beginning with ``'leaf_'`` will be passed to all leaves. And
any attribute beginning with ``'loc_'`` will be passed to all
text locations (for Trees).
"""
# Process attribs.
tree_attribs = {}
node_attribs = {}
leaf_attribs = {}
loc_attribs = {}
for (key, value) in list(attribs.items()):
if key[:5] == 'tree_':
tree_attribs[key[5:]] = value
elif key[:5] == 'node_':
node_attribs[key[5:]] = value
elif key[:5] == 'leaf_':
leaf_attribs[key[5:]] = value
elif key[:4] == 'loc_':
loc_attribs[key[4:]] = value
else:
raise ValueError('Bad attribute: %s' % key)
return _tree_to_treeseg(
canvas,
t,
make_node,
make_leaf,
tree_attribs,
node_attribs,
leaf_attribs,
loc_attribs,
)
##//////////////////////////////////////////////////////
## Tree Widget
##//////////////////////////////////////////////////////
class TreeWidget(CanvasWidget):
"""
A canvas widget that displays a single Tree.
``TreeWidget`` manages a group of ``TreeSegmentWidgets`` that are
used to display a Tree.
Attributes:
- ``node_attr``: Sets the attribute ``attr`` on all of the
node widgets for this ``TreeWidget``.
- ``node_attr``: Sets the attribute ``attr`` on all of the
leaf widgets for this ``TreeWidget``.
- ``loc_attr``: Sets the attribute ``attr`` on all of the
location widgets for this ``TreeWidget`` (if it was built from
a Tree). Note that a location widget is a ``TextWidget``.
- ``xspace``: The amount of horizontal space to leave between
subtrees when managing this widget. Default value is 10.
- ``yspace``: The amount of space to place between the node and
its children when managing this widget. Default value is 15.
- ``line_color``: The color of the lines connecting each expanded
node to its subtrees.
- ``roof_color``: The color of the outline of the triangular roof
for collapsed trees.
- ``roof_fill``: The fill color for the triangular roof for
collapsed trees.
- ``width``
- ``orientation``: Determines whether the tree branches downwards
or rightwards. Possible values are ``'horizontal'`` and
``'vertical'``. The default value is ``'vertical'`` (i.e.,
branch downwards).
- ``shapeable``: whether the subtrees can be independently
dragged by the user. THIS property simply sets the
``DRAGGABLE`` property on all of the ``TreeWidget``'s tree
segments.
- ``draggable``: whether the widget can be dragged by the user.
"""
def __init__(
self, canvas, t, make_node=TextWidget, make_leaf=TextWidget, **attribs
):
# Node & leaf canvas widget constructors
self._make_node = make_node
self._make_leaf = make_leaf
self._tree = t
# Attributes.
self._nodeattribs = {}
self._leafattribs = {}
self._locattribs = {'color': '#008000'}
self._line_color = '#008080'
self._line_width = 1
self._roof_color = '#008080'
self._roof_fill = '#c0c0c0'
self._shapeable = False
self._xspace = 10
self._yspace = 10
self._orientation = 'vertical'
self._ordered = False
# Build trees.
self._keys = {} # treeseg -> key
self._expanded_trees = {}
self._collapsed_trees = {}
self._nodes = []
self._leaves = []
# self._locs = []
self._make_collapsed_trees(canvas, t, ())
self._treeseg = self._make_expanded_tree(canvas, t, ())
self._add_child_widget(self._treeseg)
CanvasWidget.__init__(self, canvas, **attribs)
def expanded_tree(self, *path_to_tree):
"""
Return the ``TreeSegmentWidget`` for the specified subtree.
:param path_to_tree: A list of indices i1, i2, ..., in, where
the desired widget is the widget corresponding to
``tree.children()[i1].children()[i2]....children()[in]``.
For the root, the path is ``()``.
"""
return self._expanded_trees[path_to_tree]
def collapsed_tree(self, *path_to_tree):
"""
Return the ``TreeSegmentWidget`` for the specified subtree.
:param path_to_tree: A list of indices i1, i2, ..., in, where
the desired widget is the widget corresponding to
``tree.children()[i1].children()[i2]....children()[in]``.
For the root, the path is ``()``.
"""
return self._collapsed_trees[path_to_tree]
def bind_click_trees(self, callback, button=1):
"""
Add a binding to all tree segments.
"""
for tseg in list(self._expanded_trees.values()):
tseg.bind_click(callback, button)
for tseg in list(self._collapsed_trees.values()):
tseg.bind_click(callback, button)
def bind_drag_trees(self, callback, button=1):
"""
Add a binding to all tree segments.
"""
for tseg in list(self._expanded_trees.values()):
tseg.bind_drag(callback, button)
for tseg in list(self._collapsed_trees.values()):
tseg.bind_drag(callback, button)
def bind_click_leaves(self, callback, button=1):
"""
Add a binding to all leaves.
"""
for leaf in self._leaves:
leaf.bind_click(callback, button)
for leaf in self._leaves:
leaf.bind_click(callback, button)
def bind_drag_leaves(self, callback, button=1):
"""
Add a binding to all leaves.
"""
for leaf in self._leaves:
leaf.bind_drag(callback, button)
for leaf in self._leaves:
leaf.bind_drag(callback, button)
def bind_click_nodes(self, callback, button=1):
"""
Add a binding to all nodes.
"""
for node in self._nodes:
node.bind_click(callback, button)
for node in self._nodes:
node.bind_click(callback, button)
def bind_drag_nodes(self, callback, button=1):
"""
Add a binding to all nodes.
"""
for node in self._nodes:
node.bind_drag(callback, button)
for node in self._nodes:
node.bind_drag(callback, button)
def _make_collapsed_trees(self, canvas, t, key):
if not isinstance(t, Tree):
return
make_node = self._make_node
make_leaf = self._make_leaf
node = make_node(canvas, t.label(), **self._nodeattribs)
self._nodes.append(node)
leaves = [make_leaf(canvas, l, **self._leafattribs) for l in t.leaves()]
self._leaves += leaves
treeseg = TreeSegmentWidget(
canvas,
node,
leaves,
roof=1,
color=self._roof_color,
fill=self._roof_fill,
width=self._line_width,
)
self._collapsed_trees[key] = treeseg
self._keys[treeseg] = key
# self._add_child_widget(treeseg)
treeseg.hide()
# Build trees for children.
for i in range(len(t)):
child = t[i]
self._make_collapsed_trees(canvas, child, key + (i,))
def _make_expanded_tree(self, canvas, t, key):
make_node = self._make_node
make_leaf = self._make_leaf
if isinstance(t, Tree):
node = make_node(canvas, t.label(), **self._nodeattribs)
self._nodes.append(node)
children = t
subtrees = [
self._make_expanded_tree(canvas, children[i], key + (i,))
for i in range(len(children))
]
treeseg = TreeSegmentWidget(
canvas, node, subtrees, color=self._line_color, width=self._line_width
)
self._expanded_trees[key] = treeseg
self._keys[treeseg] = key
return treeseg
else:
leaf = make_leaf(canvas, t, **self._leafattribs)
self._leaves.append(leaf)
return leaf
def __setitem__(self, attr, value):
if attr[:5] == 'node_':
for node in self._nodes:
node[attr[5:]] = value
elif attr[:5] == 'leaf_':
for leaf in self._leaves:
leaf[attr[5:]] = value
elif attr == 'line_color':
self._line_color = value
for tseg in list(self._expanded_trees.values()):
tseg['color'] = value
elif attr == 'line_width':
self._line_width = value
for tseg in list(self._expanded_trees.values()):
tseg['width'] = value
for tseg in list(self._collapsed_trees.values()):
tseg['width'] = value
elif attr == 'roof_color':
self._roof_color = value
for tseg in list(self._collapsed_trees.values()):
tseg['color'] = value
elif attr == 'roof_fill':
self._roof_fill = value
for tseg in list(self._collapsed_trees.values()):
tseg['fill'] = value
elif attr == 'shapeable':
self._shapeable = value
for tseg in list(self._expanded_trees.values()):
tseg['draggable'] = value
for tseg in list(self._collapsed_trees.values()):
tseg['draggable'] = value
for leaf in self._leaves:
leaf['draggable'] = value
elif attr == 'xspace':
self._xspace = value
for tseg in list(self._expanded_trees.values()):
tseg['xspace'] = value
for tseg in list(self._collapsed_trees.values()):
tseg['xspace'] = value
self.manage()
elif attr == 'yspace':
self._yspace = value
for tseg in list(self._expanded_trees.values()):
tseg['yspace'] = value
for tseg in list(self._collapsed_trees.values()):
tseg['yspace'] = value
self.manage()
elif attr == 'orientation':
self._orientation = value
for tseg in list(self._expanded_trees.values()):
tseg['orientation'] = value
for tseg in list(self._collapsed_trees.values()):
tseg['orientation'] = value
self.manage()
elif attr == 'ordered':
self._ordered = value
for tseg in list(self._expanded_trees.values()):
tseg['ordered'] = value
for tseg in list(self._collapsed_trees.values()):
tseg['ordered'] = value
else:
CanvasWidget.__setitem__(self, attr, value)
def __getitem__(self, attr):
if attr[:5] == 'node_':
return self._nodeattribs.get(attr[5:], None)
elif attr[:5] == 'leaf_':
return self._leafattribs.get(attr[5:], None)
elif attr[:4] == 'loc_':
return self._locattribs.get(attr[4:], None)
elif attr == 'line_color':
return self._line_color
elif attr == 'line_width':
return self._line_width
elif attr == 'roof_color':
return self._roof_color
elif attr == 'roof_fill':
return self._roof_fill
elif attr == 'shapeable':
return self._shapeable
elif attr == 'xspace':
return self._xspace
elif attr == 'yspace':
return self._yspace
elif attr == 'orientation':
return self._orientation
else:
return CanvasWidget.__getitem__(self, attr)
def _tags(self):
return []
def _manage(self):
segs = list(self._expanded_trees.values()) + list(
self._collapsed_trees.values()
)
for tseg in segs:
if tseg.hidden():
tseg.show()
tseg.manage()
tseg.hide()
def toggle_collapsed(self, treeseg):
"""
Collapse/expand a tree.
"""
old_treeseg = treeseg
if old_treeseg['roof']:
new_treeseg = self._expanded_trees[self._keys[old_treeseg]]
else:
new_treeseg = self._collapsed_trees[self._keys[old_treeseg]]
# Replace the old tree with the new tree.
if old_treeseg.parent() is self:
self._remove_child_widget(old_treeseg)
self._add_child_widget(new_treeseg)
self._treeseg = new_treeseg
else:
old_treeseg.parent().replace_child(old_treeseg, new_treeseg)
# Move the new tree to where the old tree was. Show it first,
# so we can find its bounding box.
new_treeseg.show()
(newx, newy) = new_treeseg.label().bbox()[:2]
(oldx, oldy) = old_treeseg.label().bbox()[:2]
new_treeseg.move(oldx - newx, oldy - newy)
# Hide the old tree
old_treeseg.hide()
# We could do parent.manage() here instead, if we wanted.
new_treeseg.parent().update(new_treeseg)
##//////////////////////////////////////////////////////
## draw_trees
##//////////////////////////////////////////////////////
class TreeView(object):
def __init__(self, *trees):
from math import sqrt, ceil
self._trees = trees
self._top = Tk()
self._top.title('NLTK')
self._top.bind('<Control-x>', self.destroy)
self._top.bind('<Control-q>', self.destroy)
cf = self._cframe = CanvasFrame(self._top)
self._top.bind('<Control-p>', self._cframe.print_to_file)
# Size is variable.
self._size = IntVar(self._top)
self._size.set(12)
bold = ('helvetica', -self._size.get(), 'bold')
helv = ('helvetica', -self._size.get())
# Lay the trees out in a square.
self._width = int(ceil(sqrt(len(trees))))
self._widgets = []
for i in range(len(trees)):
widget = TreeWidget(
cf.canvas(),
trees[i],
node_font=bold,
leaf_color='#008040',
node_color='#004080',
roof_color='#004040',
roof_fill='white',
line_color='#004040',
draggable=1,
leaf_font=helv,
)
widget.bind_click_trees(widget.toggle_collapsed)
self._widgets.append(widget)
cf.add_widget(widget, 0, 0)
self._layout()
self._cframe.pack(expand=1, fill='both')
self._init_menubar()
def _layout(self):
i = x = y = ymax = 0
width = self._width
for i in range(len(self._widgets)):
widget = self._widgets[i]
(oldx, oldy) = widget.bbox()[:2]
if i % width == 0:
y = ymax
x = 0
widget.move(x - oldx, y - oldy)
x = widget.bbox()[2] + 10
ymax = max(ymax, widget.bbox()[3] + 10)
def _init_menubar(self):
menubar = Menu(self._top)
filemenu = Menu(menubar, tearoff=0)
filemenu.add_command(
label='Print to Postscript',
underline=0,
command=self._cframe.print_to_file,
accelerator='Ctrl-p',
)
filemenu.add_command(
label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x'
)
menubar.add_cascade(label='File', underline=0, menu=filemenu)
zoommenu = Menu(menubar, tearoff=0)
zoommenu.add_radiobutton(
label='Tiny',
variable=self._size,
underline=0,
value=10,
command=self.resize,
)
zoommenu.add_radiobutton(
label='Small',
variable=self._size,
underline=0,
value=12,
command=self.resize,
)
zoommenu.add_radiobutton(
label='Medium',
variable=self._size,
underline=0,
value=14,
command=self.resize,
)
zoommenu.add_radiobutton(
label='Large',
variable=self._size,
underline=0,
value=28,
command=self.resize,
)
zoommenu.add_radiobutton(
label='Huge',
variable=self._size,
underline=0,
value=50,
command=self.resize,
)
menubar.add_cascade(label='Zoom', underline=0, menu=zoommenu)
self._top.config(menu=menubar)
def resize(self, *e):
bold = ('helvetica', -self._size.get(), 'bold')
helv = ('helvetica', -self._size.get())
xspace = self._size.get()
yspace = self._size.get()
for widget in self._widgets:
widget['node_font'] = bold
widget['leaf_font'] = helv
widget['xspace'] = xspace
widget['yspace'] = yspace
if self._size.get() < 20:
widget['line_width'] = 1
elif self._size.get() < 30:
widget['line_width'] = 2
else:
widget['line_width'] = 3
self._layout()
def destroy(self, *e):
if self._top is None:
return
self._top.destroy()
self._top = None
def mainloop(self, *args, **kwargs):
"""
Enter the Tkinter mainloop. This function must be called if
this demo is created from a non-interactive program (e.g.
from a secript); otherwise, the demo will close as soon as
the script completes.
"""
if in_idle():
return
self._top.mainloop(*args, **kwargs)
def draw_trees(*trees):
"""
Open a new window containing a graphical diagram of the given
trees.
:rtype: None
"""
TreeView(*trees).mainloop()
return
##//////////////////////////////////////////////////////
## Demo Code
##//////////////////////////////////////////////////////
def demo():
import random
def fill(cw):
cw['fill'] = '#%06d' % random.randint(0, 999999)
cf = CanvasFrame(width=550, height=450, closeenough=2)
t = Tree.fromstring(
'''
(S (NP the very big cat)
(VP (Adv sorta) (V saw) (NP (Det the) (N dog))))'''
)
tc = TreeWidget(
cf.canvas(),
t,
draggable=1,
node_font=('helvetica', -14, 'bold'),
leaf_font=('helvetica', -12, 'italic'),
roof_fill='white',
roof_color='black',
leaf_color='green4',
node_color='blue2',
)
cf.add_widget(tc, 10, 10)
def boxit(canvas, text):
big = ('helvetica', -16, 'bold')
return BoxWidget(canvas, TextWidget(canvas, text, font=big), fill='green')
def ovalit(canvas, text):
return OvalWidget(canvas, TextWidget(canvas, text), fill='cyan')
treetok = Tree.fromstring('(S (NP this tree) (VP (V is) (AdjP shapeable)))')
tc2 = TreeWidget(cf.canvas(), treetok, boxit, ovalit, shapeable=1)
def color(node):
node['color'] = '#%04d00' % random.randint(0, 9999)
def color2(treeseg):
treeseg.label()['fill'] = '#%06d' % random.randint(0, 9999)
treeseg.label().child()['color'] = 'white'
tc.bind_click_trees(tc.toggle_collapsed)
tc2.bind_click_trees(tc2.toggle_collapsed)
tc.bind_click_nodes(color, 3)
tc2.expanded_tree(1).bind_click(color2, 3)
tc2.expanded_tree().bind_click(color2, 3)
paren = ParenWidget(cf.canvas(), tc2)
cf.add_widget(paren, tc.bbox()[2] + 10, 10)
tree3 = Tree.fromstring(
'''
(S (NP this tree) (AUX was)
(VP (V built) (PP (P with) (NP (N tree_to_treesegment)))))'''
)
tc3 = tree_to_treesegment(
cf.canvas(), tree3, tree_color='green4', tree_xspace=2, tree_width=2
)
tc3['draggable'] = 1
cf.add_widget(tc3, 10, tc.bbox()[3] + 10)
def orientswitch(treewidget):
if treewidget['orientation'] == 'horizontal':
treewidget.expanded_tree(1, 1).subtrees()[0].set_text('vertical')
treewidget.collapsed_tree(1, 1).subtrees()[0].set_text('vertical')
treewidget.collapsed_tree(1).subtrees()[1].set_text('vertical')
treewidget.collapsed_tree().subtrees()[3].set_text('vertical')
treewidget['orientation'] = 'vertical'
else:
treewidget.expanded_tree(1, 1).subtrees()[0].set_text('horizontal')
treewidget.collapsed_tree(1, 1).subtrees()[0].set_text('horizontal')
treewidget.collapsed_tree(1).subtrees()[1].set_text('horizontal')
treewidget.collapsed_tree().subtrees()[3].set_text('horizontal')
treewidget['orientation'] = 'horizontal'
text = """
Try clicking, right clicking, and dragging
different elements of each of the trees.
The top-left tree is a TreeWidget built from
a Tree. The top-right is a TreeWidget built
from a Tree, using non-default widget
constructors for the nodes & leaves (BoxWidget
and OvalWidget). The bottom-left tree is
built from tree_to_treesegment."""
twidget = TextWidget(cf.canvas(), text.strip())
textbox = BoxWidget(cf.canvas(), twidget, fill='white', draggable=1)
cf.add_widget(textbox, tc3.bbox()[2] + 10, tc2.bbox()[3] + 10)
tree4 = Tree.fromstring('(S (NP this tree) (VP (V is) (Adj horizontal)))')
tc4 = TreeWidget(
cf.canvas(),
tree4,
draggable=1,
line_color='brown2',
roof_color='brown2',
node_font=('helvetica', -12, 'bold'),
node_color='brown4',
orientation='horizontal',
)
tc4.manage()
cf.add_widget(tc4, tc3.bbox()[2] + 10, textbox.bbox()[3] + 10)
tc4.bind_click(orientswitch)
tc4.bind_click_trees(tc4.toggle_collapsed, 3)
# Run mainloop
cf.mainloop()
if __name__ == '__main__':
demo()