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