Source code for enable.layout.layout_helpers

# (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 abc import ABCMeta, abstractmethod
from collections import defaultdict
from uuid import uuid4

from kiwisolver import Variable, Constraint

from traits.api import HasTraits, Instance, Range

from .ab_constrainable import ABConstrainable
from .constraints_namespace import ConstraintsNamespace
from .geometry import Box
from .linear_symbolic import LinearSymbolic
from .utils import add_symbolic_constraints, STRENGTHS


# -----------------------------------------------------------------------------
# Default Spacing
# -----------------------------------------------------------------------------
class DefaultSpacing(HasTraits):
    """ A class which encapsulates the default spacing parameters for
    the various layout helper objects.
    """

    #: The space between abutted components
    ABUTMENT = Range(low=0, value=10)

    #: The space between aligned anchors
    ALIGNMENT = Range(low=0, value=0)

    #: The margins for box helpers
    BOX_MARGINS = Instance(Box, default=Box(0, 0, 0, 0))


# We only require a singleton of DefaultSpacing
DefaultSpacing = DefaultSpacing()


# -----------------------------------------------------------------------------
# Helper Functions
# -----------------------------------------------------------------------------
[docs]def expand_constraints(component, constraints): """ A function which expands any DeferredConstraints in the provided list. This is a generator function which yields the flattened stream of constraints. Parameters ---------- component : Constrainable The constrainable component with which the constraints are associated. This will be passed to the .get_constraints() method of any DeferredConstraint instance. constraints : list The list of constraints. Yields ------ constraints The stream of expanded constraints. """ for cn in constraints: if isinstance(cn, DeferredConstraints): for item in cn.get_constraints(component): if item is not None: yield item else: if cn is not None and isinstance(cn, Constraint): yield cn
[docs]def is_spacer(item): """ Returns True if the given item can be considered a spacer, False other otherwise. """ return isinstance(item, (Spacer, int))
# ----------------------------------------------------------------------------- # Deferred Constraints # -----------------------------------------------------------------------------
[docs]class DeferredConstraints(object, metaclass=ABCMeta): """ Abstract base class for objects that will yield lists of constraints upon request. """ def __init__(self): """ Initialize a DeferredConstraints instance. """ # __or__() will set the default_strength. If provided, it will be # combined with the constraints created by this instance. self.default_strength = None def __or__(self, other): """ Set the strength of all of the constraints to a common strength. """ if isinstance(other, (float, int)): self.default_string = float(other) elif isinstance(other, str): if other not in STRENGTHS: raise ValueError("Invalid strength %r" % other) self.default_strength = other else: msg = "Strength must be a string or number. Got %s instead." raise TypeError(msg % type(other)) return self
[docs] def when(self, switch): """ A simple method that can be used to switch off the generated constraints depending on a boolean value. """ if switch: return self
[docs] def get_constraints(self, component): """ Returns a list of constraints. Parameters ---------- component : Component or None The component that owns this DeferredConstraints. It can be None for contexts in which there is not a containing component, such as in certain nested DeferredConstraints. Returns ------- result : list of Constraints The list of Constraint objects which have been weighted by any provided strengths. """ cn_list = self._get_constraints(component) strength = self.default_strength if strength is not None: cn_list = [cn | strength for cn in cn_list] return cn_list
@abstractmethod def _get_constraints(self, component): """ Returns a list of LinearConstraint objects. Subclasses must implement this method to actually yield their constraints. Users of instances should instead call the `get_constraints()` method which will combine these constraints with the `default_strength` if provided. Parameters ---------- component : Component or None The component that owns this DeferredConstraints. It can be None for contexts in which there is not a containing component, such as in certain nested DeferredConstraints. Returns ------- result : list of LinearConstraints The list of LinearConstraint objects for this deferred instance. """ raise NotImplementedError
# ----------------------------------------------------------------------------- # Deferred Constraints Implementations # -----------------------------------------------------------------------------
[docs]class DeferredConstraintsFunction(DeferredConstraints): """ A concrete implementation of DeferredConstraints which will call a function to get the constraint list upon request. """ def __init__(self, func, *args, **kwds): """ Initialize a DeferredConstraintsFunction. Parameters ---------- func : callable A callable object which will return the list of constraints. *args The arguments to pass to 'func'. **kwds The keyword arguments to pass to 'func'. """ super().__init__() self.func = func self.args = args self.kwds = kwds def _get_constraints(self, component): """ Abstract method implementation which calls the underlying function to generate the list of constraints. """ return self.func(*self.args, **self.kwds)
[docs]class AbutmentHelper(DeferredConstraints): """ A concrete implementation of DeferredConstraints which will lay out its components by abutting them in a given orientation. """ def __init__(self, orientation, *items, **config): """ Initialize an AbutmentHelper. Parameters ---------- orientation A string which is either 'horizontal' or 'vertical' which indicates the abutment orientation. *items The components to abut in the given orientation. **config Configuration options for how this helper should behave. The following options are currently supported: spacing An integer >= 0 which indicates how many pixels of inter-element spacing to use during abutment. The default is the value of DefaultSpacing.ABUTMENT. """ super().__init__() self.orientation = orientation self.items = items self.spacing = config.get("spacing", DefaultSpacing.ABUTMENT) def __repr__(self): """ A pretty string representation of the helper. """ items = ", ".join(map(repr, self.items)) return "{0}({1})".format(self.orientation, items) def _get_constraints(self, component): """ Abstract method implementation which applies the constraints to the given items, after filtering them for None values. """ items = [item for item in self.items if item is not None] factories = AbutmentConstraintFactory.from_items( items, self.orientation, self.spacing ) cn_lists = (f.constraints() for f in factories) return list(cn for cns in cn_lists for cn in cns)
[docs]class AlignmentHelper(DeferredConstraints): """ A deferred constraints helper class that lays out with a given anchor to align. """ def __init__(self, anchor, *items, **config): """ Initialize an AlignmentHelper. Parameters ---------- anchor A string which is either 'left', 'right', 'top', 'bottom', 'v_center', or 'h_center'. *items The components to align on the given anchor. **config Configuration options for how this helper should behave. The following options are currently supported: spacing An integer >= 0 which indicates how many pixels of inter-element spacing to use during alignement. The default is the value of DefaultSpacing.ALIGNMENT. """ super().__init__() self.anchor = anchor self.items = items self.spacing = config.get("spacing", DefaultSpacing.ALIGNMENT) def __repr__(self): """ A pretty string representation of the layout helper. """ items = ", ".join(map(repr, self.items)) return "align({0!r}, {1})".format(self.anchor, items) def _get_constraints(self, component): """ Abstract method implementation which applies the constraints to the given items, after filtering them for None values. """ items = [item for item in self.items if item is not None] # If there are less than two items, no alignment needs to # happen, so return no constraints. if len(items) < 2: return [] factories = AlignmentConstraintFactory.from_items( items, self.anchor, self.spacing ) cn_lists = (f.constraints() for f in factories) return list(cn for cns in cn_lists for cn in cns)
[docs]class BoxHelper(DeferredConstraints): """ A DeferredConstraints helper class which adds a box model to the helper. The addition of the box model allows the helper to be registered as ABConstrainable which has the effect of allowing box helper instances to be nested. """ def __init__(self, name): """ Initialize a BoxHelper. Parameters ---------- name : str A string name to prepend to a unique owner id generated for this box helper, to aid in debugging. """ super().__init__() owner = uuid4().hex[:8] self.constraints_id = name + "|" + owner self._namespace = ConstraintsNamespace(name, owner) add_symbolic_constraints(self._namespace) left = property(lambda self: self._namespace.left) top = property(lambda self: self._namespace.top) right = property(lambda self: self._namespace.right) bottom = property(lambda self: self._namespace.bottom) layout_width = property(lambda self: self._namespace.layout_width) layout_height = property(lambda self: self._namespace.layout_height) v_center = property(lambda self: self._namespace.v_center) h_center = property(lambda self: self._namespace.h_center)
ABConstrainable.register(BoxHelper)
[docs]class LinearBoxHelper(BoxHelper): """ A layout helper which arranges items in a linear box. """ #: A mapping orientation to the anchor names needed to make the #: constraints on the containing component. orientation_map = { "horizontal": ("left", "right"), "vertical": ("top", "bottom"), } #: A mapping of ortho orientations ortho_map = {"horizontal": "vertical", "vertical": "horizontal"} def __init__(self, orientation, *items, **config): """ Initialize a LinearBoxHelper. Parameters ---------- orientation : str The layout orientation of the box. This must be either 'horizontal' or 'vertical'. *items The components to align on the given anchor. **config Configuration options for how this helper should behave. The following options are currently supported: spacing An integer >= 0 which indicates how many pixels of inter-element spacing to use during abutment. The default is the value of DefaultSpacing.ABUTMENT. margins A int, tuple of ints, or Box of ints >= 0 which indicate how many pixels of margin to add around the bounds of the box. The default is the value of DefaultSpacing.BOX_MARGIN. """ super().__init__(orientation[0] + "box") self.items = items self.orientation = orientation self.ortho_orientation = self.ortho_map[orientation] self.spacing = config.get("spacing", DefaultSpacing.ABUTMENT) self.margins = Box(config.get("margins", DefaultSpacing.BOX_MARGINS)) def __repr__(self): """ A pretty string representation of the layout helper. """ items = ", ".join(map(repr, self.items)) return "{0}box({1})".format(self.orientation[0], items) def _get_constraints(self, component): """ Generate the linear box constraints. This is an abstractmethod implementation which will use the space available on the provided component to layout the items. """ items = [item for item in self.items if item is not None] if len(items) == 0: return items first, last = self.orientation_map[self.orientation] first_boundary = getattr(self, first) last_boundary = getattr(self, last) first_ortho, last_ortho = self.orientation_map[self.ortho_orientation] first_ortho_boundary = getattr(self, first_ortho) last_ortho_boundary = getattr(self, last_ortho) # Setup the initial outer constraints of the box if component is not None: # This box helper is inside a real component, not just nested # inside of another box helper. Check if the component is a # PaddingConstraints object and use it's contents anchors. attrs = ["top", "bottom", "left", "right"] # XXX hack! if hasattr(component, "contents_top"): other_attrs = ["contents_" + attr for attr in attrs] else: other_attrs = attrs[:] constraints = [ getattr(self, attr) == getattr(component, other) for (attr, other) in zip(attrs, other_attrs) ] else: constraints = [] # Create the margin spacers that will be used. margins = self.margins if self.orientation == "vertical": first_spacer = EqSpacer(margins.top) last_spacer = EqSpacer(margins.bottom) first_ortho_spacer = FlexSpacer(margins.left) last_ortho_spacer = FlexSpacer(margins.right) else: first_spacer = EqSpacer(margins.left) last_spacer = EqSpacer(margins.right) first_ortho_spacer = FlexSpacer(margins.top) last_ortho_spacer = FlexSpacer(margins.bottom) # Add a pre and post padding spacer if the user hasn't specified # their own spacer as the first/last element of the box items. if not is_spacer(items[0]): pre_along_args = [first_boundary, first_spacer] else: pre_along_args = [first_boundary] if not is_spacer(items[-1]): post_along_args = [last_spacer, last_boundary] else: post_along_args = [last_boundary] # Accummulate the constraints in the direction of the layout along_args = pre_along_args + items + post_along_args kwds = dict(spacing=self.spacing) helpers = [AbutmentHelper(self.orientation, *along_args, **kwds)] ortho = self.ortho_orientation for item in items: # Add the helpers for the ortho constraints if isinstance(item, ABConstrainable): abutment_items = ( first_ortho_boundary, first_ortho_spacer, item, last_ortho_spacer, last_ortho_boundary, ) helpers.append(AbutmentHelper(ortho, *abutment_items, **kwds)) # Pull out nested helpers so that their constraints get # generated during the pass over the helpers list. if isinstance(item, DeferredConstraints): helpers.append(item) # Pass over the list of child helpers and generate the # flattened list of constraints. for helper in helpers: constraints.extend(helper.get_constraints(None)) return constraints
class _GridCell(object): """ A private class used by a GridHelper to track item cells. """ def __init__(self, item, row, col): """ Initialize a _GridCell. Parameters ---------- item : object The item contained in the cell. row : int The row index of the cell. col : int The column index of the cell. """ self.item = item self.start_row = row self.start_col = col self.end_row = row self.end_col = col def expand_to(self, row, col): """ Expand the cell to enclose the given row and column. """ self.start_row = min(row, self.start_row) self.end_row = max(row, self.end_row) self.start_col = min(col, self.start_col) self.end_col = max(col, self.end_col)
[docs]class GridHelper(BoxHelper): """ A layout helper which arranges items in a grid. """ def __init__(self, *rows, **config): """ Initialize a GridHelper. Parameters ---------- *rows: iterable of lists The rows to layout in the grid. A row must be composed of constrainable objects and None. An item will be expanded to span all of the cells in which it appears. **config Configuration options for how this helper should behave. The following options are currently supported: row_align A string which is the name of a constraint variable on a item. If given, it is used to add constraints on the alignment of items in a row. The constraints will only be applied to items that do not span rows. row_spacing An integer >= 0 which indicates how many pixels of space should be placed between rows in the grid. The default is the value of DefaultSpacing.ABUTMENT. column_align A string which is the name of a constraint variable on a item. If given, it is used to add constraints on the alignment of items in a column. The constraints will only be applied to items that do not span columns. column_spacing An integer >= 0 which indicates how many pixels of space should be placed between columns in the grid. The default is the value of DefaultSpacing.ABUTMENT. margins A int, tuple of ints, or Box of ints >= 0 which indicate how many pixels of margin to add around the bounds of the grid. The default is the value of DefaultSpacing.BOX_MARGIN. """ super().__init__("grid") self.grid_rows = rows self.row_align = config.get("row_align", "") self.col_align = config.get("col_align", "") self.row_spacing = config.get("row_spacing", DefaultSpacing.ABUTMENT) self.col_spacing = config.get( "column_spacing", DefaultSpacing.ABUTMENT ) self.margins = Box(config.get("margins", DefaultSpacing.BOX_MARGINS)) def __repr__(self): """ A pretty string representation of the layout helper. """ items = ", ".join(map(repr, self.grid_rows)) return "grid({0})".format(items) def _get_constraints(self, component): """ Generate the grid constraints. This is an abstractmethod implementation which will use the space available on the provided component to layout the items. """ grid_rows = self.grid_rows if not grid_rows: return [] # Validate and compute the cell span for the items in the grid. cells = [] cell_map = {} num_cols = 0 num_rows = len(grid_rows) for row_idx, row in enumerate(grid_rows): for col_idx, item in enumerate(row): if item is None: continue elif isinstance(item, ABConstrainable): if item in cell_map: cell_map[item].expand_to(row_idx, col_idx) else: cell = _GridCell(item, row_idx, col_idx) cell_map[item] = cell cells.append(cell) else: m = ( "Grid cells must be constrainable objects or None. " "Got object of type `%s` instead." ) raise TypeError(m % type(item).__name__) num_cols = max(num_cols, col_idx + 1) # Setup the initial outer constraints of the grid if component is not None: # This box helper is inside a real component, not just nested # inside of another box helper. Check if the component is a # PaddingConstraints object and use it's contents anchors. attrs = ["top", "bottom", "left", "right"] # XXX hack! if hasattr(component, "contents_top"): other_attrs = ["contents_" + attr for attr in attrs] else: other_attrs = attrs[:] constraints = [ getattr(self, attr) == getattr(component, other) for (attr, other) in zip(attrs, other_attrs) ] else: constraints = [] # Create the row and column constraint variables along with # some default limits row_vars = [] col_vars = [] cn_id = self.constraints_id for idx in range(num_rows + 1): name = "row" + str(idx) var = Variable("{0}|{1}".format(cn_id, name)) row_vars.append(var) constraints.append(var >= 0) for idx in range(num_cols + 1): name = "col" + str(idx) var = Variable("{0}|{1}".format(cn_id, name)) col_vars.append(var) constraints.append(var >= 0) # Add some neighbor relations to the row and column vars. for r1, r2 in zip(row_vars[:-1], row_vars[1:]): constraints.append(r1 >= r2) for c1, c2 in zip(col_vars[:-1], col_vars[1:]): constraints.append(c1 <= c2) # Setup the initial interior bounding box for the grid. margins = self.margins bottom_items = (self.bottom, EqSpacer(margins.bottom), row_vars[-1]) top_items = (row_vars[0], EqSpacer(margins.top), self.top) left_items = (self.left, EqSpacer(margins.left), col_vars[0]) right_items = (col_vars[-1], EqSpacer(margins.right), self.right) helpers = [ AbutmentHelper("vertical", *bottom_items), AbutmentHelper("vertical", *top_items), AbutmentHelper("horizontal", *left_items), AbutmentHelper("horizontal", *right_items), ] # Setup the spacer list for constraining the cell items row_spacer = FlexSpacer(self.row_spacing / 2.0) col_spacer = FlexSpacer(self.col_spacing / 2.0) rspace = [row_spacer] * len(row_vars) rspace[0] = 0 rspace[-1] = 0 cspace = [col_spacer] * len(col_vars) cspace[0] = 0 cspace[-1] = 0 # Setup the constraints for each constrainable grid cell. for cell in cells: sr = cell.start_row er = cell.end_row + 1 sc = cell.start_col ec = cell.end_col + 1 item = cell.item row_item = ( row_vars[sr], rspace[sr], item, rspace[er], row_vars[er], ) col_item = ( col_vars[sc], cspace[sc], item, cspace[ec], col_vars[ec], ) helpers.append(AbutmentHelper("vertical", *row_item)) helpers.append(AbutmentHelper("horizontal", *col_item)) if isinstance(item, DeferredConstraints): helpers.append(item) # Add the row alignment constraints if given. This will only # apply the alignment constraint to items which do not span # multiple rows. if self.row_align: row_map = defaultdict(list) for cell in cells: if cell.start_row == cell.end_row: row_map[cell.start_row].append(cell.item) for items in row_map.values(): if len(items) > 1: helpers.append(AlignmentHelper(self.row_align, *items)) # Add the column alignment constraints if given. This will only # apply the alignment constraint to items which do not span # multiple columns. if self.col_align: col_map = defaultdict(list) for cell in cells: if cell.start_col == cell.end_col: col_map[cell.start_col].append(cell.item) for items in row_map.values(): if len(items) > 1: helpers.append(AlignmentHelper(self.col_align, *items)) # Add the child helpers constraints to the constraints list. for helper in helpers: constraints.extend(helper.get_constraints(None)) return constraints
# ----------------------------------------------------------------------------- # Abstract Constraint Factory # -----------------------------------------------------------------------------
[docs]class AbstractConstraintFactory(object, metaclass=ABCMeta): """ An abstract constraint factory class. Subclasses must implement the 'constraints' method implement which returns a LinearConstraint instance. """
[docs] @staticmethod def validate(items): """ A validator staticmethod that insures a sequence of items is appropriate for generating a sequence of linear constraints. The following conditions are verified of the sequence of given items: * The number of items in the sequence is 0 or >= 2. * The first and last items are instances of either LinearSymbolic or Constrainable. * All of the items in the sequence are instances of LinearSymbolic, Constrainable, Spacer, or int. If any of the above conditions do not hold, an exception is raised with a (hopefully) useful error message. """ if len(items) == 0: return if len(items) < 2: msg = "Two or more items required to setup abutment constraints." raise ValueError(msg) extrema_types = (LinearSymbolic, ABConstrainable) def extrema_test(item): return isinstance(item, extrema_types) item_types = (LinearSymbolic, ABConstrainable, Spacer, int) def item_test(item): return isinstance(item, item_types) if not all(extrema_test(item) for item in (items[0], items[-1])): msg = ( "The first and last items of a constraint sequence " "must be anchors or Components. Got %s instead." ) args = [type(items[0]), type(items[-1])] raise TypeError(msg % args) if not all(map(item_test, items)): msg = ( "The allowed items for a constraint sequence are" "anchors, Components, Spacers, and ints. " "Got %s instead." ) args = [type(item) for item in items] raise TypeError(msg % args)
[docs] @abstractmethod def constraints(self): """ An abstract method which must be implemented by subclasses. It should return a list of LinearConstraint instances. """ raise NotImplementedError
# ----------------------------------------------------------------------------- # Abstract Constraint Factory Implementations # -----------------------------------------------------------------------------
[docs]class BaseConstraintFactory(AbstractConstraintFactory): """ A base constraint factory class that implements basic common logic. It is not meant to be used directly but should rather be subclassed to be useful. """ def __init__(self, first_anchor, spacer, second_anchor): """ Create an base constraint instance. Parameters ---------- first_anchor : LinearSymbolic A symbolic object that can be used in a constraint expression. spacer : Spacer A spacer instance to put space between the items. second_anchor : LinearSymbolic The second anchor for the constraint expression. """ self.first_anchor = first_anchor self.spacer = spacer self.second_anchor = second_anchor
[docs] def constraints(self): """ Returns LinearConstraint instance which is formed through an appropriate linear expression for the given space between the anchors. """ first = self.first_anchor second = self.second_anchor spacer = self.spacer return spacer.constrain(first, second)
[docs]class SequenceConstraintFactory(BaseConstraintFactory): """ A BaseConstraintFactory subclass that represents a constraint between two anchors of different components separated by some amount of space. It has a '_make_cns' classmethod which will create a list of constraint factory instances from a sequence of items, the two anchor names, and a default spacing. """ @classmethod def _make_cns(cls, items, first_anchor_name, second_anchor_name, spacing): """ A classmethod that generates a list of constraints factories given a sequence of items, two anchor names, and default spacing. Parameters ---------- items : sequence A valid sequence of constrainable objects. These inclue instances of Constrainable, LinearSymbolic, Spacer, and int. first_anchor_name : string The name of the anchor on the first item in a constraint pair. second_anchor_name : string The name of the anchor on the second item in a constraint pair. spacing : int The spacing to use between items if no spacing is explicitly provided by in the sequence of items. Returns ------- result : list A list of constraint factory instance. """ # Make sure the items we'll be dealing with are valid for the # algorithm. This is a basic validation. Further error handling # is performed as needed. cls.validate(items) # The list of constraints we'll be creating for the given # sequence of items. cns = [] # The list of items is treated as a stack. So we want to first # reverse it so the first items are at the top of the stack. items = list(reversed(items)) while items: # Grab the item that will provide the first anchor first_item = items.pop() # first_item will be a Constrainable or a LinearSymbolic. # For the first iteration, this is enforced by 'validate'. # For subsequent iterations, this condition is enforced by # the fact that this loop only pushes those types back onto # the stack. if isinstance(first_item, ABConstrainable): first_anchor = getattr(first_item, first_anchor_name) elif isinstance(first_item, LinearSymbolic): first_anchor = first_item else: raise TypeError("This should never happen") # Grab the next item off the stack. It will be an instance # of Constrainable, LinearSymbolic, Spacer, or int. If it # can't provide an anchor, we grab the item after it which # *should* be able to provide one. If no space is given, we # use the provided default space. next_item = items.pop() if isinstance(next_item, Spacer): spacer = next_item second_item = items.pop() elif isinstance(next_item, int): spacer = EqSpacer(next_item) second_item = items.pop() elif isinstance(next_item, (ABConstrainable, LinearSymbolic)): spacer = EqSpacer(spacing) second_item = next_item else: raise ValueError("This should never happen") # If the second_item can't provide an anchor, such as two # spacers next to each other, then this is an error and we # raise an appropriate exception. if isinstance(second_item, ABConstrainable): second_anchor = getattr(second_item, second_anchor_name) elif isinstance(second_item, LinearSymbolic): second_anchor = second_item else: msg = "Expected anchor or Constrainable. Got %r instead." raise TypeError(msg % second_item) # Create the class instance for this constraint factory = cls(first_anchor, spacer, second_anchor) # If there are still items on the stack, then the second_item # will be used as the first_item in the next iteration. # Otherwise, we have exhausted all constraints and can exit. if items: items.append(second_item) # Finally, store away the created factory for returning. cns.append(factory) return cns
[docs]class AbutmentConstraintFactory(SequenceConstraintFactory): """ A SequenceConstraintFactory subclass that represents an abutment constraint, which is a constraint between two anchors of different components separated by some amount of space. It has a 'from_items' classmethod which will create a sequence of abutment constraints from a sequence of items, a direction, and default spacing. """ #: A mapping from orientation to the order of anchor names to #: lookup for a pair of items in order to make the constraint. orientation_map = { "horizontal": ("right", "left"), "vertical": ("top", "bottom"), }
[docs] @classmethod def from_items(cls, items, orientation, spacing): """ A classmethod that generates a list of abutment constraints given a sequence of items, an orientation, and default spacing. Parameters ---------- items : sequence A valid sequence of constrainable objects. These inclue instances of Constrainable, LinearSymbolic, Spacer, and int. orientation : string Either 'vertical' or 'horizontal', which represents the orientation in which to abut the items. spacing : int The spacing to use between items if no spacing is explicitly provided by in the sequence of items. Returns ------- result : list A list of AbutmentConstraint instances. Notes ------ The order of abutment is left-to-right for horizontal direction and top-to-bottom for vertical direction. """ # Grab the tuple of anchor names to lookup for each pair of # items in order to make the connection. orient = cls.orientation_map.get(orientation) if orient is None: msg = ( "Valid orientations for abutment are 'vertical' or " "'horizontal'. Got %r instead." ) raise ValueError(msg % orientation) first_name, second_name = orient if orientation == "vertical": items.reverse() return cls._make_cns(items, first_name, second_name, spacing)
[docs]class AlignmentConstraintFactory(SequenceConstraintFactory): """ A SequenceConstraintFactory subclass which represents an alignmnent constraint, which is a constraint between two anchors of different components which are aligned but may be separated by some amount of space. It provides a 'from_items' classmethod which will create a list of alignment constraints from a sequence of items an anchor name, and a default spacing. """
[docs] @classmethod def from_items(cls, items, anchor_name, spacing): """ A classmethod that will create a seqence of alignment constraints given a sequence of items, an anchor name, and a default spacing. Parameters ---------- items : sequence A valid sequence of constrainable objects. These inclue instances of Constrainable, LinearSymbolic, Spacer, and int. anchor_name : string The name of the anchor on the components which should be aligned. Either 'left', 'right', 'top', 'bottom', 'v_center', or 'h_center'. spacing : int The spacing to use between items if no spacing is explicitly provided by in the sequence of items. Returns ------- result : list A list of AbutmentConstraint instances. Notes ----- For every item in the sequence, if the item is a component, then anchor for the given anchor_name on that component will be used. If a LinearSymbolic is given, then that symbolic will be used and the anchor_name will be ignored. Specifying space between items via integers or spacers is allowed. """ return cls._make_cns(items, anchor_name, anchor_name, spacing)
# ----------------------------------------------------------------------------- # Spacers # -----------------------------------------------------------------------------
[docs]class Spacer(object, metaclass=ABCMeta): """ An abstract base class for spacers. Subclasses must implement the 'constrain' method. """ def __init__(self, amt, strength=None): self.amt = max(0, amt) self.strength = strength
[docs] def when(self, switch): """ A simple method that can be used to switch off the generated space depending on a boolean value. """ if switch: return self
[docs] def constrain(self, first_anchor, second_anchor): """ Returns the list of generated constraints appropriately weighted by the default strength, if provided. """ constraints = self._constrain(first_anchor, second_anchor) strength = self.strength if strength is not None: constraints = [cn | strength for cn in constraints] return constraints
@abstractmethod def _constrain(self, first_anchor, second_anchor): """ An abstract method. Subclasses should implement this method to return a list of LinearConstraint instances which separate the two anchors according to the amount of space represented by the spacer. """ raise NotImplementedError
[docs]class EqSpacer(Spacer): """ A spacer which represents a fixed amount of space. """ def _constrain(self, first_anchor, second_anchor): """ A constraint of the form (anchor_1 + space == anchor_2) """ return [(first_anchor + self.amt) == second_anchor]
[docs]class LeSpacer(Spacer): """ A spacer which represents a flexible space with a maximum value. """ def _constrain(self, first_anchor, second_anchor): """ A constraint of the form (anchor_1 + space >= anchor_2) That is, the visible space must be less than or equal to the given amount. An additional constraint is applied which constrains (anchor_1 <= anchor_2) to prevent negative space. """ return [ (first_anchor + self.amt) >= second_anchor, first_anchor <= second_anchor, ]
[docs]class GeSpacer(Spacer): """ A spacer which represents a flexible space with a minimum value. """ def _constrain(self, first_anchor, second_anchor): """ A constraint of the form (anchor_1 + space <= anchor_2) That is, the visible space must be greater than or equal to the given amount. """ return [(first_anchor + self.amt) <= second_anchor]
[docs]class FlexSpacer(Spacer): """ A spacer which represents a space with a hard minimum, but also a weaker preference for being that minimum. """ def __init__(self, amt, min_strength="required", eq_strength="medium"): self.amt = max(0, amt) self.min_strength = min_strength self.eq_strength = eq_strength
[docs] def constrain(self, first_anchor, second_anchor): """ Return list of LinearConstraint objects that are appropriate to separate the two anchors according to the amount of space represented by the spacer. """ return self._constrain(first_anchor, second_anchor)
def _constrain(self, first_anchor, second_anchor): """ Constraints of the form (anchor_1 + space <= anchor_2) and (anchor_1 + space == anchor_2) """ return [ ((first_anchor + self.amt) <= second_anchor) | self.min_strength, ((first_anchor + self.amt) == second_anchor) | self.eq_strength, ]
[docs]class LayoutSpacer(Spacer): """ A Spacer instance which supplies convenience symbolic and normal methods to facilitate specifying spacers in layouts. """ def __call__(self, *args, **kwargs): return self.__class__(*args, **kwargs) def __eq__(self, other): if not isinstance(other, int): raise TypeError("space can only be created from ints") return EqSpacer(other, self.strength) def __le__(self, other): if not isinstance(other, int): raise TypeError("space can only be created from ints") return LeSpacer(other, self.strength) def __ge__(self, other): if not isinstance(other, int): raise TypeError("space can only be created from ints") return GeSpacer(other, self.strength) def _constrain(self, first_anchor, second_anchor): """ Returns a greater than or equal to spacing constraint. """ spacer = GeSpacer(self.amt, self.strength) return spacer._constrain(first_anchor, second_anchor)
[docs] def flex(self, **kwargs): """ Returns a flex spacer for the current amount. """ return FlexSpacer(self.amt, **kwargs)
# ----------------------------------------------------------------------------- # Layout Helper Functions and Objects # -----------------------------------------------------------------------------
[docs]def horizontal(*items, **config): """ Create a DeferredConstraints object composed of horizontal abutments for the given sequence of items. """ return AbutmentHelper("horizontal", *items, **config)
[docs]def vertical(*items, **config): """ Create a DeferredConstraints object composed of vertical abutments for the given sequence of items. """ return AbutmentHelper("vertical", *items, **config)
[docs]def hbox(*items, **config): """ Create a DeferredConstraints object composed of horizontal abutments for a given sequence of items. """ return LinearBoxHelper("horizontal", *items, **config)
[docs]def vbox(*items, **config): """ Create a DeferredConstraints object composed of vertical abutments for a given sequence of items. """ return LinearBoxHelper("vertical", *items, **config)
[docs]def align(anchor, *items, **config): """ Align the given anchors of the given components. Inter-component spacing is allowed. """ return AlignmentHelper(anchor, *items, **config)
[docs]def grid(*rows, **config): """ Create a DeferredConstraints object which lays out items in a grid. """ return GridHelper(*rows, **config)
spacer = LayoutSpacer(DefaultSpacing.ABUTMENT)