tree.py 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130
  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. Graphically display a Tree.
  9. """
  10. from six.moves.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()