You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2573 lines
86 KiB
Python
2573 lines
86 KiB
Python
# Natural Language Toolkit: Drawing utilities
|
|
#
|
|
# Copyright (C) 2001-2020 NLTK Project
|
|
# Author: Edward Loper <edloper@gmail.com>
|
|
# URL: <http://nltk.org/>
|
|
# For license information, see LICENSE.TXT
|
|
|
|
"""
|
|
Tools for graphically displaying and interacting with the objects and
|
|
processing classes defined by the Toolkit. These tools are primarily
|
|
intended to help students visualize the objects that they create.
|
|
|
|
The graphical tools are typically built using "canvas widgets", each
|
|
of which encapsulates the graphical elements and bindings used to
|
|
display a complex object on a Tkinter ``Canvas``. For example, NLTK
|
|
defines canvas widgets for displaying trees and directed graphs, as
|
|
well as a number of simpler widgets. These canvas widgets make it
|
|
easier to build new graphical tools and demos. See the class
|
|
documentation for ``CanvasWidget`` for more information.
|
|
|
|
The ``nltk.draw`` module defines the abstract ``CanvasWidget`` base
|
|
class, and a number of simple canvas widgets. The remaining canvas
|
|
widgets are defined by submodules, such as ``nltk.draw.tree``.
|
|
|
|
The ``nltk.draw`` module also defines ``CanvasFrame``, which
|
|
encapsulates a ``Canvas`` and its scrollbars. It uses a
|
|
``ScrollWatcherWidget`` to ensure that all canvas widgets contained on
|
|
its canvas are within the scroll region.
|
|
|
|
Acknowledgements: Many of the ideas behind the canvas widget system
|
|
are derived from ``CLIG``, a Tk-based grapher for linguistic data
|
|
structures. For more information, see the CLIG
|
|
homepage (http://www.ags.uni-sb.de/~konrad/clig.html).
|
|
|
|
"""
|
|
from abc import ABCMeta, abstractmethod
|
|
from tkinter import (
|
|
Button,
|
|
Canvas,
|
|
Entry,
|
|
Frame,
|
|
Label,
|
|
Menu,
|
|
Menubutton,
|
|
Scrollbar,
|
|
StringVar,
|
|
Text,
|
|
Tk,
|
|
Toplevel,
|
|
Widget,
|
|
RAISED,
|
|
)
|
|
from tkinter.filedialog import asksaveasfilename
|
|
|
|
from nltk.util import in_idle
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## CanvasWidget
|
|
##//////////////////////////////////////////////////////
|
|
|
|
|
|
class CanvasWidget(metaclass=ABCMeta):
|
|
"""
|
|
A collection of graphical elements and bindings used to display a
|
|
complex object on a Tkinter ``Canvas``. A canvas widget is
|
|
responsible for managing the ``Canvas`` tags and callback bindings
|
|
necessary to display and interact with the object. Canvas widgets
|
|
are often organized into hierarchies, where parent canvas widgets
|
|
control aspects of their child widgets.
|
|
|
|
Each canvas widget is bound to a single ``Canvas``. This ``Canvas``
|
|
is specified as the first argument to the ``CanvasWidget``'s
|
|
constructor.
|
|
|
|
Attributes. Each canvas widget can support a variety of
|
|
"attributes", which control how the canvas widget is displayed.
|
|
Some typical examples attributes are ``color``, ``font``, and
|
|
``radius``. Each attribute has a default value. This default
|
|
value can be overridden in the constructor, using keyword
|
|
arguments of the form ``attribute=value``:
|
|
|
|
>>> from nltk.draw.util import TextWidget
|
|
>>> cn = TextWidget(c, 'test', color='red')
|
|
|
|
Attribute values can also be changed after a canvas widget has
|
|
been constructed, using the ``__setitem__`` operator:
|
|
|
|
>>> cn['font'] = 'times'
|
|
|
|
The current value of an attribute value can be queried using the
|
|
``__getitem__`` operator:
|
|
|
|
>>> cn['color']
|
|
red
|
|
|
|
For a list of the attributes supported by a type of canvas widget,
|
|
see its class documentation.
|
|
|
|
Interaction. The attribute ``'draggable'`` controls whether the
|
|
user can drag a canvas widget around the canvas. By default,
|
|
canvas widgets are not draggable.
|
|
|
|
``CanvasWidget`` provides callback support for two types of user
|
|
interaction: clicking and dragging. The method ``bind_click``
|
|
registers a callback function that is called whenever the canvas
|
|
widget is clicked. The method ``bind_drag`` registers a callback
|
|
function that is called after the canvas widget is dragged. If
|
|
the user clicks or drags a canvas widget with no registered
|
|
callback function, then the interaction event will propagate to
|
|
its parent. For each canvas widget, only one callback function
|
|
may be registered for an interaction event. Callback functions
|
|
can be deregistered with the ``unbind_click`` and ``unbind_drag``
|
|
methods.
|
|
|
|
Subclassing. ``CanvasWidget`` is an abstract class. Subclasses
|
|
are required to implement the following methods:
|
|
|
|
- ``__init__``: Builds a new canvas widget. It must perform the
|
|
following three tasks (in order):
|
|
- Create any new graphical elements.
|
|
- Call ``_add_child_widget`` on each child widget.
|
|
- Call the ``CanvasWidget`` constructor.
|
|
- ``_tags``: Returns a list of the canvas tags for all graphical
|
|
elements managed by this canvas widget, not including
|
|
graphical elements managed by its child widgets.
|
|
- ``_manage``: Arranges the child widgets of this canvas widget.
|
|
This is typically only called when the canvas widget is
|
|
created.
|
|
- ``_update``: Update this canvas widget in response to a
|
|
change in a single child.
|
|
|
|
For a ``CanvasWidget`` with no child widgets, the default
|
|
definitions for ``_manage`` and ``_update`` may be used.
|
|
|
|
If a subclass defines any attributes, then it should implement
|
|
``__getitem__`` and ``__setitem__``. If either of these methods is
|
|
called with an unknown attribute, then they should propagate the
|
|
request to ``CanvasWidget``.
|
|
|
|
Most subclasses implement a number of additional methods that
|
|
modify the ``CanvasWidget`` in some way. These methods must call
|
|
``parent.update(self)`` after making any changes to the canvas
|
|
widget's graphical elements. The canvas widget must also call
|
|
``parent.update(self)`` after changing any attribute value that
|
|
affects the shape or position of the canvas widget's graphical
|
|
elements.
|
|
|
|
:type __canvas: Tkinter.Canvas
|
|
:ivar __canvas: This ``CanvasWidget``'s canvas.
|
|
|
|
:type __parent: CanvasWidget or None
|
|
:ivar __parent: This ``CanvasWidget``'s hierarchical parent widget.
|
|
:type __children: list(CanvasWidget)
|
|
:ivar __children: This ``CanvasWidget``'s hierarchical child widgets.
|
|
|
|
:type __updating: bool
|
|
:ivar __updating: Is this canvas widget currently performing an
|
|
update? If it is, then it will ignore any new update requests
|
|
from child widgets.
|
|
|
|
:type __draggable: bool
|
|
:ivar __draggable: Is this canvas widget draggable?
|
|
:type __press: event
|
|
:ivar __press: The ButtonPress event that we're currently handling.
|
|
:type __drag_x: int
|
|
:ivar __drag_x: Where it's been moved to (to find dx)
|
|
:type __drag_y: int
|
|
:ivar __drag_y: Where it's been moved to (to find dy)
|
|
:type __callbacks: dictionary
|
|
:ivar __callbacks: Registered callbacks. Currently, four keys are
|
|
used: ``1``, ``2``, ``3``, and ``'drag'``. The values are
|
|
callback functions. Each callback function takes a single
|
|
argument, which is the ``CanvasWidget`` that triggered the
|
|
callback.
|
|
"""
|
|
|
|
def __init__(self, canvas, parent=None, **attribs):
|
|
"""
|
|
Create a new canvas widget. This constructor should only be
|
|
called by subclass constructors; and it should be called only
|
|
"after" the subclass has constructed all graphical canvas
|
|
objects and registered all child widgets.
|
|
|
|
:param canvas: This canvas widget's canvas.
|
|
:type canvas: Tkinter.Canvas
|
|
:param parent: This canvas widget's hierarchical parent.
|
|
:type parent: CanvasWidget
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
if self.__class__ == CanvasWidget:
|
|
raise TypeError("CanvasWidget is an abstract base class")
|
|
|
|
if not isinstance(canvas, Canvas):
|
|
raise TypeError("Expected a canvas!")
|
|
|
|
self.__canvas = canvas
|
|
self.__parent = parent
|
|
|
|
# If the subclass constructor called _add_child_widget, then
|
|
# self.__children will already exist.
|
|
if not hasattr(self, "_CanvasWidget__children"):
|
|
self.__children = []
|
|
|
|
# Is this widget hidden?
|
|
self.__hidden = 0
|
|
|
|
# Update control (prevents infinite loops)
|
|
self.__updating = 0
|
|
|
|
# Button-press and drag callback handling.
|
|
self.__press = None
|
|
self.__drag_x = self.__drag_y = 0
|
|
self.__callbacks = {}
|
|
self.__draggable = 0
|
|
|
|
# Set up attributes.
|
|
for (attr, value) in list(attribs.items()):
|
|
self[attr] = value
|
|
|
|
# Manage this canvas widget
|
|
self._manage()
|
|
|
|
# Register any new bindings
|
|
for tag in self._tags():
|
|
self.__canvas.tag_bind(tag, "<ButtonPress-1>", self.__press_cb)
|
|
self.__canvas.tag_bind(tag, "<ButtonPress-2>", self.__press_cb)
|
|
self.__canvas.tag_bind(tag, "<ButtonPress-3>", self.__press_cb)
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Inherited methods.
|
|
##//////////////////////////////////////////////////////
|
|
|
|
def bbox(self):
|
|
"""
|
|
:return: A bounding box for this ``CanvasWidget``. The bounding
|
|
box is a tuple of four coordinates, *(xmin, ymin, xmax, ymax)*,
|
|
for a rectangle which encloses all of the canvas
|
|
widget's graphical elements. Bounding box coordinates are
|
|
specified with respect to the coordinate space of the ``Canvas``.
|
|
:rtype: tuple(int, int, int, int)
|
|
"""
|
|
if self.__hidden:
|
|
return (0, 0, 0, 0)
|
|
if len(self.tags()) == 0:
|
|
raise ValueError("No tags")
|
|
return self.__canvas.bbox(*self.tags())
|
|
|
|
def width(self):
|
|
"""
|
|
:return: The width of this canvas widget's bounding box, in
|
|
its ``Canvas``'s coordinate space.
|
|
:rtype: int
|
|
"""
|
|
if len(self.tags()) == 0:
|
|
raise ValueError("No tags")
|
|
bbox = self.__canvas.bbox(*self.tags())
|
|
return bbox[2] - bbox[0]
|
|
|
|
def height(self):
|
|
"""
|
|
:return: The height of this canvas widget's bounding box, in
|
|
its ``Canvas``'s coordinate space.
|
|
:rtype: int
|
|
"""
|
|
if len(self.tags()) == 0:
|
|
raise ValueError("No tags")
|
|
bbox = self.__canvas.bbox(*self.tags())
|
|
return bbox[3] - bbox[1]
|
|
|
|
def parent(self):
|
|
"""
|
|
:return: The hierarchical parent of this canvas widget.
|
|
``self`` is considered a subpart of its parent for
|
|
purposes of user interaction.
|
|
:rtype: CanvasWidget or None
|
|
"""
|
|
return self.__parent
|
|
|
|
def child_widgets(self):
|
|
"""
|
|
:return: A list of the hierarchical children of this canvas
|
|
widget. These children are considered part of ``self``
|
|
for purposes of user interaction.
|
|
:rtype: list of CanvasWidget
|
|
"""
|
|
return self.__children
|
|
|
|
def canvas(self):
|
|
"""
|
|
:return: The canvas that this canvas widget is bound to.
|
|
:rtype: Tkinter.Canvas
|
|
"""
|
|
return self.__canvas
|
|
|
|
def move(self, dx, dy):
|
|
"""
|
|
Move this canvas widget by a given distance. In particular,
|
|
shift the canvas widget right by ``dx`` pixels, and down by
|
|
``dy`` pixels. Both ``dx`` and ``dy`` may be negative, resulting
|
|
in leftward or upward movement.
|
|
|
|
:type dx: int
|
|
:param dx: The number of pixels to move this canvas widget
|
|
rightwards.
|
|
:type dy: int
|
|
:param dy: The number of pixels to move this canvas widget
|
|
downwards.
|
|
:rtype: None
|
|
"""
|
|
if dx == dy == 0:
|
|
return
|
|
for tag in self.tags():
|
|
self.__canvas.move(tag, dx, dy)
|
|
if self.__parent:
|
|
self.__parent.update(self)
|
|
|
|
def moveto(self, x, y, anchor="NW"):
|
|
"""
|
|
Move this canvas widget to the given location. In particular,
|
|
shift the canvas widget such that the corner or side of the
|
|
bounding box specified by ``anchor`` is at location (``x``,
|
|
``y``).
|
|
|
|
:param x,y: The location that the canvas widget should be moved
|
|
to.
|
|
:param anchor: The corner or side of the canvas widget that
|
|
should be moved to the specified location. ``'N'``
|
|
specifies the top center; ``'NE'`` specifies the top right
|
|
corner; etc.
|
|
"""
|
|
x1, y1, x2, y2 = self.bbox()
|
|
if anchor == "NW":
|
|
self.move(x - x1, y - y1)
|
|
if anchor == "N":
|
|
self.move(x - x1 / 2 - x2 / 2, y - y1)
|
|
if anchor == "NE":
|
|
self.move(x - x2, y - y1)
|
|
if anchor == "E":
|
|
self.move(x - x2, y - y1 / 2 - y2 / 2)
|
|
if anchor == "SE":
|
|
self.move(x - x2, y - y2)
|
|
if anchor == "S":
|
|
self.move(x - x1 / 2 - x2 / 2, y - y2)
|
|
if anchor == "SW":
|
|
self.move(x - x1, y - y2)
|
|
if anchor == "W":
|
|
self.move(x - x1, y - y1 / 2 - y2 / 2)
|
|
|
|
def destroy(self):
|
|
"""
|
|
Remove this ``CanvasWidget`` from its ``Canvas``. After a
|
|
``CanvasWidget`` has been destroyed, it should not be accessed.
|
|
|
|
Note that you only need to destroy a top-level
|
|
``CanvasWidget``; its child widgets will be destroyed
|
|
automatically. If you destroy a non-top-level
|
|
``CanvasWidget``, then the entire top-level widget will be
|
|
destroyed.
|
|
|
|
:raise ValueError: if this ``CanvasWidget`` has a parent.
|
|
:rtype: None
|
|
"""
|
|
if self.__parent is not None:
|
|
self.__parent.destroy()
|
|
return
|
|
|
|
for tag in self.tags():
|
|
self.__canvas.tag_unbind(tag, "<ButtonPress-1>")
|
|
self.__canvas.tag_unbind(tag, "<ButtonPress-2>")
|
|
self.__canvas.tag_unbind(tag, "<ButtonPress-3>")
|
|
self.__canvas.delete(*self.tags())
|
|
self.__canvas = None
|
|
|
|
def update(self, child):
|
|
"""
|
|
Update the graphical display of this canvas widget, and all of
|
|
its ancestors, in response to a change in one of this canvas
|
|
widget's children.
|
|
|
|
:param child: The child widget that changed.
|
|
:type child: CanvasWidget
|
|
"""
|
|
if self.__hidden or child.__hidden:
|
|
return
|
|
# If we're already updating, then do nothing. This prevents
|
|
# infinite loops when _update modifies its children.
|
|
if self.__updating:
|
|
return
|
|
self.__updating = 1
|
|
|
|
# Update this CanvasWidget.
|
|
self._update(child)
|
|
|
|
# Propagate update request to the parent.
|
|
if self.__parent:
|
|
self.__parent.update(self)
|
|
|
|
# We're done updating.
|
|
self.__updating = 0
|
|
|
|
def manage(self):
|
|
"""
|
|
Arrange this canvas widget and all of its descendants.
|
|
|
|
:rtype: None
|
|
"""
|
|
if self.__hidden:
|
|
return
|
|
for child in self.__children:
|
|
child.manage()
|
|
self._manage()
|
|
|
|
def tags(self):
|
|
"""
|
|
:return: a list of the canvas tags for all graphical
|
|
elements managed by this canvas widget, including
|
|
graphical elements managed by its child widgets.
|
|
:rtype: list of int
|
|
"""
|
|
if self.__canvas is None:
|
|
raise ValueError("Attempt to access a destroyed canvas widget")
|
|
tags = []
|
|
tags += self._tags()
|
|
for child in self.__children:
|
|
tags += child.tags()
|
|
return tags
|
|
|
|
def __setitem__(self, attr, value):
|
|
"""
|
|
Set the value of the attribute ``attr`` to ``value``. See the
|
|
class documentation for a list of attributes supported by this
|
|
canvas widget.
|
|
|
|
:rtype: None
|
|
"""
|
|
if attr == "draggable":
|
|
self.__draggable = value
|
|
else:
|
|
raise ValueError("Unknown attribute %r" % attr)
|
|
|
|
def __getitem__(self, attr):
|
|
"""
|
|
:return: the value of the attribute ``attr``. See the class
|
|
documentation for a list of attributes supported by this
|
|
canvas widget.
|
|
:rtype: (any)
|
|
"""
|
|
if attr == "draggable":
|
|
return self.__draggable
|
|
else:
|
|
raise ValueError("Unknown attribute %r" % attr)
|
|
|
|
def __repr__(self):
|
|
"""
|
|
:return: a string representation of this canvas widget.
|
|
:rtype: str
|
|
"""
|
|
return "<%s>" % self.__class__.__name__
|
|
|
|
def hide(self):
|
|
"""
|
|
Temporarily hide this canvas widget.
|
|
|
|
:rtype: None
|
|
"""
|
|
self.__hidden = 1
|
|
for tag in self.tags():
|
|
self.__canvas.itemconfig(tag, state="hidden")
|
|
|
|
def show(self):
|
|
"""
|
|
Show a hidden canvas widget.
|
|
|
|
:rtype: None
|
|
"""
|
|
self.__hidden = 0
|
|
for tag in self.tags():
|
|
self.__canvas.itemconfig(tag, state="normal")
|
|
|
|
def hidden(self):
|
|
"""
|
|
:return: True if this canvas widget is hidden.
|
|
:rtype: bool
|
|
"""
|
|
return self.__hidden
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Callback interface
|
|
##//////////////////////////////////////////////////////
|
|
|
|
def bind_click(self, callback, button=1):
|
|
"""
|
|
Register a new callback that will be called whenever this
|
|
``CanvasWidget`` is clicked on.
|
|
|
|
:type callback: function
|
|
:param callback: The callback function that will be called
|
|
whenever this ``CanvasWidget`` is clicked. This function
|
|
will be called with this ``CanvasWidget`` as its argument.
|
|
:type button: int
|
|
:param button: Which button the user should use to click on
|
|
this ``CanvasWidget``. Typically, this should be 1 (left
|
|
button), 3 (right button), or 2 (middle button).
|
|
"""
|
|
self.__callbacks[button] = callback
|
|
|
|
def bind_drag(self, callback):
|
|
"""
|
|
Register a new callback that will be called after this
|
|
``CanvasWidget`` is dragged. This implicitly makes this
|
|
``CanvasWidget`` draggable.
|
|
|
|
:type callback: function
|
|
:param callback: The callback function that will be called
|
|
whenever this ``CanvasWidget`` is clicked. This function
|
|
will be called with this ``CanvasWidget`` as its argument.
|
|
"""
|
|
self.__draggable = 1
|
|
self.__callbacks["drag"] = callback
|
|
|
|
def unbind_click(self, button=1):
|
|
"""
|
|
Remove a callback that was registered with ``bind_click``.
|
|
|
|
:type button: int
|
|
:param button: Which button the user should use to click on
|
|
this ``CanvasWidget``. Typically, this should be 1 (left
|
|
button), 3 (right button), or 2 (middle button).
|
|
"""
|
|
try:
|
|
del self.__callbacks[button]
|
|
except:
|
|
pass
|
|
|
|
def unbind_drag(self):
|
|
"""
|
|
Remove a callback that was registered with ``bind_drag``.
|
|
"""
|
|
try:
|
|
del self.__callbacks["drag"]
|
|
except:
|
|
pass
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Callback internals
|
|
##//////////////////////////////////////////////////////
|
|
|
|
def __press_cb(self, event):
|
|
"""
|
|
Handle a button-press event:
|
|
- record the button press event in ``self.__press``
|
|
- register a button-release callback.
|
|
- if this CanvasWidget or any of its ancestors are
|
|
draggable, then register the appropriate motion callback.
|
|
"""
|
|
# If we're already waiting for a button release, then ignore
|
|
# this new button press.
|
|
if (
|
|
self.__canvas.bind("<ButtonRelease-1>")
|
|
or self.__canvas.bind("<ButtonRelease-2>")
|
|
or self.__canvas.bind("<ButtonRelease-3>")
|
|
):
|
|
return
|
|
|
|
# Unbind motion (just in case; this shouldn't be necessary)
|
|
self.__canvas.unbind("<Motion>")
|
|
|
|
# Record the button press event.
|
|
self.__press = event
|
|
|
|
# If any ancestor is draggable, set up a motion callback.
|
|
# (Only if they pressed button number 1)
|
|
if event.num == 1:
|
|
widget = self
|
|
while widget is not None:
|
|
if widget["draggable"]:
|
|
widget.__start_drag(event)
|
|
break
|
|
widget = widget.parent()
|
|
|
|
# Set up the button release callback.
|
|
self.__canvas.bind("<ButtonRelease-%d>" % event.num, self.__release_cb)
|
|
|
|
def __start_drag(self, event):
|
|
"""
|
|
Begin dragging this object:
|
|
- register a motion callback
|
|
- record the drag coordinates
|
|
"""
|
|
self.__canvas.bind("<Motion>", self.__motion_cb)
|
|
self.__drag_x = event.x
|
|
self.__drag_y = event.y
|
|
|
|
def __motion_cb(self, event):
|
|
"""
|
|
Handle a motion event:
|
|
- move this object to the new location
|
|
- record the new drag coordinates
|
|
"""
|
|
self.move(event.x - self.__drag_x, event.y - self.__drag_y)
|
|
self.__drag_x = event.x
|
|
self.__drag_y = event.y
|
|
|
|
def __release_cb(self, event):
|
|
"""
|
|
Handle a release callback:
|
|
- unregister motion & button release callbacks.
|
|
- decide whether they clicked, dragged, or cancelled
|
|
- call the appropriate handler.
|
|
"""
|
|
# Unbind the button release & motion callbacks.
|
|
self.__canvas.unbind("<ButtonRelease-%d>" % event.num)
|
|
self.__canvas.unbind("<Motion>")
|
|
|
|
# Is it a click or a drag?
|
|
if (
|
|
event.time - self.__press.time < 100
|
|
and abs(event.x - self.__press.x) + abs(event.y - self.__press.y) < 5
|
|
):
|
|
# Move it back, if we were dragging.
|
|
if self.__draggable and event.num == 1:
|
|
self.move(
|
|
self.__press.x - self.__drag_x, self.__press.y - self.__drag_y
|
|
)
|
|
self.__click(event.num)
|
|
elif event.num == 1:
|
|
self.__drag()
|
|
|
|
self.__press = None
|
|
|
|
def __drag(self):
|
|
"""
|
|
If this ``CanvasWidget`` has a drag callback, then call it;
|
|
otherwise, find the closest ancestor with a drag callback, and
|
|
call it. If no ancestors have a drag callback, do nothing.
|
|
"""
|
|
if self.__draggable:
|
|
if "drag" in self.__callbacks:
|
|
cb = self.__callbacks["drag"]
|
|
try:
|
|
cb(self)
|
|
except:
|
|
print("Error in drag callback for %r" % self)
|
|
elif self.__parent is not None:
|
|
self.__parent.__drag()
|
|
|
|
def __click(self, button):
|
|
"""
|
|
If this ``CanvasWidget`` has a drag callback, then call it;
|
|
otherwise, find the closest ancestor with a click callback, and
|
|
call it. If no ancestors have a click callback, do nothing.
|
|
"""
|
|
if button in self.__callbacks:
|
|
cb = self.__callbacks[button]
|
|
# try:
|
|
cb(self)
|
|
# except:
|
|
# print('Error in click callback for %r' % self)
|
|
# raise
|
|
elif self.__parent is not None:
|
|
self.__parent.__click(button)
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Child/parent Handling
|
|
##//////////////////////////////////////////////////////
|
|
|
|
def _add_child_widget(self, child):
|
|
"""
|
|
Register a hierarchical child widget. The child will be
|
|
considered part of this canvas widget for purposes of user
|
|
interaction. ``_add_child_widget`` has two direct effects:
|
|
- It sets ``child``'s parent to this canvas widget.
|
|
- It adds ``child`` to the list of canvas widgets returned by
|
|
the ``child_widgets`` member function.
|
|
|
|
:param child: The new child widget. ``child`` must not already
|
|
have a parent.
|
|
:type child: CanvasWidget
|
|
"""
|
|
if not hasattr(self, "_CanvasWidget__children"):
|
|
self.__children = []
|
|
if child.__parent is not None:
|
|
raise ValueError("{} already has a parent".format(child))
|
|
child.__parent = self
|
|
self.__children.append(child)
|
|
|
|
def _remove_child_widget(self, child):
|
|
"""
|
|
Remove a hierarchical child widget. This child will no longer
|
|
be considered part of this canvas widget for purposes of user
|
|
interaction. ``_add_child_widget`` has two direct effects:
|
|
- It sets ``child``'s parent to None.
|
|
- It removes ``child`` from the list of canvas widgets
|
|
returned by the ``child_widgets`` member function.
|
|
|
|
:param child: The child widget to remove. ``child`` must be a
|
|
child of this canvas widget.
|
|
:type child: CanvasWidget
|
|
"""
|
|
self.__children.remove(child)
|
|
child.__parent = None
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Defined by subclass
|
|
##//////////////////////////////////////////////////////
|
|
|
|
@abstractmethod
|
|
def _tags(self):
|
|
"""
|
|
:return: a list of canvas tags for all graphical elements
|
|
managed by this canvas widget, not including graphical
|
|
elements managed by its child widgets.
|
|
:rtype: list of int
|
|
"""
|
|
|
|
def _manage(self):
|
|
"""
|
|
Arrange the child widgets of this canvas widget. This method
|
|
is called when the canvas widget is initially created. It is
|
|
also called if the user calls the ``manage`` method on this
|
|
canvas widget or any of its ancestors.
|
|
|
|
:rtype: None
|
|
"""
|
|
|
|
def _update(self, child):
|
|
"""
|
|
Update this canvas widget in response to a change in one of
|
|
its children.
|
|
|
|
:param child: The child that changed.
|
|
:type child: CanvasWidget
|
|
:rtype: None
|
|
"""
|
|
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Basic widgets.
|
|
##//////////////////////////////////////////////////////
|
|
|
|
|
|
class TextWidget(CanvasWidget):
|
|
"""
|
|
A canvas widget that displays a single string of text.
|
|
|
|
Attributes:
|
|
- ``color``: the color of the text.
|
|
- ``font``: the font used to display the text.
|
|
- ``justify``: justification for multi-line texts. Valid values
|
|
are ``left``, ``center``, and ``right``.
|
|
- ``width``: the width of the text. If the text is wider than
|
|
this width, it will be line-wrapped at whitespace.
|
|
- ``draggable``: whether the text can be dragged by the user.
|
|
"""
|
|
|
|
def __init__(self, canvas, text, **attribs):
|
|
"""
|
|
Create a new text widget.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:type text: str
|
|
:param text: The string of text to display.
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
self._text = text
|
|
self._tag = canvas.create_text(1, 1, text=text)
|
|
CanvasWidget.__init__(self, canvas, **attribs)
|
|
|
|
def __setitem__(self, attr, value):
|
|
if attr in ("color", "font", "justify", "width"):
|
|
if attr == "color":
|
|
attr = "fill"
|
|
self.canvas().itemconfig(self._tag, {attr: value})
|
|
else:
|
|
CanvasWidget.__setitem__(self, attr, value)
|
|
|
|
def __getitem__(self, attr):
|
|
if attr == "width":
|
|
return int(self.canvas().itemcget(self._tag, attr))
|
|
elif attr in ("color", "font", "justify"):
|
|
if attr == "color":
|
|
attr = "fill"
|
|
return self.canvas().itemcget(self._tag, attr)
|
|
else:
|
|
return CanvasWidget.__getitem__(self, attr)
|
|
|
|
def _tags(self):
|
|
return [self._tag]
|
|
|
|
def text(self):
|
|
"""
|
|
:return: The text displayed by this text widget.
|
|
:rtype: str
|
|
"""
|
|
return self.canvas().itemcget(self._tag, "TEXT")
|
|
|
|
def set_text(self, text):
|
|
"""
|
|
Change the text that is displayed by this text widget.
|
|
|
|
:type text: str
|
|
:param text: The string of text to display.
|
|
:rtype: None
|
|
"""
|
|
self.canvas().itemconfig(self._tag, text=text)
|
|
if self.parent() is not None:
|
|
self.parent().update(self)
|
|
|
|
def __repr__(self):
|
|
return "[Text: %r]" % self._text
|
|
|
|
|
|
class SymbolWidget(TextWidget):
|
|
"""
|
|
A canvas widget that displays special symbols, such as the
|
|
negation sign and the exists operator. Symbols are specified by
|
|
name. Currently, the following symbol names are defined: ``neg``,
|
|
``disj``, ``conj``, ``lambda``, ``merge``, ``forall``, ``exists``,
|
|
``subseteq``, ``subset``, ``notsubset``, ``emptyset``, ``imp``,
|
|
``rightarrow``, ``equal``, ``notequal``, ``epsilon``.
|
|
|
|
Attributes:
|
|
|
|
- ``color``: the color of the text.
|
|
- ``draggable``: whether the text can be dragged by the user.
|
|
|
|
:cvar SYMBOLS: A dictionary mapping from symbols to the character
|
|
in the ``symbol`` font used to render them.
|
|
"""
|
|
|
|
SYMBOLS = {
|
|
"neg": "\330",
|
|
"disj": "\332",
|
|
"conj": "\331",
|
|
"lambda": "\154",
|
|
"merge": "\304",
|
|
"forall": "\042",
|
|
"exists": "\044",
|
|
"subseteq": "\315",
|
|
"subset": "\314",
|
|
"notsubset": "\313",
|
|
"emptyset": "\306",
|
|
"imp": "\336",
|
|
"rightarrow": chr(222), #'\256',
|
|
"equal": "\75",
|
|
"notequal": "\271",
|
|
"intersection": "\307",
|
|
"union": "\310",
|
|
"epsilon": "e",
|
|
}
|
|
|
|
def __init__(self, canvas, symbol, **attribs):
|
|
"""
|
|
Create a new symbol widget.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:type symbol: str
|
|
:param symbol: The name of the symbol to display.
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
attribs["font"] = "symbol"
|
|
TextWidget.__init__(self, canvas, "", **attribs)
|
|
self.set_symbol(symbol)
|
|
|
|
def symbol(self):
|
|
"""
|
|
:return: the name of the symbol that is displayed by this
|
|
symbol widget.
|
|
:rtype: str
|
|
"""
|
|
return self._symbol
|
|
|
|
def set_symbol(self, symbol):
|
|
"""
|
|
Change the symbol that is displayed by this symbol widget.
|
|
|
|
:type symbol: str
|
|
:param symbol: The name of the symbol to display.
|
|
"""
|
|
if symbol not in SymbolWidget.SYMBOLS:
|
|
raise ValueError("Unknown symbol: %s" % symbol)
|
|
self._symbol = symbol
|
|
self.set_text(SymbolWidget.SYMBOLS[symbol])
|
|
|
|
def __repr__(self):
|
|
return "[Symbol: %r]" % self._symbol
|
|
|
|
@staticmethod
|
|
def symbolsheet(size=20):
|
|
"""
|
|
Open a new Tkinter window that displays the entire alphabet
|
|
for the symbol font. This is useful for constructing the
|
|
``SymbolWidget.SYMBOLS`` dictionary.
|
|
"""
|
|
top = Tk()
|
|
|
|
def destroy(e, top=top):
|
|
top.destroy()
|
|
|
|
top.bind("q", destroy)
|
|
Button(top, text="Quit", command=top.destroy).pack(side="bottom")
|
|
text = Text(top, font=("helvetica", -size), width=20, height=30)
|
|
text.pack(side="left")
|
|
sb = Scrollbar(top, command=text.yview)
|
|
text["yscrollcommand"] = sb.set
|
|
sb.pack(side="right", fill="y")
|
|
text.tag_config("symbol", font=("symbol", -size))
|
|
for i in range(256):
|
|
if i in (0, 10):
|
|
continue # null and newline
|
|
for k, v in list(SymbolWidget.SYMBOLS.items()):
|
|
if v == chr(i):
|
|
text.insert("end", "%-10s\t" % k)
|
|
break
|
|
else:
|
|
text.insert("end", "%-10d \t" % i)
|
|
text.insert("end", "[%s]\n" % chr(i), "symbol")
|
|
top.mainloop()
|
|
|
|
|
|
class AbstractContainerWidget(CanvasWidget):
|
|
"""
|
|
An abstract class for canvas widgets that contain a single child,
|
|
such as ``BoxWidget`` and ``OvalWidget``. Subclasses must define
|
|
a constructor, which should create any new graphical elements and
|
|
then call the ``AbstractCanvasContainer`` constructor. Subclasses
|
|
must also define the ``_update`` method and the ``_tags`` method;
|
|
and any subclasses that define attributes should define
|
|
``__setitem__`` and ``__getitem__``.
|
|
"""
|
|
|
|
def __init__(self, canvas, child, **attribs):
|
|
"""
|
|
Create a new container widget. This constructor should only
|
|
be called by subclass constructors.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:param child: The container's child widget. ``child`` must not
|
|
have a parent.
|
|
:type child: CanvasWidget
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
self._child = child
|
|
self._add_child_widget(child)
|
|
CanvasWidget.__init__(self, canvas, **attribs)
|
|
|
|
def _manage(self):
|
|
self._update(self._child)
|
|
|
|
def child(self):
|
|
"""
|
|
:return: The child widget contained by this container widget.
|
|
:rtype: CanvasWidget
|
|
"""
|
|
return self._child
|
|
|
|
def set_child(self, child):
|
|
"""
|
|
Change the child widget contained by this container widget.
|
|
|
|
:param child: The new child widget. ``child`` must not have a
|
|
parent.
|
|
:type child: CanvasWidget
|
|
:rtype: None
|
|
"""
|
|
self._remove_child_widget(self._child)
|
|
self._add_child_widget(child)
|
|
self._child = child
|
|
self.update(child)
|
|
|
|
def __repr__(self):
|
|
name = self.__class__.__name__
|
|
if name[-6:] == "Widget":
|
|
name = name[:-6]
|
|
return "[%s: %r]" % (name, self._child)
|
|
|
|
|
|
class BoxWidget(AbstractContainerWidget):
|
|
"""
|
|
A canvas widget that places a box around a child widget.
|
|
|
|
Attributes:
|
|
- ``fill``: The color used to fill the interior of the box.
|
|
- ``outline``: The color used to draw the outline of the box.
|
|
- ``width``: The width of the outline of the box.
|
|
- ``margin``: The number of pixels space left between the child
|
|
and the box.
|
|
- ``draggable``: whether the text can be dragged by the user.
|
|
"""
|
|
|
|
def __init__(self, canvas, child, **attribs):
|
|
"""
|
|
Create a new box widget.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:param child: The child widget. ``child`` must not have a
|
|
parent.
|
|
:type child: CanvasWidget
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
self._child = child
|
|
self._margin = 1
|
|
self._box = canvas.create_rectangle(1, 1, 1, 1)
|
|
canvas.tag_lower(self._box)
|
|
AbstractContainerWidget.__init__(self, canvas, child, **attribs)
|
|
|
|
def __setitem__(self, attr, value):
|
|
if attr == "margin":
|
|
self._margin = value
|
|
elif attr in ("outline", "fill", "width"):
|
|
self.canvas().itemconfig(self._box, {attr: value})
|
|
else:
|
|
CanvasWidget.__setitem__(self, attr, value)
|
|
|
|
def __getitem__(self, attr):
|
|
if attr == "margin":
|
|
return self._margin
|
|
elif attr == "width":
|
|
return float(self.canvas().itemcget(self._box, attr))
|
|
elif attr in ("outline", "fill", "width"):
|
|
return self.canvas().itemcget(self._box, attr)
|
|
else:
|
|
return CanvasWidget.__getitem__(self, attr)
|
|
|
|
def _update(self, child):
|
|
(x1, y1, x2, y2) = child.bbox()
|
|
margin = self._margin + self["width"] / 2
|
|
self.canvas().coords(
|
|
self._box, x1 - margin, y1 - margin, x2 + margin, y2 + margin
|
|
)
|
|
|
|
def _tags(self):
|
|
return [self._box]
|
|
|
|
|
|
class OvalWidget(AbstractContainerWidget):
|
|
"""
|
|
A canvas widget that places a oval around a child widget.
|
|
|
|
Attributes:
|
|
- ``fill``: The color used to fill the interior of the oval.
|
|
- ``outline``: The color used to draw the outline of the oval.
|
|
- ``width``: The width of the outline of the oval.
|
|
- ``margin``: The number of pixels space left between the child
|
|
and the oval.
|
|
- ``draggable``: whether the text can be dragged by the user.
|
|
- ``double``: If true, then a double-oval is drawn.
|
|
"""
|
|
|
|
def __init__(self, canvas, child, **attribs):
|
|
"""
|
|
Create a new oval widget.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:param child: The child widget. ``child`` must not have a
|
|
parent.
|
|
:type child: CanvasWidget
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
self._child = child
|
|
self._margin = 1
|
|
self._oval = canvas.create_oval(1, 1, 1, 1)
|
|
self._circle = attribs.pop("circle", False)
|
|
self._double = attribs.pop("double", False)
|
|
if self._double:
|
|
self._oval2 = canvas.create_oval(1, 1, 1, 1)
|
|
else:
|
|
self._oval2 = None
|
|
canvas.tag_lower(self._oval)
|
|
AbstractContainerWidget.__init__(self, canvas, child, **attribs)
|
|
|
|
def __setitem__(self, attr, value):
|
|
c = self.canvas()
|
|
if attr == "margin":
|
|
self._margin = value
|
|
elif attr == "double":
|
|
if value == True and self._oval2 is None:
|
|
# Copy attributes & position from self._oval.
|
|
x1, y1, x2, y2 = c.bbox(self._oval)
|
|
w = self["width"] * 2
|
|
self._oval2 = c.create_oval(
|
|
x1 - w,
|
|
y1 - w,
|
|
x2 + w,
|
|
y2 + w,
|
|
outline=c.itemcget(self._oval, "outline"),
|
|
width=c.itemcget(self._oval, "width"),
|
|
)
|
|
c.tag_lower(self._oval2)
|
|
if value == False and self._oval2 is not None:
|
|
c.delete(self._oval2)
|
|
self._oval2 = None
|
|
elif attr in ("outline", "fill", "width"):
|
|
c.itemconfig(self._oval, {attr: value})
|
|
if self._oval2 is not None and attr != "fill":
|
|
c.itemconfig(self._oval2, {attr: value})
|
|
if self._oval2 is not None and attr != "fill":
|
|
self.canvas().itemconfig(self._oval2, {attr: value})
|
|
else:
|
|
CanvasWidget.__setitem__(self, attr, value)
|
|
|
|
def __getitem__(self, attr):
|
|
if attr == "margin":
|
|
return self._margin
|
|
elif attr == "double":
|
|
return self._double is not None
|
|
elif attr == "width":
|
|
return float(self.canvas().itemcget(self._oval, attr))
|
|
elif attr in ("outline", "fill", "width"):
|
|
return self.canvas().itemcget(self._oval, attr)
|
|
else:
|
|
return CanvasWidget.__getitem__(self, attr)
|
|
|
|
# The ratio between inscribed & circumscribed ovals
|
|
RATIO = 1.4142135623730949
|
|
|
|
def _update(self, child):
|
|
R = OvalWidget.RATIO
|
|
(x1, y1, x2, y2) = child.bbox()
|
|
margin = self._margin
|
|
|
|
# If we're a circle, pretend our contents are square.
|
|
if self._circle:
|
|
dx, dy = abs(x1 - x2), abs(y1 - y2)
|
|
if dx > dy:
|
|
y = (y1 + y2) / 2
|
|
y1, y2 = y - dx / 2, y + dx / 2
|
|
elif dy > dx:
|
|
x = (x1 + x2) / 2
|
|
x1, x2 = x - dy / 2, x + dy / 2
|
|
|
|
# Find the four corners.
|
|
left = int((x1 * (1 + R) + x2 * (1 - R)) / 2)
|
|
right = left + int((x2 - x1) * R)
|
|
top = int((y1 * (1 + R) + y2 * (1 - R)) / 2)
|
|
bot = top + int((y2 - y1) * R)
|
|
self.canvas().coords(
|
|
self._oval, left - margin, top - margin, right + margin, bot + margin
|
|
)
|
|
if self._oval2 is not None:
|
|
self.canvas().coords(
|
|
self._oval2,
|
|
left - margin + 2,
|
|
top - margin + 2,
|
|
right + margin - 2,
|
|
bot + margin - 2,
|
|
)
|
|
|
|
def _tags(self):
|
|
if self._oval2 is None:
|
|
return [self._oval]
|
|
else:
|
|
return [self._oval, self._oval2]
|
|
|
|
|
|
class ParenWidget(AbstractContainerWidget):
|
|
"""
|
|
A canvas widget that places a pair of parenthases around a child
|
|
widget.
|
|
|
|
Attributes:
|
|
- ``color``: The color used to draw the parenthases.
|
|
- ``width``: The width of the parenthases.
|
|
- ``draggable``: whether the text can be dragged by the user.
|
|
"""
|
|
|
|
def __init__(self, canvas, child, **attribs):
|
|
"""
|
|
Create a new parenthasis widget.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:param child: The child widget. ``child`` must not have a
|
|
parent.
|
|
:type child: CanvasWidget
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
self._child = child
|
|
self._oparen = canvas.create_arc(1, 1, 1, 1, style="arc", start=90, extent=180)
|
|
self._cparen = canvas.create_arc(1, 1, 1, 1, style="arc", start=-90, extent=180)
|
|
AbstractContainerWidget.__init__(self, canvas, child, **attribs)
|
|
|
|
def __setitem__(self, attr, value):
|
|
if attr == "color":
|
|
self.canvas().itemconfig(self._oparen, outline=value)
|
|
self.canvas().itemconfig(self._cparen, outline=value)
|
|
elif attr == "width":
|
|
self.canvas().itemconfig(self._oparen, width=value)
|
|
self.canvas().itemconfig(self._cparen, width=value)
|
|
else:
|
|
CanvasWidget.__setitem__(self, attr, value)
|
|
|
|
def __getitem__(self, attr):
|
|
if attr == "color":
|
|
return self.canvas().itemcget(self._oparen, "outline")
|
|
elif attr == "width":
|
|
return self.canvas().itemcget(self._oparen, "width")
|
|
else:
|
|
return CanvasWidget.__getitem__(self, attr)
|
|
|
|
def _update(self, child):
|
|
(x1, y1, x2, y2) = child.bbox()
|
|
width = max((y2 - y1) / 6, 4)
|
|
self.canvas().coords(self._oparen, x1 - width, y1, x1 + width, y2)
|
|
self.canvas().coords(self._cparen, x2 - width, y1, x2 + width, y2)
|
|
|
|
def _tags(self):
|
|
return [self._oparen, self._cparen]
|
|
|
|
|
|
class BracketWidget(AbstractContainerWidget):
|
|
"""
|
|
A canvas widget that places a pair of brackets around a child
|
|
widget.
|
|
|
|
Attributes:
|
|
- ``color``: The color used to draw the brackets.
|
|
- ``width``: The width of the brackets.
|
|
- ``draggable``: whether the text can be dragged by the user.
|
|
"""
|
|
|
|
def __init__(self, canvas, child, **attribs):
|
|
"""
|
|
Create a new bracket widget.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:param child: The child widget. ``child`` must not have a
|
|
parent.
|
|
:type child: CanvasWidget
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
self._child = child
|
|
self._obrack = canvas.create_line(1, 1, 1, 1, 1, 1, 1, 1)
|
|
self._cbrack = canvas.create_line(1, 1, 1, 1, 1, 1, 1, 1)
|
|
AbstractContainerWidget.__init__(self, canvas, child, **attribs)
|
|
|
|
def __setitem__(self, attr, value):
|
|
if attr == "color":
|
|
self.canvas().itemconfig(self._obrack, fill=value)
|
|
self.canvas().itemconfig(self._cbrack, fill=value)
|
|
elif attr == "width":
|
|
self.canvas().itemconfig(self._obrack, width=value)
|
|
self.canvas().itemconfig(self._cbrack, width=value)
|
|
else:
|
|
CanvasWidget.__setitem__(self, attr, value)
|
|
|
|
def __getitem__(self, attr):
|
|
if attr == "color":
|
|
return self.canvas().itemcget(self._obrack, "outline")
|
|
elif attr == "width":
|
|
return self.canvas().itemcget(self._obrack, "width")
|
|
else:
|
|
return CanvasWidget.__getitem__(self, attr)
|
|
|
|
def _update(self, child):
|
|
(x1, y1, x2, y2) = child.bbox()
|
|
width = max((y2 - y1) / 8, 2)
|
|
self.canvas().coords(
|
|
self._obrack, x1, y1, x1 - width, y1, x1 - width, y2, x1, y2
|
|
)
|
|
self.canvas().coords(
|
|
self._cbrack, x2, y1, x2 + width, y1, x2 + width, y2, x2, y2
|
|
)
|
|
|
|
def _tags(self):
|
|
return [self._obrack, self._cbrack]
|
|
|
|
|
|
class SequenceWidget(CanvasWidget):
|
|
"""
|
|
A canvas widget that keeps a list of canvas widgets in a
|
|
horizontal line.
|
|
|
|
Attributes:
|
|
- ``align``: The vertical alignment of the children. Possible
|
|
values are ``'top'``, ``'center'``, and ``'bottom'``. By
|
|
default, children are center-aligned.
|
|
- ``space``: The amount of horizontal space to place between
|
|
children. By default, one pixel of space is used.
|
|
- ``ordered``: If true, then keep the children in their
|
|
original order.
|
|
"""
|
|
|
|
def __init__(self, canvas, *children, **attribs):
|
|
"""
|
|
Create a new sequence widget.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:param children: The widgets that should be aligned
|
|
horizontally. Each child must not have a parent.
|
|
:type children: list(CanvasWidget)
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
self._align = "center"
|
|
self._space = 1
|
|
self._ordered = False
|
|
self._children = list(children)
|
|
for child in children:
|
|
self._add_child_widget(child)
|
|
CanvasWidget.__init__(self, canvas, **attribs)
|
|
|
|
def __setitem__(self, attr, value):
|
|
if attr == "align":
|
|
if value not in ("top", "bottom", "center"):
|
|
raise ValueError("Bad alignment: %r" % value)
|
|
self._align = value
|
|
elif attr == "space":
|
|
self._space = value
|
|
elif attr == "ordered":
|
|
self._ordered = value
|
|
else:
|
|
CanvasWidget.__setitem__(self, attr, value)
|
|
|
|
def __getitem__(self, attr):
|
|
if attr == "align":
|
|
return self._align
|
|
elif attr == "space":
|
|
return self._space
|
|
elif attr == "ordered":
|
|
return self._ordered
|
|
else:
|
|
return CanvasWidget.__getitem__(self, attr)
|
|
|
|
def _tags(self):
|
|
return []
|
|
|
|
def _yalign(self, top, bot):
|
|
if self._align == "top":
|
|
return top
|
|
if self._align == "bottom":
|
|
return bot
|
|
if self._align == "center":
|
|
return (top + bot) / 2
|
|
|
|
def _update(self, child):
|
|
# Align all children with child.
|
|
(left, top, right, bot) = child.bbox()
|
|
y = self._yalign(top, bot)
|
|
for c in self._children:
|
|
(x1, y1, x2, y2) = c.bbox()
|
|
c.move(0, y - self._yalign(y1, y2))
|
|
|
|
if self._ordered and len(self._children) > 1:
|
|
index = self._children.index(child)
|
|
|
|
x = right + self._space
|
|
for i in range(index + 1, len(self._children)):
|
|
(x1, y1, x2, y2) = self._children[i].bbox()
|
|
if x > x1:
|
|
self._children[i].move(x - x1, 0)
|
|
x += x2 - x1 + self._space
|
|
|
|
x = left - self._space
|
|
for i in range(index - 1, -1, -1):
|
|
(x1, y1, x2, y2) = self._children[i].bbox()
|
|
if x < x2:
|
|
self._children[i].move(x - x2, 0)
|
|
x -= x2 - x1 + self._space
|
|
|
|
def _manage(self):
|
|
if len(self._children) == 0:
|
|
return
|
|
child = self._children[0]
|
|
|
|
# Align all children with child.
|
|
(left, top, right, bot) = child.bbox()
|
|
y = self._yalign(top, bot)
|
|
|
|
index = self._children.index(child)
|
|
|
|
# Line up children to the right of child.
|
|
x = right + self._space
|
|
for i in range(index + 1, len(self._children)):
|
|
(x1, y1, x2, y2) = self._children[i].bbox()
|
|
self._children[i].move(x - x1, y - self._yalign(y1, y2))
|
|
x += x2 - x1 + self._space
|
|
|
|
# Line up children to the left of child.
|
|
x = left - self._space
|
|
for i in range(index - 1, -1, -1):
|
|
(x1, y1, x2, y2) = self._children[i].bbox()
|
|
self._children[i].move(x - x2, y - self._yalign(y1, y2))
|
|
x -= x2 - x1 + self._space
|
|
|
|
def __repr__(self):
|
|
return "[Sequence: " + repr(self._children)[1:-1] + "]"
|
|
|
|
# Provide an alias for the child_widgets() member.
|
|
children = CanvasWidget.child_widgets
|
|
|
|
def replace_child(self, oldchild, newchild):
|
|
"""
|
|
Replace the child canvas widget ``oldchild`` with ``newchild``.
|
|
``newchild`` must not have a parent. ``oldchild``'s parent will
|
|
be set to None.
|
|
|
|
:type oldchild: CanvasWidget
|
|
:param oldchild: The child canvas widget to remove.
|
|
:type newchild: CanvasWidget
|
|
:param newchild: The canvas widget that should replace
|
|
``oldchild``.
|
|
"""
|
|
index = self._children.index(oldchild)
|
|
self._children[index] = newchild
|
|
self._remove_child_widget(oldchild)
|
|
self._add_child_widget(newchild)
|
|
self.update(newchild)
|
|
|
|
def remove_child(self, child):
|
|
"""
|
|
Remove the given child canvas widget. ``child``'s parent will
|
|
be set ot None.
|
|
|
|
:type child: CanvasWidget
|
|
:param child: The child canvas widget to remove.
|
|
"""
|
|
index = self._children.index(child)
|
|
del self._children[index]
|
|
self._remove_child_widget(child)
|
|
if len(self._children) > 0:
|
|
self.update(self._children[0])
|
|
|
|
def insert_child(self, index, child):
|
|
"""
|
|
Insert a child canvas widget before a given index.
|
|
|
|
:type child: CanvasWidget
|
|
:param child: The canvas widget that should be inserted.
|
|
:type index: int
|
|
:param index: The index where the child widget should be
|
|
inserted. In particular, the index of ``child`` will be
|
|
``index``; and the index of any children whose indices were
|
|
greater than equal to ``index`` before ``child`` was
|
|
inserted will be incremented by one.
|
|
"""
|
|
self._children.insert(index, child)
|
|
self._add_child_widget(child)
|
|
|
|
|
|
class StackWidget(CanvasWidget):
|
|
"""
|
|
A canvas widget that keeps a list of canvas widgets in a vertical
|
|
line.
|
|
|
|
Attributes:
|
|
- ``align``: The horizontal alignment of the children. Possible
|
|
values are ``'left'``, ``'center'``, and ``'right'``. By
|
|
default, children are center-aligned.
|
|
- ``space``: The amount of vertical space to place between
|
|
children. By default, one pixel of space is used.
|
|
- ``ordered``: If true, then keep the children in their
|
|
original order.
|
|
"""
|
|
|
|
def __init__(self, canvas, *children, **attribs):
|
|
"""
|
|
Create a new stack widget.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:param children: The widgets that should be aligned
|
|
vertically. Each child must not have a parent.
|
|
:type children: list(CanvasWidget)
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
self._align = "center"
|
|
self._space = 1
|
|
self._ordered = False
|
|
self._children = list(children)
|
|
for child in children:
|
|
self._add_child_widget(child)
|
|
CanvasWidget.__init__(self, canvas, **attribs)
|
|
|
|
def __setitem__(self, attr, value):
|
|
if attr == "align":
|
|
if value not in ("left", "right", "center"):
|
|
raise ValueError("Bad alignment: %r" % value)
|
|
self._align = value
|
|
elif attr == "space":
|
|
self._space = value
|
|
elif attr == "ordered":
|
|
self._ordered = value
|
|
else:
|
|
CanvasWidget.__setitem__(self, attr, value)
|
|
|
|
def __getitem__(self, attr):
|
|
if attr == "align":
|
|
return self._align
|
|
elif attr == "space":
|
|
return self._space
|
|
elif attr == "ordered":
|
|
return self._ordered
|
|
else:
|
|
return CanvasWidget.__getitem__(self, attr)
|
|
|
|
def _tags(self):
|
|
return []
|
|
|
|
def _xalign(self, left, right):
|
|
if self._align == "left":
|
|
return left
|
|
if self._align == "right":
|
|
return right
|
|
if self._align == "center":
|
|
return (left + right) / 2
|
|
|
|
def _update(self, child):
|
|
# Align all children with child.
|
|
(left, top, right, bot) = child.bbox()
|
|
x = self._xalign(left, right)
|
|
for c in self._children:
|
|
(x1, y1, x2, y2) = c.bbox()
|
|
c.move(x - self._xalign(x1, x2), 0)
|
|
|
|
if self._ordered and len(self._children) > 1:
|
|
index = self._children.index(child)
|
|
|
|
y = bot + self._space
|
|
for i in range(index + 1, len(self._children)):
|
|
(x1, y1, x2, y2) = self._children[i].bbox()
|
|
if y > y1:
|
|
self._children[i].move(0, y - y1)
|
|
y += y2 - y1 + self._space
|
|
|
|
y = top - self._space
|
|
for i in range(index - 1, -1, -1):
|
|
(x1, y1, x2, y2) = self._children[i].bbox()
|
|
if y < y2:
|
|
self._children[i].move(0, y - y2)
|
|
y -= y2 - y1 + self._space
|
|
|
|
def _manage(self):
|
|
if len(self._children) == 0:
|
|
return
|
|
child = self._children[0]
|
|
|
|
# Align all children with child.
|
|
(left, top, right, bot) = child.bbox()
|
|
x = self._xalign(left, right)
|
|
|
|
index = self._children.index(child)
|
|
|
|
# Line up children below the child.
|
|
y = bot + self._space
|
|
for i in range(index + 1, len(self._children)):
|
|
(x1, y1, x2, y2) = self._children[i].bbox()
|
|
self._children[i].move(x - self._xalign(x1, x2), y - y1)
|
|
y += y2 - y1 + self._space
|
|
|
|
# Line up children above the child.
|
|
y = top - self._space
|
|
for i in range(index - 1, -1, -1):
|
|
(x1, y1, x2, y2) = self._children[i].bbox()
|
|
self._children[i].move(x - self._xalign(x1, x2), y - y2)
|
|
y -= y2 - y1 + self._space
|
|
|
|
def __repr__(self):
|
|
return "[Stack: " + repr(self._children)[1:-1] + "]"
|
|
|
|
# Provide an alias for the child_widgets() member.
|
|
children = CanvasWidget.child_widgets
|
|
|
|
def replace_child(self, oldchild, newchild):
|
|
"""
|
|
Replace the child canvas widget ``oldchild`` with ``newchild``.
|
|
``newchild`` must not have a parent. ``oldchild``'s parent will
|
|
be set to None.
|
|
|
|
:type oldchild: CanvasWidget
|
|
:param oldchild: The child canvas widget to remove.
|
|
:type newchild: CanvasWidget
|
|
:param newchild: The canvas widget that should replace
|
|
``oldchild``.
|
|
"""
|
|
index = self._children.index(oldchild)
|
|
self._children[index] = newchild
|
|
self._remove_child_widget(oldchild)
|
|
self._add_child_widget(newchild)
|
|
self.update(newchild)
|
|
|
|
def remove_child(self, child):
|
|
"""
|
|
Remove the given child canvas widget. ``child``'s parent will
|
|
be set ot None.
|
|
|
|
:type child: CanvasWidget
|
|
:param child: The child canvas widget to remove.
|
|
"""
|
|
index = self._children.index(child)
|
|
del self._children[index]
|
|
self._remove_child_widget(child)
|
|
if len(self._children) > 0:
|
|
self.update(self._children[0])
|
|
|
|
def insert_child(self, index, child):
|
|
"""
|
|
Insert a child canvas widget before a given index.
|
|
|
|
:type child: CanvasWidget
|
|
:param child: The canvas widget that should be inserted.
|
|
:type index: int
|
|
:param index: The index where the child widget should be
|
|
inserted. In particular, the index of ``child`` will be
|
|
``index``; and the index of any children whose indices were
|
|
greater than equal to ``index`` before ``child`` was
|
|
inserted will be incremented by one.
|
|
"""
|
|
self._children.insert(index, child)
|
|
self._add_child_widget(child)
|
|
|
|
|
|
class SpaceWidget(CanvasWidget):
|
|
"""
|
|
A canvas widget that takes up space but does not display
|
|
anything. A ``SpaceWidget`` can be used to add space between
|
|
elements. Each space widget is characterized by a width and a
|
|
height. If you wish to only create horizontal space, then use a
|
|
height of zero; and if you wish to only create vertical space, use
|
|
a width of zero.
|
|
"""
|
|
|
|
def __init__(self, canvas, width, height, **attribs):
|
|
"""
|
|
Create a new space widget.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:type width: int
|
|
:param width: The width of the new space widget.
|
|
:type height: int
|
|
:param height: The height of the new space widget.
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
# For some reason,
|
|
if width > 4:
|
|
width -= 4
|
|
if height > 4:
|
|
height -= 4
|
|
self._tag = canvas.create_line(1, 1, width, height, fill="")
|
|
CanvasWidget.__init__(self, canvas, **attribs)
|
|
|
|
# note: width() and height() are already defined by CanvasWidget.
|
|
def set_width(self, width):
|
|
"""
|
|
Change the width of this space widget.
|
|
|
|
:param width: The new width.
|
|
:type width: int
|
|
:rtype: None
|
|
"""
|
|
[x1, y1, x2, y2] = self.bbox()
|
|
self.canvas().coords(self._tag, x1, y1, x1 + width, y2)
|
|
|
|
def set_height(self, height):
|
|
"""
|
|
Change the height of this space widget.
|
|
|
|
:param height: The new height.
|
|
:type height: int
|
|
:rtype: None
|
|
"""
|
|
[x1, y1, x2, y2] = self.bbox()
|
|
self.canvas().coords(self._tag, x1, y1, x2, y1 + height)
|
|
|
|
def _tags(self):
|
|
return [self._tag]
|
|
|
|
def __repr__(self):
|
|
return "[Space]"
|
|
|
|
|
|
class ScrollWatcherWidget(CanvasWidget):
|
|
"""
|
|
A special canvas widget that adjusts its ``Canvas``'s scrollregion
|
|
to always include the bounding boxes of all of its children. The
|
|
scroll-watcher widget will only increase the size of the
|
|
``Canvas``'s scrollregion; it will never decrease it.
|
|
"""
|
|
|
|
def __init__(self, canvas, *children, **attribs):
|
|
"""
|
|
Create a new scroll-watcher widget.
|
|
|
|
:type canvas: Tkinter.Canvas
|
|
:param canvas: This canvas widget's canvas.
|
|
:type children: list(CanvasWidget)
|
|
:param children: The canvas widgets watched by the
|
|
scroll-watcher. The scroll-watcher will ensure that these
|
|
canvas widgets are always contained in their canvas's
|
|
scrollregion.
|
|
:param attribs: The new canvas widget's attributes.
|
|
"""
|
|
for child in children:
|
|
self._add_child_widget(child)
|
|
CanvasWidget.__init__(self, canvas, **attribs)
|
|
|
|
def add_child(self, canvaswidget):
|
|
"""
|
|
Add a new canvas widget to the scroll-watcher. The
|
|
scroll-watcher will ensure that the new canvas widget is
|
|
always contained in its canvas's scrollregion.
|
|
|
|
:param canvaswidget: The new canvas widget.
|
|
:type canvaswidget: CanvasWidget
|
|
:rtype: None
|
|
"""
|
|
self._add_child_widget(canvaswidget)
|
|
self.update(canvaswidget)
|
|
|
|
def remove_child(self, canvaswidget):
|
|
"""
|
|
Remove a canvas widget from the scroll-watcher. The
|
|
scroll-watcher will no longer ensure that the new canvas
|
|
widget is always contained in its canvas's scrollregion.
|
|
|
|
:param canvaswidget: The canvas widget to remove.
|
|
:type canvaswidget: CanvasWidget
|
|
:rtype: None
|
|
"""
|
|
self._remove_child_widget(canvaswidget)
|
|
|
|
def _tags(self):
|
|
return []
|
|
|
|
def _update(self, child):
|
|
self._adjust_scrollregion()
|
|
|
|
def _adjust_scrollregion(self):
|
|
"""
|
|
Adjust the scrollregion of this scroll-watcher's ``Canvas`` to
|
|
include the bounding boxes of all of its children.
|
|
"""
|
|
bbox = self.bbox()
|
|
canvas = self.canvas()
|
|
scrollregion = [int(n) for n in canvas["scrollregion"].split()]
|
|
if len(scrollregion) != 4:
|
|
return
|
|
if (
|
|
bbox[0] < scrollregion[0]
|
|
or bbox[1] < scrollregion[1]
|
|
or bbox[2] > scrollregion[2]
|
|
or bbox[3] > scrollregion[3]
|
|
):
|
|
scrollregion = "%d %d %d %d" % (
|
|
min(bbox[0], scrollregion[0]),
|
|
min(bbox[1], scrollregion[1]),
|
|
max(bbox[2], scrollregion[2]),
|
|
max(bbox[3], scrollregion[3]),
|
|
)
|
|
canvas["scrollregion"] = scrollregion
|
|
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Canvas Frame
|
|
##//////////////////////////////////////////////////////
|
|
|
|
|
|
class CanvasFrame(object):
|
|
"""
|
|
A ``Tkinter`` frame containing a canvas and scrollbars.
|
|
``CanvasFrame`` uses a ``ScrollWatcherWidget`` to ensure that all of
|
|
the canvas widgets contained on its canvas are within its
|
|
scrollregion. In order for ``CanvasFrame`` to make these checks,
|
|
all canvas widgets must be registered with ``add_widget`` when they
|
|
are added to the canvas; and destroyed with ``destroy_widget`` when
|
|
they are no longer needed.
|
|
|
|
If a ``CanvasFrame`` is created with no parent, then it will create
|
|
its own main window, including a "Done" button and a "Print"
|
|
button.
|
|
"""
|
|
|
|
def __init__(self, parent=None, **kw):
|
|
"""
|
|
Create a new ``CanvasFrame``.
|
|
|
|
:type parent: Tkinter.BaseWidget or Tkinter.Tk
|
|
:param parent: The parent ``Tkinter`` widget. If no parent is
|
|
specified, then ``CanvasFrame`` will create a new main
|
|
window.
|
|
:param kw: Keyword arguments for the new ``Canvas``. See the
|
|
documentation for ``Tkinter.Canvas`` for more information.
|
|
"""
|
|
# If no parent was given, set up a top-level window.
|
|
if parent is None:
|
|
self._parent = Tk()
|
|
self._parent.title("NLTK")
|
|
self._parent.bind("<Control-p>", lambda e: self.print_to_file())
|
|
self._parent.bind("<Control-x>", self.destroy)
|
|
self._parent.bind("<Control-q>", self.destroy)
|
|
else:
|
|
self._parent = parent
|
|
|
|
# Create a frame for the canvas & scrollbars
|
|
self._frame = frame = Frame(self._parent)
|
|
self._canvas = canvas = Canvas(frame, **kw)
|
|
xscrollbar = Scrollbar(self._frame, orient="horizontal")
|
|
yscrollbar = Scrollbar(self._frame, orient="vertical")
|
|
xscrollbar["command"] = canvas.xview
|
|
yscrollbar["command"] = canvas.yview
|
|
canvas["xscrollcommand"] = xscrollbar.set
|
|
canvas["yscrollcommand"] = yscrollbar.set
|
|
yscrollbar.pack(fill="y", side="right")
|
|
xscrollbar.pack(fill="x", side="bottom")
|
|
canvas.pack(expand=1, fill="both", side="left")
|
|
|
|
# Set initial scroll region.
|
|
scrollregion = "0 0 %s %s" % (canvas["width"], canvas["height"])
|
|
canvas["scrollregion"] = scrollregion
|
|
|
|
self._scrollwatcher = ScrollWatcherWidget(canvas)
|
|
|
|
# If no parent was given, pack the frame, and add a menu.
|
|
if parent is None:
|
|
self.pack(expand=1, fill="both")
|
|
self._init_menubar()
|
|
|
|
def _init_menubar(self):
|
|
menubar = Menu(self._parent)
|
|
|
|
filemenu = Menu(menubar, tearoff=0)
|
|
filemenu.add_command(
|
|
label="Print to Postscript",
|
|
underline=0,
|
|
command=self.print_to_file,
|
|
accelerator="Ctrl-p",
|
|
)
|
|
filemenu.add_command(
|
|
label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-x"
|
|
)
|
|
menubar.add_cascade(label="File", underline=0, menu=filemenu)
|
|
|
|
self._parent.config(menu=menubar)
|
|
|
|
def print_to_file(self, filename=None):
|
|
"""
|
|
Print the contents of this ``CanvasFrame`` to a postscript
|
|
file. If no filename is given, then prompt the user for one.
|
|
|
|
:param filename: The name of the file to print the tree to.
|
|
:type filename: str
|
|
:rtype: None
|
|
"""
|
|
if filename is None:
|
|
ftypes = [("Postscript files", ".ps"), ("All files", "*")]
|
|
filename = asksaveasfilename(filetypes=ftypes, defaultextension=".ps")
|
|
if not filename:
|
|
return
|
|
(x0, y0, w, h) = self.scrollregion()
|
|
postscript = self._canvas.postscript(
|
|
x=x0,
|
|
y=y0,
|
|
width=w + 2,
|
|
height=h + 2,
|
|
pagewidth=w + 2, # points = 1/72 inch
|
|
pageheight=h + 2, # points = 1/72 inch
|
|
pagex=0,
|
|
pagey=0,
|
|
)
|
|
# workaround for bug in Tk font handling
|
|
postscript = postscript.replace(" 0 scalefont ", " 9 scalefont ")
|
|
with open(filename, "wb") as f:
|
|
f.write(postscript.encode("utf8"))
|
|
|
|
def scrollregion(self):
|
|
"""
|
|
:return: The current scroll region for the canvas managed by
|
|
this ``CanvasFrame``.
|
|
:rtype: 4-tuple of int
|
|
"""
|
|
(x1, y1, x2, y2) = self._canvas["scrollregion"].split()
|
|
return (int(x1), int(y1), int(x2), int(y2))
|
|
|
|
def canvas(self):
|
|
"""
|
|
:return: The canvas managed by this ``CanvasFrame``.
|
|
:rtype: Tkinter.Canvas
|
|
"""
|
|
return self._canvas
|
|
|
|
def add_widget(self, canvaswidget, x=None, y=None):
|
|
"""
|
|
Register a canvas widget with this ``CanvasFrame``. The
|
|
``CanvasFrame`` will ensure that this canvas widget is always
|
|
within the ``Canvas``'s scrollregion. If no coordinates are
|
|
given for the canvas widget, then the ``CanvasFrame`` will
|
|
attempt to find a clear area of the canvas for it.
|
|
|
|
:type canvaswidget: CanvasWidget
|
|
:param canvaswidget: The new canvas widget. ``canvaswidget``
|
|
must have been created on this ``CanvasFrame``'s canvas.
|
|
:type x: int
|
|
:param x: The initial x coordinate for the upper left hand
|
|
corner of ``canvaswidget``, in the canvas's coordinate
|
|
space.
|
|
:type y: int
|
|
:param y: The initial y coordinate for the upper left hand
|
|
corner of ``canvaswidget``, in the canvas's coordinate
|
|
space.
|
|
"""
|
|
if x is None or y is None:
|
|
(x, y) = self._find_room(canvaswidget, x, y)
|
|
|
|
# Move to (x,y)
|
|
(x1, y1, x2, y2) = canvaswidget.bbox()
|
|
canvaswidget.move(x - x1, y - y1)
|
|
|
|
# Register with scrollwatcher.
|
|
self._scrollwatcher.add_child(canvaswidget)
|
|
|
|
def _find_room(self, widget, desired_x, desired_y):
|
|
"""
|
|
Try to find a space for a given widget.
|
|
"""
|
|
(left, top, right, bot) = self.scrollregion()
|
|
w = widget.width()
|
|
h = widget.height()
|
|
|
|
if w >= (right - left):
|
|
return (0, 0)
|
|
if h >= (bot - top):
|
|
return (0, 0)
|
|
|
|
# Move the widget out of the way, for now.
|
|
(x1, y1, x2, y2) = widget.bbox()
|
|
widget.move(left - x2 - 50, top - y2 - 50)
|
|
|
|
if desired_x is not None:
|
|
x = desired_x
|
|
for y in range(top, bot - h, int((bot - top - h) / 10)):
|
|
if not self._canvas.find_overlapping(
|
|
x - 5, y - 5, x + w + 5, y + h + 5
|
|
):
|
|
return (x, y)
|
|
|
|
if desired_y is not None:
|
|
y = desired_y
|
|
for x in range(left, right - w, int((right - left - w) / 10)):
|
|
if not self._canvas.find_overlapping(
|
|
x - 5, y - 5, x + w + 5, y + h + 5
|
|
):
|
|
return (x, y)
|
|
|
|
for y in range(top, bot - h, int((bot - top - h) / 10)):
|
|
for x in range(left, right - w, int((right - left - w) / 10)):
|
|
if not self._canvas.find_overlapping(
|
|
x - 5, y - 5, x + w + 5, y + h + 5
|
|
):
|
|
return (x, y)
|
|
return (0, 0)
|
|
|
|
def destroy_widget(self, canvaswidget):
|
|
"""
|
|
Remove a canvas widget from this ``CanvasFrame``. This
|
|
deregisters the canvas widget, and destroys it.
|
|
"""
|
|
self.remove_widget(canvaswidget)
|
|
canvaswidget.destroy()
|
|
|
|
def remove_widget(self, canvaswidget):
|
|
# Deregister with scrollwatcher.
|
|
self._scrollwatcher.remove_child(canvaswidget)
|
|
|
|
def pack(self, cnf={}, **kw):
|
|
"""
|
|
Pack this ``CanvasFrame``. See the documentation for
|
|
``Tkinter.Pack`` for more information.
|
|
"""
|
|
self._frame.pack(cnf, **kw)
|
|
# Adjust to be big enough for kids?
|
|
|
|
def destroy(self, *e):
|
|
"""
|
|
Destroy this ``CanvasFrame``. If this ``CanvasFrame`` created a
|
|
top-level window, then this will close that window.
|
|
"""
|
|
if self._parent is None:
|
|
return
|
|
self._parent.destroy()
|
|
self._parent = None
|
|
|
|
def mainloop(self, *args, **kwargs):
|
|
"""
|
|
Enter the Tkinter mainloop. This function must be called if
|
|
this frame is created from a non-interactive program (e.g.
|
|
from a secript); otherwise, the frame will close as soon as
|
|
the script completes.
|
|
"""
|
|
if in_idle():
|
|
return
|
|
self._parent.mainloop(*args, **kwargs)
|
|
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Text display
|
|
##//////////////////////////////////////////////////////
|
|
|
|
|
|
class ShowText(object):
|
|
"""
|
|
A ``Tkinter`` window used to display a text. ``ShowText`` is
|
|
typically used by graphical tools to display help text, or similar
|
|
information.
|
|
"""
|
|
|
|
def __init__(self, root, title, text, width=None, height=None, **textbox_options):
|
|
if width is None or height is None:
|
|
(width, height) = self.find_dimentions(text, width, height)
|
|
|
|
# Create the main window.
|
|
if root is None:
|
|
self._top = top = Tk()
|
|
else:
|
|
self._top = top = Toplevel(root)
|
|
top.title(title)
|
|
|
|
b = Button(top, text="Ok", command=self.destroy)
|
|
b.pack(side="bottom")
|
|
|
|
tbf = Frame(top)
|
|
tbf.pack(expand=1, fill="both")
|
|
scrollbar = Scrollbar(tbf, orient="vertical")
|
|
scrollbar.pack(side="right", fill="y")
|
|
textbox = Text(tbf, wrap="word", width=width, height=height, **textbox_options)
|
|
textbox.insert("end", text)
|
|
textbox["state"] = "disabled"
|
|
textbox.pack(side="left", expand=1, fill="both")
|
|
scrollbar["command"] = textbox.yview
|
|
textbox["yscrollcommand"] = scrollbar.set
|
|
|
|
# Make it easy to close the window.
|
|
top.bind("q", self.destroy)
|
|
top.bind("x", self.destroy)
|
|
top.bind("c", self.destroy)
|
|
top.bind("<Return>", self.destroy)
|
|
top.bind("<Escape>", self.destroy)
|
|
|
|
# Focus the scrollbar, so they can use up/down, etc.
|
|
scrollbar.focus()
|
|
|
|
def find_dimentions(self, text, width, height):
|
|
lines = text.split("\n")
|
|
if width is None:
|
|
maxwidth = max(len(line) for line in lines)
|
|
width = min(maxwidth, 80)
|
|
|
|
# Now, find height.
|
|
height = 0
|
|
for line in lines:
|
|
while len(line) > width:
|
|
brk = line[:width].rfind(" ")
|
|
line = line[brk:]
|
|
height += 1
|
|
height += 1
|
|
height = min(height, 25)
|
|
|
|
return (width, height)
|
|
|
|
def destroy(self, *e):
|
|
if self._top is None:
|
|
return
|
|
self._top.destroy()
|
|
self._top = None
|
|
|
|
def mainloop(self, *args, **kwargs):
|
|
"""
|
|
Enter the Tkinter mainloop. This function must be called if
|
|
this window is created from a non-interactive program (e.g.
|
|
from a secript); otherwise, the window will close as soon as
|
|
the script completes.
|
|
"""
|
|
if in_idle():
|
|
return
|
|
self._top.mainloop(*args, **kwargs)
|
|
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Entry dialog
|
|
##//////////////////////////////////////////////////////
|
|
|
|
|
|
class EntryDialog(object):
|
|
"""
|
|
A dialog box for entering
|
|
"""
|
|
|
|
def __init__(
|
|
self, parent, original_text="", instructions="", set_callback=None, title=None
|
|
):
|
|
self._parent = parent
|
|
self._original_text = original_text
|
|
self._set_callback = set_callback
|
|
|
|
width = int(max(30, len(original_text) * 3 / 2))
|
|
self._top = Toplevel(parent)
|
|
|
|
if title:
|
|
self._top.title(title)
|
|
|
|
# The text entry box.
|
|
entryframe = Frame(self._top)
|
|
entryframe.pack(expand=1, fill="both", padx=5, pady=5, ipady=10)
|
|
if instructions:
|
|
l = Label(entryframe, text=instructions)
|
|
l.pack(side="top", anchor="w", padx=30)
|
|
self._entry = Entry(entryframe, width=width)
|
|
self._entry.pack(expand=1, fill="x", padx=30)
|
|
self._entry.insert(0, original_text)
|
|
|
|
# A divider
|
|
divider = Frame(self._top, borderwidth=1, relief="sunken")
|
|
divider.pack(fill="x", ipady=1, padx=10)
|
|
|
|
# The buttons.
|
|
buttons = Frame(self._top)
|
|
buttons.pack(expand=0, fill="x", padx=5, pady=5)
|
|
b = Button(buttons, text="Cancel", command=self._cancel, width=8)
|
|
b.pack(side="right", padx=5)
|
|
b = Button(buttons, text="Ok", command=self._ok, width=8, default="active")
|
|
b.pack(side="left", padx=5)
|
|
b = Button(buttons, text="Apply", command=self._apply, width=8)
|
|
b.pack(side="left")
|
|
|
|
self._top.bind("<Return>", self._ok)
|
|
self._top.bind("<Control-q>", self._cancel)
|
|
self._top.bind("<Escape>", self._cancel)
|
|
|
|
self._entry.focus()
|
|
|
|
def _reset(self, *e):
|
|
self._entry.delete(0, "end")
|
|
self._entry.insert(0, self._original_text)
|
|
if self._set_callback:
|
|
self._set_callback(self._original_text)
|
|
|
|
def _cancel(self, *e):
|
|
try:
|
|
self._reset()
|
|
except:
|
|
pass
|
|
self._destroy()
|
|
|
|
def _ok(self, *e):
|
|
self._apply()
|
|
self._destroy()
|
|
|
|
def _apply(self, *e):
|
|
if self._set_callback:
|
|
self._set_callback(self._entry.get())
|
|
|
|
def _destroy(self, *e):
|
|
if self._top is None:
|
|
return
|
|
self._top.destroy()
|
|
self._top = None
|
|
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Colorized List
|
|
##//////////////////////////////////////////////////////
|
|
|
|
|
|
class ColorizedList(object):
|
|
"""
|
|
An abstract base class for displaying a colorized list of items.
|
|
Subclasses should define:
|
|
- ``_init_colortags``, which sets up Text color tags that
|
|
will be used by the list.
|
|
- ``_item_repr``, which returns a list of (text,colortag)
|
|
tuples that make up the colorized representation of the
|
|
item.
|
|
:note: Typically, you will want to register a callback for
|
|
``'select'`` that calls ``mark`` on the given item.
|
|
"""
|
|
|
|
def __init__(self, parent, items=[], **options):
|
|
"""
|
|
Construct a new list.
|
|
|
|
:param parent: The Tk widget that contains the colorized list
|
|
:param items: The initial contents of the colorized list.
|
|
:param options:
|
|
"""
|
|
self._parent = parent
|
|
self._callbacks = {}
|
|
|
|
# Which items are marked?
|
|
self._marks = {}
|
|
|
|
# Initialize the Tkinter frames.
|
|
self._init_itemframe(options.copy())
|
|
|
|
# Set up key & mouse bindings.
|
|
self._textwidget.bind("<KeyPress>", self._keypress)
|
|
self._textwidget.bind("<ButtonPress>", self._buttonpress)
|
|
|
|
# Fill in the given CFG's items.
|
|
self._items = None
|
|
self.set(items)
|
|
|
|
# ////////////////////////////////////////////////////////////
|
|
# Abstract methods
|
|
# ////////////////////////////////////////////////////////////
|
|
@abstractmethod
|
|
def _init_colortags(self, textwidget, options):
|
|
"""
|
|
Set up any colortags that will be used by this colorized list.
|
|
E.g.:
|
|
>>> textwidget.tag_config('terminal', foreground='black')
|
|
"""
|
|
|
|
@abstractmethod
|
|
def _item_repr(self, item):
|
|
"""
|
|
Return a list of (text, colortag) tuples that make up the
|
|
colorized representation of the item. Colorized
|
|
representations may not span multiple lines. I.e., the text
|
|
strings returned may not contain newline characters.
|
|
"""
|
|
|
|
# ////////////////////////////////////////////////////////////
|
|
# Item Access
|
|
# ////////////////////////////////////////////////////////////
|
|
|
|
def get(self, index=None):
|
|
"""
|
|
:return: A list of the items contained by this list.
|
|
"""
|
|
if index is None:
|
|
return self._items[:]
|
|
else:
|
|
return self._items[index]
|
|
|
|
def set(self, items):
|
|
"""
|
|
Modify the list of items contained by this list.
|
|
"""
|
|
items = list(items)
|
|
if self._items == items:
|
|
return
|
|
self._items = list(items)
|
|
|
|
self._textwidget["state"] = "normal"
|
|
self._textwidget.delete("1.0", "end")
|
|
for item in items:
|
|
for (text, colortag) in self._item_repr(item):
|
|
assert "\n" not in text, "item repr may not contain newline"
|
|
self._textwidget.insert("end", text, colortag)
|
|
self._textwidget.insert("end", "\n")
|
|
# Remove the final newline
|
|
self._textwidget.delete("end-1char", "end")
|
|
self._textwidget.mark_set("insert", "1.0")
|
|
self._textwidget["state"] = "disabled"
|
|
# Clear all marks
|
|
self._marks.clear()
|
|
|
|
def unmark(self, item=None):
|
|
"""
|
|
Remove highlighting from the given item; or from every item,
|
|
if no item is given.
|
|
:raise ValueError: If ``item`` is not contained in the list.
|
|
:raise KeyError: If ``item`` is not marked.
|
|
"""
|
|
if item is None:
|
|
self._marks.clear()
|
|
self._textwidget.tag_remove("highlight", "1.0", "end+1char")
|
|
else:
|
|
index = self._items.index(item)
|
|
del self._marks[item]
|
|
(start, end) = ("%d.0" % (index + 1), "%d.0" % (index + 2))
|
|
self._textwidget.tag_remove("highlight", start, end)
|
|
|
|
def mark(self, item):
|
|
"""
|
|
Highlight the given item.
|
|
:raise ValueError: If ``item`` is not contained in the list.
|
|
"""
|
|
self._marks[item] = 1
|
|
index = self._items.index(item)
|
|
(start, end) = ("%d.0" % (index + 1), "%d.0" % (index + 2))
|
|
self._textwidget.tag_add("highlight", start, end)
|
|
|
|
def markonly(self, item):
|
|
"""
|
|
Remove any current highlighting, and mark the given item.
|
|
:raise ValueError: If ``item`` is not contained in the list.
|
|
"""
|
|
self.unmark()
|
|
self.mark(item)
|
|
|
|
def view(self, item):
|
|
"""
|
|
Adjust the view such that the given item is visible. If
|
|
the item is already visible, then do nothing.
|
|
"""
|
|
index = self._items.index(item)
|
|
self._textwidget.see("%d.0" % (index + 1))
|
|
|
|
# ////////////////////////////////////////////////////////////
|
|
# Callbacks
|
|
# ////////////////////////////////////////////////////////////
|
|
|
|
def add_callback(self, event, func):
|
|
"""
|
|
Register a callback function with the list. This function
|
|
will be called whenever the given event occurs.
|
|
|
|
:param event: The event that will trigger the callback
|
|
function. Valid events are: click1, click2, click3,
|
|
space, return, select, up, down, next, prior, move
|
|
:param func: The function that should be called when
|
|
the event occurs. ``func`` will be called with a
|
|
single item as its argument. (The item selected
|
|
or the item moved to).
|
|
"""
|
|
if event == "select":
|
|
events = ["click1", "space", "return"]
|
|
elif event == "move":
|
|
events = ["up", "down", "next", "prior"]
|
|
else:
|
|
events = [event]
|
|
|
|
for e in events:
|
|
self._callbacks.setdefault(e, {})[func] = 1
|
|
|
|
def remove_callback(self, event, func=None):
|
|
"""
|
|
Deregister a callback function. If ``func`` is none, then
|
|
all callbacks are removed for the given event.
|
|
"""
|
|
if event is None:
|
|
events = list(self._callbacks.keys())
|
|
elif event == "select":
|
|
events = ["click1", "space", "return"]
|
|
elif event == "move":
|
|
events = ["up", "down", "next", "prior"]
|
|
else:
|
|
events = [event]
|
|
|
|
for e in events:
|
|
if func is None:
|
|
del self._callbacks[e]
|
|
else:
|
|
try:
|
|
del self._callbacks[e][func]
|
|
except:
|
|
pass
|
|
|
|
# ////////////////////////////////////////////////////////////
|
|
# Tkinter Methods
|
|
# ////////////////////////////////////////////////////////////
|
|
|
|
def pack(self, cnf={}, **kw):
|
|
# "@include: Tkinter.Pack.pack"
|
|
self._itemframe.pack(cnf, **kw)
|
|
|
|
def grid(self, cnf={}, **kw):
|
|
# "@include: Tkinter.Grid.grid"
|
|
self._itemframe.grid(cnf, *kw)
|
|
|
|
def focus(self):
|
|
# "@include: Tkinter.Widget.focus"
|
|
self._textwidget.focus()
|
|
|
|
# ////////////////////////////////////////////////////////////
|
|
# Internal Methods
|
|
# ////////////////////////////////////////////////////////////
|
|
|
|
def _init_itemframe(self, options):
|
|
self._itemframe = Frame(self._parent)
|
|
|
|
# Create the basic Text widget & scrollbar.
|
|
options.setdefault("background", "#e0e0e0")
|
|
self._textwidget = Text(self._itemframe, **options)
|
|
self._textscroll = Scrollbar(self._itemframe, takefocus=0, orient="vertical")
|
|
self._textwidget.config(yscrollcommand=self._textscroll.set)
|
|
self._textscroll.config(command=self._textwidget.yview)
|
|
self._textscroll.pack(side="right", fill="y")
|
|
self._textwidget.pack(expand=1, fill="both", side="left")
|
|
|
|
# Initialize the colorization tags
|
|
self._textwidget.tag_config(
|
|
"highlight", background="#e0ffff", border="1", relief="raised"
|
|
)
|
|
self._init_colortags(self._textwidget, options)
|
|
|
|
# How do I want to mark keyboard selection?
|
|
self._textwidget.tag_config("sel", foreground="")
|
|
self._textwidget.tag_config(
|
|
"sel", foreground="", background="", border="", underline=1
|
|
)
|
|
self._textwidget.tag_lower("highlight", "sel")
|
|
|
|
def _fire_callback(self, event, itemnum):
|
|
if event not in self._callbacks:
|
|
return
|
|
if 0 <= itemnum < len(self._items):
|
|
item = self._items[itemnum]
|
|
else:
|
|
item = None
|
|
for cb_func in list(self._callbacks[event].keys()):
|
|
cb_func(item)
|
|
|
|
def _buttonpress(self, event):
|
|
clickloc = "@%d,%d" % (event.x, event.y)
|
|
insert_point = self._textwidget.index(clickloc)
|
|
itemnum = int(insert_point.split(".")[0]) - 1
|
|
self._fire_callback("click%d" % event.num, itemnum)
|
|
|
|
def _keypress(self, event):
|
|
if event.keysym == "Return" or event.keysym == "space":
|
|
insert_point = self._textwidget.index("insert")
|
|
itemnum = int(insert_point.split(".")[0]) - 1
|
|
self._fire_callback(event.keysym.lower(), itemnum)
|
|
return
|
|
elif event.keysym == "Down":
|
|
delta = "+1line"
|
|
elif event.keysym == "Up":
|
|
delta = "-1line"
|
|
elif event.keysym == "Next":
|
|
delta = "+10lines"
|
|
elif event.keysym == "Prior":
|
|
delta = "-10lines"
|
|
else:
|
|
return "continue"
|
|
|
|
self._textwidget.mark_set("insert", "insert" + delta)
|
|
self._textwidget.see("insert")
|
|
self._textwidget.tag_remove("sel", "1.0", "end+1char")
|
|
self._textwidget.tag_add("sel", "insert linestart", "insert lineend")
|
|
|
|
insert_point = self._textwidget.index("insert")
|
|
itemnum = int(insert_point.split(".")[0]) - 1
|
|
self._fire_callback(event.keysym.lower(), itemnum)
|
|
|
|
return "break"
|
|
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Improved OptionMenu
|
|
##//////////////////////////////////////////////////////
|
|
|
|
|
|
class MutableOptionMenu(Menubutton):
|
|
def __init__(self, master, values, **options):
|
|
self._callback = options.get("command")
|
|
if "command" in options:
|
|
del options["command"]
|
|
|
|
# Create a variable
|
|
self._variable = variable = StringVar()
|
|
if len(values) > 0:
|
|
variable.set(values[0])
|
|
|
|
kw = {
|
|
"borderwidth": 2,
|
|
"textvariable": variable,
|
|
"indicatoron": 1,
|
|
"relief": RAISED,
|
|
"anchor": "c",
|
|
"highlightthickness": 2,
|
|
}
|
|
kw.update(options)
|
|
Widget.__init__(self, master, "menubutton", kw)
|
|
self.widgetName = "tk_optionMenu"
|
|
self._menu = Menu(self, name="menu", tearoff=0)
|
|
self.menuname = self._menu._w
|
|
|
|
self._values = []
|
|
for value in values:
|
|
self.add(value)
|
|
|
|
self["menu"] = self._menu
|
|
|
|
def add(self, value):
|
|
if value in self._values:
|
|
return
|
|
|
|
def set(value=value):
|
|
self.set(value)
|
|
|
|
self._menu.add_command(label=value, command=set)
|
|
self._values.append(value)
|
|
|
|
def set(self, value):
|
|
self._variable.set(value)
|
|
if self._callback:
|
|
self._callback(value)
|
|
|
|
def remove(self, value):
|
|
# Might raise indexerror: pass to parent.
|
|
i = self._values.index(value)
|
|
del self._values[i]
|
|
self._menu.delete(i, i)
|
|
|
|
def __getitem__(self, name):
|
|
if name == "menu":
|
|
return self.__menu
|
|
return Widget.__getitem__(self, name)
|
|
|
|
def destroy(self):
|
|
"""Destroy this widget and the associated menu."""
|
|
Menubutton.destroy(self)
|
|
self._menu = None
|
|
|
|
|
|
##//////////////////////////////////////////////////////
|
|
## Test code.
|
|
##//////////////////////////////////////////////////////
|
|
|
|
|
|
def demo():
|
|
"""
|
|
A simple demonstration showing how to use canvas widgets.
|
|
"""
|
|
|
|
def fill(cw):
|
|
from random import randint
|
|
|
|
cw["fill"] = "#00%04d" % randint(0, 9999)
|
|
|
|
def color(cw):
|
|
from random import randint
|
|
|
|
cw["color"] = "#ff%04d" % randint(0, 9999)
|
|
|
|
cf = CanvasFrame(closeenough=10, width=300, height=300)
|
|
c = cf.canvas()
|
|
ct3 = TextWidget(c, "hiya there", draggable=1)
|
|
ct2 = TextWidget(c, "o o\n||\n___\n U", draggable=1, justify="center")
|
|
co = OvalWidget(c, ct2, outline="red")
|
|
ct = TextWidget(c, "o o\n||\n\\___/", draggable=1, justify="center")
|
|
cp = ParenWidget(c, ct, color="red")
|
|
cb = BoxWidget(c, cp, fill="cyan", draggable=1, width=3, margin=10)
|
|
equation = SequenceWidget(
|
|
c,
|
|
SymbolWidget(c, "forall"),
|
|
TextWidget(c, "x"),
|
|
SymbolWidget(c, "exists"),
|
|
TextWidget(c, "y: "),
|
|
TextWidget(c, "x"),
|
|
SymbolWidget(c, "notequal"),
|
|
TextWidget(c, "y"),
|
|
)
|
|
space = SpaceWidget(c, 0, 30)
|
|
cstack = StackWidget(c, cb, ct3, space, co, equation, align="center")
|
|
prompt_msg = TextWidget(
|
|
c, "try clicking\nand dragging", draggable=1, justify="center"
|
|
)
|
|
cs = SequenceWidget(c, cstack, prompt_msg)
|
|
zz = BracketWidget(c, cs, color="green4", width=3)
|
|
cf.add_widget(zz, 60, 30)
|
|
|
|
cb.bind_click(fill)
|
|
ct.bind_click(color)
|
|
co.bind_click(fill)
|
|
ct2.bind_click(color)
|
|
ct3.bind_click(color)
|
|
|
|
cf.mainloop()
|
|
# ShowText(None, 'title', ((('this is text'*150)+'\n')*5))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
demo()
|