tree.py 37 KB

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