Source code for enable.constraints_container
# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
from collections import deque
# traits imports
from traits.api import (
Any,
Bool,
Callable,
Dict,
Either,
Instance,
List,
Property,
)
# local imports
from .container import Container
from .coordinate_box import CoordinateBox
from .layout.layout_helpers import expand_constraints
from .layout.layout_manager import LayoutManager
from .layout.utils import (
add_symbolic_contents_constraints,
get_from_constraints_namespace,
)
[docs]class ConstraintsContainer(Container):
""" A Container which lays out its child components using a
constraints-based layout solver.
"""
# A read-only symbolic object that represents the left boundary of
# the component
contents_left = Property(fget=get_from_constraints_namespace)
# A read-only symbolic object that represents the right boundary
# of the component
contents_right = Property(fget=get_from_constraints_namespace)
# A read-only symbolic object that represents the bottom boundary
# of the component
contents_bottom = Property(fget=get_from_constraints_namespace)
# A read-only symbolic object that represents the top boundary of
# the component
contents_top = Property(fget=get_from_constraints_namespace)
# A read-only symbolic object that represents the width of the
# component
contents_width = Property(fget=get_from_constraints_namespace)
# A read-only symbolic object that represents the height of the
# component
contents_height = Property(fget=get_from_constraints_namespace)
# A read-only symbolic object that represents the vertical center
# of the component
contents_v_center = Property(fget=get_from_constraints_namespace)
# A read-only symbolic object that represents the horizontal
# center of the component
contents_h_center = Property(fget=get_from_constraints_namespace)
# The layout constraints for this container.
# This can either be a list or a callable. If it is a callable, it will be
# called with a single argument, the ConstraintsContainer, and be expected
# to return a list of constraints.
layout_constraints = Either(List, Callable)
# A boolean which indicates whether or not to allow the layout
# ownership of this container to be transferred to an ancestor.
# This is False by default, which means that every container
# get its own layout solver. This improves speed and reduces
# memory use (by keeping a solver's internal tableaux small)
# but at the cost of not being able to share constraints
# across Container boundaries. This flag must be explicitly
# marked as True to enable sharing.
share_layout = Bool(False)
# Sharing related private traits
_owns_layout = Bool(True)
_layout_owner = Any
# The contents box constraints for this container
_contents_constraints = Property
# The user-specified layout constraints, with layout helpers expanded
_layout_constraints = Property
# A dictionary of components added to this container
_component_map = Dict
# The kiwi solver
_layout_manager = Instance(LayoutManager, allow_none=True)
_offset_table = List
_layout_table = List
# ------------------------------------------------------------------------
# Public methods
# ------------------------------------------------------------------------
[docs] def do_layout(self, size=None, force=False):
""" Make sure child components get a chance to refresh their layout.
"""
for component in self.components:
component.do_layout(size=size, force=force)
[docs] def refresh(self):
""" Re-run the constraints solver in response to a resize or
constraints modification.
"""
if self._owns_layout:
if self._layout_manager is None:
return
mgr_layout = self._layout_manager.layout
offset_table = self._offset_table
width_var = self.layout_width
height_var = self.layout_height
width, height = self.bounds
def layout():
running_index = 1
for offset_index, item in self._layout_table:
dx, dy = offset_table[offset_index]
nx, ny = item.left.value(), item.bottom.value()
item.position = (nx - dx, ny - dy)
item.bounds = (
item.layout_width.value(),
item.layout_height.value(),
)
offset_table[running_index] = (nx, ny)
running_index += 1
mgr_layout(layout, width_var, height_var, (width, height))
self.invalidate_draw()
else:
self._layout_owner.refresh()
[docs] def relayout(self):
""" Explicitly regenerate the container's constraints and refresh the
layout.
"""
if not self.share_layout:
self._init_layout()
self.refresh()
elif self._layout_owner is not None:
self._layout_owner.relayout()
# ------------------------------------------------------------------------
# Layout Sharing
# ------------------------------------------------------------------------
[docs] def transfer_layout_ownership(self, owner):
""" A method which can be called by other components in the
hierarchy to gain ownership responsibility for the layout
of the children of this container. By default, the transfer
is allowed and is the mechanism which allows constraints to
cross widget boundaries. Subclasses should reimplement this
method if different behavior is desired.
Parameters
----------
owner : ConstraintsContainer
The container which has taken ownership responsibility
for laying out the children of this component. All
relayout and refresh requests will be forwarded to this
component.
Returns
-------
results : bool
True if the transfer was allowed, False otherwise.
"""
if not self.share_layout:
return False
self._owns_layout = False
self._layout_owner = owner
self._layout_manager = None
return True
[docs] def will_transfer(self):
""" Whether or not the container expects to transfer its layout
ownership to its parent.
This method is predictive in nature and exists so that layout
managers are not senslessly created during the bottom-up layout
initialization pass. It is declared public so that subclasses
can override the behavior if necessary.
"""
cls = ConstraintsContainer
if self.share_layout:
if self.container and isinstance(self.container, cls):
return True
return False
# ------------------------------------------------------------------------
# Traits methods
# ------------------------------------------------------------------------
def _bounds_changed(self, old, new):
""" Run the solver when the container's bounds change.
"""
super()._bounds_changed(old, new)
self.refresh()
def _layout_constraints_changed(self):
""" Refresh the layout when the user constraints change.
"""
self.relayout()
def _get__contents_constraints(self):
""" Return the constraints which define the content box of this
container.
"""
add_symbolic_contents_constraints(self._constraints_vars)
return [
self.contents_left == self.left,
self.contents_bottom == self.bottom,
self.contents_right == self.left + self.layout_width,
self.contents_top == self.bottom + self.layout_height,
]
def _get__layout_constraints(self):
""" React to changes of the user controlled constraints.
"""
if self.layout_constraints is None:
return []
if callable(self.layout_constraints):
new = self.layout_constraints(self)
else:
new = self.layout_constraints
# Expand any layout helpers
return [cns for cns in expand_constraints(self, new)]
def __components_items_changed(self, event):
""" Make sure components that are added can be used with constraints.
"""
# Remove stale components from the map
for item in event.removed:
item.observe(
self._handle_changed_component_size_hint,
"layout_size_hint",
remove=True,
)
del self._component_map[item.id]
# Check the added components
self._check_and_add_components(event.added)
def __components_changed(self, new):
""" Make sure components that are added can be used with constraints.
"""
# Clear the component maps
for key, item in self._component_map.items():
item.observe(
self._handle_changed_component_size_hint,
"layout_size_hint",
remove=True,
)
self._component_map = {}
# Check the new components
self._check_and_add_components(new)
def _handle_changed_component_size_hint(self, event):
""" Refresh the size hint contraints for a child component
"""
self.relayout()
# ------------------------------------------------------------------------
# Protected methods
# ------------------------------------------------------------------------
def _build_layout_table(self):
""" Build the layout and offset tables for this container.
A layout table is a pair of flat lists which hold the required
objects for laying out the child widgets of this container.
The flat table is built in advance (and rebuilt if and when
the tree structure changes) so that it's not necessary to
perform an expensive tree traversal to layout the children
on every resize event.
Returns
-------
result : (list, list)
The offset table and layout table to use during a resize
event.
"""
# The offset table is a list of (dx, dy) tuples which are the
# x, y offsets of children expressed in the coordinates of the
# layout owner container. This owner container may be different
# from the parent of the widget, and so the delta offset must
# be subtracted from the computed geometry values during layout.
# The offset table is updated during a layout pass in breadth
# first order.
#
# The layout table is a flat list of (idx, updater) tuples. The
# idx is an index into the offset table where the given child
# can find the offset to use for its layout. The updater is a
# callable provided by the widget which accepts the dx, dy
# offset and will update the layout geometry of the widget.
zero_offset = (0, 0)
offset_table = [zero_offset]
layout_table = []
queue = deque((0, child) for child in self._component_map.values())
# Micro-optimization: pre-fetch bound methods and store globals
# as locals. This method is not on the code path of a resize
# event, but it is on the code path of a relayout. If there
# are many children, the queue could potentially grow large.
push_offset = offset_table.append
push_item = layout_table.append
push = queue.append
pop = queue.popleft
CoordinateBox_ = CoordinateBox
Container_ = ConstraintsContainer
isinst = isinstance
# The queue yields the items in the tree in breadth-first order
# starting with the immediate children of this container. If a
# given child is a container that will share its layout, then
# the children of that container are added to the queue to be
# added to the layout table.
running_index = 0
while queue:
offset_index, item = pop()
if isinst(item, CoordinateBox_):
push_item((offset_index, item))
push_offset(zero_offset)
running_index += 1
if isinst(item, Container_):
if item.transfer_layout_ownership(self):
for child in item._component_map.values():
push((running_index, child))
return offset_table, layout_table
def _check_and_add_components(self, components):
""" Make sure components can be used with constraints.
"""
for item in components:
key = item.id
if len(key) == 0:
msg = "Components added to a {0} must have a valid 'id' trait."
name = type(self).__name__
raise ValueError(msg.format(name))
elif key in self._component_map:
msg = "A Component with id '{0}' has already been added."
raise ValueError(msg.format(key))
elif key == self.id:
msg = "Can't add a Component with the same id as its parent."
raise ValueError(msg)
self._component_map[key] = item
item.observe(
self._handle_changed_component_size_hint, "layout_size_hint"
)
# Update the layout
self.relayout()
def _generate_constraints(self, layout_table):
""" Creates the list of kiwi Constraint objects for
the widgets for which this container owns the layout.
This method walks over the items in the given layout table and
aggregates their constraints into a single list of kiwi
Constraint objects which can be given to the layout
manager.
Parameters
----------
layout_table : list
The layout table created by a call to _build_layout_table.
Returns
-------
result : list
The list of kiwi Constraint instances to pass to
the layout manager.
"""
user_cns = self._layout_constraints
user_cns_extend = user_cns.extend
# The list of raw kiwi constraints which will be returned
# from this method to be added to the kiwi solver.
raw_cns = self._hard_constraints + self._contents_constraints
raw_cns_extend = raw_cns.extend
isinst = isinstance
Container_ = ConstraintsContainer
# The first element in a layout table item is its offset index
# which is not relevant to constraints generation.
for _, child in layout_table:
raw_cns_extend(child._hard_constraints)
if isinst(child, Container_):
if child.transfer_layout_ownership(self):
user_cns_extend(child._layout_constraints)
raw_cns_extend(child._contents_constraints)
else:
raw_cns_extend(child._size_constraints)
else:
raw_cns_extend(child._size_constraints)
return raw_cns + user_cns
def _init_layout(self):
""" Initializes the layout for the container.
"""
# Layout ownership can only be transferred *after* this init
# layout method is called, since layout occurs bottom up. So,
# we only initialize a layout manager if we are not going to
# transfer ownership at some point.
if not self.will_transfer():
offset_table, layout_table = self._build_layout_table()
cns = self._generate_constraints(layout_table)
# Initializing the layout manager can fail if the objective
# function is unbounded. We let that failure occur so it can
# be logged. Nothing is stored until it succeeds.
manager = LayoutManager()
manager.initialize(cns)
self._offset_table = offset_table
self._layout_table = layout_table
self._layout_manager = manager