Spaces:
Sleeping
Sleeping
# Natural Language Toolkit: Graphical Representations for Trees | |
# | |
# Copyright (C) 2001-2023 NLTK Project | |
# Author: Edward Loper <[email protected]> | |
# URL: <https://www.nltk.org/> | |
# For license information, see LICENSE.TXT | |
""" | |
Graphically display a Tree. | |
""" | |
from tkinter import IntVar, Menu, Tk | |
from nltk.draw.util import ( | |
BoxWidget, | |
CanvasFrame, | |
CanvasWidget, | |
OvalWidget, | |
ParenWidget, | |
TextWidget, | |
) | |
from nltk.tree import Tree | |
from nltk.util import in_idle | |
##////////////////////////////////////////////////////// | |
## 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 f"[TreeSeg {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: | |
def __init__(self, *trees): | |
from math import ceil, sqrt | |
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() | |