1# Natural Language Toolkit: Graphical Representations for Trees
2#
3# Copyright (C) 2001-2019 NLTK Project
4# Author: Edward Loper <edloper@gmail.com>
5# URL: <http://nltk.org/>
6# For license information, see LICENSE.TXT
7
8"""
9Graphically display a Tree.
10"""
11
12from six.moves.tkinter import IntVar, Menu, Tk
13
14from nltk.util import in_idle
15from nltk.tree import Tree
16from nltk.draw.util import (
17    CanvasFrame,
18    CanvasWidget,
19    BoxWidget,
20    TextWidget,
21    ParenWidget,
22    OvalWidget,
23)
24
25##//////////////////////////////////////////////////////
26##  Tree Segment
27##//////////////////////////////////////////////////////
28
29
30class TreeSegmentWidget(CanvasWidget):
31    """
32    A canvas widget that displays a single segment of a hierarchical
33    tree.  Each ``TreeSegmentWidget`` connects a single "node widget"
34    to a sequence of zero or more "subtree widgets".  By default, the
35    bottom of the node is connected to the top of each subtree by a
36    single line.  However, if the ``roof`` attribute is set, then a
37    single triangular "roof" will connect the node to all of its
38    children.
39
40    Attributes:
41      - ``roof``: What sort of connection to draw between the node and
42        its subtrees.  If ``roof`` is true, draw a single triangular
43        "roof" over the subtrees.  If ``roof`` is false, draw a line
44        between each subtree and the node.  Default value is false.
45      - ``xspace``: The amount of horizontal space to leave between
46        subtrees when managing this widget.  Default value is 10.
47      - ``yspace``: The amount of space to place between the node and
48        its children when managing this widget.  Default value is 15.
49      - ``color``: The color of the lines connecting the node to its
50        subtrees; and of the outline of the triangular roof.  Default
51        value is ``'#006060'``.
52      - ``fill``: The fill color for the triangular roof.  Default
53        value is ``''`` (no fill).
54      - ``width``: The width of the lines connecting the node to its
55        subtrees; and of the outline of the triangular roof.  Default
56        value is 1.
57      - ``orientation``: Determines whether the tree branches downwards
58        or rightwards.  Possible values are ``'horizontal'`` and
59        ``'vertical'``.  The default value is ``'vertical'`` (i.e.,
60        branch downwards).
61      - ``draggable``: whether the widget can be dragged by the user.
62    """
63
64    def __init__(self, canvas, label, subtrees, **attribs):
65        """
66        :type node:
67        :type subtrees: list(CanvasWidgetI)
68        """
69        self._label = label
70        self._subtrees = subtrees
71
72        # Attributes
73        self._horizontal = 0
74        self._roof = 0
75        self._xspace = 10
76        self._yspace = 15
77        self._ordered = False
78
79        # Create canvas objects.
80        self._lines = [canvas.create_line(0, 0, 0, 0, fill='#006060') for c in subtrees]
81        self._polygon = canvas.create_polygon(
82            0, 0, fill='', state='hidden', outline='#006060'
83        )
84
85        # Register child widgets (label + subtrees)
86        self._add_child_widget(label)
87        for subtree in subtrees:
88            self._add_child_widget(subtree)
89
90        # Are we currently managing?
91        self._managing = False
92
93        CanvasWidget.__init__(self, canvas, **attribs)
94
95    def __setitem__(self, attr, value):
96        canvas = self.canvas()
97        if attr == 'roof':
98            self._roof = value
99            if self._roof:
100                for l in self._lines:
101                    canvas.itemconfig(l, state='hidden')
102                canvas.itemconfig(self._polygon, state='normal')
103            else:
104                for l in self._lines:
105                    canvas.itemconfig(l, state='normal')
106                canvas.itemconfig(self._polygon, state='hidden')
107        elif attr == 'orientation':
108            if value == 'horizontal':
109                self._horizontal = 1
110            elif value == 'vertical':
111                self._horizontal = 0
112            else:
113                raise ValueError('orientation must be horizontal or vertical')
114        elif attr == 'color':
115            for l in self._lines:
116                canvas.itemconfig(l, fill=value)
117            canvas.itemconfig(self._polygon, outline=value)
118        elif isinstance(attr, tuple) and attr[0] == 'color':
119            # Set the color of an individual line.
120            l = self._lines[int(attr[1])]
121            canvas.itemconfig(l, fill=value)
122        elif attr == 'fill':
123            canvas.itemconfig(self._polygon, fill=value)
124        elif attr == 'width':
125            canvas.itemconfig(self._polygon, {attr: value})
126            for l in self._lines:
127                canvas.itemconfig(l, {attr: value})
128        elif attr in ('xspace', 'yspace'):
129            if attr == 'xspace':
130                self._xspace = value
131            elif attr == 'yspace':
132                self._yspace = value
133            self.update(self._label)
134        elif attr == 'ordered':
135            self._ordered = value
136        else:
137            CanvasWidget.__setitem__(self, attr, value)
138
139    def __getitem__(self, attr):
140        if attr == 'roof':
141            return self._roof
142        elif attr == 'width':
143            return self.canvas().itemcget(self._polygon, attr)
144        elif attr == 'color':
145            return self.canvas().itemcget(self._polygon, 'outline')
146        elif isinstance(attr, tuple) and attr[0] == 'color':
147            l = self._lines[int(attr[1])]
148            return self.canvas().itemcget(l, 'fill')
149        elif attr == 'xspace':
150            return self._xspace
151        elif attr == 'yspace':
152            return self._yspace
153        elif attr == 'orientation':
154            if self._horizontal:
155                return 'horizontal'
156            else:
157                return 'vertical'
158        elif attr == 'ordered':
159            return self._ordered
160        else:
161            return CanvasWidget.__getitem__(self, attr)
162
163    def label(self):
164        return self._label
165
166    def subtrees(self):
167        return self._subtrees[:]
168
169    def set_label(self, label):
170        """
171        Set the node label to ``label``.
172        """
173        self._remove_child_widget(self._label)
174        self._add_child_widget(label)
175        self._label = label
176        self.update(self._label)
177
178    def replace_child(self, oldchild, newchild):
179        """
180        Replace the child ``oldchild`` with ``newchild``.
181        """
182        index = self._subtrees.index(oldchild)
183        self._subtrees[index] = newchild
184        self._remove_child_widget(oldchild)
185        self._add_child_widget(newchild)
186        self.update(newchild)
187
188    def remove_child(self, child):
189        index = self._subtrees.index(child)
190        del self._subtrees[index]
191        self._remove_child_widget(child)
192        self.canvas().delete(self._lines.pop())
193        self.update(self._label)
194
195    def insert_child(self, index, child):
196        canvas = self.canvas()
197        self._subtrees.insert(index, child)
198        self._add_child_widget(child)
199        self._lines.append(canvas.create_line(0, 0, 0, 0, fill='#006060'))
200        self.update(self._label)
201
202    # but.. lines???
203
204    def _tags(self):
205        if self._roof:
206            return [self._polygon]
207        else:
208            return self._lines
209
210    def _subtree_top(self, child):
211        if isinstance(child, TreeSegmentWidget):
212            bbox = child.label().bbox()
213        else:
214            bbox = child.bbox()
215        if self._horizontal:
216            return (bbox[0], (bbox[1] + bbox[3]) / 2.0)
217        else:
218            return ((bbox[0] + bbox[2]) / 2.0, bbox[1])
219
220    def _node_bottom(self):
221        bbox = self._label.bbox()
222        if self._horizontal:
223            return (bbox[2], (bbox[1] + bbox[3]) / 2.0)
224        else:
225            return ((bbox[0] + bbox[2]) / 2.0, bbox[3])
226
227    def _update(self, child):
228        if len(self._subtrees) == 0:
229            return
230        if self._label.bbox() is None:
231            return  # [XX] ???
232
233        # Which lines need to be redrawn?
234        if child is self._label:
235            need_update = self._subtrees
236        else:
237            need_update = [child]
238
239        if self._ordered and not self._managing:
240            need_update = self._maintain_order(child)
241
242        # Update the polygon.
243        (nodex, nodey) = self._node_bottom()
244        (xmin, ymin, xmax, ymax) = self._subtrees[0].bbox()
245        for subtree in self._subtrees[1:]:
246            bbox = subtree.bbox()
247            xmin = min(xmin, bbox[0])
248            ymin = min(ymin, bbox[1])
249            xmax = max(xmax, bbox[2])
250            ymax = max(ymax, bbox[3])
251
252        if self._horizontal:
253            self.canvas().coords(
254                self._polygon, nodex, nodey, xmin, ymin, xmin, ymax, nodex, nodey
255            )
256        else:
257            self.canvas().coords(
258                self._polygon, nodex, nodey, xmin, ymin, xmax, ymin, nodex, nodey
259            )
260
261        # Redraw all lines that need it.
262        for subtree in need_update:
263            (nodex, nodey) = self._node_bottom()
264            line = self._lines[self._subtrees.index(subtree)]
265            (subtreex, subtreey) = self._subtree_top(subtree)
266            self.canvas().coords(line, nodex, nodey, subtreex, subtreey)
267
268    def _maintain_order(self, child):
269        if self._horizontal:
270            return self._maintain_order_horizontal(child)
271        else:
272            return self._maintain_order_vertical(child)
273
274    def _maintain_order_vertical(self, child):
275        (left, top, right, bot) = child.bbox()
276
277        if child is self._label:
278            # Check all the leaves
279            for subtree in self._subtrees:
280                (x1, y1, x2, y2) = subtree.bbox()
281                if bot + self._yspace > y1:
282                    subtree.move(0, bot + self._yspace - y1)
283
284            return self._subtrees
285        else:
286            moved = [child]
287            index = self._subtrees.index(child)
288
289            # Check leaves to our right.
290            x = right + self._xspace
291            for i in range(index + 1, len(self._subtrees)):
292                (x1, y1, x2, y2) = self._subtrees[i].bbox()
293                if x > x1:
294                    self._subtrees[i].move(x - x1, 0)
295                    x += x2 - x1 + self._xspace
296                    moved.append(self._subtrees[i])
297
298            # Check leaves to our left.
299            x = left - self._xspace
300            for i in range(index - 1, -1, -1):
301                (x1, y1, x2, y2) = self._subtrees[i].bbox()
302                if x < x2:
303                    self._subtrees[i].move(x - x2, 0)
304                    x -= x2 - x1 + self._xspace
305                    moved.append(self._subtrees[i])
306
307            # Check the node
308            (x1, y1, x2, y2) = self._label.bbox()
309            if y2 > top - self._yspace:
310                self._label.move(0, top - self._yspace - y2)
311                moved = self._subtrees
312
313        # Return a list of the nodes we moved
314        return moved
315
316    def _maintain_order_horizontal(self, child):
317        (left, top, right, bot) = child.bbox()
318
319        if child is self._label:
320            # Check all the leaves
321            for subtree in self._subtrees:
322                (x1, y1, x2, y2) = subtree.bbox()
323                if right + self._xspace > x1:
324                    subtree.move(right + self._xspace - x1)
325
326            return self._subtrees
327        else:
328            moved = [child]
329            index = self._subtrees.index(child)
330
331            # Check leaves below us.
332            y = bot + self._yspace
333            for i in range(index + 1, len(self._subtrees)):
334                (x1, y1, x2, y2) = self._subtrees[i].bbox()
335                if y > y1:
336                    self._subtrees[i].move(0, y - y1)
337                    y += y2 - y1 + self._yspace
338                    moved.append(self._subtrees[i])
339
340            # Check leaves above us
341            y = top - self._yspace
342            for i in range(index - 1, -1, -1):
343                (x1, y1, x2, y2) = self._subtrees[i].bbox()
344                if y < y2:
345                    self._subtrees[i].move(0, y - y2)
346                    y -= y2 - y1 + self._yspace
347                    moved.append(self._subtrees[i])
348
349            # Check the node
350            (x1, y1, x2, y2) = self._label.bbox()
351            if x2 > left - self._xspace:
352                self._label.move(left - self._xspace - x2, 0)
353                moved = self._subtrees
354
355        # Return a list of the nodes we moved
356        return moved
357
358    def _manage_horizontal(self):
359        (nodex, nodey) = self._node_bottom()
360
361        # Put the subtrees in a line.
362        y = 20
363        for subtree in self._subtrees:
364            subtree_bbox = subtree.bbox()
365            dx = nodex - subtree_bbox[0] + self._xspace
366            dy = y - subtree_bbox[1]
367            subtree.move(dx, dy)
368            y += subtree_bbox[3] - subtree_bbox[1] + self._yspace
369
370        # Find the center of their tops.
371        center = 0.0
372        for subtree in self._subtrees:
373            center += self._subtree_top(subtree)[1]
374        center /= len(self._subtrees)
375
376        # Center the subtrees with the node.
377        for subtree in self._subtrees:
378            subtree.move(0, nodey - center)
379
380    def _manage_vertical(self):
381        (nodex, nodey) = self._node_bottom()
382
383        # Put the subtrees in a line.
384        x = 0
385        for subtree in self._subtrees:
386            subtree_bbox = subtree.bbox()
387            dy = nodey - subtree_bbox[1] + self._yspace
388            dx = x - subtree_bbox[0]
389            subtree.move(dx, dy)
390            x += subtree_bbox[2] - subtree_bbox[0] + self._xspace
391
392        # Find the center of their tops.
393        center = 0.0
394        for subtree in self._subtrees:
395            center += self._subtree_top(subtree)[0] / len(self._subtrees)
396
397        # Center the subtrees with the node.
398        for subtree in self._subtrees:
399            subtree.move(nodex - center, 0)
400
401    def _manage(self):
402        self._managing = True
403        (nodex, nodey) = self._node_bottom()
404        if len(self._subtrees) == 0:
405            return
406
407        if self._horizontal:
408            self._manage_horizontal()
409        else:
410            self._manage_vertical()
411
412        # Update lines to subtrees.
413        for subtree in self._subtrees:
414            self._update(subtree)
415
416        self._managing = False
417
418    def __repr__(self):
419        return '[TreeSeg %s: %s]' % (self._label, self._subtrees)
420
421
422def _tree_to_treeseg(
423    canvas,
424    t,
425    make_node,
426    make_leaf,
427    tree_attribs,
428    node_attribs,
429    leaf_attribs,
430    loc_attribs,
431):
432    if isinstance(t, Tree):
433        label = make_node(canvas, t.label(), **node_attribs)
434        subtrees = [
435            _tree_to_treeseg(
436                canvas,
437                child,
438                make_node,
439                make_leaf,
440                tree_attribs,
441                node_attribs,
442                leaf_attribs,
443                loc_attribs,
444            )
445            for child in t
446        ]
447        return TreeSegmentWidget(canvas, label, subtrees, **tree_attribs)
448    else:
449        return make_leaf(canvas, t, **leaf_attribs)
450
451
452def tree_to_treesegment(
453    canvas, t, make_node=TextWidget, make_leaf=TextWidget, **attribs
454):
455    """
456    Convert a Tree into a ``TreeSegmentWidget``.
457
458    :param make_node: A ``CanvasWidget`` constructor or a function that
459        creates ``CanvasWidgets``.  ``make_node`` is used to convert
460        the Tree's nodes into ``CanvasWidgets``.  If no constructor
461        is specified, then ``TextWidget`` will be used.
462    :param make_leaf: A ``CanvasWidget`` constructor or a function that
463        creates ``CanvasWidgets``.  ``make_leaf`` is used to convert
464        the Tree's leafs into ``CanvasWidgets``.  If no constructor
465        is specified, then ``TextWidget`` will be used.
466    :param attribs: Attributes for the canvas widgets that make up the
467        returned ``TreeSegmentWidget``.  Any attribute beginning with
468        ``'tree_'`` will be passed to all ``TreeSegmentWidgets`` (with
469        the ``'tree_'`` prefix removed.  Any attribute beginning with
470        ``'node_'`` will be passed to all nodes.  Any attribute
471        beginning with ``'leaf_'`` will be passed to all leaves.  And
472        any attribute beginning with ``'loc_'`` will be passed to all
473        text locations (for Trees).
474    """
475    # Process attribs.
476    tree_attribs = {}
477    node_attribs = {}
478    leaf_attribs = {}
479    loc_attribs = {}
480
481    for (key, value) in list(attribs.items()):
482        if key[:5] == 'tree_':
483            tree_attribs[key[5:]] = value
484        elif key[:5] == 'node_':
485            node_attribs[key[5:]] = value
486        elif key[:5] == 'leaf_':
487            leaf_attribs[key[5:]] = value
488        elif key[:4] == 'loc_':
489            loc_attribs[key[4:]] = value
490        else:
491            raise ValueError('Bad attribute: %s' % key)
492    return _tree_to_treeseg(
493        canvas,
494        t,
495        make_node,
496        make_leaf,
497        tree_attribs,
498        node_attribs,
499        leaf_attribs,
500        loc_attribs,
501    )
502
503
504##//////////////////////////////////////////////////////
505##  Tree Widget
506##//////////////////////////////////////////////////////
507
508
509class TreeWidget(CanvasWidget):
510    """
511    A canvas widget that displays a single Tree.
512    ``TreeWidget`` manages a group of ``TreeSegmentWidgets`` that are
513    used to display a Tree.
514
515    Attributes:
516
517      - ``node_attr``: Sets the attribute ``attr`` on all of the
518        node widgets for this ``TreeWidget``.
519      - ``node_attr``: Sets the attribute ``attr`` on all of the
520        leaf widgets for this ``TreeWidget``.
521      - ``loc_attr``: Sets the attribute ``attr`` on all of the
522        location widgets for this ``TreeWidget`` (if it was built from
523        a Tree).  Note that a location widget is a ``TextWidget``.
524
525      - ``xspace``: The amount of horizontal space to leave between
526        subtrees when managing this widget.  Default value is 10.
527      - ``yspace``: The amount of space to place between the node and
528        its children when managing this widget.  Default value is 15.
529
530      - ``line_color``: The color of the lines connecting each expanded
531        node to its subtrees.
532      - ``roof_color``: The color of the outline of the triangular roof
533        for collapsed trees.
534      - ``roof_fill``: The fill color for the triangular roof for
535        collapsed trees.
536      - ``width``
537
538      - ``orientation``: Determines whether the tree branches downwards
539        or rightwards.  Possible values are ``'horizontal'`` and
540        ``'vertical'``.  The default value is ``'vertical'`` (i.e.,
541        branch downwards).
542
543      - ``shapeable``: whether the subtrees can be independently
544        dragged by the user.  THIS property simply sets the
545        ``DRAGGABLE`` property on all of the ``TreeWidget``'s tree
546        segments.
547      - ``draggable``: whether the widget can be dragged by the user.
548    """
549
550    def __init__(
551        self, canvas, t, make_node=TextWidget, make_leaf=TextWidget, **attribs
552    ):
553        # Node & leaf canvas widget constructors
554        self._make_node = make_node
555        self._make_leaf = make_leaf
556        self._tree = t
557
558        # Attributes.
559        self._nodeattribs = {}
560        self._leafattribs = {}
561        self._locattribs = {'color': '#008000'}
562        self._line_color = '#008080'
563        self._line_width = 1
564        self._roof_color = '#008080'
565        self._roof_fill = '#c0c0c0'
566        self._shapeable = False
567        self._xspace = 10
568        self._yspace = 10
569        self._orientation = 'vertical'
570        self._ordered = False
571
572        # Build trees.
573        self._keys = {}  # treeseg -> key
574        self._expanded_trees = {}
575        self._collapsed_trees = {}
576        self._nodes = []
577        self._leaves = []
578        # self._locs = []
579        self._make_collapsed_trees(canvas, t, ())
580        self._treeseg = self._make_expanded_tree(canvas, t, ())
581        self._add_child_widget(self._treeseg)
582
583        CanvasWidget.__init__(self, canvas, **attribs)
584
585    def expanded_tree(self, *path_to_tree):
586        """
587        Return the ``TreeSegmentWidget`` for the specified subtree.
588
589        :param path_to_tree: A list of indices i1, i2, ..., in, where
590            the desired widget is the widget corresponding to
591            ``tree.children()[i1].children()[i2]....children()[in]``.
592            For the root, the path is ``()``.
593        """
594        return self._expanded_trees[path_to_tree]
595
596    def collapsed_tree(self, *path_to_tree):
597        """
598        Return the ``TreeSegmentWidget`` for the specified subtree.
599
600        :param path_to_tree: A list of indices i1, i2, ..., in, where
601            the desired widget is the widget corresponding to
602            ``tree.children()[i1].children()[i2]....children()[in]``.
603            For the root, the path is ``()``.
604        """
605        return self._collapsed_trees[path_to_tree]
606
607    def bind_click_trees(self, callback, button=1):
608        """
609        Add a binding to all tree segments.
610        """
611        for tseg in list(self._expanded_trees.values()):
612            tseg.bind_click(callback, button)
613        for tseg in list(self._collapsed_trees.values()):
614            tseg.bind_click(callback, button)
615
616    def bind_drag_trees(self, callback, button=1):
617        """
618        Add a binding to all tree segments.
619        """
620        for tseg in list(self._expanded_trees.values()):
621            tseg.bind_drag(callback, button)
622        for tseg in list(self._collapsed_trees.values()):
623            tseg.bind_drag(callback, button)
624
625    def bind_click_leaves(self, callback, button=1):
626        """
627        Add a binding to all leaves.
628        """
629        for leaf in self._leaves:
630            leaf.bind_click(callback, button)
631        for leaf in self._leaves:
632            leaf.bind_click(callback, button)
633
634    def bind_drag_leaves(self, callback, button=1):
635        """
636        Add a binding to all leaves.
637        """
638        for leaf in self._leaves:
639            leaf.bind_drag(callback, button)
640        for leaf in self._leaves:
641            leaf.bind_drag(callback, button)
642
643    def bind_click_nodes(self, callback, button=1):
644        """
645        Add a binding to all nodes.
646        """
647        for node in self._nodes:
648            node.bind_click(callback, button)
649        for node in self._nodes:
650            node.bind_click(callback, button)
651
652    def bind_drag_nodes(self, callback, button=1):
653        """
654        Add a binding to all nodes.
655        """
656        for node in self._nodes:
657            node.bind_drag(callback, button)
658        for node in self._nodes:
659            node.bind_drag(callback, button)
660
661    def _make_collapsed_trees(self, canvas, t, key):
662        if not isinstance(t, Tree):
663            return
664        make_node = self._make_node
665        make_leaf = self._make_leaf
666
667        node = make_node(canvas, t.label(), **self._nodeattribs)
668        self._nodes.append(node)
669        leaves = [make_leaf(canvas, l, **self._leafattribs) for l in t.leaves()]
670        self._leaves += leaves
671        treeseg = TreeSegmentWidget(
672            canvas,
673            node,
674            leaves,
675            roof=1,
676            color=self._roof_color,
677            fill=self._roof_fill,
678            width=self._line_width,
679        )
680
681        self._collapsed_trees[key] = treeseg
682        self._keys[treeseg] = key
683        # self._add_child_widget(treeseg)
684        treeseg.hide()
685
686        # Build trees for children.
687        for i in range(len(t)):
688            child = t[i]
689            self._make_collapsed_trees(canvas, child, key + (i,))
690
691    def _make_expanded_tree(self, canvas, t, key):
692        make_node = self._make_node
693        make_leaf = self._make_leaf
694
695        if isinstance(t, Tree):
696            node = make_node(canvas, t.label(), **self._nodeattribs)
697            self._nodes.append(node)
698            children = t
699            subtrees = [
700                self._make_expanded_tree(canvas, children[i], key + (i,))
701                for i in range(len(children))
702            ]
703            treeseg = TreeSegmentWidget(
704                canvas, node, subtrees, color=self._line_color, width=self._line_width
705            )
706            self._expanded_trees[key] = treeseg
707            self._keys[treeseg] = key
708            return treeseg
709        else:
710            leaf = make_leaf(canvas, t, **self._leafattribs)
711            self._leaves.append(leaf)
712            return leaf
713
714    def __setitem__(self, attr, value):
715        if attr[:5] == 'node_':
716            for node in self._nodes:
717                node[attr[5:]] = value
718        elif attr[:5] == 'leaf_':
719            for leaf in self._leaves:
720                leaf[attr[5:]] = value
721        elif attr == 'line_color':
722            self._line_color = value
723            for tseg in list(self._expanded_trees.values()):
724                tseg['color'] = value
725        elif attr == 'line_width':
726            self._line_width = value
727            for tseg in list(self._expanded_trees.values()):
728                tseg['width'] = value
729            for tseg in list(self._collapsed_trees.values()):
730                tseg['width'] = value
731        elif attr == 'roof_color':
732            self._roof_color = value
733            for tseg in list(self._collapsed_trees.values()):
734                tseg['color'] = value
735        elif attr == 'roof_fill':
736            self._roof_fill = value
737            for tseg in list(self._collapsed_trees.values()):
738                tseg['fill'] = value
739        elif attr == 'shapeable':
740            self._shapeable = value
741            for tseg in list(self._expanded_trees.values()):
742                tseg['draggable'] = value
743            for tseg in list(self._collapsed_trees.values()):
744                tseg['draggable'] = value
745            for leaf in self._leaves:
746                leaf['draggable'] = value
747        elif attr == 'xspace':
748            self._xspace = value
749            for tseg in list(self._expanded_trees.values()):
750                tseg['xspace'] = value
751            for tseg in list(self._collapsed_trees.values()):
752                tseg['xspace'] = value
753            self.manage()
754        elif attr == 'yspace':
755            self._yspace = value
756            for tseg in list(self._expanded_trees.values()):
757                tseg['yspace'] = value
758            for tseg in list(self._collapsed_trees.values()):
759                tseg['yspace'] = value
760            self.manage()
761        elif attr == 'orientation':
762            self._orientation = value
763            for tseg in list(self._expanded_trees.values()):
764                tseg['orientation'] = value
765            for tseg in list(self._collapsed_trees.values()):
766                tseg['orientation'] = value
767            self.manage()
768        elif attr == 'ordered':
769            self._ordered = value
770            for tseg in list(self._expanded_trees.values()):
771                tseg['ordered'] = value
772            for tseg in list(self._collapsed_trees.values()):
773                tseg['ordered'] = value
774        else:
775            CanvasWidget.__setitem__(self, attr, value)
776
777    def __getitem__(self, attr):
778        if attr[:5] == 'node_':
779            return self._nodeattribs.get(attr[5:], None)
780        elif attr[:5] == 'leaf_':
781            return self._leafattribs.get(attr[5:], None)
782        elif attr[:4] == 'loc_':
783            return self._locattribs.get(attr[4:], None)
784        elif attr == 'line_color':
785            return self._line_color
786        elif attr == 'line_width':
787            return self._line_width
788        elif attr == 'roof_color':
789            return self._roof_color
790        elif attr == 'roof_fill':
791            return self._roof_fill
792        elif attr == 'shapeable':
793            return self._shapeable
794        elif attr == 'xspace':
795            return self._xspace
796        elif attr == 'yspace':
797            return self._yspace
798        elif attr == 'orientation':
799            return self._orientation
800        else:
801            return CanvasWidget.__getitem__(self, attr)
802
803    def _tags(self):
804        return []
805
806    def _manage(self):
807        segs = list(self._expanded_trees.values()) + list(
808            self._collapsed_trees.values()
809        )
810        for tseg in segs:
811            if tseg.hidden():
812                tseg.show()
813                tseg.manage()
814                tseg.hide()
815
816    def toggle_collapsed(self, treeseg):
817        """
818        Collapse/expand a tree.
819        """
820        old_treeseg = treeseg
821        if old_treeseg['roof']:
822            new_treeseg = self._expanded_trees[self._keys[old_treeseg]]
823        else:
824            new_treeseg = self._collapsed_trees[self._keys[old_treeseg]]
825
826        # Replace the old tree with the new tree.
827        if old_treeseg.parent() is self:
828            self._remove_child_widget(old_treeseg)
829            self._add_child_widget(new_treeseg)
830            self._treeseg = new_treeseg
831        else:
832            old_treeseg.parent().replace_child(old_treeseg, new_treeseg)
833
834        # Move the new tree to where the old tree was.  Show it first,
835        # so we can find its bounding box.
836        new_treeseg.show()
837        (newx, newy) = new_treeseg.label().bbox()[:2]
838        (oldx, oldy) = old_treeseg.label().bbox()[:2]
839        new_treeseg.move(oldx - newx, oldy - newy)
840
841        # Hide the old tree
842        old_treeseg.hide()
843
844        # We could do parent.manage() here instead, if we wanted.
845        new_treeseg.parent().update(new_treeseg)
846
847
848##//////////////////////////////////////////////////////
849##  draw_trees
850##//////////////////////////////////////////////////////
851
852
853class TreeView(object):
854    def __init__(self, *trees):
855        from math import sqrt, ceil
856
857        self._trees = trees
858
859        self._top = Tk()
860        self._top.title('NLTK')
861        self._top.bind('<Control-x>', self.destroy)
862        self._top.bind('<Control-q>', self.destroy)
863
864        cf = self._cframe = CanvasFrame(self._top)
865        self._top.bind('<Control-p>', self._cframe.print_to_file)
866
867        # Size is variable.
868        self._size = IntVar(self._top)
869        self._size.set(12)
870        bold = ('helvetica', -self._size.get(), 'bold')
871        helv = ('helvetica', -self._size.get())
872
873        # Lay the trees out in a square.
874        self._width = int(ceil(sqrt(len(trees))))
875        self._widgets = []
876        for i in range(len(trees)):
877            widget = TreeWidget(
878                cf.canvas(),
879                trees[i],
880                node_font=bold,
881                leaf_color='#008040',
882                node_color='#004080',
883                roof_color='#004040',
884                roof_fill='white',
885                line_color='#004040',
886                draggable=1,
887                leaf_font=helv,
888            )
889            widget.bind_click_trees(widget.toggle_collapsed)
890            self._widgets.append(widget)
891            cf.add_widget(widget, 0, 0)
892
893        self._layout()
894        self._cframe.pack(expand=1, fill='both')
895        self._init_menubar()
896
897    def _layout(self):
898        i = x = y = ymax = 0
899        width = self._width
900        for i in range(len(self._widgets)):
901            widget = self._widgets[i]
902            (oldx, oldy) = widget.bbox()[:2]
903            if i % width == 0:
904                y = ymax
905                x = 0
906            widget.move(x - oldx, y - oldy)
907            x = widget.bbox()[2] + 10
908            ymax = max(ymax, widget.bbox()[3] + 10)
909
910    def _init_menubar(self):
911        menubar = Menu(self._top)
912
913        filemenu = Menu(menubar, tearoff=0)
914        filemenu.add_command(
915            label='Print to Postscript',
916            underline=0,
917            command=self._cframe.print_to_file,
918            accelerator='Ctrl-p',
919        )
920        filemenu.add_command(
921            label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x'
922        )
923        menubar.add_cascade(label='File', underline=0, menu=filemenu)
924
925        zoommenu = Menu(menubar, tearoff=0)
926        zoommenu.add_radiobutton(
927            label='Tiny',
928            variable=self._size,
929            underline=0,
930            value=10,
931            command=self.resize,
932        )
933        zoommenu.add_radiobutton(
934            label='Small',
935            variable=self._size,
936            underline=0,
937            value=12,
938            command=self.resize,
939        )
940        zoommenu.add_radiobutton(
941            label='Medium',
942            variable=self._size,
943            underline=0,
944            value=14,
945            command=self.resize,
946        )
947        zoommenu.add_radiobutton(
948            label='Large',
949            variable=self._size,
950            underline=0,
951            value=28,
952            command=self.resize,
953        )
954        zoommenu.add_radiobutton(
955            label='Huge',
956            variable=self._size,
957            underline=0,
958            value=50,
959            command=self.resize,
960        )
961        menubar.add_cascade(label='Zoom', underline=0, menu=zoommenu)
962
963        self._top.config(menu=menubar)
964
965    def resize(self, *e):
966        bold = ('helvetica', -self._size.get(), 'bold')
967        helv = ('helvetica', -self._size.get())
968        xspace = self._size.get()
969        yspace = self._size.get()
970        for widget in self._widgets:
971            widget['node_font'] = bold
972            widget['leaf_font'] = helv
973            widget['xspace'] = xspace
974            widget['yspace'] = yspace
975            if self._size.get() < 20:
976                widget['line_width'] = 1
977            elif self._size.get() < 30:
978                widget['line_width'] = 2
979            else:
980                widget['line_width'] = 3
981        self._layout()
982
983    def destroy(self, *e):
984        if self._top is None:
985            return
986        self._top.destroy()
987        self._top = None
988
989    def mainloop(self, *args, **kwargs):
990        """
991        Enter the Tkinter mainloop.  This function must be called if
992        this demo is created from a non-interactive program (e.g.
993        from a secript); otherwise, the demo will close as soon as
994        the script completes.
995        """
996        if in_idle():
997            return
998        self._top.mainloop(*args, **kwargs)
999
1000
1001def draw_trees(*trees):
1002    """
1003    Open a new window containing a graphical diagram of the given
1004    trees.
1005
1006    :rtype: None
1007    """
1008    TreeView(*trees).mainloop()
1009    return
1010
1011
1012##//////////////////////////////////////////////////////
1013##  Demo Code
1014##//////////////////////////////////////////////////////
1015
1016
1017def demo():
1018    import random
1019
1020    def fill(cw):
1021        cw['fill'] = '#%06d' % random.randint(0, 999999)
1022
1023    cf = CanvasFrame(width=550, height=450, closeenough=2)
1024
1025    t = Tree.fromstring(
1026        '''
1027    (S (NP the very big cat)
1028       (VP (Adv sorta) (V saw) (NP (Det the) (N dog))))'''
1029    )
1030
1031    tc = TreeWidget(
1032        cf.canvas(),
1033        t,
1034        draggable=1,
1035        node_font=('helvetica', -14, 'bold'),
1036        leaf_font=('helvetica', -12, 'italic'),
1037        roof_fill='white',
1038        roof_color='black',
1039        leaf_color='green4',
1040        node_color='blue2',
1041    )
1042    cf.add_widget(tc, 10, 10)
1043
1044    def boxit(canvas, text):
1045        big = ('helvetica', -16, 'bold')
1046        return BoxWidget(canvas, TextWidget(canvas, text, font=big), fill='green')
1047
1048    def ovalit(canvas, text):
1049        return OvalWidget(canvas, TextWidget(canvas, text), fill='cyan')
1050
1051    treetok = Tree.fromstring('(S (NP this tree) (VP (V is) (AdjP shapeable)))')
1052    tc2 = TreeWidget(cf.canvas(), treetok, boxit, ovalit, shapeable=1)
1053
1054    def color(node):
1055        node['color'] = '#%04d00' % random.randint(0, 9999)
1056
1057    def color2(treeseg):
1058        treeseg.label()['fill'] = '#%06d' % random.randint(0, 9999)
1059        treeseg.label().child()['color'] = 'white'
1060
1061    tc.bind_click_trees(tc.toggle_collapsed)
1062    tc2.bind_click_trees(tc2.toggle_collapsed)
1063    tc.bind_click_nodes(color, 3)
1064    tc2.expanded_tree(1).bind_click(color2, 3)
1065    tc2.expanded_tree().bind_click(color2, 3)
1066
1067    paren = ParenWidget(cf.canvas(), tc2)
1068    cf.add_widget(paren, tc.bbox()[2] + 10, 10)
1069
1070    tree3 = Tree.fromstring(
1071        '''
1072    (S (NP this tree) (AUX was)
1073       (VP (V built) (PP (P with) (NP (N tree_to_treesegment)))))'''
1074    )
1075    tc3 = tree_to_treesegment(
1076        cf.canvas(), tree3, tree_color='green4', tree_xspace=2, tree_width=2
1077    )
1078    tc3['draggable'] = 1
1079    cf.add_widget(tc3, 10, tc.bbox()[3] + 10)
1080
1081    def orientswitch(treewidget):
1082        if treewidget['orientation'] == 'horizontal':
1083            treewidget.expanded_tree(1, 1).subtrees()[0].set_text('vertical')
1084            treewidget.collapsed_tree(1, 1).subtrees()[0].set_text('vertical')
1085            treewidget.collapsed_tree(1).subtrees()[1].set_text('vertical')
1086            treewidget.collapsed_tree().subtrees()[3].set_text('vertical')
1087            treewidget['orientation'] = 'vertical'
1088        else:
1089            treewidget.expanded_tree(1, 1).subtrees()[0].set_text('horizontal')
1090            treewidget.collapsed_tree(1, 1).subtrees()[0].set_text('horizontal')
1091            treewidget.collapsed_tree(1).subtrees()[1].set_text('horizontal')
1092            treewidget.collapsed_tree().subtrees()[3].set_text('horizontal')
1093            treewidget['orientation'] = 'horizontal'
1094
1095    text = """
1096Try clicking, right clicking, and dragging
1097different elements of each of the trees.
1098The top-left tree is a TreeWidget built from
1099a Tree.  The top-right is a TreeWidget built
1100from a Tree, using non-default widget
1101constructors for the nodes & leaves (BoxWidget
1102and OvalWidget).  The bottom-left tree is
1103built from tree_to_treesegment."""
1104    twidget = TextWidget(cf.canvas(), text.strip())
1105    textbox = BoxWidget(cf.canvas(), twidget, fill='white', draggable=1)
1106    cf.add_widget(textbox, tc3.bbox()[2] + 10, tc2.bbox()[3] + 10)
1107
1108    tree4 = Tree.fromstring('(S (NP this tree) (VP (V is) (Adj horizontal)))')
1109    tc4 = TreeWidget(
1110        cf.canvas(),
1111        tree4,
1112        draggable=1,
1113        line_color='brown2',
1114        roof_color='brown2',
1115        node_font=('helvetica', -12, 'bold'),
1116        node_color='brown4',
1117        orientation='horizontal',
1118    )
1119    tc4.manage()
1120    cf.add_widget(tc4, tc3.bbox()[2] + 10, textbox.bbox()[3] + 10)
1121    tc4.bind_click(orientswitch)
1122    tc4.bind_click_trees(tc4.toggle_collapsed, 3)
1123
1124    # Run mainloop
1125    cf.mainloop()
1126
1127
1128if __name__ == '__main__':
1129    demo()
1130