util.py 86 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575
  1. # Natural Language Toolkit: Drawing utilities
  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. Tools for graphically displaying and interacting with the objects and
  9. processing classes defined by the Toolkit. These tools are primarily
  10. intended to help students visualize the objects that they create.
  11. The graphical tools are typically built using "canvas widgets", each
  12. of which encapsulates the graphical elements and bindings used to
  13. display a complex object on a Tkinter ``Canvas``. For example, NLTK
  14. defines canvas widgets for displaying trees and directed graphs, as
  15. well as a number of simpler widgets. These canvas widgets make it
  16. easier to build new graphical tools and demos. See the class
  17. documentation for ``CanvasWidget`` for more information.
  18. The ``nltk.draw`` module defines the abstract ``CanvasWidget`` base
  19. class, and a number of simple canvas widgets. The remaining canvas
  20. widgets are defined by submodules, such as ``nltk.draw.tree``.
  21. The ``nltk.draw`` module also defines ``CanvasFrame``, which
  22. encapsulates a ``Canvas`` and its scrollbars. It uses a
  23. ``ScrollWatcherWidget`` to ensure that all canvas widgets contained on
  24. its canvas are within the scroll region.
  25. Acknowledgements: Many of the ideas behind the canvas widget system
  26. are derived from ``CLIG``, a Tk-based grapher for linguistic data
  27. structures. For more information, see the CLIG
  28. homepage (http://www.ags.uni-sb.de/~konrad/clig.html).
  29. """
  30. from abc import ABCMeta, abstractmethod
  31. from six import add_metaclass
  32. from six.moves.tkinter import (
  33. Button,
  34. Canvas,
  35. Entry,
  36. Frame,
  37. Label,
  38. Menu,
  39. Menubutton,
  40. Scrollbar,
  41. StringVar,
  42. Text,
  43. Tk,
  44. Toplevel,
  45. Widget,
  46. RAISED,
  47. )
  48. from six.moves.tkinter_tkfiledialog import asksaveasfilename
  49. from nltk.util import in_idle
  50. ##//////////////////////////////////////////////////////
  51. ## CanvasWidget
  52. ##//////////////////////////////////////////////////////
  53. @add_metaclass(ABCMeta)
  54. class CanvasWidget(object):
  55. """
  56. A collection of graphical elements and bindings used to display a
  57. complex object on a Tkinter ``Canvas``. A canvas widget is
  58. responsible for managing the ``Canvas`` tags and callback bindings
  59. necessary to display and interact with the object. Canvas widgets
  60. are often organized into hierarchies, where parent canvas widgets
  61. control aspects of their child widgets.
  62. Each canvas widget is bound to a single ``Canvas``. This ``Canvas``
  63. is specified as the first argument to the ``CanvasWidget``'s
  64. constructor.
  65. Attributes. Each canvas widget can support a variety of
  66. "attributes", which control how the canvas widget is displayed.
  67. Some typical examples attributes are ``color``, ``font``, and
  68. ``radius``. Each attribute has a default value. This default
  69. value can be overridden in the constructor, using keyword
  70. arguments of the form ``attribute=value``:
  71. >>> from nltk.draw.util import TextWidget
  72. >>> cn = TextWidget(c, 'test', color='red')
  73. Attribute values can also be changed after a canvas widget has
  74. been constructed, using the ``__setitem__`` operator:
  75. >>> cn['font'] = 'times'
  76. The current value of an attribute value can be queried using the
  77. ``__getitem__`` operator:
  78. >>> cn['color']
  79. red
  80. For a list of the attributes supported by a type of canvas widget,
  81. see its class documentation.
  82. Interaction. The attribute ``'draggable'`` controls whether the
  83. user can drag a canvas widget around the canvas. By default,
  84. canvas widgets are not draggable.
  85. ``CanvasWidget`` provides callback support for two types of user
  86. interaction: clicking and dragging. The method ``bind_click``
  87. registers a callback function that is called whenever the canvas
  88. widget is clicked. The method ``bind_drag`` registers a callback
  89. function that is called after the canvas widget is dragged. If
  90. the user clicks or drags a canvas widget with no registered
  91. callback function, then the interaction event will propagate to
  92. its parent. For each canvas widget, only one callback function
  93. may be registered for an interaction event. Callback functions
  94. can be deregistered with the ``unbind_click`` and ``unbind_drag``
  95. methods.
  96. Subclassing. ``CanvasWidget`` is an abstract class. Subclasses
  97. are required to implement the following methods:
  98. - ``__init__``: Builds a new canvas widget. It must perform the
  99. following three tasks (in order):
  100. - Create any new graphical elements.
  101. - Call ``_add_child_widget`` on each child widget.
  102. - Call the ``CanvasWidget`` constructor.
  103. - ``_tags``: Returns a list of the canvas tags for all graphical
  104. elements managed by this canvas widget, not including
  105. graphical elements managed by its child widgets.
  106. - ``_manage``: Arranges the child widgets of this canvas widget.
  107. This is typically only called when the canvas widget is
  108. created.
  109. - ``_update``: Update this canvas widget in response to a
  110. change in a single child.
  111. For a ``CanvasWidget`` with no child widgets, the default
  112. definitions for ``_manage`` and ``_update`` may be used.
  113. If a subclass defines any attributes, then it should implement
  114. ``__getitem__`` and ``__setitem__``. If either of these methods is
  115. called with an unknown attribute, then they should propagate the
  116. request to ``CanvasWidget``.
  117. Most subclasses implement a number of additional methods that
  118. modify the ``CanvasWidget`` in some way. These methods must call
  119. ``parent.update(self)`` after making any changes to the canvas
  120. widget's graphical elements. The canvas widget must also call
  121. ``parent.update(self)`` after changing any attribute value that
  122. affects the shape or position of the canvas widget's graphical
  123. elements.
  124. :type __canvas: Tkinter.Canvas
  125. :ivar __canvas: This ``CanvasWidget``'s canvas.
  126. :type __parent: CanvasWidget or None
  127. :ivar __parent: This ``CanvasWidget``'s hierarchical parent widget.
  128. :type __children: list(CanvasWidget)
  129. :ivar __children: This ``CanvasWidget``'s hierarchical child widgets.
  130. :type __updating: bool
  131. :ivar __updating: Is this canvas widget currently performing an
  132. update? If it is, then it will ignore any new update requests
  133. from child widgets.
  134. :type __draggable: bool
  135. :ivar __draggable: Is this canvas widget draggable?
  136. :type __press: event
  137. :ivar __press: The ButtonPress event that we're currently handling.
  138. :type __drag_x: int
  139. :ivar __drag_x: Where it's been moved to (to find dx)
  140. :type __drag_y: int
  141. :ivar __drag_y: Where it's been moved to (to find dy)
  142. :type __callbacks: dictionary
  143. :ivar __callbacks: Registered callbacks. Currently, four keys are
  144. used: ``1``, ``2``, ``3``, and ``'drag'``. The values are
  145. callback functions. Each callback function takes a single
  146. argument, which is the ``CanvasWidget`` that triggered the
  147. callback.
  148. """
  149. def __init__(self, canvas, parent=None, **attribs):
  150. """
  151. Create a new canvas widget. This constructor should only be
  152. called by subclass constructors; and it should be called only
  153. "after" the subclass has constructed all graphical canvas
  154. objects and registered all child widgets.
  155. :param canvas: This canvas widget's canvas.
  156. :type canvas: Tkinter.Canvas
  157. :param parent: This canvas widget's hierarchical parent.
  158. :type parent: CanvasWidget
  159. :param attribs: The new canvas widget's attributes.
  160. """
  161. if self.__class__ == CanvasWidget:
  162. raise TypeError('CanvasWidget is an abstract base class')
  163. if not isinstance(canvas, Canvas):
  164. raise TypeError('Expected a canvas!')
  165. self.__canvas = canvas
  166. self.__parent = parent
  167. # If the subclass constructor called _add_child_widget, then
  168. # self.__children will already exist.
  169. if not hasattr(self, '_CanvasWidget__children'):
  170. self.__children = []
  171. # Is this widget hidden?
  172. self.__hidden = 0
  173. # Update control (prevents infinite loops)
  174. self.__updating = 0
  175. # Button-press and drag callback handling.
  176. self.__press = None
  177. self.__drag_x = self.__drag_y = 0
  178. self.__callbacks = {}
  179. self.__draggable = 0
  180. # Set up attributes.
  181. for (attr, value) in list(attribs.items()):
  182. self[attr] = value
  183. # Manage this canvas widget
  184. self._manage()
  185. # Register any new bindings
  186. for tag in self._tags():
  187. self.__canvas.tag_bind(tag, '<ButtonPress-1>', self.__press_cb)
  188. self.__canvas.tag_bind(tag, '<ButtonPress-2>', self.__press_cb)
  189. self.__canvas.tag_bind(tag, '<ButtonPress-3>', self.__press_cb)
  190. ##//////////////////////////////////////////////////////
  191. ## Inherited methods.
  192. ##//////////////////////////////////////////////////////
  193. def bbox(self):
  194. """
  195. :return: A bounding box for this ``CanvasWidget``. The bounding
  196. box is a tuple of four coordinates, *(xmin, ymin, xmax, ymax)*,
  197. for a rectangle which encloses all of the canvas
  198. widget's graphical elements. Bounding box coordinates are
  199. specified with respect to the coordinate space of the ``Canvas``.
  200. :rtype: tuple(int, int, int, int)
  201. """
  202. if self.__hidden:
  203. return (0, 0, 0, 0)
  204. if len(self.tags()) == 0:
  205. raise ValueError('No tags')
  206. return self.__canvas.bbox(*self.tags())
  207. def width(self):
  208. """
  209. :return: The width of this canvas widget's bounding box, in
  210. its ``Canvas``'s coordinate space.
  211. :rtype: int
  212. """
  213. if len(self.tags()) == 0:
  214. raise ValueError('No tags')
  215. bbox = self.__canvas.bbox(*self.tags())
  216. return bbox[2] - bbox[0]
  217. def height(self):
  218. """
  219. :return: The height of this canvas widget's bounding box, in
  220. its ``Canvas``'s coordinate space.
  221. :rtype: int
  222. """
  223. if len(self.tags()) == 0:
  224. raise ValueError('No tags')
  225. bbox = self.__canvas.bbox(*self.tags())
  226. return bbox[3] - bbox[1]
  227. def parent(self):
  228. """
  229. :return: The hierarchical parent of this canvas widget.
  230. ``self`` is considered a subpart of its parent for
  231. purposes of user interaction.
  232. :rtype: CanvasWidget or None
  233. """
  234. return self.__parent
  235. def child_widgets(self):
  236. """
  237. :return: A list of the hierarchical children of this canvas
  238. widget. These children are considered part of ``self``
  239. for purposes of user interaction.
  240. :rtype: list of CanvasWidget
  241. """
  242. return self.__children
  243. def canvas(self):
  244. """
  245. :return: The canvas that this canvas widget is bound to.
  246. :rtype: Tkinter.Canvas
  247. """
  248. return self.__canvas
  249. def move(self, dx, dy):
  250. """
  251. Move this canvas widget by a given distance. In particular,
  252. shift the canvas widget right by ``dx`` pixels, and down by
  253. ``dy`` pixels. Both ``dx`` and ``dy`` may be negative, resulting
  254. in leftward or upward movement.
  255. :type dx: int
  256. :param dx: The number of pixels to move this canvas widget
  257. rightwards.
  258. :type dy: int
  259. :param dy: The number of pixels to move this canvas widget
  260. downwards.
  261. :rtype: None
  262. """
  263. if dx == dy == 0:
  264. return
  265. for tag in self.tags():
  266. self.__canvas.move(tag, dx, dy)
  267. if self.__parent:
  268. self.__parent.update(self)
  269. def moveto(self, x, y, anchor='NW'):
  270. """
  271. Move this canvas widget to the given location. In particular,
  272. shift the canvas widget such that the corner or side of the
  273. bounding box specified by ``anchor`` is at location (``x``,
  274. ``y``).
  275. :param x,y: The location that the canvas widget should be moved
  276. to.
  277. :param anchor: The corner or side of the canvas widget that
  278. should be moved to the specified location. ``'N'``
  279. specifies the top center; ``'NE'`` specifies the top right
  280. corner; etc.
  281. """
  282. x1, y1, x2, y2 = self.bbox()
  283. if anchor == 'NW':
  284. self.move(x - x1, y - y1)
  285. if anchor == 'N':
  286. self.move(x - x1 / 2 - x2 / 2, y - y1)
  287. if anchor == 'NE':
  288. self.move(x - x2, y - y1)
  289. if anchor == 'E':
  290. self.move(x - x2, y - y1 / 2 - y2 / 2)
  291. if anchor == 'SE':
  292. self.move(x - x2, y - y2)
  293. if anchor == 'S':
  294. self.move(x - x1 / 2 - x2 / 2, y - y2)
  295. if anchor == 'SW':
  296. self.move(x - x1, y - y2)
  297. if anchor == 'W':
  298. self.move(x - x1, y - y1 / 2 - y2 / 2)
  299. def destroy(self):
  300. """
  301. Remove this ``CanvasWidget`` from its ``Canvas``. After a
  302. ``CanvasWidget`` has been destroyed, it should not be accessed.
  303. Note that you only need to destroy a top-level
  304. ``CanvasWidget``; its child widgets will be destroyed
  305. automatically. If you destroy a non-top-level
  306. ``CanvasWidget``, then the entire top-level widget will be
  307. destroyed.
  308. :raise ValueError: if this ``CanvasWidget`` has a parent.
  309. :rtype: None
  310. """
  311. if self.__parent is not None:
  312. self.__parent.destroy()
  313. return
  314. for tag in self.tags():
  315. self.__canvas.tag_unbind(tag, '<ButtonPress-1>')
  316. self.__canvas.tag_unbind(tag, '<ButtonPress-2>')
  317. self.__canvas.tag_unbind(tag, '<ButtonPress-3>')
  318. self.__canvas.delete(*self.tags())
  319. self.__canvas = None
  320. def update(self, child):
  321. """
  322. Update the graphical display of this canvas widget, and all of
  323. its ancestors, in response to a change in one of this canvas
  324. widget's children.
  325. :param child: The child widget that changed.
  326. :type child: CanvasWidget
  327. """
  328. if self.__hidden or child.__hidden:
  329. return
  330. # If we're already updating, then do nothing. This prevents
  331. # infinite loops when _update modifies its children.
  332. if self.__updating:
  333. return
  334. self.__updating = 1
  335. # Update this CanvasWidget.
  336. self._update(child)
  337. # Propagate update request to the parent.
  338. if self.__parent:
  339. self.__parent.update(self)
  340. # We're done updating.
  341. self.__updating = 0
  342. def manage(self):
  343. """
  344. Arrange this canvas widget and all of its descendants.
  345. :rtype: None
  346. """
  347. if self.__hidden:
  348. return
  349. for child in self.__children:
  350. child.manage()
  351. self._manage()
  352. def tags(self):
  353. """
  354. :return: a list of the canvas tags for all graphical
  355. elements managed by this canvas widget, including
  356. graphical elements managed by its child widgets.
  357. :rtype: list of int
  358. """
  359. if self.__canvas is None:
  360. raise ValueError('Attempt to access a destroyed canvas widget')
  361. tags = []
  362. tags += self._tags()
  363. for child in self.__children:
  364. tags += child.tags()
  365. return tags
  366. def __setitem__(self, attr, value):
  367. """
  368. Set the value of the attribute ``attr`` to ``value``. See the
  369. class documentation for a list of attributes supported by this
  370. canvas widget.
  371. :rtype: None
  372. """
  373. if attr == 'draggable':
  374. self.__draggable = value
  375. else:
  376. raise ValueError('Unknown attribute %r' % attr)
  377. def __getitem__(self, attr):
  378. """
  379. :return: the value of the attribute ``attr``. See the class
  380. documentation for a list of attributes supported by this
  381. canvas widget.
  382. :rtype: (any)
  383. """
  384. if attr == 'draggable':
  385. return self.__draggable
  386. else:
  387. raise ValueError('Unknown attribute %r' % attr)
  388. def __repr__(self):
  389. """
  390. :return: a string representation of this canvas widget.
  391. :rtype: str
  392. """
  393. return '<%s>' % self.__class__.__name__
  394. def hide(self):
  395. """
  396. Temporarily hide this canvas widget.
  397. :rtype: None
  398. """
  399. self.__hidden = 1
  400. for tag in self.tags():
  401. self.__canvas.itemconfig(tag, state='hidden')
  402. def show(self):
  403. """
  404. Show a hidden canvas widget.
  405. :rtype: None
  406. """
  407. self.__hidden = 0
  408. for tag in self.tags():
  409. self.__canvas.itemconfig(tag, state='normal')
  410. def hidden(self):
  411. """
  412. :return: True if this canvas widget is hidden.
  413. :rtype: bool
  414. """
  415. return self.__hidden
  416. ##//////////////////////////////////////////////////////
  417. ## Callback interface
  418. ##//////////////////////////////////////////////////////
  419. def bind_click(self, callback, button=1):
  420. """
  421. Register a new callback that will be called whenever this
  422. ``CanvasWidget`` is clicked on.
  423. :type callback: function
  424. :param callback: The callback function that will be called
  425. whenever this ``CanvasWidget`` is clicked. This function
  426. will be called with this ``CanvasWidget`` as its argument.
  427. :type button: int
  428. :param button: Which button the user should use to click on
  429. this ``CanvasWidget``. Typically, this should be 1 (left
  430. button), 3 (right button), or 2 (middle button).
  431. """
  432. self.__callbacks[button] = callback
  433. def bind_drag(self, callback):
  434. """
  435. Register a new callback that will be called after this
  436. ``CanvasWidget`` is dragged. This implicitly makes this
  437. ``CanvasWidget`` draggable.
  438. :type callback: function
  439. :param callback: The callback function that will be called
  440. whenever this ``CanvasWidget`` is clicked. This function
  441. will be called with this ``CanvasWidget`` as its argument.
  442. """
  443. self.__draggable = 1
  444. self.__callbacks['drag'] = callback
  445. def unbind_click(self, button=1):
  446. """
  447. Remove a callback that was registered with ``bind_click``.
  448. :type button: int
  449. :param button: Which button the user should use to click on
  450. this ``CanvasWidget``. Typically, this should be 1 (left
  451. button), 3 (right button), or 2 (middle button).
  452. """
  453. try:
  454. del self.__callbacks[button]
  455. except:
  456. pass
  457. def unbind_drag(self):
  458. """
  459. Remove a callback that was registered with ``bind_drag``.
  460. """
  461. try:
  462. del self.__callbacks['drag']
  463. except:
  464. pass
  465. ##//////////////////////////////////////////////////////
  466. ## Callback internals
  467. ##//////////////////////////////////////////////////////
  468. def __press_cb(self, event):
  469. """
  470. Handle a button-press event:
  471. - record the button press event in ``self.__press``
  472. - register a button-release callback.
  473. - if this CanvasWidget or any of its ancestors are
  474. draggable, then register the appropriate motion callback.
  475. """
  476. # If we're already waiting for a button release, then ignore
  477. # this new button press.
  478. if (
  479. self.__canvas.bind('<ButtonRelease-1>')
  480. or self.__canvas.bind('<ButtonRelease-2>')
  481. or self.__canvas.bind('<ButtonRelease-3>')
  482. ):
  483. return
  484. # Unbind motion (just in case; this shouldn't be necessary)
  485. self.__canvas.unbind('<Motion>')
  486. # Record the button press event.
  487. self.__press = event
  488. # If any ancestor is draggable, set up a motion callback.
  489. # (Only if they pressed button number 1)
  490. if event.num == 1:
  491. widget = self
  492. while widget is not None:
  493. if widget['draggable']:
  494. widget.__start_drag(event)
  495. break
  496. widget = widget.parent()
  497. # Set up the button release callback.
  498. self.__canvas.bind('<ButtonRelease-%d>' % event.num, self.__release_cb)
  499. def __start_drag(self, event):
  500. """
  501. Begin dragging this object:
  502. - register a motion callback
  503. - record the drag coordinates
  504. """
  505. self.__canvas.bind('<Motion>', self.__motion_cb)
  506. self.__drag_x = event.x
  507. self.__drag_y = event.y
  508. def __motion_cb(self, event):
  509. """
  510. Handle a motion event:
  511. - move this object to the new location
  512. - record the new drag coordinates
  513. """
  514. self.move(event.x - self.__drag_x, event.y - self.__drag_y)
  515. self.__drag_x = event.x
  516. self.__drag_y = event.y
  517. def __release_cb(self, event):
  518. """
  519. Handle a release callback:
  520. - unregister motion & button release callbacks.
  521. - decide whether they clicked, dragged, or cancelled
  522. - call the appropriate handler.
  523. """
  524. # Unbind the button release & motion callbacks.
  525. self.__canvas.unbind('<ButtonRelease-%d>' % event.num)
  526. self.__canvas.unbind('<Motion>')
  527. # Is it a click or a drag?
  528. if (
  529. event.time - self.__press.time < 100
  530. and abs(event.x - self.__press.x) + abs(event.y - self.__press.y) < 5
  531. ):
  532. # Move it back, if we were dragging.
  533. if self.__draggable and event.num == 1:
  534. self.move(
  535. self.__press.x - self.__drag_x, self.__press.y - self.__drag_y
  536. )
  537. self.__click(event.num)
  538. elif event.num == 1:
  539. self.__drag()
  540. self.__press = None
  541. def __drag(self):
  542. """
  543. If this ``CanvasWidget`` has a drag callback, then call it;
  544. otherwise, find the closest ancestor with a drag callback, and
  545. call it. If no ancestors have a drag callback, do nothing.
  546. """
  547. if self.__draggable:
  548. if 'drag' in self.__callbacks:
  549. cb = self.__callbacks['drag']
  550. try:
  551. cb(self)
  552. except:
  553. print('Error in drag callback for %r' % self)
  554. elif self.__parent is not None:
  555. self.__parent.__drag()
  556. def __click(self, button):
  557. """
  558. If this ``CanvasWidget`` has a drag callback, then call it;
  559. otherwise, find the closest ancestor with a click callback, and
  560. call it. If no ancestors have a click callback, do nothing.
  561. """
  562. if button in self.__callbacks:
  563. cb = self.__callbacks[button]
  564. # try:
  565. cb(self)
  566. # except:
  567. # print 'Error in click callback for %r' % self
  568. # raise
  569. elif self.__parent is not None:
  570. self.__parent.__click(button)
  571. ##//////////////////////////////////////////////////////
  572. ## Child/parent Handling
  573. ##//////////////////////////////////////////////////////
  574. def _add_child_widget(self, child):
  575. """
  576. Register a hierarchical child widget. The child will be
  577. considered part of this canvas widget for purposes of user
  578. interaction. ``_add_child_widget`` has two direct effects:
  579. - It sets ``child``'s parent to this canvas widget.
  580. - It adds ``child`` to the list of canvas widgets returned by
  581. the ``child_widgets`` member function.
  582. :param child: The new child widget. ``child`` must not already
  583. have a parent.
  584. :type child: CanvasWidget
  585. """
  586. if not hasattr(self, '_CanvasWidget__children'):
  587. self.__children = []
  588. if child.__parent is not None:
  589. raise ValueError('{} already has a parent'.format(child))
  590. child.__parent = self
  591. self.__children.append(child)
  592. def _remove_child_widget(self, child):
  593. """
  594. Remove a hierarchical child widget. This child will no longer
  595. be considered part of this canvas widget for purposes of user
  596. interaction. ``_add_child_widget`` has two direct effects:
  597. - It sets ``child``'s parent to None.
  598. - It removes ``child`` from the list of canvas widgets
  599. returned by the ``child_widgets`` member function.
  600. :param child: The child widget to remove. ``child`` must be a
  601. child of this canvas widget.
  602. :type child: CanvasWidget
  603. """
  604. self.__children.remove(child)
  605. child.__parent = None
  606. ##//////////////////////////////////////////////////////
  607. ## Defined by subclass
  608. ##//////////////////////////////////////////////////////
  609. @abstractmethod
  610. def _tags(self):
  611. """
  612. :return: a list of canvas tags for all graphical elements
  613. managed by this canvas widget, not including graphical
  614. elements managed by its child widgets.
  615. :rtype: list of int
  616. """
  617. def _manage(self):
  618. """
  619. Arrange the child widgets of this canvas widget. This method
  620. is called when the canvas widget is initially created. It is
  621. also called if the user calls the ``manage`` method on this
  622. canvas widget or any of its ancestors.
  623. :rtype: None
  624. """
  625. def _update(self, child):
  626. """
  627. Update this canvas widget in response to a change in one of
  628. its children.
  629. :param child: The child that changed.
  630. :type child: CanvasWidget
  631. :rtype: None
  632. """
  633. ##//////////////////////////////////////////////////////
  634. ## Basic widgets.
  635. ##//////////////////////////////////////////////////////
  636. class TextWidget(CanvasWidget):
  637. """
  638. A canvas widget that displays a single string of text.
  639. Attributes:
  640. - ``color``: the color of the text.
  641. - ``font``: the font used to display the text.
  642. - ``justify``: justification for multi-line texts. Valid values
  643. are ``left``, ``center``, and ``right``.
  644. - ``width``: the width of the text. If the text is wider than
  645. this width, it will be line-wrapped at whitespace.
  646. - ``draggable``: whether the text can be dragged by the user.
  647. """
  648. def __init__(self, canvas, text, **attribs):
  649. """
  650. Create a new text widget.
  651. :type canvas: Tkinter.Canvas
  652. :param canvas: This canvas widget's canvas.
  653. :type text: str
  654. :param text: The string of text to display.
  655. :param attribs: The new canvas widget's attributes.
  656. """
  657. self._text = text
  658. self._tag = canvas.create_text(1, 1, text=text)
  659. CanvasWidget.__init__(self, canvas, **attribs)
  660. def __setitem__(self, attr, value):
  661. if attr in ('color', 'font', 'justify', 'width'):
  662. if attr == 'color':
  663. attr = 'fill'
  664. self.canvas().itemconfig(self._tag, {attr: value})
  665. else:
  666. CanvasWidget.__setitem__(self, attr, value)
  667. def __getitem__(self, attr):
  668. if attr == 'width':
  669. return int(self.canvas().itemcget(self._tag, attr))
  670. elif attr in ('color', 'font', 'justify'):
  671. if attr == 'color':
  672. attr = 'fill'
  673. return self.canvas().itemcget(self._tag, attr)
  674. else:
  675. return CanvasWidget.__getitem__(self, attr)
  676. def _tags(self):
  677. return [self._tag]
  678. def text(self):
  679. """
  680. :return: The text displayed by this text widget.
  681. :rtype: str
  682. """
  683. return self.canvas().itemcget(self._tag, 'TEXT')
  684. def set_text(self, text):
  685. """
  686. Change the text that is displayed by this text widget.
  687. :type text: str
  688. :param text: The string of text to display.
  689. :rtype: None
  690. """
  691. self.canvas().itemconfig(self._tag, text=text)
  692. if self.parent() is not None:
  693. self.parent().update(self)
  694. def __repr__(self):
  695. return '[Text: %r]' % self._text
  696. class SymbolWidget(TextWidget):
  697. """
  698. A canvas widget that displays special symbols, such as the
  699. negation sign and the exists operator. Symbols are specified by
  700. name. Currently, the following symbol names are defined: ``neg``,
  701. ``disj``, ``conj``, ``lambda``, ``merge``, ``forall``, ``exists``,
  702. ``subseteq``, ``subset``, ``notsubset``, ``emptyset``, ``imp``,
  703. ``rightarrow``, ``equal``, ``notequal``, ``epsilon``.
  704. Attributes:
  705. - ``color``: the color of the text.
  706. - ``draggable``: whether the text can be dragged by the user.
  707. :cvar SYMBOLS: A dictionary mapping from symbols to the character
  708. in the ``symbol`` font used to render them.
  709. """
  710. SYMBOLS = {
  711. 'neg': '\330',
  712. 'disj': '\332',
  713. 'conj': '\331',
  714. 'lambda': '\154',
  715. 'merge': '\304',
  716. 'forall': '\042',
  717. 'exists': '\044',
  718. 'subseteq': '\315',
  719. 'subset': '\314',
  720. 'notsubset': '\313',
  721. 'emptyset': '\306',
  722. 'imp': '\336',
  723. 'rightarrow': chr(222), #'\256',
  724. 'equal': '\75',
  725. 'notequal': '\271',
  726. 'intersection': '\307',
  727. 'union': '\310',
  728. 'epsilon': 'e',
  729. }
  730. def __init__(self, canvas, symbol, **attribs):
  731. """
  732. Create a new symbol widget.
  733. :type canvas: Tkinter.Canvas
  734. :param canvas: This canvas widget's canvas.
  735. :type symbol: str
  736. :param symbol: The name of the symbol to display.
  737. :param attribs: The new canvas widget's attributes.
  738. """
  739. attribs['font'] = 'symbol'
  740. TextWidget.__init__(self, canvas, '', **attribs)
  741. self.set_symbol(symbol)
  742. def symbol(self):
  743. """
  744. :return: the name of the symbol that is displayed by this
  745. symbol widget.
  746. :rtype: str
  747. """
  748. return self._symbol
  749. def set_symbol(self, symbol):
  750. """
  751. Change the symbol that is displayed by this symbol widget.
  752. :type symbol: str
  753. :param symbol: The name of the symbol to display.
  754. """
  755. if symbol not in SymbolWidget.SYMBOLS:
  756. raise ValueError('Unknown symbol: %s' % symbol)
  757. self._symbol = symbol
  758. self.set_text(SymbolWidget.SYMBOLS[symbol])
  759. def __repr__(self):
  760. return '[Symbol: %r]' % self._symbol
  761. @staticmethod
  762. def symbolsheet(size=20):
  763. """
  764. Open a new Tkinter window that displays the entire alphabet
  765. for the symbol font. This is useful for constructing the
  766. ``SymbolWidget.SYMBOLS`` dictionary.
  767. """
  768. top = Tk()
  769. def destroy(e, top=top):
  770. top.destroy()
  771. top.bind('q', destroy)
  772. Button(top, text='Quit', command=top.destroy).pack(side='bottom')
  773. text = Text(top, font=('helvetica', -size), width=20, height=30)
  774. text.pack(side='left')
  775. sb = Scrollbar(top, command=text.yview)
  776. text['yscrollcommand'] = sb.set
  777. sb.pack(side='right', fill='y')
  778. text.tag_config('symbol', font=('symbol', -size))
  779. for i in range(256):
  780. if i in (0, 10):
  781. continue # null and newline
  782. for k, v in list(SymbolWidget.SYMBOLS.items()):
  783. if v == chr(i):
  784. text.insert('end', '%-10s\t' % k)
  785. break
  786. else:
  787. text.insert('end', '%-10d \t' % i)
  788. text.insert('end', '[%s]\n' % chr(i), 'symbol')
  789. top.mainloop()
  790. class AbstractContainerWidget(CanvasWidget):
  791. """
  792. An abstract class for canvas widgets that contain a single child,
  793. such as ``BoxWidget`` and ``OvalWidget``. Subclasses must define
  794. a constructor, which should create any new graphical elements and
  795. then call the ``AbstractCanvasContainer`` constructor. Subclasses
  796. must also define the ``_update`` method and the ``_tags`` method;
  797. and any subclasses that define attributes should define
  798. ``__setitem__`` and ``__getitem__``.
  799. """
  800. def __init__(self, canvas, child, **attribs):
  801. """
  802. Create a new container widget. This constructor should only
  803. be called by subclass constructors.
  804. :type canvas: Tkinter.Canvas
  805. :param canvas: This canvas widget's canvas.
  806. :param child: The container's child widget. ``child`` must not
  807. have a parent.
  808. :type child: CanvasWidget
  809. :param attribs: The new canvas widget's attributes.
  810. """
  811. self._child = child
  812. self._add_child_widget(child)
  813. CanvasWidget.__init__(self, canvas, **attribs)
  814. def _manage(self):
  815. self._update(self._child)
  816. def child(self):
  817. """
  818. :return: The child widget contained by this container widget.
  819. :rtype: CanvasWidget
  820. """
  821. return self._child
  822. def set_child(self, child):
  823. """
  824. Change the child widget contained by this container widget.
  825. :param child: The new child widget. ``child`` must not have a
  826. parent.
  827. :type child: CanvasWidget
  828. :rtype: None
  829. """
  830. self._remove_child_widget(self._child)
  831. self._add_child_widget(child)
  832. self._child = child
  833. self.update(child)
  834. def __repr__(self):
  835. name = self.__class__.__name__
  836. if name[-6:] == 'Widget':
  837. name = name[:-6]
  838. return '[%s: %r]' % (name, self._child)
  839. class BoxWidget(AbstractContainerWidget):
  840. """
  841. A canvas widget that places a box around a child widget.
  842. Attributes:
  843. - ``fill``: The color used to fill the interior of the box.
  844. - ``outline``: The color used to draw the outline of the box.
  845. - ``width``: The width of the outline of the box.
  846. - ``margin``: The number of pixels space left between the child
  847. and the box.
  848. - ``draggable``: whether the text can be dragged by the user.
  849. """
  850. def __init__(self, canvas, child, **attribs):
  851. """
  852. Create a new box widget.
  853. :type canvas: Tkinter.Canvas
  854. :param canvas: This canvas widget's canvas.
  855. :param child: The child widget. ``child`` must not have a
  856. parent.
  857. :type child: CanvasWidget
  858. :param attribs: The new canvas widget's attributes.
  859. """
  860. self._child = child
  861. self._margin = 1
  862. self._box = canvas.create_rectangle(1, 1, 1, 1)
  863. canvas.tag_lower(self._box)
  864. AbstractContainerWidget.__init__(self, canvas, child, **attribs)
  865. def __setitem__(self, attr, value):
  866. if attr == 'margin':
  867. self._margin = value
  868. elif attr in ('outline', 'fill', 'width'):
  869. self.canvas().itemconfig(self._box, {attr: value})
  870. else:
  871. CanvasWidget.__setitem__(self, attr, value)
  872. def __getitem__(self, attr):
  873. if attr == 'margin':
  874. return self._margin
  875. elif attr == 'width':
  876. return float(self.canvas().itemcget(self._box, attr))
  877. elif attr in ('outline', 'fill', 'width'):
  878. return self.canvas().itemcget(self._box, attr)
  879. else:
  880. return CanvasWidget.__getitem__(self, attr)
  881. def _update(self, child):
  882. (x1, y1, x2, y2) = child.bbox()
  883. margin = self._margin + self['width'] / 2
  884. self.canvas().coords(
  885. self._box, x1 - margin, y1 - margin, x2 + margin, y2 + margin
  886. )
  887. def _tags(self):
  888. return [self._box]
  889. class OvalWidget(AbstractContainerWidget):
  890. """
  891. A canvas widget that places a oval around a child widget.
  892. Attributes:
  893. - ``fill``: The color used to fill the interior of the oval.
  894. - ``outline``: The color used to draw the outline of the oval.
  895. - ``width``: The width of the outline of the oval.
  896. - ``margin``: The number of pixels space left between the child
  897. and the oval.
  898. - ``draggable``: whether the text can be dragged by the user.
  899. - ``double``: If true, then a double-oval is drawn.
  900. """
  901. def __init__(self, canvas, child, **attribs):
  902. """
  903. Create a new oval widget.
  904. :type canvas: Tkinter.Canvas
  905. :param canvas: This canvas widget's canvas.
  906. :param child: The child widget. ``child`` must not have a
  907. parent.
  908. :type child: CanvasWidget
  909. :param attribs: The new canvas widget's attributes.
  910. """
  911. self._child = child
  912. self._margin = 1
  913. self._oval = canvas.create_oval(1, 1, 1, 1)
  914. self._circle = attribs.pop('circle', False)
  915. self._double = attribs.pop('double', False)
  916. if self._double:
  917. self._oval2 = canvas.create_oval(1, 1, 1, 1)
  918. else:
  919. self._oval2 = None
  920. canvas.tag_lower(self._oval)
  921. AbstractContainerWidget.__init__(self, canvas, child, **attribs)
  922. def __setitem__(self, attr, value):
  923. c = self.canvas()
  924. if attr == 'margin':
  925. self._margin = value
  926. elif attr == 'double':
  927. if value == True and self._oval2 is None:
  928. # Copy attributes & position from self._oval.
  929. x1, y1, x2, y2 = c.bbox(self._oval)
  930. w = self['width'] * 2
  931. self._oval2 = c.create_oval(
  932. x1 - w,
  933. y1 - w,
  934. x2 + w,
  935. y2 + w,
  936. outline=c.itemcget(self._oval, 'outline'),
  937. width=c.itemcget(self._oval, 'width'),
  938. )
  939. c.tag_lower(self._oval2)
  940. if value == False and self._oval2 is not None:
  941. c.delete(self._oval2)
  942. self._oval2 = None
  943. elif attr in ('outline', 'fill', 'width'):
  944. c.itemconfig(self._oval, {attr: value})
  945. if self._oval2 is not None and attr != 'fill':
  946. c.itemconfig(self._oval2, {attr: value})
  947. if self._oval2 is not None and attr != 'fill':
  948. self.canvas().itemconfig(self._oval2, {attr: value})
  949. else:
  950. CanvasWidget.__setitem__(self, attr, value)
  951. def __getitem__(self, attr):
  952. if attr == 'margin':
  953. return self._margin
  954. elif attr == 'double':
  955. return self._double is not None
  956. elif attr == 'width':
  957. return float(self.canvas().itemcget(self._oval, attr))
  958. elif attr in ('outline', 'fill', 'width'):
  959. return self.canvas().itemcget(self._oval, attr)
  960. else:
  961. return CanvasWidget.__getitem__(self, attr)
  962. # The ratio between inscribed & circumscribed ovals
  963. RATIO = 1.4142135623730949
  964. def _update(self, child):
  965. R = OvalWidget.RATIO
  966. (x1, y1, x2, y2) = child.bbox()
  967. margin = self._margin
  968. # If we're a circle, pretend our contents are square.
  969. if self._circle:
  970. dx, dy = abs(x1 - x2), abs(y1 - y2)
  971. if dx > dy:
  972. y = (y1 + y2) / 2
  973. y1, y2 = y - dx / 2, y + dx / 2
  974. elif dy > dx:
  975. x = (x1 + x2) / 2
  976. x1, x2 = x - dy / 2, x + dy / 2
  977. # Find the four corners.
  978. left = int((x1 * (1 + R) + x2 * (1 - R)) / 2)
  979. right = left + int((x2 - x1) * R)
  980. top = int((y1 * (1 + R) + y2 * (1 - R)) / 2)
  981. bot = top + int((y2 - y1) * R)
  982. self.canvas().coords(
  983. self._oval, left - margin, top - margin, right + margin, bot + margin
  984. )
  985. if self._oval2 is not None:
  986. self.canvas().coords(
  987. self._oval2,
  988. left - margin + 2,
  989. top - margin + 2,
  990. right + margin - 2,
  991. bot + margin - 2,
  992. )
  993. def _tags(self):
  994. if self._oval2 is None:
  995. return [self._oval]
  996. else:
  997. return [self._oval, self._oval2]
  998. class ParenWidget(AbstractContainerWidget):
  999. """
  1000. A canvas widget that places a pair of parenthases around a child
  1001. widget.
  1002. Attributes:
  1003. - ``color``: The color used to draw the parenthases.
  1004. - ``width``: The width of the parenthases.
  1005. - ``draggable``: whether the text can be dragged by the user.
  1006. """
  1007. def __init__(self, canvas, child, **attribs):
  1008. """
  1009. Create a new parenthasis widget.
  1010. :type canvas: Tkinter.Canvas
  1011. :param canvas: This canvas widget's canvas.
  1012. :param child: The child widget. ``child`` must not have a
  1013. parent.
  1014. :type child: CanvasWidget
  1015. :param attribs: The new canvas widget's attributes.
  1016. """
  1017. self._child = child
  1018. self._oparen = canvas.create_arc(1, 1, 1, 1, style='arc', start=90, extent=180)
  1019. self._cparen = canvas.create_arc(1, 1, 1, 1, style='arc', start=-90, extent=180)
  1020. AbstractContainerWidget.__init__(self, canvas, child, **attribs)
  1021. def __setitem__(self, attr, value):
  1022. if attr == 'color':
  1023. self.canvas().itemconfig(self._oparen, outline=value)
  1024. self.canvas().itemconfig(self._cparen, outline=value)
  1025. elif attr == 'width':
  1026. self.canvas().itemconfig(self._oparen, width=value)
  1027. self.canvas().itemconfig(self._cparen, width=value)
  1028. else:
  1029. CanvasWidget.__setitem__(self, attr, value)
  1030. def __getitem__(self, attr):
  1031. if attr == 'color':
  1032. return self.canvas().itemcget(self._oparen, 'outline')
  1033. elif attr == 'width':
  1034. return self.canvas().itemcget(self._oparen, 'width')
  1035. else:
  1036. return CanvasWidget.__getitem__(self, attr)
  1037. def _update(self, child):
  1038. (x1, y1, x2, y2) = child.bbox()
  1039. width = max((y2 - y1) / 6, 4)
  1040. self.canvas().coords(self._oparen, x1 - width, y1, x1 + width, y2)
  1041. self.canvas().coords(self._cparen, x2 - width, y1, x2 + width, y2)
  1042. def _tags(self):
  1043. return [self._oparen, self._cparen]
  1044. class BracketWidget(AbstractContainerWidget):
  1045. """
  1046. A canvas widget that places a pair of brackets around a child
  1047. widget.
  1048. Attributes:
  1049. - ``color``: The color used to draw the brackets.
  1050. - ``width``: The width of the brackets.
  1051. - ``draggable``: whether the text can be dragged by the user.
  1052. """
  1053. def __init__(self, canvas, child, **attribs):
  1054. """
  1055. Create a new bracket widget.
  1056. :type canvas: Tkinter.Canvas
  1057. :param canvas: This canvas widget's canvas.
  1058. :param child: The child widget. ``child`` must not have a
  1059. parent.
  1060. :type child: CanvasWidget
  1061. :param attribs: The new canvas widget's attributes.
  1062. """
  1063. self._child = child
  1064. self._obrack = canvas.create_line(1, 1, 1, 1, 1, 1, 1, 1)
  1065. self._cbrack = canvas.create_line(1, 1, 1, 1, 1, 1, 1, 1)
  1066. AbstractContainerWidget.__init__(self, canvas, child, **attribs)
  1067. def __setitem__(self, attr, value):
  1068. if attr == 'color':
  1069. self.canvas().itemconfig(self._obrack, fill=value)
  1070. self.canvas().itemconfig(self._cbrack, fill=value)
  1071. elif attr == 'width':
  1072. self.canvas().itemconfig(self._obrack, width=value)
  1073. self.canvas().itemconfig(self._cbrack, width=value)
  1074. else:
  1075. CanvasWidget.__setitem__(self, attr, value)
  1076. def __getitem__(self, attr):
  1077. if attr == 'color':
  1078. return self.canvas().itemcget(self._obrack, 'outline')
  1079. elif attr == 'width':
  1080. return self.canvas().itemcget(self._obrack, 'width')
  1081. else:
  1082. return CanvasWidget.__getitem__(self, attr)
  1083. def _update(self, child):
  1084. (x1, y1, x2, y2) = child.bbox()
  1085. width = max((y2 - y1) / 8, 2)
  1086. self.canvas().coords(
  1087. self._obrack, x1, y1, x1 - width, y1, x1 - width, y2, x1, y2
  1088. )
  1089. self.canvas().coords(
  1090. self._cbrack, x2, y1, x2 + width, y1, x2 + width, y2, x2, y2
  1091. )
  1092. def _tags(self):
  1093. return [self._obrack, self._cbrack]
  1094. class SequenceWidget(CanvasWidget):
  1095. """
  1096. A canvas widget that keeps a list of canvas widgets in a
  1097. horizontal line.
  1098. Attributes:
  1099. - ``align``: The vertical alignment of the children. Possible
  1100. values are ``'top'``, ``'center'``, and ``'bottom'``. By
  1101. default, children are center-aligned.
  1102. - ``space``: The amount of horizontal space to place between
  1103. children. By default, one pixel of space is used.
  1104. - ``ordered``: If true, then keep the children in their
  1105. original order.
  1106. """
  1107. def __init__(self, canvas, *children, **attribs):
  1108. """
  1109. Create a new sequence widget.
  1110. :type canvas: Tkinter.Canvas
  1111. :param canvas: This canvas widget's canvas.
  1112. :param children: The widgets that should be aligned
  1113. horizontally. Each child must not have a parent.
  1114. :type children: list(CanvasWidget)
  1115. :param attribs: The new canvas widget's attributes.
  1116. """
  1117. self._align = 'center'
  1118. self._space = 1
  1119. self._ordered = False
  1120. self._children = list(children)
  1121. for child in children:
  1122. self._add_child_widget(child)
  1123. CanvasWidget.__init__(self, canvas, **attribs)
  1124. def __setitem__(self, attr, value):
  1125. if attr == 'align':
  1126. if value not in ('top', 'bottom', 'center'):
  1127. raise ValueError('Bad alignment: %r' % value)
  1128. self._align = value
  1129. elif attr == 'space':
  1130. self._space = value
  1131. elif attr == 'ordered':
  1132. self._ordered = value
  1133. else:
  1134. CanvasWidget.__setitem__(self, attr, value)
  1135. def __getitem__(self, attr):
  1136. if attr == 'align':
  1137. return self._align
  1138. elif attr == 'space':
  1139. return self._space
  1140. elif attr == 'ordered':
  1141. return self._ordered
  1142. else:
  1143. return CanvasWidget.__getitem__(self, attr)
  1144. def _tags(self):
  1145. return []
  1146. def _yalign(self, top, bot):
  1147. if self._align == 'top':
  1148. return top
  1149. if self._align == 'bottom':
  1150. return bot
  1151. if self._align == 'center':
  1152. return (top + bot) / 2
  1153. def _update(self, child):
  1154. # Align all children with child.
  1155. (left, top, right, bot) = child.bbox()
  1156. y = self._yalign(top, bot)
  1157. for c in self._children:
  1158. (x1, y1, x2, y2) = c.bbox()
  1159. c.move(0, y - self._yalign(y1, y2))
  1160. if self._ordered and len(self._children) > 1:
  1161. index = self._children.index(child)
  1162. x = right + self._space
  1163. for i in range(index + 1, len(self._children)):
  1164. (x1, y1, x2, y2) = self._children[i].bbox()
  1165. if x > x1:
  1166. self._children[i].move(x - x1, 0)
  1167. x += x2 - x1 + self._space
  1168. x = left - self._space
  1169. for i in range(index - 1, -1, -1):
  1170. (x1, y1, x2, y2) = self._children[i].bbox()
  1171. if x < x2:
  1172. self._children[i].move(x - x2, 0)
  1173. x -= x2 - x1 + self._space
  1174. def _manage(self):
  1175. if len(self._children) == 0:
  1176. return
  1177. child = self._children[0]
  1178. # Align all children with child.
  1179. (left, top, right, bot) = child.bbox()
  1180. y = self._yalign(top, bot)
  1181. index = self._children.index(child)
  1182. # Line up children to the right of child.
  1183. x = right + self._space
  1184. for i in range(index + 1, len(self._children)):
  1185. (x1, y1, x2, y2) = self._children[i].bbox()
  1186. self._children[i].move(x - x1, y - self._yalign(y1, y2))
  1187. x += x2 - x1 + self._space
  1188. # Line up children to the left of child.
  1189. x = left - self._space
  1190. for i in range(index - 1, -1, -1):
  1191. (x1, y1, x2, y2) = self._children[i].bbox()
  1192. self._children[i].move(x - x2, y - self._yalign(y1, y2))
  1193. x -= x2 - x1 + self._space
  1194. def __repr__(self):
  1195. return '[Sequence: ' + repr(self._children)[1:-1] + ']'
  1196. # Provide an alias for the child_widgets() member.
  1197. children = CanvasWidget.child_widgets
  1198. def replace_child(self, oldchild, newchild):
  1199. """
  1200. Replace the child canvas widget ``oldchild`` with ``newchild``.
  1201. ``newchild`` must not have a parent. ``oldchild``'s parent will
  1202. be set to None.
  1203. :type oldchild: CanvasWidget
  1204. :param oldchild: The child canvas widget to remove.
  1205. :type newchild: CanvasWidget
  1206. :param newchild: The canvas widget that should replace
  1207. ``oldchild``.
  1208. """
  1209. index = self._children.index(oldchild)
  1210. self._children[index] = newchild
  1211. self._remove_child_widget(oldchild)
  1212. self._add_child_widget(newchild)
  1213. self.update(newchild)
  1214. def remove_child(self, child):
  1215. """
  1216. Remove the given child canvas widget. ``child``'s parent will
  1217. be set ot None.
  1218. :type child: CanvasWidget
  1219. :param child: The child canvas widget to remove.
  1220. """
  1221. index = self._children.index(child)
  1222. del self._children[index]
  1223. self._remove_child_widget(child)
  1224. if len(self._children) > 0:
  1225. self.update(self._children[0])
  1226. def insert_child(self, index, child):
  1227. """
  1228. Insert a child canvas widget before a given index.
  1229. :type child: CanvasWidget
  1230. :param child: The canvas widget that should be inserted.
  1231. :type index: int
  1232. :param index: The index where the child widget should be
  1233. inserted. In particular, the index of ``child`` will be
  1234. ``index``; and the index of any children whose indices were
  1235. greater than equal to ``index`` before ``child`` was
  1236. inserted will be incremented by one.
  1237. """
  1238. self._children.insert(index, child)
  1239. self._add_child_widget(child)
  1240. class StackWidget(CanvasWidget):
  1241. """
  1242. A canvas widget that keeps a list of canvas widgets in a vertical
  1243. line.
  1244. Attributes:
  1245. - ``align``: The horizontal alignment of the children. Possible
  1246. values are ``'left'``, ``'center'``, and ``'right'``. By
  1247. default, children are center-aligned.
  1248. - ``space``: The amount of vertical space to place between
  1249. children. By default, one pixel of space is used.
  1250. - ``ordered``: If true, then keep the children in their
  1251. original order.
  1252. """
  1253. def __init__(self, canvas, *children, **attribs):
  1254. """
  1255. Create a new stack widget.
  1256. :type canvas: Tkinter.Canvas
  1257. :param canvas: This canvas widget's canvas.
  1258. :param children: The widgets that should be aligned
  1259. vertically. Each child must not have a parent.
  1260. :type children: list(CanvasWidget)
  1261. :param attribs: The new canvas widget's attributes.
  1262. """
  1263. self._align = 'center'
  1264. self._space = 1
  1265. self._ordered = False
  1266. self._children = list(children)
  1267. for child in children:
  1268. self._add_child_widget(child)
  1269. CanvasWidget.__init__(self, canvas, **attribs)
  1270. def __setitem__(self, attr, value):
  1271. if attr == 'align':
  1272. if value not in ('left', 'right', 'center'):
  1273. raise ValueError('Bad alignment: %r' % value)
  1274. self._align = value
  1275. elif attr == 'space':
  1276. self._space = value
  1277. elif attr == 'ordered':
  1278. self._ordered = value
  1279. else:
  1280. CanvasWidget.__setitem__(self, attr, value)
  1281. def __getitem__(self, attr):
  1282. if attr == 'align':
  1283. return self._align
  1284. elif attr == 'space':
  1285. return self._space
  1286. elif attr == 'ordered':
  1287. return self._ordered
  1288. else:
  1289. return CanvasWidget.__getitem__(self, attr)
  1290. def _tags(self):
  1291. return []
  1292. def _xalign(self, left, right):
  1293. if self._align == 'left':
  1294. return left
  1295. if self._align == 'right':
  1296. return right
  1297. if self._align == 'center':
  1298. return (left + right) / 2
  1299. def _update(self, child):
  1300. # Align all children with child.
  1301. (left, top, right, bot) = child.bbox()
  1302. x = self._xalign(left, right)
  1303. for c in self._children:
  1304. (x1, y1, x2, y2) = c.bbox()
  1305. c.move(x - self._xalign(x1, x2), 0)
  1306. if self._ordered and len(self._children) > 1:
  1307. index = self._children.index(child)
  1308. y = bot + self._space
  1309. for i in range(index + 1, len(self._children)):
  1310. (x1, y1, x2, y2) = self._children[i].bbox()
  1311. if y > y1:
  1312. self._children[i].move(0, y - y1)
  1313. y += y2 - y1 + self._space
  1314. y = top - self._space
  1315. for i in range(index - 1, -1, -1):
  1316. (x1, y1, x2, y2) = self._children[i].bbox()
  1317. if y < y2:
  1318. self._children[i].move(0, y - y2)
  1319. y -= y2 - y1 + self._space
  1320. def _manage(self):
  1321. if len(self._children) == 0:
  1322. return
  1323. child = self._children[0]
  1324. # Align all children with child.
  1325. (left, top, right, bot) = child.bbox()
  1326. x = self._xalign(left, right)
  1327. index = self._children.index(child)
  1328. # Line up children below the child.
  1329. y = bot + self._space
  1330. for i in range(index + 1, len(self._children)):
  1331. (x1, y1, x2, y2) = self._children[i].bbox()
  1332. self._children[i].move(x - self._xalign(x1, x2), y - y1)
  1333. y += y2 - y1 + self._space
  1334. # Line up children above the child.
  1335. y = top - self._space
  1336. for i in range(index - 1, -1, -1):
  1337. (x1, y1, x2, y2) = self._children[i].bbox()
  1338. self._children[i].move(x - self._xalign(x1, x2), y - y2)
  1339. y -= y2 - y1 + self._space
  1340. def __repr__(self):
  1341. return '[Stack: ' + repr(self._children)[1:-1] + ']'
  1342. # Provide an alias for the child_widgets() member.
  1343. children = CanvasWidget.child_widgets
  1344. def replace_child(self, oldchild, newchild):
  1345. """
  1346. Replace the child canvas widget ``oldchild`` with ``newchild``.
  1347. ``newchild`` must not have a parent. ``oldchild``'s parent will
  1348. be set to None.
  1349. :type oldchild: CanvasWidget
  1350. :param oldchild: The child canvas widget to remove.
  1351. :type newchild: CanvasWidget
  1352. :param newchild: The canvas widget that should replace
  1353. ``oldchild``.
  1354. """
  1355. index = self._children.index(oldchild)
  1356. self._children[index] = newchild
  1357. self._remove_child_widget(oldchild)
  1358. self._add_child_widget(newchild)
  1359. self.update(newchild)
  1360. def remove_child(self, child):
  1361. """
  1362. Remove the given child canvas widget. ``child``'s parent will
  1363. be set ot None.
  1364. :type child: CanvasWidget
  1365. :param child: The child canvas widget to remove.
  1366. """
  1367. index = self._children.index(child)
  1368. del self._children[index]
  1369. self._remove_child_widget(child)
  1370. if len(self._children) > 0:
  1371. self.update(self._children[0])
  1372. def insert_child(self, index, child):
  1373. """
  1374. Insert a child canvas widget before a given index.
  1375. :type child: CanvasWidget
  1376. :param child: The canvas widget that should be inserted.
  1377. :type index: int
  1378. :param index: The index where the child widget should be
  1379. inserted. In particular, the index of ``child`` will be
  1380. ``index``; and the index of any children whose indices were
  1381. greater than equal to ``index`` before ``child`` was
  1382. inserted will be incremented by one.
  1383. """
  1384. self._children.insert(index, child)
  1385. self._add_child_widget(child)
  1386. class SpaceWidget(CanvasWidget):
  1387. """
  1388. A canvas widget that takes up space but does not display
  1389. anything. A ``SpaceWidget`` can be used to add space between
  1390. elements. Each space widget is characterized by a width and a
  1391. height. If you wish to only create horizontal space, then use a
  1392. height of zero; and if you wish to only create vertical space, use
  1393. a width of zero.
  1394. """
  1395. def __init__(self, canvas, width, height, **attribs):
  1396. """
  1397. Create a new space widget.
  1398. :type canvas: Tkinter.Canvas
  1399. :param canvas: This canvas widget's canvas.
  1400. :type width: int
  1401. :param width: The width of the new space widget.
  1402. :type height: int
  1403. :param height: The height of the new space widget.
  1404. :param attribs: The new canvas widget's attributes.
  1405. """
  1406. # For some reason,
  1407. if width > 4:
  1408. width -= 4
  1409. if height > 4:
  1410. height -= 4
  1411. self._tag = canvas.create_line(1, 1, width, height, fill='')
  1412. CanvasWidget.__init__(self, canvas, **attribs)
  1413. # note: width() and height() are already defined by CanvasWidget.
  1414. def set_width(self, width):
  1415. """
  1416. Change the width of this space widget.
  1417. :param width: The new width.
  1418. :type width: int
  1419. :rtype: None
  1420. """
  1421. [x1, y1, x2, y2] = self.bbox()
  1422. self.canvas().coords(self._tag, x1, y1, x1 + width, y2)
  1423. def set_height(self, height):
  1424. """
  1425. Change the height of this space widget.
  1426. :param height: The new height.
  1427. :type height: int
  1428. :rtype: None
  1429. """
  1430. [x1, y1, x2, y2] = self.bbox()
  1431. self.canvas().coords(self._tag, x1, y1, x2, y1 + height)
  1432. def _tags(self):
  1433. return [self._tag]
  1434. def __repr__(self):
  1435. return '[Space]'
  1436. class ScrollWatcherWidget(CanvasWidget):
  1437. """
  1438. A special canvas widget that adjusts its ``Canvas``'s scrollregion
  1439. to always include the bounding boxes of all of its children. The
  1440. scroll-watcher widget will only increase the size of the
  1441. ``Canvas``'s scrollregion; it will never decrease it.
  1442. """
  1443. def __init__(self, canvas, *children, **attribs):
  1444. """
  1445. Create a new scroll-watcher widget.
  1446. :type canvas: Tkinter.Canvas
  1447. :param canvas: This canvas widget's canvas.
  1448. :type children: list(CanvasWidget)
  1449. :param children: The canvas widgets watched by the
  1450. scroll-watcher. The scroll-watcher will ensure that these
  1451. canvas widgets are always contained in their canvas's
  1452. scrollregion.
  1453. :param attribs: The new canvas widget's attributes.
  1454. """
  1455. for child in children:
  1456. self._add_child_widget(child)
  1457. CanvasWidget.__init__(self, canvas, **attribs)
  1458. def add_child(self, canvaswidget):
  1459. """
  1460. Add a new canvas widget to the scroll-watcher. The
  1461. scroll-watcher will ensure that the new canvas widget is
  1462. always contained in its canvas's scrollregion.
  1463. :param canvaswidget: The new canvas widget.
  1464. :type canvaswidget: CanvasWidget
  1465. :rtype: None
  1466. """
  1467. self._add_child_widget(canvaswidget)
  1468. self.update(canvaswidget)
  1469. def remove_child(self, canvaswidget):
  1470. """
  1471. Remove a canvas widget from the scroll-watcher. The
  1472. scroll-watcher will no longer ensure that the new canvas
  1473. widget is always contained in its canvas's scrollregion.
  1474. :param canvaswidget: The canvas widget to remove.
  1475. :type canvaswidget: CanvasWidget
  1476. :rtype: None
  1477. """
  1478. self._remove_child_widget(canvaswidget)
  1479. def _tags(self):
  1480. return []
  1481. def _update(self, child):
  1482. self._adjust_scrollregion()
  1483. def _adjust_scrollregion(self):
  1484. """
  1485. Adjust the scrollregion of this scroll-watcher's ``Canvas`` to
  1486. include the bounding boxes of all of its children.
  1487. """
  1488. bbox = self.bbox()
  1489. canvas = self.canvas()
  1490. scrollregion = [int(n) for n in canvas['scrollregion'].split()]
  1491. if len(scrollregion) != 4:
  1492. return
  1493. if (
  1494. bbox[0] < scrollregion[0]
  1495. or bbox[1] < scrollregion[1]
  1496. or bbox[2] > scrollregion[2]
  1497. or bbox[3] > scrollregion[3]
  1498. ):
  1499. scrollregion = '%d %d %d %d' % (
  1500. min(bbox[0], scrollregion[0]),
  1501. min(bbox[1], scrollregion[1]),
  1502. max(bbox[2], scrollregion[2]),
  1503. max(bbox[3], scrollregion[3]),
  1504. )
  1505. canvas['scrollregion'] = scrollregion
  1506. ##//////////////////////////////////////////////////////
  1507. ## Canvas Frame
  1508. ##//////////////////////////////////////////////////////
  1509. class CanvasFrame(object):
  1510. """
  1511. A ``Tkinter`` frame containing a canvas and scrollbars.
  1512. ``CanvasFrame`` uses a ``ScrollWatcherWidget`` to ensure that all of
  1513. the canvas widgets contained on its canvas are within its
  1514. scrollregion. In order for ``CanvasFrame`` to make these checks,
  1515. all canvas widgets must be registered with ``add_widget`` when they
  1516. are added to the canvas; and destroyed with ``destroy_widget`` when
  1517. they are no longer needed.
  1518. If a ``CanvasFrame`` is created with no parent, then it will create
  1519. its own main window, including a "Done" button and a "Print"
  1520. button.
  1521. """
  1522. def __init__(self, parent=None, **kw):
  1523. """
  1524. Create a new ``CanvasFrame``.
  1525. :type parent: Tkinter.BaseWidget or Tkinter.Tk
  1526. :param parent: The parent ``Tkinter`` widget. If no parent is
  1527. specified, then ``CanvasFrame`` will create a new main
  1528. window.
  1529. :param kw: Keyword arguments for the new ``Canvas``. See the
  1530. documentation for ``Tkinter.Canvas`` for more information.
  1531. """
  1532. # If no parent was given, set up a top-level window.
  1533. if parent is None:
  1534. self._parent = Tk()
  1535. self._parent.title('NLTK')
  1536. self._parent.bind('<Control-p>', lambda e: self.print_to_file())
  1537. self._parent.bind('<Control-x>', self.destroy)
  1538. self._parent.bind('<Control-q>', self.destroy)
  1539. else:
  1540. self._parent = parent
  1541. # Create a frame for the canvas & scrollbars
  1542. self._frame = frame = Frame(self._parent)
  1543. self._canvas = canvas = Canvas(frame, **kw)
  1544. xscrollbar = Scrollbar(self._frame, orient='horizontal')
  1545. yscrollbar = Scrollbar(self._frame, orient='vertical')
  1546. xscrollbar['command'] = canvas.xview
  1547. yscrollbar['command'] = canvas.yview
  1548. canvas['xscrollcommand'] = xscrollbar.set
  1549. canvas['yscrollcommand'] = yscrollbar.set
  1550. yscrollbar.pack(fill='y', side='right')
  1551. xscrollbar.pack(fill='x', side='bottom')
  1552. canvas.pack(expand=1, fill='both', side='left')
  1553. # Set initial scroll region.
  1554. scrollregion = '0 0 %s %s' % (canvas['width'], canvas['height'])
  1555. canvas['scrollregion'] = scrollregion
  1556. self._scrollwatcher = ScrollWatcherWidget(canvas)
  1557. # If no parent was given, pack the frame, and add a menu.
  1558. if parent is None:
  1559. self.pack(expand=1, fill='both')
  1560. self._init_menubar()
  1561. def _init_menubar(self):
  1562. menubar = Menu(self._parent)
  1563. filemenu = Menu(menubar, tearoff=0)
  1564. filemenu.add_command(
  1565. label='Print to Postscript',
  1566. underline=0,
  1567. command=self.print_to_file,
  1568. accelerator='Ctrl-p',
  1569. )
  1570. filemenu.add_command(
  1571. label='Exit', underline=1, command=self.destroy, accelerator='Ctrl-x'
  1572. )
  1573. menubar.add_cascade(label='File', underline=0, menu=filemenu)
  1574. self._parent.config(menu=menubar)
  1575. def print_to_file(self, filename=None):
  1576. """
  1577. Print the contents of this ``CanvasFrame`` to a postscript
  1578. file. If no filename is given, then prompt the user for one.
  1579. :param filename: The name of the file to print the tree to.
  1580. :type filename: str
  1581. :rtype: None
  1582. """
  1583. if filename is None:
  1584. ftypes = [('Postscript files', '.ps'), ('All files', '*')]
  1585. filename = asksaveasfilename(filetypes=ftypes, defaultextension='.ps')
  1586. if not filename:
  1587. return
  1588. (x0, y0, w, h) = self.scrollregion()
  1589. postscript = self._canvas.postscript(
  1590. x=x0,
  1591. y=y0,
  1592. width=w + 2,
  1593. height=h + 2,
  1594. pagewidth=w + 2, # points = 1/72 inch
  1595. pageheight=h + 2, # points = 1/72 inch
  1596. pagex=0,
  1597. pagey=0,
  1598. )
  1599. # workaround for bug in Tk font handling
  1600. postscript = postscript.replace(' 0 scalefont ', ' 9 scalefont ')
  1601. with open(filename, 'wb') as f:
  1602. f.write(postscript.encode('utf8'))
  1603. def scrollregion(self):
  1604. """
  1605. :return: The current scroll region for the canvas managed by
  1606. this ``CanvasFrame``.
  1607. :rtype: 4-tuple of int
  1608. """
  1609. (x1, y1, x2, y2) = self._canvas['scrollregion'].split()
  1610. return (int(x1), int(y1), int(x2), int(y2))
  1611. def canvas(self):
  1612. """
  1613. :return: The canvas managed by this ``CanvasFrame``.
  1614. :rtype: Tkinter.Canvas
  1615. """
  1616. return self._canvas
  1617. def add_widget(self, canvaswidget, x=None, y=None):
  1618. """
  1619. Register a canvas widget with this ``CanvasFrame``. The
  1620. ``CanvasFrame`` will ensure that this canvas widget is always
  1621. within the ``Canvas``'s scrollregion. If no coordinates are
  1622. given for the canvas widget, then the ``CanvasFrame`` will
  1623. attempt to find a clear area of the canvas for it.
  1624. :type canvaswidget: CanvasWidget
  1625. :param canvaswidget: The new canvas widget. ``canvaswidget``
  1626. must have been created on this ``CanvasFrame``'s canvas.
  1627. :type x: int
  1628. :param x: The initial x coordinate for the upper left hand
  1629. corner of ``canvaswidget``, in the canvas's coordinate
  1630. space.
  1631. :type y: int
  1632. :param y: The initial y coordinate for the upper left hand
  1633. corner of ``canvaswidget``, in the canvas's coordinate
  1634. space.
  1635. """
  1636. if x is None or y is None:
  1637. (x, y) = self._find_room(canvaswidget, x, y)
  1638. # Move to (x,y)
  1639. (x1, y1, x2, y2) = canvaswidget.bbox()
  1640. canvaswidget.move(x - x1, y - y1)
  1641. # Register with scrollwatcher.
  1642. self._scrollwatcher.add_child(canvaswidget)
  1643. def _find_room(self, widget, desired_x, desired_y):
  1644. """
  1645. Try to find a space for a given widget.
  1646. """
  1647. (left, top, right, bot) = self.scrollregion()
  1648. w = widget.width()
  1649. h = widget.height()
  1650. if w >= (right - left):
  1651. return (0, 0)
  1652. if h >= (bot - top):
  1653. return (0, 0)
  1654. # Move the widget out of the way, for now.
  1655. (x1, y1, x2, y2) = widget.bbox()
  1656. widget.move(left - x2 - 50, top - y2 - 50)
  1657. if desired_x is not None:
  1658. x = desired_x
  1659. for y in range(top, bot - h, int((bot - top - h) / 10)):
  1660. if not self._canvas.find_overlapping(
  1661. x - 5, y - 5, x + w + 5, y + h + 5
  1662. ):
  1663. return (x, y)
  1664. if desired_y is not None:
  1665. y = desired_y
  1666. for x in range(left, right - w, int((right - left - w) / 10)):
  1667. if not self._canvas.find_overlapping(
  1668. x - 5, y - 5, x + w + 5, y + h + 5
  1669. ):
  1670. return (x, y)
  1671. for y in range(top, bot - h, int((bot - top - h) / 10)):
  1672. for x in range(left, right - w, int((right - left - w) / 10)):
  1673. if not self._canvas.find_overlapping(
  1674. x - 5, y - 5, x + w + 5, y + h + 5
  1675. ):
  1676. return (x, y)
  1677. return (0, 0)
  1678. def destroy_widget(self, canvaswidget):
  1679. """
  1680. Remove a canvas widget from this ``CanvasFrame``. This
  1681. deregisters the canvas widget, and destroys it.
  1682. """
  1683. self.remove_widget(canvaswidget)
  1684. canvaswidget.destroy()
  1685. def remove_widget(self, canvaswidget):
  1686. # Deregister with scrollwatcher.
  1687. self._scrollwatcher.remove_child(canvaswidget)
  1688. def pack(self, cnf={}, **kw):
  1689. """
  1690. Pack this ``CanvasFrame``. See the documentation for
  1691. ``Tkinter.Pack`` for more information.
  1692. """
  1693. self._frame.pack(cnf, **kw)
  1694. # Adjust to be big enough for kids?
  1695. def destroy(self, *e):
  1696. """
  1697. Destroy this ``CanvasFrame``. If this ``CanvasFrame`` created a
  1698. top-level window, then this will close that window.
  1699. """
  1700. if self._parent is None:
  1701. return
  1702. self._parent.destroy()
  1703. self._parent = None
  1704. def mainloop(self, *args, **kwargs):
  1705. """
  1706. Enter the Tkinter mainloop. This function must be called if
  1707. this frame is created from a non-interactive program (e.g.
  1708. from a secript); otherwise, the frame will close as soon as
  1709. the script completes.
  1710. """
  1711. if in_idle():
  1712. return
  1713. self._parent.mainloop(*args, **kwargs)
  1714. ##//////////////////////////////////////////////////////
  1715. ## Text display
  1716. ##//////////////////////////////////////////////////////
  1717. class ShowText(object):
  1718. """
  1719. A ``Tkinter`` window used to display a text. ``ShowText`` is
  1720. typically used by graphical tools to display help text, or similar
  1721. information.
  1722. """
  1723. def __init__(self, root, title, text, width=None, height=None, **textbox_options):
  1724. if width is None or height is None:
  1725. (width, height) = self.find_dimentions(text, width, height)
  1726. # Create the main window.
  1727. if root is None:
  1728. self._top = top = Tk()
  1729. else:
  1730. self._top = top = Toplevel(root)
  1731. top.title(title)
  1732. b = Button(top, text='Ok', command=self.destroy)
  1733. b.pack(side='bottom')
  1734. tbf = Frame(top)
  1735. tbf.pack(expand=1, fill='both')
  1736. scrollbar = Scrollbar(tbf, orient='vertical')
  1737. scrollbar.pack(side='right', fill='y')
  1738. textbox = Text(tbf, wrap='word', width=width, height=height, **textbox_options)
  1739. textbox.insert('end', text)
  1740. textbox['state'] = 'disabled'
  1741. textbox.pack(side='left', expand=1, fill='both')
  1742. scrollbar['command'] = textbox.yview
  1743. textbox['yscrollcommand'] = scrollbar.set
  1744. # Make it easy to close the window.
  1745. top.bind('q', self.destroy)
  1746. top.bind('x', self.destroy)
  1747. top.bind('c', self.destroy)
  1748. top.bind('<Return>', self.destroy)
  1749. top.bind('<Escape>', self.destroy)
  1750. # Focus the scrollbar, so they can use up/down, etc.
  1751. scrollbar.focus()
  1752. def find_dimentions(self, text, width, height):
  1753. lines = text.split('\n')
  1754. if width is None:
  1755. maxwidth = max(len(line) for line in lines)
  1756. width = min(maxwidth, 80)
  1757. # Now, find height.
  1758. height = 0
  1759. for line in lines:
  1760. while len(line) > width:
  1761. brk = line[:width].rfind(' ')
  1762. line = line[brk:]
  1763. height += 1
  1764. height += 1
  1765. height = min(height, 25)
  1766. return (width, height)
  1767. def destroy(self, *e):
  1768. if self._top is None:
  1769. return
  1770. self._top.destroy()
  1771. self._top = None
  1772. def mainloop(self, *args, **kwargs):
  1773. """
  1774. Enter the Tkinter mainloop. This function must be called if
  1775. this window is created from a non-interactive program (e.g.
  1776. from a secript); otherwise, the window will close as soon as
  1777. the script completes.
  1778. """
  1779. if in_idle():
  1780. return
  1781. self._top.mainloop(*args, **kwargs)
  1782. ##//////////////////////////////////////////////////////
  1783. ## Entry dialog
  1784. ##//////////////////////////////////////////////////////
  1785. class EntryDialog(object):
  1786. """
  1787. A dialog box for entering
  1788. """
  1789. def __init__(
  1790. self, parent, original_text='', instructions='', set_callback=None, title=None
  1791. ):
  1792. self._parent = parent
  1793. self._original_text = original_text
  1794. self._set_callback = set_callback
  1795. width = int(max(30, len(original_text) * 3 / 2))
  1796. self._top = Toplevel(parent)
  1797. if title:
  1798. self._top.title(title)
  1799. # The text entry box.
  1800. entryframe = Frame(self._top)
  1801. entryframe.pack(expand=1, fill='both', padx=5, pady=5, ipady=10)
  1802. if instructions:
  1803. l = Label(entryframe, text=instructions)
  1804. l.pack(side='top', anchor='w', padx=30)
  1805. self._entry = Entry(entryframe, width=width)
  1806. self._entry.pack(expand=1, fill='x', padx=30)
  1807. self._entry.insert(0, original_text)
  1808. # A divider
  1809. divider = Frame(self._top, borderwidth=1, relief='sunken')
  1810. divider.pack(fill='x', ipady=1, padx=10)
  1811. # The buttons.
  1812. buttons = Frame(self._top)
  1813. buttons.pack(expand=0, fill='x', padx=5, pady=5)
  1814. b = Button(buttons, text='Cancel', command=self._cancel, width=8)
  1815. b.pack(side='right', padx=5)
  1816. b = Button(buttons, text='Ok', command=self._ok, width=8, default='active')
  1817. b.pack(side='left', padx=5)
  1818. b = Button(buttons, text='Apply', command=self._apply, width=8)
  1819. b.pack(side='left')
  1820. self._top.bind('<Return>', self._ok)
  1821. self._top.bind('<Control-q>', self._cancel)
  1822. self._top.bind('<Escape>', self._cancel)
  1823. self._entry.focus()
  1824. def _reset(self, *e):
  1825. self._entry.delete(0, 'end')
  1826. self._entry.insert(0, self._original_text)
  1827. if self._set_callback:
  1828. self._set_callback(self._original_text)
  1829. def _cancel(self, *e):
  1830. try:
  1831. self._reset()
  1832. except:
  1833. pass
  1834. self._destroy()
  1835. def _ok(self, *e):
  1836. self._apply()
  1837. self._destroy()
  1838. def _apply(self, *e):
  1839. if self._set_callback:
  1840. self._set_callback(self._entry.get())
  1841. def _destroy(self, *e):
  1842. if self._top is None:
  1843. return
  1844. self._top.destroy()
  1845. self._top = None
  1846. ##//////////////////////////////////////////////////////
  1847. ## Colorized List
  1848. ##//////////////////////////////////////////////////////
  1849. class ColorizedList(object):
  1850. """
  1851. An abstract base class for displaying a colorized list of items.
  1852. Subclasses should define:
  1853. - ``_init_colortags``, which sets up Text color tags that
  1854. will be used by the list.
  1855. - ``_item_repr``, which returns a list of (text,colortag)
  1856. tuples that make up the colorized representation of the
  1857. item.
  1858. :note: Typically, you will want to register a callback for
  1859. ``'select'`` that calls ``mark`` on the given item.
  1860. """
  1861. def __init__(self, parent, items=[], **options):
  1862. """
  1863. Construct a new list.
  1864. :param parent: The Tk widget that contains the colorized list
  1865. :param items: The initial contents of the colorized list.
  1866. :param options:
  1867. """
  1868. self._parent = parent
  1869. self._callbacks = {}
  1870. # Which items are marked?
  1871. self._marks = {}
  1872. # Initialize the Tkinter frames.
  1873. self._init_itemframe(options.copy())
  1874. # Set up key & mouse bindings.
  1875. self._textwidget.bind('<KeyPress>', self._keypress)
  1876. self._textwidget.bind('<ButtonPress>', self._buttonpress)
  1877. # Fill in the given CFG's items.
  1878. self._items = None
  1879. self.set(items)
  1880. # ////////////////////////////////////////////////////////////
  1881. # Abstract methods
  1882. # ////////////////////////////////////////////////////////////
  1883. @abstractmethod
  1884. def _init_colortags(self, textwidget, options):
  1885. """
  1886. Set up any colortags that will be used by this colorized list.
  1887. E.g.:
  1888. >>> textwidget.tag_config('terminal', foreground='black')
  1889. """
  1890. @abstractmethod
  1891. def _item_repr(self, item):
  1892. """
  1893. Return a list of (text, colortag) tuples that make up the
  1894. colorized representation of the item. Colorized
  1895. representations may not span multiple lines. I.e., the text
  1896. strings returned may not contain newline characters.
  1897. """
  1898. # ////////////////////////////////////////////////////////////
  1899. # Item Access
  1900. # ////////////////////////////////////////////////////////////
  1901. def get(self, index=None):
  1902. """
  1903. :return: A list of the items contained by this list.
  1904. """
  1905. if index is None:
  1906. return self._items[:]
  1907. else:
  1908. return self._items[index]
  1909. def set(self, items):
  1910. """
  1911. Modify the list of items contained by this list.
  1912. """
  1913. items = list(items)
  1914. if self._items == items:
  1915. return
  1916. self._items = list(items)
  1917. self._textwidget['state'] = 'normal'
  1918. self._textwidget.delete('1.0', 'end')
  1919. for item in items:
  1920. for (text, colortag) in self._item_repr(item):
  1921. assert '\n' not in text, 'item repr may not contain newline'
  1922. self._textwidget.insert('end', text, colortag)
  1923. self._textwidget.insert('end', '\n')
  1924. # Remove the final newline
  1925. self._textwidget.delete('end-1char', 'end')
  1926. self._textwidget.mark_set('insert', '1.0')
  1927. self._textwidget['state'] = 'disabled'
  1928. # Clear all marks
  1929. self._marks.clear()
  1930. def unmark(self, item=None):
  1931. """
  1932. Remove highlighting from the given item; or from every item,
  1933. if no item is given.
  1934. :raise ValueError: If ``item`` is not contained in the list.
  1935. :raise KeyError: If ``item`` is not marked.
  1936. """
  1937. if item is None:
  1938. self._marks.clear()
  1939. self._textwidget.tag_remove('highlight', '1.0', 'end+1char')
  1940. else:
  1941. index = self._items.index(item)
  1942. del self._marks[item]
  1943. (start, end) = ('%d.0' % (index + 1), '%d.0' % (index + 2))
  1944. self._textwidget.tag_remove('highlight', start, end)
  1945. def mark(self, item):
  1946. """
  1947. Highlight the given item.
  1948. :raise ValueError: If ``item`` is not contained in the list.
  1949. """
  1950. self._marks[item] = 1
  1951. index = self._items.index(item)
  1952. (start, end) = ('%d.0' % (index + 1), '%d.0' % (index + 2))
  1953. self._textwidget.tag_add('highlight', start, end)
  1954. def markonly(self, item):
  1955. """
  1956. Remove any current highlighting, and mark the given item.
  1957. :raise ValueError: If ``item`` is not contained in the list.
  1958. """
  1959. self.unmark()
  1960. self.mark(item)
  1961. def view(self, item):
  1962. """
  1963. Adjust the view such that the given item is visible. If
  1964. the item is already visible, then do nothing.
  1965. """
  1966. index = self._items.index(item)
  1967. self._textwidget.see('%d.0' % (index + 1))
  1968. # ////////////////////////////////////////////////////////////
  1969. # Callbacks
  1970. # ////////////////////////////////////////////////////////////
  1971. def add_callback(self, event, func):
  1972. """
  1973. Register a callback function with the list. This function
  1974. will be called whenever the given event occurs.
  1975. :param event: The event that will trigger the callback
  1976. function. Valid events are: click1, click2, click3,
  1977. space, return, select, up, down, next, prior, move
  1978. :param func: The function that should be called when
  1979. the event occurs. ``func`` will be called with a
  1980. single item as its argument. (The item selected
  1981. or the item moved to).
  1982. """
  1983. if event == 'select':
  1984. events = ['click1', 'space', 'return']
  1985. elif event == 'move':
  1986. events = ['up', 'down', 'next', 'prior']
  1987. else:
  1988. events = [event]
  1989. for e in events:
  1990. self._callbacks.setdefault(e, {})[func] = 1
  1991. def remove_callback(self, event, func=None):
  1992. """
  1993. Deregister a callback function. If ``func`` is none, then
  1994. all callbacks are removed for the given event.
  1995. """
  1996. if event is None:
  1997. events = list(self._callbacks.keys())
  1998. elif event == 'select':
  1999. events = ['click1', 'space', 'return']
  2000. elif event == 'move':
  2001. events = ['up', 'down', 'next', 'prior']
  2002. else:
  2003. events = [event]
  2004. for e in events:
  2005. if func is None:
  2006. del self._callbacks[e]
  2007. else:
  2008. try:
  2009. del self._callbacks[e][func]
  2010. except:
  2011. pass
  2012. # ////////////////////////////////////////////////////////////
  2013. # Tkinter Methods
  2014. # ////////////////////////////////////////////////////////////
  2015. def pack(self, cnf={}, **kw):
  2016. # "@include: Tkinter.Pack.pack"
  2017. self._itemframe.pack(cnf, **kw)
  2018. def grid(self, cnf={}, **kw):
  2019. # "@include: Tkinter.Grid.grid"
  2020. self._itemframe.grid(cnf, *kw)
  2021. def focus(self):
  2022. # "@include: Tkinter.Widget.focus"
  2023. self._textwidget.focus()
  2024. # ////////////////////////////////////////////////////////////
  2025. # Internal Methods
  2026. # ////////////////////////////////////////////////////////////
  2027. def _init_itemframe(self, options):
  2028. self._itemframe = Frame(self._parent)
  2029. # Create the basic Text widget & scrollbar.
  2030. options.setdefault('background', '#e0e0e0')
  2031. self._textwidget = Text(self._itemframe, **options)
  2032. self._textscroll = Scrollbar(self._itemframe, takefocus=0, orient='vertical')
  2033. self._textwidget.config(yscrollcommand=self._textscroll.set)
  2034. self._textscroll.config(command=self._textwidget.yview)
  2035. self._textscroll.pack(side='right', fill='y')
  2036. self._textwidget.pack(expand=1, fill='both', side='left')
  2037. # Initialize the colorization tags
  2038. self._textwidget.tag_config(
  2039. 'highlight', background='#e0ffff', border='1', relief='raised'
  2040. )
  2041. self._init_colortags(self._textwidget, options)
  2042. # How do I want to mark keyboard selection?
  2043. self._textwidget.tag_config('sel', foreground='')
  2044. self._textwidget.tag_config(
  2045. 'sel', foreground='', background='', border='', underline=1
  2046. )
  2047. self._textwidget.tag_lower('highlight', 'sel')
  2048. def _fire_callback(self, event, itemnum):
  2049. if event not in self._callbacks:
  2050. return
  2051. if 0 <= itemnum < len(self._items):
  2052. item = self._items[itemnum]
  2053. else:
  2054. item = None
  2055. for cb_func in list(self._callbacks[event].keys()):
  2056. cb_func(item)
  2057. def _buttonpress(self, event):
  2058. clickloc = '@%d,%d' % (event.x, event.y)
  2059. insert_point = self._textwidget.index(clickloc)
  2060. itemnum = int(insert_point.split('.')[0]) - 1
  2061. self._fire_callback('click%d' % event.num, itemnum)
  2062. def _keypress(self, event):
  2063. if event.keysym == 'Return' or event.keysym == 'space':
  2064. insert_point = self._textwidget.index('insert')
  2065. itemnum = int(insert_point.split('.')[0]) - 1
  2066. self._fire_callback(event.keysym.lower(), itemnum)
  2067. return
  2068. elif event.keysym == 'Down':
  2069. delta = '+1line'
  2070. elif event.keysym == 'Up':
  2071. delta = '-1line'
  2072. elif event.keysym == 'Next':
  2073. delta = '+10lines'
  2074. elif event.keysym == 'Prior':
  2075. delta = '-10lines'
  2076. else:
  2077. return 'continue'
  2078. self._textwidget.mark_set('insert', 'insert' + delta)
  2079. self._textwidget.see('insert')
  2080. self._textwidget.tag_remove('sel', '1.0', 'end+1char')
  2081. self._textwidget.tag_add('sel', 'insert linestart', 'insert lineend')
  2082. insert_point = self._textwidget.index('insert')
  2083. itemnum = int(insert_point.split('.')[0]) - 1
  2084. self._fire_callback(event.keysym.lower(), itemnum)
  2085. return 'break'
  2086. ##//////////////////////////////////////////////////////
  2087. ## Improved OptionMenu
  2088. ##//////////////////////////////////////////////////////
  2089. class MutableOptionMenu(Menubutton):
  2090. def __init__(self, master, values, **options):
  2091. self._callback = options.get('command')
  2092. if 'command' in options:
  2093. del options['command']
  2094. # Create a variable
  2095. self._variable = variable = StringVar()
  2096. if len(values) > 0:
  2097. variable.set(values[0])
  2098. kw = {
  2099. "borderwidth": 2,
  2100. "textvariable": variable,
  2101. "indicatoron": 1,
  2102. "relief": RAISED,
  2103. "anchor": "c",
  2104. "highlightthickness": 2,
  2105. }
  2106. kw.update(options)
  2107. Widget.__init__(self, master, "menubutton", kw)
  2108. self.widgetName = 'tk_optionMenu'
  2109. self._menu = Menu(self, name="menu", tearoff=0)
  2110. self.menuname = self._menu._w
  2111. self._values = []
  2112. for value in values:
  2113. self.add(value)
  2114. self["menu"] = self._menu
  2115. def add(self, value):
  2116. if value in self._values:
  2117. return
  2118. def set(value=value):
  2119. self.set(value)
  2120. self._menu.add_command(label=value, command=set)
  2121. self._values.append(value)
  2122. def set(self, value):
  2123. self._variable.set(value)
  2124. if self._callback:
  2125. self._callback(value)
  2126. def remove(self, value):
  2127. # Might raise indexerror: pass to parent.
  2128. i = self._values.index(value)
  2129. del self._values[i]
  2130. self._menu.delete(i, i)
  2131. def __getitem__(self, name):
  2132. if name == 'menu':
  2133. return self.__menu
  2134. return Widget.__getitem__(self, name)
  2135. def destroy(self):
  2136. """Destroy this widget and the associated menu."""
  2137. Menubutton.destroy(self)
  2138. self._menu = None
  2139. ##//////////////////////////////////////////////////////
  2140. ## Test code.
  2141. ##//////////////////////////////////////////////////////
  2142. def demo():
  2143. """
  2144. A simple demonstration showing how to use canvas widgets.
  2145. """
  2146. def fill(cw):
  2147. from random import randint
  2148. cw['fill'] = '#00%04d' % randint(0, 9999)
  2149. def color(cw):
  2150. from random import randint
  2151. cw['color'] = '#ff%04d' % randint(0, 9999)
  2152. cf = CanvasFrame(closeenough=10, width=300, height=300)
  2153. c = cf.canvas()
  2154. ct3 = TextWidget(c, 'hiya there', draggable=1)
  2155. ct2 = TextWidget(c, 'o o\n||\n___\n U', draggable=1, justify='center')
  2156. co = OvalWidget(c, ct2, outline='red')
  2157. ct = TextWidget(c, 'o o\n||\n\\___/', draggable=1, justify='center')
  2158. cp = ParenWidget(c, ct, color='red')
  2159. cb = BoxWidget(c, cp, fill='cyan', draggable=1, width=3, margin=10)
  2160. equation = SequenceWidget(
  2161. c,
  2162. SymbolWidget(c, 'forall'),
  2163. TextWidget(c, 'x'),
  2164. SymbolWidget(c, 'exists'),
  2165. TextWidget(c, 'y: '),
  2166. TextWidget(c, 'x'),
  2167. SymbolWidget(c, 'notequal'),
  2168. TextWidget(c, 'y'),
  2169. )
  2170. space = SpaceWidget(c, 0, 30)
  2171. cstack = StackWidget(c, cb, ct3, space, co, equation, align='center')
  2172. prompt_msg = TextWidget(
  2173. c, 'try clicking\nand dragging', draggable=1, justify='center'
  2174. )
  2175. cs = SequenceWidget(c, cstack, prompt_msg)
  2176. zz = BracketWidget(c, cs, color='green4', width=3)
  2177. cf.add_widget(zz, 60, 30)
  2178. cb.bind_click(fill)
  2179. ct.bind_click(color)
  2180. co.bind_click(fill)
  2181. ct2.bind_click(color)
  2182. ct3.bind_click(color)
  2183. cf.mainloop()
  2184. # ShowText(None, 'title', ((('this is text'*150)+'\n')*5))
  2185. if __name__ == '__main__':
  2186. demo()