diff --git a/enable/abstract_layout_controller.py b/enable/abstract_layout_controller.py deleted file mode 100644 index d01366db3..000000000 --- a/enable/abstract_layout_controller.py +++ /dev/null @@ -1,8 +0,0 @@ - -from traits.api import HasTraits - -class AbstractLayoutController(HasTraits): - - def layout(self, component): - raise NotImplementedError - diff --git a/enable/api.py b/enable/api.py index 111d85640..c104ff5a6 100644 --- a/enable/api.py +++ b/enable/api.py @@ -34,6 +34,7 @@ from container import Container from coordinate_box import CoordinateBox from component_editor import ComponentEditor +from constraints_container import ConstraintsContainer from overlay_container import OverlayContainer # Breaks code that does not use numpy diff --git a/enable/component.py b/enable/component.py index 9f210f713..554ca41fe 100644 --- a/enable/component.py +++ b/enable/component.py @@ -2,6 +2,8 @@ from __future__ import with_statement +from uuid import uuid4 + # Enthought library imports from traits.api \ import Any, Bool, Delegate, Enum, Float, Instance, Int, List, \ @@ -20,6 +22,7 @@ DEFAULT_DRAWING_ORDER = ["background", "underlay", "mainlayer", "border", "overlay"] + class Component(CoordinateBox, Interactor): """ Component is the base class for most Enable objects. In addition to the @@ -313,8 +316,8 @@ class Component(CoordinateBox, Interactor): # into more than one class and that class classes = List - # The optional element ID of this component. - id = Str("") + # The element ID of this component. + id = Str # These will be used by the new layout system, but are currently unused. #max_width = Any @@ -1027,7 +1030,6 @@ def _set_active_tool(self, tool): def _get_layout_needed(self): return self._layout_needed - def _tools_items_changed(self): self.invalidate_and_redraw() @@ -1035,6 +1037,12 @@ def _tools_items_changed(self): # Event handlers #------------------------------------------------------------------------ + def _id_default(self): + """ Generate a random UUID for the ID. + """ + # The first 32bits is plenty. + return uuid4().hex[:8] + def _aspect_ratio_changed(self, old, new): if new is not None: self._enforce_aspect_ratio() diff --git a/enable/component_layout_category.py b/enable/component_layout_category.py deleted file mode 100644 index c404cbebe..000000000 --- a/enable/component_layout_category.py +++ /dev/null @@ -1,33 +0,0 @@ -""" FIXME: -Tentative implementation of a new layout mechanism. Unused and unworking. -""" - - -# Enthought library imports -from traits.api import Any, Category, Enum - -# Singleton representing the default Enable layout manager -DefaultLayoutController = LayoutController() - - -class ComponentLayoutCategory(Category, Component): - - """ Properties defining how a component should be laid out. """ - - resizable = Enum('h', 'v') - - max_width = Any - - min_width = Any - - max_height = Any - - min_height = Any - - # Various alignment and positioning functions - - def set_padding(self, left, right, top, bottom): - pass - - def get_padding(self): - pass diff --git a/enable/component_render_category.py b/enable/component_render_category.py deleted file mode 100644 index 345aeebb9..000000000 --- a/enable/component_render_category.py +++ /dev/null @@ -1,175 +0,0 @@ -""" -This module contains the RenderingCategory, which defines the rendering aspect of -components. It uses Traits Categories to extend the Component class defined -in component.py. - -NOTE: This means that you should import enable.Component from enable.api, and -not directly from component.py. -""" - -from __future__ import with_statement - -# Enthought library imports -from traits.api import Any, Bool, Category, Enum, Instance, \ - Int, List - -# Local relative imports -from colors import black_color_trait, white_color_trait -from abstract_component import AbstractComponent -from enable_traits import LineStyle -from render_controllers import AbstractRenderController, OldEnableRenderController, \ - RenderController - - -# Singleton representing the default Enable render controller -DefaultRenderController = OldEnableRenderController() - - -class ComponentRenderCategory(Category, AbstractComponent): - """ Encapsulates the rendering-related aspects of a component and - extends the Component class in component.py. - - !! This class should not be instantiated or subclassed !! Please refer - to traits.Category for more information. - """ - - # The controller that determines how this component renders. By default, - # this is the singleton - render_controller = Instance(AbstractRenderController, factory = DefaultRenderController) - - # Is the component visible? - visible = Bool(True) - - # Does this container prefer to draw all of its components in one pass, or - # does it prefer to cooperate in its container's layer-by-layer drawing? - # If unified_draw is on, then this component will draw as a unified whole, - # and its parent container will call our _draw() method when drawing the - # layer indicated in self.draw_layer. - # If unified_draw is off, then our parent container will call - # self._dispatch_draw() with the name of each layer as it goes through its - # list of layers. - unified_draw = Bool(False) - - # A list of the order in which various layers of this component - # should be rendered. This is only used if the component does - # unified draw. - draw_order = List - - # If unified_draw is True for this component, then this attribute - # determines what layer it will be drawn on. This is used by containers - # and external classes whose drawing loops will call this component. - draw_layer = Enum(RenderController.LAYERS) - - #------------------------------------------------------------------------ - # Background and padding - #------------------------------------------------------------------------ - - # Should the padding area be filled with the background color? - fill_padding = Bool(False) - - # The background color of this component. By default all components have - # a white background. This can be set to "transparent" or "none" if the - # component should be see-through. - bgcolor = white_color_trait - - #------------------------------------------------------------------------ - # Border traits - #------------------------------------------------------------------------ - - # The width of the border around this component. This is taken into account - # during layout, but only if the border is visible. - border_width = Int(1) - - # Is the border visible? If this is false, then all the other border - # properties are not - border_visible = Bool(False) - - # The line style (i.e. dash pattern) of the border. - border_dash = LineStyle - - # The color of the border. Only used if border_visible is True. - border_color = black_color_trait - - # Should the border be drawn as part of the overlay or the background? - overlay_border = Bool(True) - - # Should the border be drawn inset (on the plot) or outside the plot - # area? - inset_border = Bool(True) - - - #------------------------------------------------------------------------ - # Backbuffer traits - #------------------------------------------------------------------------ - - # Should this component do a backbuffered draw, i.e. render itself - # to an offscreen buffer that is cached for later use? If False, - # then the component will never render itself backbufferd, even - # if explicitly asked to do so. - use_backbuffer = Bool(False) - - # Reflects the validity state of the backbuffer. This is usually set by - # the component itself or set on the component by calling - # _invalidate_draw(). It is exposed as a public trait for the rare cases - # when another components wants to know the validity of this component's - # backbuffer, i.e. if a draw were to occur, whether the component would - # actually change. - draw_valid = Bool(False) - - # Should the backbuffer include the padding area? - # TODO: verify that backbuffer invalidation occurs if this attribute - # is changed. - backbuffer_padding = Bool(True) - - #------------------------------------------------------------------------ - # Private traits - #------------------------------------------------------------------------ - - _backbuffer = Any - - - def draw(self, gc, view_bounds=None, mode="default"): - """ - Renders this component onto a GraphicsContext. - - "view_bounds" is a 4-tuple (x, y, dx, dy) of the viewed region relative - to the CTM of the gc. - - "mode" can be used to require this component render itself in a particular - fashion, and can be "default" or any of the enumeration values of - self.default_draw_mode. - """ - self.render_controller.draw(self, gc, view_bounds, mode) - return - - def _draw_component(self, gc, view_bounds=None, mode="default"): - """ This function actually draws the core parts of the component - itself, i.e. the parts that belong on the "main" layer. Subclasses - should implement this. - """ - pass - - def _draw_border(self, gc, view_bounds=None, mode="default"): - """ Utility method to draw the borders around this component """ - if not self.border_visible: - return - - border_width = self.border_width - with gc: - gc.set_line_width(border_width) - gc.set_line_dash(self.border_dash_) - gc.set_stroke_color(self.border_color_) - gc.begin_path() - gc.rect(self.x - border_width/2.0, - self.y - border_width/2.0, - self.width + 2*border_width - 1, - self.height + 2*border_width - 1) - gc.stroke_path() - return - - def _draw_background(self, gc, view_bounds=None, mode="default"): - if self.bgcolor not in ("transparent", "none"): - gc.set_fill_color(self.bgcolor_) - gc.rect(*(self.position + self.bounds)) - gc.fill_path() - return diff --git a/enable/constraints_container.py b/enable/constraints_container.py new file mode 100644 index 000000000..612f65931 --- /dev/null +++ b/enable/constraints_container.py @@ -0,0 +1,420 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ +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, get_from_constraints_namespace +from layout.layout_helpers import expand_constraints +from layout.layout_manager import LayoutManager +from layout.utils import add_symbolic_contents_constraints + + +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 casuarius solver + _layout_manager = Instance(LayoutManager, allow_none=True) + _offset_table = List + _layout_table = List + + #------------------------------------------------------------------------ + # Public methods + #------------------------------------------------------------------------ + + 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) + + 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() + + 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 + #------------------------------------------------------------------------ + 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 + + 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(ConstraintsContainer, self)._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) + + contents_left = self.contents_left + contents_right = self.contents_right + contents_top = self.contents_top + contents_bottom = self.contents_bottom + + return [contents_left == self.left, + contents_bottom == self.bottom, + contents_right == self.left + self.layout_width, + 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.on_trait_change(self._component_size_hint_changed, + '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.iteritems(): + item.on_trait_change(self._component_size_hint_changed, + 'layout_size_hint', remove=True) + self._component_map = {} + + # Check the new components + self._check_and_add_components(new) + + def _component_size_hint_changed(self): + """ 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.itervalues()) + + # 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.itervalues(): + 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.on_trait_change(self._component_size_hint_changed, + 'layout_size_hint') + + # Update the layout + self.relayout() + + def _generate_constraints(self, layout_table): + """ Creates the list of casuarius LinearConstraint 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 casuarius + LinearConstraint 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 casuarius LinearConstraints instances to pass to + the layout manager. + + """ + user_cns = self._layout_constraints + user_cns_extend = user_cns.extend + + # The list of raw casuarius constraints which will be returned + # from this method to be added to the casuarius 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 diff --git a/enable/container.py b/enable/container.py index b5f6d5e7f..fc21b6880 100644 --- a/enable/container.py +++ b/enable/container.py @@ -14,36 +14,6 @@ from base import empty_rectangle, intersect_bounds from component import Component from events import BlobEvent, BlobFrameEvent, DragEvent, MouseEvent -from abstract_layout_controller import AbstractLayoutController - - -class AbstractResolver(HasTraits): - """ - A Resolver traverses a component DB and matches a specifier. - """ - - def match(self, db, query): - """ Queries a component DB using a dict of keyword-val conditions. - Each resolver defines its set of allowed keywords. - """ - raise NotImplementedError - - -class DefaultResolver(AbstractResolver): - """ - Basic resolver that searches a container's DB of components using the - following conditions: - - id=foo : the component's .id must be 'foo' - - class=['foo','bar'] : the component's .class must be in the list - - target='foo' : the component's .target is 'foo'; this usually applies - to tools, overlays, and decorators - """ - - def match(self, db, query): - pass class Container(Component): @@ -104,21 +74,6 @@ class Container(Component): # under the component layers of the same name. container_under_layers = Tuple("background", "image", "underlay", "mainlayer") - #------------------------------------------------------------------------ - # DOM-related traits - # (Note: These are unused as of 8/13/2007) - #------------------------------------------------------------------------ - - # The layout controller determines how the container's internal layout - # mechanism works. It can perform the actual layout or defer to an - # enclosing container's layout controller. The default controller is - # a cooperative/recursive layout controller. - layout_controller = Instance(AbstractLayoutController) - - # This object resolves queries for components - resolver = Instance(AbstractResolver) - - #------------------------------------------------------------------------ # Private traits #------------------------------------------------------------------------ @@ -131,9 +86,6 @@ class Container(Component): # our own. _prev_event_handlers = Instance( set, () ) - # Used by the resolver to cache previous lookups - _lookup_cache = Any - # This container can render itself in a different mode than what it asks of # its contained components. This attribute stores the rendering mode that # this container requests of its children when it does a _draw(). If the @@ -224,13 +176,6 @@ def lower_component(self, component): """ Puts the indicated component to the very bottom of the Z-order """ raise NotImplementedError - def get(self, **kw): - """ - Allows for querying of this container's components. - """ - # TODO: cache requests - return self.resolver.query(self._components, kw) - def cleanup(self, window): """When a window viewing or containing a component is destroyed, cleanup is called on the component to give it the opportunity to diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index ba5d626a7..f010a27d9 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -1,9 +1,23 @@ # Enthought library imports -from traits.api import HasTraits, Property +from traits.api import HasTraits, Enum, Instance, Property, Tuple # Local, relative imports from enable_traits import bounds_trait, coordinate_trait +from layout.ab_constrainable import ABConstrainable +from layout.constraints_namespace import ConstraintsNamespace +from layout.utils import add_symbolic_constraints, STRENGTHS + + +ConstraintPolicyEnum = Enum('ignore', *STRENGTHS) + + +def get_from_constraints_namespace(self, name): + """ Property getter for all attributes that come from the constraints + namespace. + + """ + return getattr(self._constraints_vars, name) class CoordinateBox(HasTraits): @@ -43,6 +57,70 @@ class CoordinateBox(HasTraits): height = Property + #------------------------------------------------------------------------ + # Constraints-based layout + #------------------------------------------------------------------------ + + # A read-only symbolic object that represents the left boundary of + # the component + left = Property(fget=get_from_constraints_namespace) + + # A read-only symbolic object that represents the right boundary + # of the component + right = Property(fget=get_from_constraints_namespace) + + # A read-only symbolic object that represents the bottom boundary + # of the component + bottom = Property(fget=get_from_constraints_namespace) + + # A read-only symbolic object that represents the top boundary of + # the component + top = Property(fget=get_from_constraints_namespace) + + # A read-only symbolic object that represents the width of the + # component + layout_width = Property(fget=get_from_constraints_namespace) + + # A read-only symbolic object that represents the height of the + # component + layout_height = Property(fget=get_from_constraints_namespace) + + # A read-only symbolic object that represents the vertical center + # of the component + v_center = Property(fget=get_from_constraints_namespace) + + # A read-only symbolic object that represents the horizontal + # center of the component + h_center = Property(fget=get_from_constraints_namespace) + + # A size hint for the layout + layout_size_hint = Tuple(0.0, 0.0) + + # How strongly a layout box hugs it's width hint. + hug_width = ConstraintPolicyEnum('weak') + + # How strongly a layout box hugs it's height hint. + hug_height = ConstraintPolicyEnum('weak') + + # How strongly a layout box resists clipping its contents. + resist_width = ConstraintPolicyEnum('strong') + + # How strongly a layout box resists clipping its contents. + resist_height = ConstraintPolicyEnum('strong') + + # A namespace containing the constraints for this CoordinateBox + _constraints_vars = Instance(ConstraintsNamespace) + + # The list of hard constraints which must be applied to the object. + _hard_constraints = Property + + # The list of size constraints to apply to the object. + _size_constraints = Property + + #------------------------------------------------------------------------ + # Public methods + #------------------------------------------------------------------------ + def is_in(self, x, y): "Returns if the point x,y is in the box" p = self.position @@ -55,7 +133,7 @@ def as_coordinates(self): "Returns a 4-tuple (x, y, x2, y2)" p = self.position b = self.bounds - return (p[0], p[1], p[0]+b[0]-1, p[1]+b[1]-1) + return (p[0], p[1], p[0] + b[0] - 1, p[1] + b[1] - 1) #------------------------------------------------------------------------ # Property setters and getters @@ -88,7 +166,7 @@ def _set_width(self, val): old_value = self.bounds[0] self.bounds[0] = val - self.trait_property_changed( 'width', old_value, val ) + self.trait_property_changed('width', old_value, val) return def _get_height(self): @@ -102,11 +180,12 @@ def _set_height(self, val): pass old_value = self.bounds[1] self.bounds[1] = val - self.trait_property_changed( 'height', old_value, val ) + self.trait_property_changed('height', old_value, val) return def _get_x2(self): - if self.bounds[0] == 0: return self.position[0] + if self.bounds[0] == 0: + return self.position[0] return self.position[0] + self.bounds[0] - 1 def _set_x2(self, val): @@ -116,7 +195,7 @@ def _set_x2(self, val): def _old_set_x2(self, val): new_width = val - self.position[0] + 1 if new_width < 0.0: - raise RuntimeError, "Attempted to set negative component width." + raise RuntimeError("Attempted to set negative component width.") else: self.bounds[0] = new_width return @@ -133,10 +212,55 @@ def _set_y2(self, val): def _old_set_y2(self, val): new_height = val - self.position[1] + 1 if new_height < 0.0: - raise RuntimeError, "Attempted to set negative component height." + raise RuntimeError("Attempted to set negative component height.") else: self.bounds[1] = new_height return - -# EOF + def __constraints_vars_default(self): + obj_name = self.id if hasattr(self, 'id') else '' + cns_names = ConstraintsNamespace(type(self).__name__, obj_name) + add_symbolic_constraints(cns_names) + return cns_names + + def _get__hard_constraints(self): + """ Generate the constraints which must always be applied. + """ + left = self.left + bottom = self.bottom + width = self.layout_width + height = self.layout_height + cns = [left >= 0, bottom >= 0, width >= 0, height >= 0] + return cns + + def _get__size_constraints(self): + """ Creates the list of size hint constraints for this box. + """ + cns = [] + push = cns.append + width_hint, height_hint = self.layout_size_hint + width = self.layout_width + height = self.layout_height + hug_width, hug_height = self.hug_width, self.hug_height + resist_width, resist_height = self.resist_width, self.resist_height + if width_hint >= 0: + if hug_width != 'ignore': + cn = (width == width_hint) | hug_width + push(cn) + if resist_width != 'ignore': + cn = (width >= width_hint) | resist_width + push(cn) + if height_hint >= 0: + if hug_height != 'ignore': + cn = (height == height_hint) | hug_height + push(cn) + if resist_height != 'ignore': + cn = (height >= height_hint) | resist_height + push(cn) + + return cns + + +# Register with ABConstrainable so that layout helpers will recognize +# CoordinateBox instances. +ABConstrainable.register(CoordinateBox) diff --git a/enable/layout/__init__.py b/enable/layout/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/enable/layout/ab_constrainable.py b/enable/layout/ab_constrainable.py new file mode 100644 index 000000000..cea36d892 --- /dev/null +++ b/enable/layout/ab_constrainable.py @@ -0,0 +1,18 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2012, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ +from abc import ABCMeta + + +class ABConstrainable(object): + """ An abstract base class for objects that can be laid out using + layout helpers. + + Minimally, instances need to have `top`, `bottom`, `left`, `right`, + `layout_width`, `layout_height`, `v_center` and `h_center` attributes + which are `LinearSymbolic` instances. + + """ + __metaclass__ = ABCMeta + diff --git a/enable/layout/api.py b/enable/layout/api.py new file mode 100644 index 000000000..c0a243eef --- /dev/null +++ b/enable/layout/api.py @@ -0,0 +1,8 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ + +from .layout_helpers import (horizontal, vertical, hbox, vbox, align, grid, + spacer, expand_constraints, is_spacer) + diff --git a/enable/layout/constraints_namespace.py b/enable/layout/constraints_namespace.py new file mode 100644 index 000000000..185e777c2 --- /dev/null +++ b/enable/layout/constraints_namespace.py @@ -0,0 +1,74 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ + +from casuarius import ConstraintVariable, LinearSymbolic + + +class ConstraintsNamespace(object): + """ A class which acts as a namespace for casuarius constraint variables. + + The constraint variables are created on an as-needed basis, this + allows components to define new constraints and build layouts + with them, without having to specifically update this client code. + + """ + def __init__(self, name, owner): + """ Initialize a ConstraintsNamespace. + + Parameters + ---------- + name : str + A name to use in the label for the constraint variables in + this namespace. + + owner : str + The owner id to use in the label for the constraint variables + in this namespace. + + """ + self._name = name + self._owner = owner + self._constraints = {} + + def __getattr__(self, name): + """ Returns a casuarius constraint variable for the given name, + unless the name is already in the instance dictionary. + + Parameters + ---------- + name : str + The name of the constraint variable to return. + + """ + try: + return super(ConstraintsNamespace, self).__getattr__(name) + except AttributeError: + pass + + constraints = self._constraints + if name in constraints: + res = constraints[name] + else: + label = '{0}|{1}|{2}'.format(self._name, self._owner, name) + res = constraints[name] = ConstraintVariable(label) + return res + + def __setattr__(self, name, value): + """ Adds a casuarius constraint variable to the constraints dictionary. + + Parameters + ---------- + name : str + The name of the constraint variable to set. + + value : LinearSymbolic + The casuarius variable to add to the constraints dictionary. + + """ + if isinstance(value, LinearSymbolic): + self._constraints[name] = value + else: + super(ConstraintsNamespace, self).__setattr__(name, value) + diff --git a/enable/layout/geometry.py b/enable/layout/geometry.py new file mode 100644 index 000000000..94d43a3b4 --- /dev/null +++ b/enable/layout/geometry.py @@ -0,0 +1,388 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2011, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ + + +#------------------------------------------------------------------------------ +# Rect +#------------------------------------------------------------------------------ +class BaseRect(tuple): + """ A tuple subclass representing an (x, y, width, height) + bounding box. Subclasses should override the __new__ method + to enforce any necessary typing. + + """ + __slots__ = () + + def __new__(cls, x, y, width, height): + return super(BaseRect, cls).__new__(cls, (x, y, width, height)) + + def __getnewargs__(self): + return tuple(self) + + def __repr__(self): + template = '%s(x=%s, y=%s, width=%s, height=%s)' + values = (self.__class__.__name__,) + self + return template % values + + @property + def x(self): + """ The 'x' position component of the rect. + + """ + return self[0] + + @property + def y(self): + """ The 'y' position component of the rect. + + """ + return self[1] + + @property + def width(self): + """ The 'width' size component of the rect. + + """ + return self[2] + + @property + def height(self): + """ The 'height' size component of the rect. + + """ + return self[3] + + +class Rect(BaseRect): + """ A BaseRect implementation for integer values. + + """ + __slots__ = () + + def __new__(cls, x, y, width, height): + i = int + return super(Rect, cls).__new__(cls, i(x), i(y), i(width), i(height)) + + @property + def box(self): + """ The equivalent Box for this rect. + + """ + x, y, width, height = self + return Box(y, x + width, y + height, x) + + @property + def pos(self): + """ The position of the rect as a Pos object. + + """ + return Pos(self.x, self.y) + + @property + def size(self): + """ The size of the rect as a Size object. + + """ + return Size(self.width, self.height) + + +class RectF(BaseRect): + """ A BaseRect implementation for floating point values. + + """ + __slots__ = () + + def __new__(cls, x, y, width, height): + f = float + return super(RectF, cls).__new__(cls, f(x), f(y), f(width), f(height)) + + @property + def box(self): + """ The equivalent Box for this rect. + + """ + x, y, width, height = self + return BoxF(y, x + width, y + height, x) + + @property + def pos(self): + """ The position of the rect as a Pos object. + + """ + return PosF(self.x, self.y) + + @property + def size(self): + """ The size of the rect as a Size object. + + """ + return SizeF(self.width, self.height) + + +#------------------------------------------------------------------------------ +# Box +#------------------------------------------------------------------------------ +class BaseBox(tuple): + """ A tuple subclass representing a (top, right, bottom, left) box. + Subclasses should override the __new__ method to enforce any typing. + + """ + __slots__ = () + + @staticmethod + def coerce_type(item): + return item + + def __new__(cls, top=None, right=None, bottom=None, left=None): + if isinstance(top, (tuple, BaseBox)): + return cls(*top) + c = cls.coerce_type + top = c(top) + if right is None: + right = top + else: + right = c(right) + if bottom is None: + bottom = top + else: + bottom = c(bottom) + if left is None: + left = right + else: + left = c(left) + return super(BaseBox, cls).__new__(cls, (top, right, bottom, left)) + + def __getnewargs__(self): + return tuple(self) + + def __repr__(self): + template = '%s(top=%s, right=%s, bottom=%s, left=%s)' + values = (self.__class__.__name__,) + self + return template % values + + @property + def top(self): + """ The 'top' component of the box. + + """ + return self[0] + + @property + def right(self): + """ The 'right' component of the box. + + """ + return self[1] + + @property + def bottom(self): + """ The 'bottom' component of the box. + + """ + return self[2] + + @property + def left(self): + """ The 'left' component of the box. + + """ + return self[3] + + +class Box(BaseBox): + """ A BaseBox implementation for integer values. + + """ + __slots__ = () + + @staticmethod + def coerce_type(item): + return 0 if item is None else int(item) + + @property + def rect(self): + """ The equivalent Rect for this box. + + """ + top, right, bottom, left = self + return Rect(left, top, right - left, bottom - top) + + @property + def size(self): + """ The Size of this box. + + """ + top, right, bottom, left = self + return Size(right - left, bottom - top) + + @property + def pos(self): + """ The Pos of this box. + + """ + return Pos(self.left, self.top) + + +class BoxF(BaseBox): + """ A BaseBox implementation for floating point values. + + """ + __slots__ = () + + @staticmethod + def coerce_type(item): + return 0.0 if item is None else float(item) + + @property + def rect(self): + """ The equivalent Rect for this box. + + """ + top, right, bottom, left = self + return RectF(left, top, right - left, bottom - top) + + @property + def size(self): + """ The Size of this box. + + """ + top, right, bottom, left = self + return SizeF(right - left, bottom - top) + + @property + def pos(self): + """ The Pos of this box. + + """ + return PosF(self.left, self.top) + + +#------------------------------------------------------------------------------ +# Pos +#------------------------------------------------------------------------------ +class BasePos(tuple): + """ A tuple subclass representing a (x, y) positions. Subclasses + should override the __new__ method to enforce any necessary typing. + + """ + __slots__ = () + + def __new__(cls, x, y): + return super(BasePos, cls).__new__(cls, (x, y)) + + def __getnewargs__(self): + return tuple(self) + + def __repr__(self): + template = '%s(x=%s, y=%s)' + values = (self.__class__.__name__,) + self + return template % values + + @property + def x(self): + """ The 'x' component of the size. + + """ + return self[0] + + @property + def y(self): + """ The 'y' component of the size. + + """ + return self[1] + + +class Pos(BasePos): + """ An implementation of BasePos for integer values. + + """ + __slots__ = () + + def __new__(cls, x, y): + i = int + return super(Pos, cls).__new__(cls, i(x), i(y)) + + +class PosF(BasePos): + """ An implementation of BasePos of floating point values. + + """ + __slots__ = () + + def __new__(cls, x, y): + f = float + return super(PosF, cls).__new__(cls, f(x), f(y)) + + +#------------------------------------------------------------------------------ +# Size +#------------------------------------------------------------------------------ +class BaseSize(tuple): + """ A tuple subclass representing a (width, height) size. Subclasses + should override the __new__ method to enforce any necessary typing. + + """ + __slots__ = () + + @staticmethod + def coerce_type(item): + return item + + def __new__(cls, width=None, height=None): + if isinstance(width, (tuple, BaseSize)): + return cls(*width) + c = cls.coerce_type + width = c(width) + if height is None: + height = width + else: + height = c(height) + return super(BaseSize, cls).__new__(cls, (width, height)) + + def __getnewargs__(self): + return tuple(self) + + def __repr__(self): + template = '%s(width=%s, height=%s)' + values = (self.__class__.__name__,) + self + return template % values + + @property + def width(self): + """ The 'width' component of the size. + + """ + return self[0] + + @property + def height(self): + """ The 'height' component of the size. + + """ + return self[1] + + +class Size(BaseSize): + """ A BaseSize implementation for integer values. + + """ + __slots__ = () + + @staticmethod + def coerce_type(item): + return 0 if item is None else int(item) + + +class SizeF(BaseSize): + """ A BaseSize implementation for floating point values. + + """ + __slots__ = () + + @staticmethod + def coerce_type(item): + return 0.0 if item is None else float(item) + diff --git a/enable/layout/layout_helpers.py b/enable/layout/layout_helpers.py new file mode 100644 index 000000000..175af9258 --- /dev/null +++ b/enable/layout/layout_helpers.py @@ -0,0 +1,1280 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ +from abc import ABCMeta, abstractmethod +from collections import defaultdict +from uuid import uuid4 + +from casuarius import ConstraintVariable, LinearSymbolic, LinearConstraint +from traits.api import HasTraits, Instance, Range + +from .ab_constrainable import ABConstrainable +from .constraints_namespace import ConstraintsNamespace +from .geometry import Box +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 +#------------------------------------------------------------------------------ +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. + + Paramters + --------- + 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, LinearConstraint): + yield cn + + +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 +#------------------------------------------------------------------------------ +class DeferredConstraints(object): + """ Abstract base class for objects that will yield lists of + constraints upon request. + + """ + __metaclass__ = ABCMeta + + def __init__(self): + """ Initialize a DeferredConstraints instance. + + """ + # __or__() will set these default strength and weight. If + # provided, they will be combined with the constraints created + # by this instance. + self.default_strength = None + self.default_weight = None + + def __or__(self, other): + """ Set the strength of all of the constraints to a common + strength. + + """ + if isinstance(other, (float, int, long)): + self.default_weight = float(other) + elif isinstance(other, basestring): + if other not in STRENGTHS: + raise ValueError('Invalid strength %r' % other) + self.default_strength = other + else: + msg = 'Strength must be a string. Got %s instead.' + raise TypeError(msg % type(other)) + return self + + 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 + + def get_constraints(self, component): + """ Returns a list of weighted LinearConstraints. + + 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 which have been + weighted by any provided strengths and weights. + + """ + cn_list = self._get_constraints(component) + strength = self.default_strength + if strength is not None: + cn_list = [cn | strength for cn in cn_list] + weight = self.default_weight + if weight is not None: + cn_list = [cn | weight 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` and/or the + `default_weight` if one or both are 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 +#------------------------------------------------------------------------------ +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(DeferredConstraintsFunction, self).__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) + + +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(AbutmentHelper, self).__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) + + +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(AlignmentHelper, self).__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) + + +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(BoxHelper, self).__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) + + +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(LinearBoxHelper, self).__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) + + +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(GridHelper, self).__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 xrange(num_rows + 1): + name = 'row' + str(idx) + var = ConstraintVariable('{0}|{1}'.format(cn_id, name)) + row_vars.append(var) + constraints.append(var >= 0) + for idx in xrange(num_cols + 1): + name = 'col' + str(idx) + var = ConstraintVariable('{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.) + col_spacer = FlexSpacer(self.col_spacing / 2.) + 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.itervalues(): + 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.itervalues(): + 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 +#------------------------------------------------------------------------------ +class AbstractConstraintFactory(object): + """ An abstract constraint factory class. Subclasses must implement + the 'constraints' method implement which returns a LinearConstraint + instance. + + """ + __metaclass__ = ABCMeta + + @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) + + @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 +#------------------------------------------------------------------------------ +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 + + 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) + + +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 + + +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'), + } + + @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) + + +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. + + """ + @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 +#------------------------------------------------------------------------------ +class Spacer(object): + """ An abstract base class for spacers. Subclasses must implement + the 'constrain' method. + + """ + __metaclass__ = ABCMeta + + def __init__(self, amt, strength=None, weight=None): + self.amt = max(0, amt) + self.strength = strength + self.weight = weight + + 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 + + def constrain(self, first_anchor, second_anchor): + """ Returns the list of generated constraints appropriately + weighted by the default strength and weight, if provided. + + """ + constraints = self._constrain(first_anchor, second_anchor) + strength = self.strength + if strength is not None: + constraints = [cn | strength for cn in constraints] + weight = self.weight + if weight is not None: + constraints = [cn | weight 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 + + +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] + + +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] + + +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] + + +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', min_weight=1.0, eq_strength='medium', eq_weight=1.25): + self.amt = max(0, amt) + self.min_strength = min_strength + self.min_weight = min_weight + self.eq_strength = eq_strength + self.eq_weight = eq_weight + + 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 | self.min_weight, + ((first_anchor + self.amt) == second_anchor) | self.eq_strength | self.eq_weight, + ] + + +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, self.weight) + + def __le__(self, other): + if not isinstance(other, int): + raise TypeError('space can only be created from ints') + return LeSpacer(other, self.strength, self.weight) + + def __ge__(self, other): + if not isinstance(other, int): + raise TypeError('space can only be created from ints') + return GeSpacer(other, self.strength, self.weight) + + def _constrain(self, first_anchor, second_anchor): + """ Returns a greater than or equal to spacing constraint. + + """ + spacer = GeSpacer(self.amt, self.strength, self.weight) + return spacer._constrain(first_anchor, second_anchor) + + def flex(self, **kwargs): + """ Returns a flex spacer for the current amount. + + """ + return FlexSpacer(self.amt, **kwargs) + + +#------------------------------------------------------------------------------ +# Layout Helper Functions and Objects +#------------------------------------------------------------------------------ +def horizontal(*items, **config): + """ Create a DeferredConstraints object composed of horizontal + abutments for the given sequence of items. + + """ + return AbutmentHelper('horizontal', *items, **config) + + +def vertical(*items, **config): + """ Create a DeferredConstraints object composed of vertical + abutments for the given sequence of items. + + """ + return AbutmentHelper('vertical', *items, **config) + + +def hbox(*items, **config): + """ Create a DeferredConstraints object composed of horizontal + abutments for a given sequence of items. + + """ + return LinearBoxHelper('horizontal', *items, **config) + + +def vbox(*items, **config): + """ Create a DeferredConstraints object composed of vertical abutments + for a given sequence of items. + + """ + return LinearBoxHelper('vertical', *items, **config) + + +def align(anchor, *items, **config): + """ Align the given anchors of the given components. Inter-component + spacing is allowed. + + """ + return AlignmentHelper(anchor, *items, **config) + + +def grid(*rows, **config): + """ Create a DeferredConstraints object which lays out items in a + grid. + + """ + return GridHelper(*rows, **config) + + +spacer = LayoutSpacer(DefaultSpacing.ABUTMENT) + diff --git a/enable/layout/layout_manager.py b/enable/layout/layout_manager.py new file mode 100644 index 000000000..33778d5d4 --- /dev/null +++ b/enable/layout/layout_manager.py @@ -0,0 +1,197 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ +from casuarius import Solver, medium + + +class LayoutManager(object): + """ A class which uses a casuarius solver to manage a system + of constraints. + + """ + def __init__(self): + self._solver = Solver(autosolve=False) + self._initialized = False + self._running = False + + def initialize(self, constraints): + """ Initialize the solver with the given constraints. + + Parameters + ---------- + constraints : Iterable + An iterable that yields the constraints to add to the + solvers. + + """ + if self._initialized: + raise RuntimeError('Solver already initialized') + solver = self._solver + solver.autosolve = False + for cn in constraints: + solver.add_constraint(cn) + solver.autosolve = True + self._initialized = True + + def replace_constraints(self, old_cns, new_cns): + """ Replace constraints in the solver. + + Parameters + ---------- + old_cns : list + The list of casuarius constraints to remove from the + solver. + + new_cns : list + The list of casuarius constraints to add to the solver. + + """ + if not self._initialized: + raise RuntimeError('Solver not yet initialized') + solver = self._solver + solver.autosolve = False + for cn in old_cns: + solver.remove_constraint(cn) + for cn in new_cns: + solver.add_constraint(cn) + solver.autosolve = True + + def layout(self, cb, width, height, size, strength=medium, weight=1.0): + """ Perform an iteration of the solver for the new width and + height constraint variables. + + Parameters + ---------- + cb : callable + A callback which will be called when new values from the + solver are available. This will be called from within a + solver context while the solved values are valid. Thus + the new values should be consumed before the callback + returns. + + width : Constraint Variable + The constraint variable representing the width of the + main layout container. + + height : Constraint Variable + The constraint variable representing the height of the + main layout container. + + size : (int, int) + The (width, height) size tuple which is the current size + of the main layout container. + + strength : casuarius strength, optional + The strength with which to perform the layout using the + current size of the container. i.e. the strength of the + resize. The default is casuarius.medium. + + weight : float, optional + The weight to apply to the strength. The default is 1.0 + + """ + if not self._initialized: + raise RuntimeError('Layout with uninitialized solver') + if self._running: + return + try: + self._running = True + w, h = size + values = [(width, w), (height, h)] + with self._solver.suggest_values(values, strength, weight): + cb() + finally: + self._running = False + + def get_min_size(self, width, height, strength=medium, weight=0.1): + """ Run an iteration of the solver with the suggested size of the + component set to (0, 0). This will cause the solver to effectively + compute the minimum size that the window can be to solve the + system. + + Parameters + ---------- + width : Constraint Variable + The constraint variable representing the width of the + main layout container. + + height : Constraint Variable + The constraint variable representing the height of the + main layout container. + + strength : casuarius strength, optional + The strength with which to perform the layout using the + current size of the container. i.e. the strength of the + resize. The default is casuarius.medium. + + weight : float, optional + The weight to apply to the strength. The default is 0.1 + so that constraints of medium strength but default weight + have a higher precedence than the minimum size. + + Returns + ------- + result : (float, float) + The floating point (min_width, min_height) size of the + container which would best satisfy the set of constraints. + + """ + if not self._initialized: + raise RuntimeError('Get min size on uninitialized solver') + values = [(width, 0.0), (height, 0.0)] + with self._solver.suggest_values(values, strength, weight): + min_width = width.value + min_height = height.value + return (min_width, min_height) + + def get_max_size(self, width, height, strength=medium, weight=0.1): + """ Run an iteration of the solver with the suggested size of + the component set to a very large value. This will cause the + solver to effectively compute the maximum size that the window + can be to solve the system. The return value is a tuple numbers. + If one of the numbers is -1, it indicates there is no maximum in + that direction. + + Parameters + ---------- + width : Constraint Variable + The constraint variable representing the width of the + main layout container. + + height : Constraint Variable + The constraint variable representing the height of the + main layout container. + + strength : casuarius strength, optional + The strength with which to perform the layout using the + current size of the container. i.e. the strength of the + resize. The default is casuarius.medium. + + weight : float, optional + The weight to apply to the strength. The default is 0.1 + so that constraints of medium strength but default weight + have a higher precedence than the minimum size. + + Returns + ------- + result : (float or -1, float or -1) + The floating point (max_width, max_height) size of the + container which would best satisfy the set of constraints. + + """ + if not self._initialized: + raise RuntimeError('Get max size on uninitialized solver') + max_val = 2**24 - 1 # Arbitrary, but the max allowed by Qt. + values = [(width, max_val), (height, max_val)] + with self._solver.suggest_values(values, strength, weight): + max_width = width.value + max_height = height.value + width_diff = abs(max_val - int(round(max_width))) + height_diff = abs(max_val - int(round(max_height))) + if width_diff <= 1: + max_width = -1 + if height_diff <= 1: + max_height = -1 + return (max_width, max_height) + diff --git a/enable/layout/utils.py b/enable/layout/utils.py new file mode 100644 index 000000000..37f3a4202 --- /dev/null +++ b/enable/layout/utils.py @@ -0,0 +1,40 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ + + +STRENGTHS = set(['required', 'strong', 'medium', 'weak']) + + +def add_symbolic_constraints(namespace): + """ Add constraints to a namespace that are LinearExpressions of basic + constraints. + + """ + bottom = namespace.bottom + left = namespace.left + width = namespace.layout_width + height = namespace.layout_height + + namespace.right = left + width + namespace.top = bottom + height + namespace.h_center = left + width / 2.0 + namespace.v_center = bottom + height / 2.0 + + +def add_symbolic_contents_constraints(namespace): + """ Add constraints to a namespace that are LinearExpressions of basic + constraints. + + """ + left = namespace.contents_left + right = namespace.contents_right + top = namespace.contents_top + bottom = namespace.contents_bottom + + namespace.contents_width = right - left + namespace.contents_height = top - bottom + namespace.contents_v_center = bottom + namespace.contents_height / 2.0 + namespace.contents_h_center = left + namespace.contents_width / 2.0 + diff --git a/enable/new_abstract_component.py b/enable/new_abstract_component.py deleted file mode 100644 index 579e71869..000000000 --- a/enable/new_abstract_component.py +++ /dev/null @@ -1,178 +0,0 @@ -""" Defines the AbstractComponent class """ - -# Enthought library imports -from traits.api import Any, Enum, Instance, List, Property - -# Local relative imports -from enable_traits import coordinate_trait -from interactor import Interactor - -class AbstractComponent(Interactor): - """ - AbstractComponent is the primitive base class for Component. It only - requires the ability to handle events and render itself. It supports - being contained within a parent graphical object and defines methods - to handle positions. It does not necessarily have bounds, nor does it - have any notion of viewports. - """ - - #------------------------------------------------------------------------ - # Positioning traits - #------------------------------------------------------------------------ - - # The position relative to the container. If container is None, then - # position will be set to (0,0). - position = coordinate_trait - - # X-coordinate of our position - x = Property - - # Y-coordinate of our position - y = Property - - #------------------------------------------------------------------------ - # Object/containment hierarchy traits - #------------------------------------------------------------------------ - - # Our container object - container = Any # Instance("Container") - - # A reference to our top-level Enable Window - window = Property # Instance("Window") - - # The list of viewport that are viewing this component - viewports = List(Instance("enable.Viewport")) - - #------------------------------------------------------------------------ - # Other public traits - #------------------------------------------------------------------------ - - # How this component should draw itself when draw() is called with - # a mode of "default". (If a draw mode is explicitly passed in to - # draw(), then this is overruled.) - # FIXME: Appears to be unused 5/3/6 - default_draw_mode = Enum("normal", "interactive") - - #------------------------------------------------------------------------ - # Private traits - #------------------------------------------------------------------------ - - # Shadow trait for self.window. Only gets set if this is the top-level - # enable component in a Window. - _window = Any # Instance("Window") - - #------------------------------------------------------------------------ - # Public concrete methods - # (Subclasses should not override these; they provide an extension point - # for mix-in classes.) - #------------------------------------------------------------------------ - - def __init__(self, **traits): - # The only reason we need the constructor is to make sure our container - # gets notified of our being added to it. - if traits.has_key("container"): - container = traits.pop("container") - Interactor.__init__(self, **traits) - container.add(self) - else: - Interactor.__init__(self, **traits) - return - - def get_absolute_coords(self, *coords): - """ Given coordinates relative to this component's origin, returns - the "absolute" coordinates in the frame of the top-level parent - Window enclosing this component's ancestor containers. - - Can be called in two ways: - get_absolute_coords(x, y) - get_absolute_coords( (x,y) ) - - Returns a tuple (x,y) representing the new coordinates. - """ - if self.container is not None: - offset_x, offset_y = self.container.get_absolute_coords(*self.position) - else: - offset_x, offset_y = self.position - return (offset_x + coords[0], offset_y + coords[1]) - - def request_redraw(self): - """ - Requests that the component redraw itself. Usually this means asking - its parent for a repaint. - """ - for view in self.viewports: - view.request_redraw() - self._request_redraw() - return - - - #------------------------------------------------------------------------ - # Abstract public and protected methods that subclasses can override - #------------------------------------------------------------------------ - - def is_in(self, x, y): - """ - Returns True if the point (x,y) is inside this component, False - otherwise. Even though AbstractComponents are not required to have - bounds, they are still expected to be able to answer the question, - "Does the point (x,y) lie within my region of interest?" If so, - then is_in() should return True. - """ - raise NotImplementedError - - def _request_redraw(self): - if self.container is not None: - self.container.request_redraw() - elif self._window: - self._window.redraw() - return - - - #------------------------------------------------------------------------ - # Event handlers, getters & setters - #------------------------------------------------------------------------ - - def _container_changed(self, old, new): - # We don't notify our container of this change b/c the - # caller who changed our .container should take care of that. - if new is None: - self.position = [0,0] - return - - def _position_changed(self): - if self.container is not None: - self.container._component_position_changed(self) - return - - def _get_x(self): - return self.position[0] - - def _set_x(self, val): - self.position[0] = val - return - - def _get_y(self): - return self.position[1] - - def _set_y(self, val): - self.position[1] = val - return - - def _get_window(self, win): - return self._window - - def _set_window(self, win): - self._window = win - return - - ### Persistence ########################################################### - - def __getstate__(self): - state = super(AbstractComponent,self).__getstate__() - for key in ['_window', 'viewports']: - if state.has_key(key): - del state[key] - - return state - -# EOF diff --git a/enable/new_component.py b/enable/new_component.py deleted file mode 100644 index b9f1e61bb..000000000 --- a/enable/new_component.py +++ /dev/null @@ -1,349 +0,0 @@ -""" Defines the Component class. - -FIXME: this appears to be unfinished and unworking as of 2008-08-03. -""" - -# Enthought library imports -from traits.api import Any, Bool, Delegate, HasTraits, Instance, \ - Int, List, Property - -# Local relative imports -from abstract_component import AbstractComponent -from abstract_layout_controller import AbstractLayoutController -from coordinate_box import CoordinateBox -from render_controllers import AbstractRenderController - - -coordinate_delegate = Delegate("inner", modify=True) - -class Component(CoordinateBox, AbstractComponent): - """ - Component is the base class for most Enable objects. In addition to the - basic position and container features of AbstractComponent, it also supports - Viewports and has finite bounds. - - Since Components can have a border and padding, there is an additional set - of bounds and position attributes that define the "outer box" of the component. - These cannot be set, since they are secondary attributes (computed from - the component's "inner" size and margin-area attributes). - """ - - #------------------------------------------------------------------------ - # Padding-related traits - # Padding in each dimension is defined as the number of pixels that are - # part of the component but outside of its position and bounds. Containers - # need to be aware of padding when doing layout, object collision/overlay - # calculations, etc. - #------------------------------------------------------------------------ - - # The amount of space to put on the left side of the component - padding_left = Int(0) - - # The amount of space to put on the right side of the component - padding_right = Int(0) - - # The amount of space to put on top of the component - padding_top = Int(0) - - # The amount of space to put below the component - padding_bottom = Int(0) - - # This property allows a way to set the padding in bulk. It can either be - # set to a single Int (which sets padding on all sides) or a tuple/list of - # 4 Ints representing the left, right, top, bottom padding amounts. When - # it is read, this property always returns the padding as a list of 4 elements, - # even if they are all the same. - padding = Property - - # Readonly property expressing the total amount of horizontal padding - hpadding = Property - - # Readonly property expressing the total amount of vertical padding - vpadding = Property - - # Does the component respond to mouse events occurring over the padding area? - padding_accepts_focus = Bool(True) - - #------------------------------------------------------------------------ - # Position and bounds of outer box (encloses the padding and border area) - # All of these are read-only properties. To set them directly, use - # set_outer_coordinates() or set_outer_pos_bounds(). - #------------------------------------------------------------------------ - - # The x,y point of the lower left corner of the padding outer box around - # the component. Setting this position will move the component, but - # will not change the padding or bounds. - # This returns a tuple because modifying the returned value has no effect. - # To modify outer_position element-wise, use set_outer_position(). - outer_position = Property - - # The number of horizontal and vertical pixels in the padding outer box. - # Setting these bounds will modify the bounds of the component, but - # will not change the lower-left position (self.outer_position) or - # the padding. - # This returns a tuple because modifying the returned value has no effect. - # To modify outer_bounds element-wise, use set_outer_bounds(). - outer_bounds = Property - - outer_x = Property - outer_x2 = Property - outer_y = Property - outer_y2 = Property - outer_width = Property - outer_height = Property - - - #------------------------------------------------------------------------ - # Public methods - #------------------------------------------------------------------------ - - def set_outer_position(self, ndx, val): - """ - Since self.outer_position is a property whose value is determined - by other (primary) attributes, it cannot return a mutable type. - This method allows generic (i.e. orientation-independent) code - to set the value of self.outer_position[0] or self.outer_position[1]. - """ - if ndx == 0: - self.outer_x = val - else: - self.outer_y = val - return - - def set_outer_bounds(self, ndx, val): - """ - Since self.outer_bounds is a property whose value is determined - by other (primary) attributes, it cannot return a mutable type. - This method allows generic (i.e. orientation-independent) code - to set the value of self.outer_bounds[0] or self.outer_bounds[1]. - """ - if ndx == 0: - self.outer_width = val - else: - self.outer_height = val - return - - #------------------------------------------------------------------------ - # AbstractComponent interface - #------------------------------------------------------------------------ - - def is_in(self, x, y): - # A basic implementation of is_in(); subclasses should provide their - # own if they are more accurate/faster/shinier. - - if self.padding_accepts_focus: - bounds = self.outer_bounds - pos = self.outer_position - else: - bounds = self.bounds - pos = self.position - - return (x >= pos[0]) and (x < pos[0] + bounds[0]) and \ - (y >= pos[1]) and (y < pos[1] + bounds[1]) - - def cleanup(self, window): - """When a window viewing or containing a component is destroyed, - cleanup is called on the component to give it the opportunity to - delete any transient state it may have (such as backbuffers).""" - return - - #------------------------------------------------------------------------ - # Protected methods - #------------------------------------------------------------------------ - - def _get_visible_border(self): - """ Helper function to return the amount of border, if visible """ - if self.border_visible: - return self.border_width - else: - return 0 - - #------------------------------------------------------------------------ - # Event handlers - #------------------------------------------------------------------------ - - def _bounds_changed(self, old, new): - self.cursor_bounds = new - if self.container is not None: - self.container._component_bounds_changed(self) - return - - def _bounds_items_changed(self, event): - self.cursor_bounds = self.bounds[:] - if self.container is not None: - self.container._component_bounds_changed(self) - return - - #------------------------------------------------------------------------ - # Padding setters and getters - #------------------------------------------------------------------------ - - def _get_padding(self): - return [self.padding_left, self.padding_right, self.padding_top, self.padding_bottom] - - def _set_padding(self, val): - old_padding = self.padding - - if type(val) == int: - self.padding_left = self.padding_right = \ - self.padding_top = self.padding_bottom = val - self.trait_property_changed("padding", old_padding, [val]*4) - else: - # assume padding is some sort of array type - if len(val) != 4: - raise RuntimeError, "Padding must be a 4-element sequence type or an int. Instead, got" + str(val) - self.padding_left = val[0] - self.padding_right = val[1] - self.padding_top = val[2] - self.padding_bottom = val[3] - self.trait_property_changed("padding", old_padding, val) - return - - def _get_hpadding(self): - return 2*self._get_visible_border() + self.padding_right + self.padding_left - - def _get_vpadding(self): - return 2*self._get_visible_border() + self.padding_bottom + self.padding_top - - #------------------------------------------------------------------------ - # Outer position setters and getters - #------------------------------------------------------------------------ - - def _get_outer_position(self): - border = self._get_visible_border() - pos = self.position - return (pos[0] - self.padding_left - border, - pos[1] - self.padding_bottom - border) - - def _set_outer_position(self, new_pos): - border = self._get_visible_border() - self.position = [new_pos[0] + self.padding_left + border, - new_pos[1] + self.padding_bottom + border] - return - - def _get_outer_x(self): - return self.x - self.padding_left - self._get_visible_border() - - def _set_outer_x(self, val): - self.position[0] = val + self.padding_left + self._get_visible_border() - return - - def _get_outer_x2(self): - return self.x2 + self.padding_right + self._get_visible_border() - - def _set_outer_x2(self, val): - self.x2 = val - self.hpadding - return - - def _get_outer_y(self): - return self.y - self.padding_bottom - self._get_visible_border() - - def _set_outer_y(self, val): - self.position[1] = val + self.padding_bottom + self._get_visible_border() - return - - def _get_outer_y2(self): - return self.y2 + self.padding_top + self._get_visible_border() - - def _set_outer_y2(self, val): - self.y2 = val - self.vpadding - return - - #------------------------------------------------------------------------ - # Outer bounds setters and getters - #------------------------------------------------------------------------ - - def _get_outer_bounds(self): - border = self._get_visible_border() - bounds = self.bounds - return (bounds[0] + self.hpadding, bounds[1] + self.vpadding) - - def _set_outer_bounds(self, bounds): - self.bounds = [bounds[0] - self.hpadding, bounds[1] - self.vpadding] - return - - def _get_outer_width(self): - return self.outer_bounds[0] - - def _set_outer_width(self, width): - self.bounds[0] = width - self.hpadding - return - - def _get_outer_height(self): - return self.outer_bounds[1] - - def _set_outer_height(self, height): - self.bounds[1] = height - self.vpadding - return - - - - -class NewComponent(CoordinateBox, AbstractComponent): - - # A list of strings defining the classes to which this component belongs. - # These classes will be used to determine how this component is styled, - # is rendered, is laid out, and receives events. There is no automatic - # management of conflicting class names, so if a component is placed - # into more than one class and that class - classes = List - - # The optional element ID of this component. - - - #------------------------------------------------------------------------ - # Layout traits - #------------------------------------------------------------------------ - - layout_info = Instance(LayoutInfo, args=()) - - # backwards-compatible layout properties - padding_left = Property - padding_right = Property - padding_top = Property - padding_bottom = Property - - padding = Property - hpadding = Property - vpadding = Property - - - padding_accepts_focus = Bool(True) - - - -class AbstractResolver(HasTraits): - """ - A Resolver traverses a component DB and matches a specifier. - """ - - def match(self, db, query): - raise NotImplementedError - - -class NewContainer(NewComponent): - - # The layout controller determines how the container's internal layout - # mechanism works. It can perform the actual layout or defer to an - # enclosing container's layout controller. The default controller is - # a cooperative/recursive layout controller. - layout_controller = Instance(AbstractLayoutController) - - - # The render controller determines how this container and its enclosed - # components are rendered. It can actually perform the rendering calls, - # or defer to an enclosing container's render controller. - render_controller = Instance(AbstractRenderController) - - - resolver = Instance(AbstractResolver) - - # Dict that caches previous lookups. - _lookup_cache = Any - - def lookup(self, query): - """ - Returns the component or components matching the given specifier. - - """ - diff --git a/examples/enable/constraints_demo.enaml b/examples/enable/constraints_demo.enaml new file mode 100644 index 000000000..f7167ba96 --- /dev/null +++ b/examples/enable/constraints_demo.enaml @@ -0,0 +1,29 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ +""" Implements the constraints_demo in Enaml +""" +from enaml.layout.api import align, hbox +from enaml.widgets.api import Window, Container, Html + + +enamldef Main(Window): + Container: + constraints = [ + hbox(one, two, three, four), + align('width', one, two, three, four), + ] + Html: + id: one + source = '' + Html: + id: two + source = '' + Html: + id: three + source = '' + Html: + id: four + source = '' + diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py new file mode 100644 index 000000000..62bb38722 --- /dev/null +++ b/examples/enable/constraints_demo.py @@ -0,0 +1,132 @@ + +from enable.api import Component, ComponentEditor, ConstraintsContainer +from enable.layout.api import (align, grid, horizontal, hbox, vbox, spacer, + vertical) +from traits.api import HasTraits, Bool, Instance, Str +from traitsui.api import Item, View, HGroup, VGroup, CodeEditor + + +class Demo(HasTraits): + canvas = Instance(ConstraintsContainer) + child_canvas = Instance(ConstraintsContainer) + + constraints_def = Str + child_constraints_def = Str + share_layout = Bool(False) + + traits_view = View( + HGroup( + VGroup( + Item('constraints_def', + editor=CodeEditor(), + height=100, + show_label=False, + ), + Item('share_layout'), + Item('child_constraints_def', + editor=CodeEditor(), + height=100, + show_label=False, + ), + ), + Item('canvas', + editor=ComponentEditor(), + show_label=False, + ), + ), + resizable=True, + title="Constraints Demo", + width=1000, + height=500, + ) + + def _canvas_default(self): + parent = ConstraintsContainer(bounds=(500,500), padding=20) + + one = Component(id="r", bgcolor=0xFF0000) + two = Component(id="g", bgcolor=0x00FF00) + three = Component(id="b", bgcolor=0x0000FF) + + parent.add(one, two, three, self.child_canvas) + return parent + + def _child_canvas_default(self): + parent = ConstraintsContainer(id="child", share_layout=self.share_layout) + + one = Component(id="c", bgcolor=0x00FFFF) + two = Component(id="m", bgcolor=0xFF00FF) + three = Component(id="y", bgcolor=0xFFFF00) + four = Component(id="k", bgcolor=0x000000) + + parent.add(one, two, three, four) + return parent + + def _constraints_def_changed(self): + if self.canvas is None: + return + + canvas = self.canvas + components = canvas._components + r = components[0] + g = components[1] + b = components[2] + child = components[3] + + components = child._components + c = components[0] + m = components[1] + y = components[2] + k = components[3] + + try: + new_cns = eval(self.constraints_def) + except Exception, ex: + return + + canvas.layout_constraints = new_cns + canvas.request_redraw() + + def _child_constraints_def_changed(self): + if self.child_canvas is None: + return + + canvas = self.child_canvas + components = canvas._components + c = components[0] + m = components[1] + y = components[2] + k = components[3] + + try: + new_cns = eval(self.child_constraints_def) + except Exception, ex: + return + + canvas.layout_constraints = new_cns + canvas.request_redraw() + + def _share_layout_changed(self): + self.child_canvas.share_layout = self.share_layout + self.canvas.relayout() + self.canvas.request_redraw() + + def _constraints_def_default(self): + return """[ + hbox(r, g, b, child), + align('layout_height', r,g,b,child), + align('layout_width', r,g,b,child), +]""" + + def _child_constraints_def_default(self): + return """[ + vbox(c,m,y,k), + align('layout_height', c,m,y,k), + align('layout_width', c,m,y,k), +]""" + + +if __name__ == "__main__": + demo = Demo() + demo._child_constraints_def_changed() + demo._constraints_def_changed() + demo.configure_traits()