From 9d611820f92c7ed6652109cc21137222211d51ef Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 8 Feb 2013 17:09:39 -0600 Subject: [PATCH 01/57] Copy LayoutBox from Enaml. --- enable/layout_box.py | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 enable/layout_box.py diff --git a/enable/layout_box.py b/enable/layout_box.py new file mode 100644 index 000000000..9309d8e65 --- /dev/null +++ b/enable/layout_box.py @@ -0,0 +1,53 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ + +from casuarius import ConstraintVariable + + +class LayoutBox(object): + """ A class which encapsulates a layout box using 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 LayoutBox. + + Parameters + ---------- + name : str + A name to use in the label for the constraint variables in + this layout box. + + owner : str + The owner id to use in the label for the constraint variables + in this layout box. + + """ + self._name = name + self._owner = owner + self._primitives = {} + + def primitive(self, name): + """ Returns a primitive casuarius constraint variable for the + given name. + + Parameters + ---------- + name : str + The name of the constraint variable to return. + + """ + primitives = self._primitives + if name in primitives: + res = primitives[name] + else: + label = '{0}|{1}|{2}'.format(self._name, self._owner, name) + res = primitives[name] = ConstraintVariable(label) + return res + From 66f1909523a5d33dbb3d47b84cff410b92140b15 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 8 Feb 2013 18:59:37 -0600 Subject: [PATCH 02/57] Add a ConstraintsContainer which doesn't do much yet. --- enable/constraints_container.py | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 enable/constraints_container.py diff --git a/enable/constraints_container.py b/enable/constraints_container.py new file mode 100644 index 000000000..c08fe296a --- /dev/null +++ b/enable/constraints_container.py @@ -0,0 +1,72 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ + +# traits imports +from traits.api import Dict + +# local imports +from container import Container + + +class ConstraintsContainer(Container): + """ A Container which lays out its child components using a + constraints-based layout solver. + + """ + + # A dictionary of components added to this container + _component_map = Dict + + #------------------------------------------------------------------------ + # Public methods + #------------------------------------------------------------------------ + + def relayout(self): + """ Re-run the constraints solver in response to a resize or + component removal. + """ + pass + + #------------------------------------------------------------------------ + # Traits methods + #------------------------------------------------------------------------ + + def __components_items_changed(self, event): + """ Make sure components that are added can be used with constraints. + """ + # Check the added components + self._check_and_add_components(event.added) + + # Remove stale components from the map + for item in event.removed: + del self._component_map[item.id] + + def __components_changed(self, new): + """ Make sure components that are added can be used with constraints. + """ + # Clear the component map + self._component_map = {} + + # Check the new components + self._check_and_add_components(new) + + #------------------------------------------------------------------------ + # Protected methods + #------------------------------------------------------------------------ + + def _check_and_add_components(self, components): + """ Make sure components can be used with constraints. + """ + for item in components: + if len(item.id) == 0: + msg = "Components added to a {0} must have a valid 'id' trait." + name = type(self).__name__ + raise ValueError(msg.format(name)) + elif item.id in self._component_map: + msg = "A Component with that id has already been added." + raise ValueError(msg) + + self._component_map[item.id] = item + From 6a44715ddcb6e7ed847587cd4099e36a9a41bddb Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 8 Feb 2013 21:21:20 -0600 Subject: [PATCH 03/57] Construct a simple demo for testing constraints layout. --- enable/api.py | 1 + examples/enable/constraints_demo.py | 48 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 examples/enable/constraints_demo.py 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/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py new file mode 100644 index 000000000..95dfb482a --- /dev/null +++ b/examples/enable/constraints_demo.py @@ -0,0 +1,48 @@ + +from enable.api import Component, ComponentEditor, ConstraintsContainer +from traits.api import HasTraits, Instance +from traitsui.api import Item, View + + +class Demo(HasTraits): + canvas = Instance(Component) + + traits_view = View( + Item('canvas', + editor=ComponentEditor(), + show_label=False, + ), + resizable=True, + title="Constraints Demo", + width=500, + height=500, + ) + + def _canvas_default(self): + container = ConstraintsContainer() + + container.add(Component(id="one", bgcolor="red")) + container.add(Component(id="two", bgcolor="green")) + container.add(Component(id="three", bgcolor="blue")) + container.add(Component(id="four", bgcolor="white")) + + container.constraints = [ + "parent.top == one.top", + "parent.left == one.left", + "parent.top == two.top", + "parent.right == two.right", + "parent.bottom == three.bottom", + "parent.left == three.left", + "parent.bottom == four.bottom", + "parent.right == four.right", + "one.right == two.left", + "three.right == four.left", + "one.bottom == three.top", + "two.bottom == four.top", + ] + + return container + + +if __name__ == "__main__": + Demo().configure_traits() From 71ff33422f120c71bf3eef8f4680b2bfee30ca63 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 8 Feb 2013 22:07:40 -0600 Subject: [PATCH 04/57] Add the LayoutManager class from Enaml. --- enable/layout/__init__.py | 0 enable/{ => layout}/layout_box.py | 0 enable/layout/layout_manager.py | 197 ++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 enable/layout/__init__.py rename enable/{ => layout}/layout_box.py (100%) create mode 100644 enable/layout/layout_manager.py diff --git a/enable/layout/__init__.py b/enable/layout/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/enable/layout_box.py b/enable/layout/layout_box.py similarity index 100% rename from enable/layout_box.py rename to enable/layout/layout_box.py 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) + From 24708da80b2009fd051239e826e1a755678c198c Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 8 Feb 2013 22:29:23 -0600 Subject: [PATCH 05/57] Add a little more to the ConstraintsContainer. --- enable/constraints_container.py | 27 +++++++++++++++++++++++++-- examples/enable/constraints_demo.py | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index c08fe296a..a953032d0 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -8,7 +8,7 @@ # local imports from container import Container - +from layout.layout_managet import LayoutManager class ConstraintsContainer(Container): """ A Container which lays out its child components using a @@ -16,16 +16,22 @@ class ConstraintsContainer(Container): """ + # The layout constraints for this container. + layout_constraints = List + # A dictionary of components added to this container _component_map = Dict + # The casuarius solver + _layout_manager = Instance(LayoutManager) + #------------------------------------------------------------------------ # Public methods #------------------------------------------------------------------------ def relayout(self): """ Re-run the constraints solver in response to a resize or - component removal. + constraints modification. """ pass @@ -33,6 +39,16 @@ def relayout(self): # Traits methods #------------------------------------------------------------------------ + def _layout_constraints_changed(self): + """ Invalidate the layout when constraints change + """ + self.relayout() + + def _layout_constraints_items_changed(self, event): + """ Invalidate the layout when constraints change + """ + self.relayout() + def __components_items_changed(self, event): """ Make sure components that are added can be used with constraints. """ @@ -52,6 +68,13 @@ def __components_changed(self, new): # Check the new components self._check_and_add_components(new) + def __layout_manager_default(self): + """ Create the layout manager. + """ + lm = LayoutManager() + lm.initialize([]) + return lm + #------------------------------------------------------------------------ # Protected methods #------------------------------------------------------------------------ diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 95dfb482a..ae8a301e9 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -26,7 +26,7 @@ def _canvas_default(self): container.add(Component(id="three", bgcolor="blue")) container.add(Component(id="four", bgcolor="white")) - container.constraints = [ + container.layout_constraints = [ "parent.top == one.top", "parent.left == one.left", "parent.top == two.top", From d56e2129b4bbd385fe7f902d13318257a07c4c52 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 8 Feb 2013 23:11:08 -0600 Subject: [PATCH 06/57] Start adding constraints to Component. --- enable/component.py | 55 ++++++++++++++++++++++++++++++++- enable/constraints_container.py | 12 ++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/enable/component.py b/enable/component.py index 9f210f713..beff56752 100644 --- a/enable/component.py +++ b/enable/component.py @@ -13,7 +13,7 @@ from coordinate_box import CoordinateBox from enable_traits import bounds_trait, coordinate_trait, LineStyle from interactor import Interactor - +from layout.layout_box import LayoutBox coordinate_delegate = Delegate("inner", modify=True) @@ -112,6 +112,19 @@ class Component(CoordinateBox, Interactor): # always be twice as wide as the former. fixed_preferred_size = Trait(None, None, bounds_trait) + #------------------------------------------------------------------------ + # Constraints-based layout + #------------------------------------------------------------------------ + + # The source for constraints variables for this component + layout_box = Instance(LayoutBox) + + # The list of hard constraints which must be applied to the widget. + hard_constraints = Property + + # The list of size constraints to apply to the widget. + size_constraints = Property + #------------------------------------------------------------------------ # Overlays and underlays #------------------------------------------------------------------------ @@ -1027,6 +1040,46 @@ def _set_active_tool(self, tool): def _get_layout_needed(self): return self._layout_needed + def _layout_box_default(self): + return LayoutBox(type(self).__name__, self.id) + + def _get_hard_constraints(self): + """ Generate the constraints which must always be applied. + """ + primitive = self.layout_box.primitive + left = primitive('left') + top = primitive('top') + width = primitive('width') + height = primitive('height') + cns = [left >= 0, top >= 0, width >= 0, height >= 0] + return cns + + def _get_size_constraints(self): + """ Creates the list of size hint constraints for this widget. + """ + cns = [] + push = cns.append + width_hint, height_hint = self.get_preferred_size() + primitive = self.layout_box.primitive + width = primitive('width') + height = primitive('height') + hug_width, hug_height = ('strong', 'strong') + resist_width, resist_height = ('strong', 'strong') + 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 def _tools_items_changed(self): self.invalidate_and_redraw() diff --git a/enable/constraints_container.py b/enable/constraints_container.py index a953032d0..0d962cb57 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -4,7 +4,7 @@ #------------------------------------------------------------------------------ # traits imports -from traits.api import Dict +from traits.api import Dict, Instance, List # local imports from container import Container @@ -16,12 +16,22 @@ class ConstraintsContainer(Container): """ + # The ID for this component. This ID can be used by the layout constraints + # when referencing the container. + id = "parent" + # The layout constraints for this container. layout_constraints = List # A dictionary of components added to this container _component_map = Dict + # All the hard constraints for child components + _hard_constraints = List + + # The size constraints for child components + _size_constraints = List + # The casuarius solver _layout_manager = Instance(LayoutManager) From baea8fc1c72597dac6e7551c29f560ecfa6feb48 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Sat, 9 Feb 2013 09:45:42 -0600 Subject: [PATCH 07/57] Fix a typo. --- enable/constraints_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 0d962cb57..7933d0625 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -8,7 +8,7 @@ # local imports from container import Container -from layout.layout_managet import LayoutManager +from layout.layout_manager import LayoutManager class ConstraintsContainer(Container): """ A Container which lays out its child components using a From 8e1de32b61b4800149ef84392d02b0f6b42245d2 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Sat, 9 Feb 2013 10:37:23 -0600 Subject: [PATCH 08/57] Add fixed constraints management. --- enable/component.py | 2 +- enable/constraints_container.py | 51 ++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/enable/component.py b/enable/component.py index beff56752..0e225dd41 100644 --- a/enable/component.py +++ b/enable/component.py @@ -1048,7 +1048,7 @@ def _get_hard_constraints(self): """ primitive = self.layout_box.primitive left = primitive('left') - top = primitive('top') + top = primitive('bottom') width = primitive('width') height = primitive('height') cns = [left >= 0, top >= 0, width >= 0, height >= 0] diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 7933d0625..d228c3237 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -4,7 +4,7 @@ #------------------------------------------------------------------------------ # traits imports -from traits.api import Dict, Instance, List +from traits.api import Dict, Instance, List, Str # local imports from container import Container @@ -27,9 +27,11 @@ class ConstraintsContainer(Container): _component_map = Dict # All the hard constraints for child components + _hard_constraints_map = Dict(Str, List) _hard_constraints = List # The size constraints for child components + _size_constraints_map = Dict(Str, List) _size_constraints = List # The casuarius solver @@ -59,6 +61,12 @@ def _layout_constraints_items_changed(self, event): """ self.relayout() + def __component_map_default(self): + """ The default component map should include this container so that + name collisions are avoided. + """ + return {self.id:None} + def __components_items_changed(self, event): """ Make sure components that are added can be used with constraints. """ @@ -67,17 +75,26 @@ def __components_items_changed(self, event): # Remove stale components from the map for item in event.removed: - del self._component_map[item.id] + key = item.id + del self._hard_constraints_map[key] + del self._size_constraints_map[key] + del self._component_map[key] + + # Update the fixed constraints + self._update_fixed_constraints() def __components_changed(self, new): """ Make sure components that are added can be used with constraints. """ # Clear the component map - self._component_map = {} + self._component_map = self.__component_map_default() # Check the new components self._check_and_add_components(new) + # Update the fixed constraints + self._update_fixed_constraints() + def __layout_manager_default(self): """ Create the layout manager. """ @@ -93,13 +110,33 @@ def _check_and_add_components(self, components): """ Make sure components can be used with constraints. """ for item in components: - if len(item.id) == 0: + 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 item.id in self._component_map: - msg = "A Component with that id has already been added." - raise ValueError(msg) + msg = "A Component with id '{0}' has already been added." + raise ValueError(msg.format(key)) + + self._hard_constraints_map[key] = item.hard_constraints + self._size_constraints_map[key] = item.size_constraints + self._component_map[key] = item - self._component_map[item.id] = item + def _update_fixed_constraints(self): + """ Resolve the differences between the list of constraints and the + map of child component constraints for both types of fixed constraints. + """ + old_cns, all_new_cns = [], [] + for name in ('hard', 'size'): + map_attr = getattr(self, '_{0}_constraints_map'.format(name)) + list_name = '_{0}_constraints'.format(name) + old_cns.extend(getattr(self, list_name)) + new_cns = [] + for item in map_attr.itervalues(): + new_cns.extend(item) + all_new_cns.extend(new_cns) + setattr(self, list_name, new_cns) + + self._layout_manager.replace_constraints(old_cns, all_new_cns) From 03fda63d16c77b50accc144f4bd8cd4bab72c000 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Sat, 9 Feb 2013 10:41:10 -0600 Subject: [PATCH 09/57] Tie up some loose ends. --- enable/constraints_container.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index d228c3237..17172848d 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -70,9 +70,6 @@ def __component_map_default(self): def __components_items_changed(self, event): """ Make sure components that are added can be used with constraints. """ - # Check the added components - self._check_and_add_components(event.added) - # Remove stale components from the map for item in event.removed: key = item.id @@ -80,21 +77,20 @@ def __components_items_changed(self, event): del self._size_constraints_map[key] del self._component_map[key] - # Update the fixed constraints - self._update_fixed_constraints() + # 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 map + # Clear the component maps self._component_map = self.__component_map_default() + self._hard_constraints_map = {} + self._size_constraints_map = {} # Check the new components self._check_and_add_components(new) - # Update the fixed constraints - self._update_fixed_constraints() - def __layout_manager_default(self): """ Create the layout manager. """ @@ -123,6 +119,9 @@ def _check_and_add_components(self, components): self._size_constraints_map[key] = item.size_constraints self._component_map[key] = item + # Update the fixed constraints + self._update_fixed_constraints() + def _update_fixed_constraints(self): """ Resolve the differences between the list of constraints and the map of child component constraints for both types of fixed constraints. From c46c471c1f0e9f545300ab9f7dcfc965b2dabf6c Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Mon, 11 Feb 2013 11:45:32 -0600 Subject: [PATCH 10/57] Add simple eval()-based parsing for user-specified constraints. --- enable/component.py | 16 ++++++++-------- enable/constraints_container.py | 31 +++++++++++++++++++++++++++---- enable/layout/layout_box.py | 2 +- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/enable/component.py b/enable/component.py index 0e225dd41..31d7d1b11 100644 --- a/enable/component.py +++ b/enable/component.py @@ -1046,11 +1046,11 @@ def _layout_box_default(self): def _get_hard_constraints(self): """ Generate the constraints which must always be applied. """ - primitive = self.layout_box.primitive - left = primitive('left') - top = primitive('bottom') - width = primitive('width') - height = primitive('height') + box = self.layout_box + left = box.left + top = box.bottom + width = box.width + height = box.height cns = [left >= 0, top >= 0, width >= 0, height >= 0] return cns @@ -1060,9 +1060,9 @@ def _get_size_constraints(self): cns = [] push = cns.append width_hint, height_hint = self.get_preferred_size() - primitive = self.layout_box.primitive - width = primitive('width') - height = primitive('height') + box = self.layout_box + width = box.width + height = box.height hug_width, hug_height = ('strong', 'strong') resist_width, resist_height = ('strong', 'strong') if width_hint >= 0: diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 17172848d..feb231cb2 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -3,6 +3,9 @@ # All rights reserved. #------------------------------------------------------------------------------ +# std library imports +from copy import copy + # traits imports from traits.api import Dict, Instance, List, Str @@ -10,6 +13,7 @@ from container import Container from layout.layout_manager import LayoutManager + class ConstraintsContainer(Container): """ A Container which lays out its child components using a constraints-based layout solver. @@ -51,21 +55,25 @@ def relayout(self): # Traits methods #------------------------------------------------------------------------ - def _layout_constraints_changed(self): + def _layout_constraints_changed(self, name, old, new): """ Invalidate the layout when constraints change """ + new = self._parse_constraint_strs(new) + self._layout_manager.replace_constraints(old, new) self.relayout() def _layout_constraints_items_changed(self, event): """ Invalidate the layout when constraints change """ + added = self._parse_constraint_strs(event.added) + self._layout_manager.replace_constraints(event.removed, added) self.relayout() def __component_map_default(self): """ The default component map should include this container so that name collisions are avoided. """ - return {self.id:None} + return {self.id : self.layout_box} def __components_items_changed(self, event): """ Make sure components that are added can be used with constraints. @@ -111,17 +119,32 @@ def _check_and_add_components(self, components): msg = "Components added to a {0} must have a valid 'id' trait." name = type(self).__name__ raise ValueError(msg.format(name)) - elif item.id in self._component_map: + elif key in self._component_map: msg = "A Component with id '{0}' has already been added." raise ValueError(msg.format(key)) self._hard_constraints_map[key] = item.hard_constraints self._size_constraints_map[key] = item.size_constraints - self._component_map[key] = item + self._component_map[key] = item.layout_box # Update the fixed constraints self._update_fixed_constraints() + def _parse_constraint_strs(self, constraint_strs): + """ Given a list of strings with each describing a constraint, + return a list of casuarius constraint objects that can be added to this + container's solver. + """ + eval_dict = copy(self._component_map) + eval_dict['__builtins__'] = None + + constraints = [] + push = constraints.append + for cns_str in constraint_strs: + push(eval(cns_str, eval_dict)) + + return constraints + def _update_fixed_constraints(self): """ Resolve the differences between the list of constraints and the map of child component constraints for both types of fixed constraints. diff --git a/enable/layout/layout_box.py b/enable/layout/layout_box.py index 9309d8e65..aab8f93f5 100644 --- a/enable/layout/layout_box.py +++ b/enable/layout/layout_box.py @@ -33,7 +33,7 @@ def __init__(self, name, owner): self._owner = owner self._primitives = {} - def primitive(self, name): + def __getattr__(self, name): """ Returns a primitive casuarius constraint variable for the given name. From 314fcfb32fd8a634259bc898dfe28481e4972270 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Mon, 11 Feb 2013 14:11:19 -0600 Subject: [PATCH 11/57] More constraints cleanup. Added a relayout() implementation. --- enable/component.py | 4 ++-- enable/constraints_container.py | 13 +++++++++++-- enable/layout/layout_box.py | 14 +++++++++++++- examples/enable/constraints_demo.py | 2 +- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/enable/component.py b/enable/component.py index 31d7d1b11..4e771a3c1 100644 --- a/enable/component.py +++ b/enable/component.py @@ -1048,10 +1048,10 @@ def _get_hard_constraints(self): """ box = self.layout_box left = box.left - top = box.bottom + bottom = box.bottom width = box.width height = box.height - cns = [left >= 0, top >= 0, width >= 0, height >= 0] + cns = [left >= 0, bottom >= 0, width >= 0, height >= 0] return cns def _get_size_constraints(self): diff --git a/enable/constraints_container.py b/enable/constraints_container.py index feb231cb2..a2e02486f 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -49,7 +49,16 @@ def relayout(self): """ Re-run the constraints solver in response to a resize or constraints modification. """ - pass + mgr_layout = self._layout_manager.layout + box = self.layout_box + width_var = box.width + height_var = box.height + width, height = self.bounds + def layout(): + for name, box in self._component_map.iteritems(): + print name, ":", + print [(n, p.value) for n,p in box._primitives.items()] + mgr_layout(layout, width_var, height_var, (width, height)) #------------------------------------------------------------------------ # Traits methods @@ -103,7 +112,7 @@ def __layout_manager_default(self): """ Create the layout manager. """ lm = LayoutManager() - lm.initialize([]) + lm.initialize(self.hard_constraints) return lm #------------------------------------------------------------------------ diff --git a/enable/layout/layout_box.py b/enable/layout/layout_box.py index aab8f93f5..9fb84f80c 100644 --- a/enable/layout/layout_box.py +++ b/enable/layout/layout_box.py @@ -6,6 +6,9 @@ from casuarius import ConstraintVariable +KNOWN_CONSTRAINTS = ('left', 'right', 'top', 'bottom', 'width', 'height') + + class LayoutBox(object): """ A class which encapsulates a layout box using casuarius constraint variables. @@ -33,7 +36,7 @@ def __init__(self, name, owner): self._owner = owner self._primitives = {} - def __getattr__(self, name): + def primitive(self, name): """ Returns a primitive casuarius constraint variable for the given name. @@ -51,3 +54,12 @@ def __getattr__(self, name): res = primitives[name] = ConstraintVariable(label) return res + def __getattr__(self, name): + """ Allow the primitive dictionary to act as an extension to the + object's namespace. + """ + if name in KNOWN_CONSTRAINTS: + return self.primitive(name) + + return super(LayoutBox, self).__getattr__(name) + diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index ae8a301e9..5055ec7d3 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -19,7 +19,7 @@ class Demo(HasTraits): ) def _canvas_default(self): - container = ConstraintsContainer() + container = ConstraintsContainer(bounds=(500,500)) container.add(Component(id="one", bgcolor="red")) container.add(Component(id="two", bgcolor="green")) From 36c214eaf3159051bd5b1dc52f25dccc5f584f84 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Mon, 11 Feb 2013 16:50:39 -0600 Subject: [PATCH 12/57] Get the constraints demo sorta working. Resizing does nothing. --- enable/component.py | 15 ++++++++++++++- enable/constraints_container.py | 22 ++++++++++++++-------- examples/enable/constraints_demo.py | 8 ++++---- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/enable/component.py b/enable/component.py index 4e771a3c1..afe853d6f 100644 --- a/enable/component.py +++ b/enable/component.py @@ -1048,10 +1048,14 @@ def _get_hard_constraints(self): """ box = self.layout_box left = box.left + right = box.right bottom = box.bottom + top = box.top width = box.width height = box.height - cns = [left >= 0, bottom >= 0, width >= 0, height >= 0] + cns = [left >= 0, right >=0, + top >= 0, bottom >= 0, + width >= 0, height >= 0] return cns def _get_size_constraints(self): @@ -1079,6 +1083,15 @@ def _get_size_constraints(self): if resist_height != 'ignore': cn = (height >= height_hint) | resist_height push(cn) + + top = box.top + bottom = box.bottom + left = box.left + right = box.right + + push((top >= bottom + height) | 'strong') + push((right >= left + width) | 'strong') + return cns def _tools_items_changed(self): diff --git a/enable/constraints_container.py b/enable/constraints_container.py index a2e02486f..06184fb27 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -3,9 +3,6 @@ # All rights reserved. #------------------------------------------------------------------------------ -# std library imports -from copy import copy - # traits imports from traits.api import Dict, Instance, List, Str @@ -55,9 +52,12 @@ def relayout(self): height_var = box.height width, height = self.bounds def layout(): - for name, box in self._component_map.iteritems(): - print name, ":", - print [(n, p.value) for n,p in box._primitives.items()] + for name, component in self._component_map.iteritems(): + if name == self.id: + continue + box = component.layout_box + component.position = (box.left.value, box.bottom.value) + component.bounds = (box.width.value, box.height.value) mgr_layout(layout, width_var, height_var, (width, height)) #------------------------------------------------------------------------ @@ -134,7 +134,7 @@ def _check_and_add_components(self, components): self._hard_constraints_map[key] = item.hard_constraints self._size_constraints_map[key] = item.size_constraints - self._component_map[key] = item.layout_box + self._component_map[key] = item # Update the fixed constraints self._update_fixed_constraints() @@ -144,8 +144,14 @@ def _parse_constraint_strs(self, constraint_strs): return a list of casuarius constraint objects that can be added to this container's solver. """ - eval_dict = copy(self._component_map) + eval_dict = {} eval_dict['__builtins__'] = None + components = self._component_map + for key in components.iterkeys(): + if key == self.id: + eval_dict[key] = components[key] + else: + eval_dict[key] = components[key].layout_box constraints = [] push = constraints.append diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 5055ec7d3..ebdb9d71f 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -21,10 +21,10 @@ class Demo(HasTraits): def _canvas_default(self): container = ConstraintsContainer(bounds=(500,500)) - container.add(Component(id="one", bgcolor="red")) - container.add(Component(id="two", bgcolor="green")) - container.add(Component(id="three", bgcolor="blue")) - container.add(Component(id="four", bgcolor="white")) + container.add(Component(id="one", bgcolor="red", fixed_preferred_size=(250,250))) + container.add(Component(id="two", bgcolor="green", fixed_preferred_size=(250,250))) + container.add(Component(id="three", bgcolor="blue", fixed_preferred_size=(250,250))) + container.add(Component(id="four", bgcolor="white", fixed_preferred_size=(250,250))) container.layout_constraints = [ "parent.top == one.top", From 7c37befc81f8cc5c385d3ac9917a37796a30a2e1 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Mon, 11 Feb 2013 17:15:20 -0600 Subject: [PATCH 13/57] Relayout on resize. Still not quite there. --- enable/component.py | 1 + enable/constraints_container.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/enable/component.py b/enable/component.py index afe853d6f..ea26814d9 100644 --- a/enable/component.py +++ b/enable/component.py @@ -1084,6 +1084,7 @@ def _get_size_constraints(self): cn = (height >= height_hint) | resist_height push(cn) + # XXX: Should these two be conditionally created? top = box.top bottom = box.bottom left = box.left diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 06184fb27..f78cdba9c 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -63,6 +63,10 @@ def layout(): #------------------------------------------------------------------------ # Traits methods #------------------------------------------------------------------------ + def _bounds_changed(self, old, new): + super(ConstraintsContainer, self)._bounds_changed(old, new) + self.relayout() + self.invalidate_draw() def _layout_constraints_changed(self, name, old, new): """ Invalidate the layout when constraints change @@ -112,7 +116,19 @@ def __layout_manager_default(self): """ Create the layout manager. """ lm = LayoutManager() - lm.initialize(self.hard_constraints) + + constraints = self.hard_constraints + box = self.layout_box + top = box.top + bottom = box.bottom + left = box.left + right = box.right + height = box.height + width = box.width + constraints.append((top >= bottom + height) | 'strong') + constraints.append((right >= left + width) | 'strong') + + lm.initialize(constraints) return lm #------------------------------------------------------------------------ From 98ce8d0bed7967ffab7b401d6941afabb48b258b Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Mon, 11 Feb 2013 17:18:55 -0600 Subject: [PATCH 14/57] Change the lower right corner to black to help testing. --- examples/enable/constraints_demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index ebdb9d71f..185a3963a 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -24,7 +24,7 @@ def _canvas_default(self): container.add(Component(id="one", bgcolor="red", fixed_preferred_size=(250,250))) container.add(Component(id="two", bgcolor="green", fixed_preferred_size=(250,250))) container.add(Component(id="three", bgcolor="blue", fixed_preferred_size=(250,250))) - container.add(Component(id="four", bgcolor="white", fixed_preferred_size=(250,250))) + container.add(Component(id="four", bgcolor="black", fixed_preferred_size=(250,250))) container.layout_constraints = [ "parent.top == one.top", From 36ba9f43e94b096dee4cd3c2daca91316eacee29 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 12 Feb 2013 10:38:01 -0600 Subject: [PATCH 15/57] Add an Enaml file which mirrors the constraints demo (for debug). --- examples/enable/constraints_demo.enaml | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 examples/enable/constraints_demo.enaml diff --git a/examples/enable/constraints_demo.enaml b/examples/enable/constraints_demo.enaml new file mode 100644 index 000000000..98ecc0aa7 --- /dev/null +++ b/examples/enable/constraints_demo.enaml @@ -0,0 +1,45 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2012, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ +""" An example which demonstrates the use of the `align` layout helper. + +In this example, we use the `align` layout helper to align various +constraints of the children of a container. The layout consists of +a Field pinned to the top of the Container. Below the Field are two +PushButtons each of which have their `left` boundary aligned. The +top PushButton is then aligned with the `h_center` of the Field. + +""" +from enaml.widgets.api import Window, Container, Html + + +enamldef Main(Window): + Container: + constraints = [ + top == one.top, + left == one.left, + top == two.top, + right == two.right, + bottom == three.bottom, + left == three.left, + bottom == four.bottom, + right == four.right, + one.right == two.left, + three.right == four.left, + one.bottom == three.top, + two.bottom == four.top, + ] + Html: + id: one + source = '' + Html: + id: two + source = '' + Html: + id: three + source = '' + Html: + id: four + source = '' + From 33954e7047a2197ee309c5c83863fe09a4728317 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 12 Feb 2013 12:29:40 -0600 Subject: [PATCH 16/57] Implement symbolic constraints and un-hardcode 'top' and 'right'. --- enable/component.py | 15 ++---------- enable/constraints_container.py | 10 -------- enable/layout/layout_box.py | 43 ++++++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/enable/component.py b/enable/component.py index ea26814d9..09edfe8d9 100644 --- a/enable/component.py +++ b/enable/component.py @@ -1053,9 +1053,7 @@ def _get_hard_constraints(self): top = box.top width = box.width height = box.height - cns = [left >= 0, right >=0, - top >= 0, bottom >= 0, - width >= 0, height >= 0] + cns = [left >= 0, bottom >= 0, width >= 0, height >= 0] return cns def _get_size_constraints(self): @@ -1067,7 +1065,7 @@ def _get_size_constraints(self): box = self.layout_box width = box.width height = box.height - hug_width, hug_height = ('strong', 'strong') + hug_width, hug_height = ('ignore', 'ignore') resist_width, resist_height = ('strong', 'strong') if width_hint >= 0: if hug_width != 'ignore': @@ -1084,15 +1082,6 @@ def _get_size_constraints(self): cn = (height >= height_hint) | resist_height push(cn) - # XXX: Should these two be conditionally created? - top = box.top - bottom = box.bottom - left = box.left - right = box.right - - push((top >= bottom + height) | 'strong') - push((right >= left + width) | 'strong') - return cns def _tools_items_changed(self): diff --git a/enable/constraints_container.py b/enable/constraints_container.py index f78cdba9c..014ceae9f 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -118,16 +118,6 @@ def __layout_manager_default(self): lm = LayoutManager() constraints = self.hard_constraints - box = self.layout_box - top = box.top - bottom = box.bottom - left = box.left - right = box.right - height = box.height - width = box.width - constraints.append((top >= bottom + height) | 'strong') - constraints.append((right >= left + width) | 'strong') - lm.initialize(constraints) return lm diff --git a/enable/layout/layout_box.py b/enable/layout/layout_box.py index 9fb84f80c..e105e2776 100644 --- a/enable/layout/layout_box.py +++ b/enable/layout/layout_box.py @@ -3,10 +3,20 @@ # All rights reserved. #------------------------------------------------------------------------------ +from collections import Callable +from operator import add, mul + from casuarius import ConstraintVariable -KNOWN_CONSTRAINTS = ('left', 'right', 'top', 'bottom', 'width', 'height') +KNOWN_CONSTRAINTS = ('left', 'right', 'top', 'bottom', 'width', 'height', + 'h_center', 'v_center') +SYMBOLIC_CONSTRAINTS = { + 'right': ['left', 'width', add], + 'top': ['bottom', 'height', add], + 'h_center': ['left', 'width', 0.5, mul, add], + 'v_center': ['bottom', 'height', 0.5, mul, add], +} class LayoutBox(object): @@ -49,11 +59,42 @@ def primitive(self, name): primitives = self._primitives if name in primitives: res = primitives[name] + elif name in SYMBOLIC_CONSTRAINTS: + res = primitives[name] = self._compose_symbolic(name) else: label = '{0}|{1}|{2}'.format(self._name, self._owner, name) res = primitives[name] = ConstraintVariable(label) return res + def _compose_symbolic(self, name): + """ Returns a casuarius constraint variable for the given symbolic + constraint name. + + Parameters + ---------- + name : str + The name of the symbolic constraint variable to return. + + """ + symbolic_desc = SYMBOLIC_CONSTRAINTS[name] + operands = [] + push = operands.append + pop = operands.pop + + # RPN evaluation + for part in symbolic_desc: + if isinstance(part, Callable): + op2 = pop() + op1 = pop() + push(part(op1, op2)) + elif isinstance(part, basestring): + push(self.primitive(part)) + elif isinstance(part, (float, int, long)): + push(part) + + assert len(operands) == 1 + return pop() + def __getattr__(self, name): """ Allow the primitive dictionary to act as an extension to the object's namespace. From dab21e940972475c0a091c69bafb0d51a4ce1103 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 12 Feb 2013 12:42:32 -0600 Subject: [PATCH 17/57] Refine the constraints demo a bit. It works! --- examples/enable/constraints_demo.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 185a3963a..3761c693e 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -21,10 +21,10 @@ class Demo(HasTraits): def _canvas_default(self): container = ConstraintsContainer(bounds=(500,500)) - container.add(Component(id="one", bgcolor="red", fixed_preferred_size=(250,250))) - container.add(Component(id="two", bgcolor="green", fixed_preferred_size=(250,250))) - container.add(Component(id="three", bgcolor="blue", fixed_preferred_size=(250,250))) - container.add(Component(id="four", bgcolor="black", fixed_preferred_size=(250,250))) + container.add(Component(id="one", bgcolor="red")) + container.add(Component(id="two", bgcolor="green")) + container.add(Component(id="three", bgcolor="blue")) + container.add(Component(id="four", bgcolor="black")) container.layout_constraints = [ "parent.top == one.top", @@ -39,6 +39,12 @@ def _canvas_default(self): "three.right == four.left", "one.bottom == three.top", "two.bottom == four.top", + "one.width == two.width", + "one.width == three.width", + "one.width == four.width", + "one.height == three.height", + "one.height == two.height", + "one.height == four.height", ] return container From 37e8c84d3b1fbc6a4026ded72fe2e9e4cfb4aaac Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 12 Feb 2013 13:57:55 -0600 Subject: [PATCH 18/57] Remove the __getattr__ nonsense from LayoutBox. --- enable/component.py | 18 ++++++++---------- enable/constraints_container.py | 24 ++++++++++++++++-------- enable/layout/layout_box.py | 11 ----------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/enable/component.py b/enable/component.py index 09edfe8d9..e619811e8 100644 --- a/enable/component.py +++ b/enable/component.py @@ -1046,13 +1046,11 @@ def _layout_box_default(self): def _get_hard_constraints(self): """ Generate the constraints which must always be applied. """ - box = self.layout_box - left = box.left - right = box.right - bottom = box.bottom - top = box.top - width = box.width - height = box.height + primitive = self.layout_box.primitive + left = primitive('left') + bottom = primitive('bottom') + width = primitive('width') + height = primitive('height') cns = [left >= 0, bottom >= 0, width >= 0, height >= 0] return cns @@ -1062,9 +1060,9 @@ def _get_size_constraints(self): cns = [] push = cns.append width_hint, height_hint = self.get_preferred_size() - box = self.layout_box - width = box.width - height = box.height + primitive = self.layout_box.primitive + width = primitive('width') + height = primitive('height') hug_width, hug_height = ('ignore', 'ignore') resist_width, resist_height = ('strong', 'strong') if width_hint >= 0: diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 014ceae9f..93bd81e52 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -47,17 +47,19 @@ def relayout(self): constraints modification. """ mgr_layout = self._layout_manager.layout - box = self.layout_box - width_var = box.width - height_var = box.height + primitive = self.layout_box.primitive + width_var = primitive('width') + height_var = primitive('height') width, height = self.bounds def layout(): for name, component in self._component_map.iteritems(): if name == self.id: continue - box = component.layout_box - component.position = (box.left.value, box.bottom.value) - component.bounds = (box.width.value, box.height.value) + prim = component.layout_box.primitive + component.position = (prim('left').value, + prim('bottom').value) + component.bounds = (prim('width').value, + prim('height').value) mgr_layout(layout, width_var, height_var, (width, height)) #------------------------------------------------------------------------ @@ -150,14 +152,20 @@ def _parse_constraint_strs(self, constraint_strs): return a list of casuarius constraint objects that can be added to this container's solver. """ + class PrimitiveGetter(object): + def __init__(self, obj): + self._object = obj + def __getattr__(self, name): + return self._object.primitive(name) + eval_dict = {} eval_dict['__builtins__'] = None components = self._component_map for key in components.iterkeys(): if key == self.id: - eval_dict[key] = components[key] + eval_dict[key] = PrimitiveGetter(components[key]) else: - eval_dict[key] = components[key].layout_box + eval_dict[key] = PrimitiveGetter(components[key].layout_box) constraints = [] push = constraints.append diff --git a/enable/layout/layout_box.py b/enable/layout/layout_box.py index e105e2776..14960ff03 100644 --- a/enable/layout/layout_box.py +++ b/enable/layout/layout_box.py @@ -9,8 +9,6 @@ from casuarius import ConstraintVariable -KNOWN_CONSTRAINTS = ('left', 'right', 'top', 'bottom', 'width', 'height', - 'h_center', 'v_center') SYMBOLIC_CONSTRAINTS = { 'right': ['left', 'width', add], 'top': ['bottom', 'height', add], @@ -95,12 +93,3 @@ def _compose_symbolic(self, name): assert len(operands) == 1 return pop() - def __getattr__(self, name): - """ Allow the primitive dictionary to act as an extension to the - object's namespace. - """ - if name in KNOWN_CONSTRAINTS: - return self.primitive(name) - - return super(LayoutBox, self).__getattr__(name) - From 2d09c45b7e252f8f305526f386c3a92f211046e3 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 12 Feb 2013 14:22:28 -0600 Subject: [PATCH 19/57] Add hug and resist control for a Component's size constraints. --- enable/component.py | 18 ++++++++++++++++-- examples/enable/constraints_demo.py | 9 +++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/enable/component.py b/enable/component.py index e619811e8..a14302bcc 100644 --- a/enable/component.py +++ b/enable/component.py @@ -16,10 +16,12 @@ from layout.layout_box import LayoutBox coordinate_delegate = Delegate("inner", modify=True) +ConstraintPolicyEnum = Enum('ignore', 'weak', 'medium', 'strong', 'required') 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 @@ -125,6 +127,18 @@ class Component(CoordinateBox, Interactor): # The list of size constraints to apply to the widget. size_constraints = Property + # How strongly a component hugs it's width hint. + hug_width = ConstraintPolicyEnum('strong') + + # How strongly a component hugs it's height hint. + hug_height = ConstraintPolicyEnum('strong') + + # How strongly a component resists clipping its contents. + resist_width = ConstraintPolicyEnum('strong') + + # How strongly a component resists clipping its contents. + resist_height = ConstraintPolicyEnum('strong') + #------------------------------------------------------------------------ # Overlays and underlays #------------------------------------------------------------------------ @@ -1063,8 +1077,8 @@ def _get_size_constraints(self): primitive = self.layout_box.primitive width = primitive('width') height = primitive('height') - hug_width, hug_height = ('ignore', 'ignore') - resist_width, resist_height = ('strong', 'strong') + 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 diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 3761c693e..d70d08584 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -21,10 +21,11 @@ class Demo(HasTraits): def _canvas_default(self): container = ConstraintsContainer(bounds=(500,500)) - container.add(Component(id="one", bgcolor="red")) - container.add(Component(id="two", bgcolor="green")) - container.add(Component(id="three", bgcolor="blue")) - container.add(Component(id="four", bgcolor="black")) + hugs = {'hug_width':'weak', 'hug_height':'weak'} + container.add(Component(id="one", bgcolor="red", **hugs)) + container.add(Component(id="two", bgcolor="green", **hugs)) + container.add(Component(id="three", bgcolor="blue", **hugs)) + container.add(Component(id="four", bgcolor="black", **hugs)) container.layout_constraints = [ "parent.top == one.top", From bf065fe850253c16f38c8bd7dba50d1129d1906d Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 12 Feb 2013 14:41:59 -0600 Subject: [PATCH 20/57] Make the constraints demos to match more closely. --- examples/enable/constraints_demo.enaml | 33 +++++++++++++------------- examples/enable/constraints_demo.py | 8 +++---- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/examples/enable/constraints_demo.enaml b/examples/enable/constraints_demo.enaml index 98ecc0aa7..e52e34fec 100644 --- a/examples/enable/constraints_demo.enaml +++ b/examples/enable/constraints_demo.enaml @@ -1,15 +1,8 @@ #------------------------------------------------------------------------------ -# Copyright (c) 2012, Enthought, Inc. +# Copyright (c) 2013, Enthought, Inc. # All rights reserved. #------------------------------------------------------------------------------ -""" An example which demonstrates the use of the `align` layout helper. - -In this example, we use the `align` layout helper to align various -constraints of the children of a container. The layout consists of -a Field pinned to the top of the Container. Below the Field are two -PushButtons each of which have their `left` boundary aligned. The -top PushButton is then aligned with the `h_center` of the Field. - +""" Implements the constraints_demo in Enaml """ from enaml.widgets.api import Window, Container, Html @@ -17,18 +10,24 @@ from enaml.widgets.api import Window, Container, Html enamldef Main(Window): Container: constraints = [ - top == one.top, - left == one.left, - top == two.top, - right == two.right, - bottom == three.bottom, - left == three.left, - bottom == four.bottom, - right == four.right, + self.top == one.top, + self.left == one.left, + self.top == two.top, + self.right == two.right, + self.bottom == three.bottom, + self.left == three.left, + self.bottom == four.bottom, + self.right == four.right, one.right == two.left, three.right == four.left, one.bottom == three.top, two.bottom == four.top, + one.width == two.width, + one.width == three.width, + one.width == four.width, + one.height == three.height, + one.height == two.height, + one.height == four.height, ] Html: id: one diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index d70d08584..e81eff085 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -22,10 +22,10 @@ def _canvas_default(self): container = ConstraintsContainer(bounds=(500,500)) hugs = {'hug_width':'weak', 'hug_height':'weak'} - container.add(Component(id="one", bgcolor="red", **hugs)) - container.add(Component(id="two", bgcolor="green", **hugs)) - container.add(Component(id="three", bgcolor="blue", **hugs)) - container.add(Component(id="four", bgcolor="black", **hugs)) + container.add(Component(id="one", bgcolor=0xFF0000, **hugs)) + container.add(Component(id="two", bgcolor=0x00FF00, **hugs)) + container.add(Component(id="three", bgcolor=0x0000FF, **hugs)) + container.add(Component(id="four", bgcolor=0x000000, **hugs)) container.layout_constraints = [ "parent.top == one.top", From 38f811da267c4ba4dbf17946d16667fab29edc0d Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 12 Feb 2013 15:36:46 -0600 Subject: [PATCH 21/57] Remove the container's layout box from its list of components. --- enable/constraints_container.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 93bd81e52..ced508570 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -52,9 +52,7 @@ def relayout(self): height_var = primitive('height') width, height = self.bounds def layout(): - for name, component in self._component_map.iteritems(): - if name == self.id: - continue + for component in self._component_map.itervalues(): prim = component.layout_box.primitive component.position = (prim('left').value, prim('bottom').value) @@ -88,7 +86,7 @@ def __component_map_default(self): """ The default component map should include this container so that name collisions are avoided. """ - return {self.id : self.layout_box} + return {} def __components_items_changed(self, event): """ Make sure components that are added can be used with constraints. @@ -139,6 +137,9 @@ def _check_and_add_components(self, components): 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._hard_constraints_map[key] = item.hard_constraints self._size_constraints_map[key] = item.size_constraints @@ -158,14 +159,10 @@ def __init__(self, obj): def __getattr__(self, name): return self._object.primitive(name) - eval_dict = {} + eval_dict = {self.id: PrimitiveGetter(self.layout_box)} eval_dict['__builtins__'] = None - components = self._component_map - for key in components.iterkeys(): - if key == self.id: - eval_dict[key] = PrimitiveGetter(components[key]) - else: - eval_dict[key] = PrimitiveGetter(components[key].layout_box) + for key, comp in self._component_map.iteritems(): + eval_dict[key] = PrimitiveGetter(comp.layout_box) constraints = [] push = constraints.append From cb3eb2cb81eb1568cbbf728d2b609d45e4f02c73 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 12 Feb 2013 15:44:33 -0600 Subject: [PATCH 22/57] Minor tweaks to ConstraintsContainer. --- enable/constraints_container.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index ced508570..b6ad25dd6 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -60,13 +60,16 @@ def layout(): prim('height').value) mgr_layout(layout, width_var, height_var, (width, height)) + self.invalidate_draw() + #------------------------------------------------------------------------ # 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.relayout() - self.invalidate_draw() def _layout_constraints_changed(self, name, old, new): """ Invalidate the layout when constraints change @@ -78,8 +81,9 @@ def _layout_constraints_changed(self, name, old, new): def _layout_constraints_items_changed(self, event): """ Invalidate the layout when constraints change """ + removed = self._parse_constraint_strs(event.removed) added = self._parse_constraint_strs(event.added) - self._layout_manager.replace_constraints(event.removed, added) + self._layout_manager.replace_constraints(removed, added) self.relayout() def __component_map_default(self): From a14685a6c54a8ef19d40f660f2d67b1c6414ac6d Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Wed, 13 Feb 2013 12:08:32 -0600 Subject: [PATCH 23/57] Remove new_component.py and new_abstract_component.py. They were never used by anything. --- enable/new_abstract_component.py | 178 ---------------- enable/new_component.py | 349 ------------------------------- 2 files changed, 527 deletions(-) delete mode 100644 enable/new_abstract_component.py delete mode 100644 enable/new_component.py 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. - - """ - From 06c17730a6f6364cbcbcc048da9918aa37f593fe Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Wed, 13 Feb 2013 12:18:34 -0600 Subject: [PATCH 24/57] Remove the old layout_controller bits from Container. No references to this code were found in Enable or Chaco. --- enable/abstract_layout_controller.py | 8 ---- enable/container.py | 55 ---------------------------- 2 files changed, 63 deletions(-) delete mode 100644 enable/abstract_layout_controller.py 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/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 From e93472cbf7a09b3277c3fad1d6a9f18018d0c8f2 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Wed, 13 Feb 2013 12:45:42 -0600 Subject: [PATCH 25/57] Push the constraints layout bits down into CoordinateBox. --- enable/component.py | 70 +---------------------------- enable/constraints_container.py | 26 +++++------ enable/coordinate_box.py | 79 ++++++++++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 83 deletions(-) diff --git a/enable/component.py b/enable/component.py index a14302bcc..efca0a09e 100644 --- a/enable/component.py +++ b/enable/component.py @@ -13,10 +13,9 @@ from coordinate_box import CoordinateBox from enable_traits import bounds_trait, coordinate_trait, LineStyle from interactor import Interactor -from layout.layout_box import LayoutBox + coordinate_delegate = Delegate("inner", modify=True) -ConstraintPolicyEnum = Enum('ignore', 'weak', 'medium', 'strong', 'required') DEFAULT_DRAWING_ORDER = ["background", "underlay", "mainlayer", "border", "overlay"] @@ -114,31 +113,6 @@ class Component(CoordinateBox, Interactor): # always be twice as wide as the former. fixed_preferred_size = Trait(None, None, bounds_trait) - #------------------------------------------------------------------------ - # Constraints-based layout - #------------------------------------------------------------------------ - - # The source for constraints variables for this component - layout_box = Instance(LayoutBox) - - # The list of hard constraints which must be applied to the widget. - hard_constraints = Property - - # The list of size constraints to apply to the widget. - size_constraints = Property - - # How strongly a component hugs it's width hint. - hug_width = ConstraintPolicyEnum('strong') - - # How strongly a component hugs it's height hint. - hug_height = ConstraintPolicyEnum('strong') - - # How strongly a component resists clipping its contents. - resist_width = ConstraintPolicyEnum('strong') - - # How strongly a component resists clipping its contents. - resist_height = ConstraintPolicyEnum('strong') - #------------------------------------------------------------------------ # Overlays and underlays #------------------------------------------------------------------------ @@ -1054,48 +1028,6 @@ def _set_active_tool(self, tool): def _get_layout_needed(self): return self._layout_needed - def _layout_box_default(self): - return LayoutBox(type(self).__name__, self.id) - - def _get_hard_constraints(self): - """ Generate the constraints which must always be applied. - """ - primitive = self.layout_box.primitive - left = primitive('left') - bottom = primitive('bottom') - width = primitive('width') - height = primitive('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 widget. - """ - cns = [] - push = cns.append - width_hint, height_hint = self.get_preferred_size() - primitive = self.layout_box.primitive - width = primitive('width') - height = primitive('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 - def _tools_items_changed(self): self.invalidate_and_redraw() diff --git a/enable/constraints_container.py b/enable/constraints_container.py index b6ad25dd6..58c93b89c 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -28,12 +28,12 @@ class ConstraintsContainer(Container): _component_map = Dict # All the hard constraints for child components - _hard_constraints_map = Dict(Str, List) - _hard_constraints = List + _child_hard_constraints_map = Dict(Str, List) + _child_hard_constraints = List # The size constraints for child components - _size_constraints_map = Dict(Str, List) - _size_constraints = List + _child_size_constraints_map = Dict(Str, List) + _child_size_constraints = List # The casuarius solver _layout_manager = Instance(LayoutManager) @@ -98,8 +98,8 @@ def __components_items_changed(self, event): # Remove stale components from the map for item in event.removed: key = item.id - del self._hard_constraints_map[key] - del self._size_constraints_map[key] + del self._child_hard_constraints_map[key] + del self._child_size_constraints_map[key] del self._component_map[key] # Check the added components @@ -110,8 +110,8 @@ def __components_changed(self, new): """ # Clear the component maps self._component_map = self.__component_map_default() - self._hard_constraints_map = {} - self._size_constraints_map = {} + self._child_hard_constraints_map = {} + self._child_size_constraints_map = {} # Check the new components self._check_and_add_components(new) @@ -121,7 +121,7 @@ def __layout_manager_default(self): """ lm = LayoutManager() - constraints = self.hard_constraints + constraints = self._hard_constraints lm.initialize(constraints) return lm @@ -145,8 +145,8 @@ def _check_and_add_components(self, components): msg = "Can't add a Component with the same id as its parent." raise ValueError(msg) - self._hard_constraints_map[key] = item.hard_constraints - self._size_constraints_map[key] = item.size_constraints + self._child_hard_constraints_map[key] = item._hard_constraints + self._child_size_constraints_map[key] = item._size_constraints self._component_map[key] = item # Update the fixed constraints @@ -181,8 +181,8 @@ def _update_fixed_constraints(self): """ old_cns, all_new_cns = [], [] for name in ('hard', 'size'): - map_attr = getattr(self, '_{0}_constraints_map'.format(name)) - list_name = '_{0}_constraints'.format(name) + map_attr = getattr(self, '_child_{0}_constraints_map'.format(name)) + list_name = '_child_{0}_constraints'.format(name) old_cns.extend(getattr(self, list_name)) new_cns = [] for item in map_attr.itervalues(): diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index ba5d626a7..e9f3e7de1 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -1,9 +1,15 @@ +from uuid import uuid4 + # Enthought library imports -from traits.api import HasTraits, Property +from traits.api import HasTraits, Enum, Instance, Property # Local, relative imports from enable_traits import bounds_trait, coordinate_trait +from layout.layout_box import LayoutBox + + +ConstraintPolicyEnum = Enum('ignore', 'weak', 'medium', 'strong', 'required') class CoordinateBox(HasTraits): @@ -43,6 +49,35 @@ class CoordinateBox(HasTraits): height = Property + #------------------------------------------------------------------------ + # Constraints-based layout + #------------------------------------------------------------------------ + + # The source for constraints variables for this component + layout_box = Instance(LayoutBox) + + # How strongly a component hugs it's width hint. + hug_width = ConstraintPolicyEnum('strong') + + # How strongly a component hugs it's height hint. + hug_height = ConstraintPolicyEnum('strong') + + # How strongly a component resists clipping its contents. + resist_width = ConstraintPolicyEnum('strong') + + # How strongly a component resists clipping its contents. + resist_height = ConstraintPolicyEnum('strong') + + # The list of hard constraints which must be applied to the widget. + _hard_constraints = Property + + # The list of size constraints to apply to the widget. + _size_constraints = Property + + #------------------------------------------------------------------------ + # Public methods + #------------------------------------------------------------------------ + def is_in(self, x, y): "Returns if the point x,y is in the box" p = self.position @@ -138,5 +173,47 @@ def _old_set_y2(self, val): self.bounds[1] = new_height return + def _layout_box_default(self): + return LayoutBox(type(self).__name__, uuid4().hex) + + def _get__hard_constraints(self): + """ Generate the constraints which must always be applied. + """ + primitive = self.layout_box.primitive + left = primitive('left') + bottom = primitive('bottom') + width = primitive('width') + height = primitive('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.bounds + primitive = self.layout_box.primitive + width = primitive('width') + height = primitive('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 + # EOF From 0a365905e87b8d64eabb3c3c99d809629f799f9f Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Wed, 13 Feb 2013 14:21:51 -0600 Subject: [PATCH 26/57] Stop using strings to specify constraints. This adds a `constraints` trait to CoordinateBox which acts as a namespace for all the constraint variables owned by the object. --- enable/constraints_container.py | 42 ++++------------------- enable/coordinate_box.py | 50 ++++++++++++++++++--------- examples/enable/constraints_demo.py | 53 +++++++++++++++-------------- 3 files changed, 67 insertions(+), 78 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 58c93b89c..06d9cc20d 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -47,17 +47,15 @@ def relayout(self): constraints modification. """ mgr_layout = self._layout_manager.layout - primitive = self.layout_box.primitive - width_var = primitive('width') - height_var = primitive('height') + constraints = self.constraints + width_var = constraints.width + height_var = constraints.height width, height = self.bounds def layout(): for component in self._component_map.itervalues(): - prim = component.layout_box.primitive - component.position = (prim('left').value, - prim('bottom').value) - component.bounds = (prim('width').value, - prim('height').value) + cns = component.constraints + component.position = (cns.left.value, cns.bottom.value) + component.bounds = (cns.width.value, cns.height.value) mgr_layout(layout, width_var, height_var, (width, height)) self.invalidate_draw() @@ -74,16 +72,13 @@ def _bounds_changed(self, old, new): def _layout_constraints_changed(self, name, old, new): """ Invalidate the layout when constraints change """ - new = self._parse_constraint_strs(new) self._layout_manager.replace_constraints(old, new) self.relayout() def _layout_constraints_items_changed(self, event): """ Invalidate the layout when constraints change """ - removed = self._parse_constraint_strs(event.removed) - added = self._parse_constraint_strs(event.added) - self._layout_manager.replace_constraints(removed, added) + self._layout_manager.replace_constraints(event.removed, event.added) self.relayout() def __component_map_default(self): @@ -152,29 +147,6 @@ def _check_and_add_components(self, components): # Update the fixed constraints self._update_fixed_constraints() - def _parse_constraint_strs(self, constraint_strs): - """ Given a list of strings with each describing a constraint, - return a list of casuarius constraint objects that can be added to this - container's solver. - """ - class PrimitiveGetter(object): - def __init__(self, obj): - self._object = obj - def __getattr__(self, name): - return self._object.primitive(name) - - eval_dict = {self.id: PrimitiveGetter(self.layout_box)} - eval_dict['__builtins__'] = None - for key, comp in self._component_map.iteritems(): - eval_dict[key] = PrimitiveGetter(comp.layout_box) - - constraints = [] - push = constraints.append - for cns_str in constraint_strs: - push(eval(cns_str, eval_dict)) - - return constraints - def _update_fixed_constraints(self): """ Resolve the differences between the list of constraints and the map of child component constraints for both types of fixed constraints. diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index e9f3e7de1..f4f6ae73e 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -12,6 +12,16 @@ ConstraintPolicyEnum = Enum('ignore', 'weak', 'medium', 'strong', 'required') +class _ConstraintsProxy(object): + """ A simple wrapper for accessing a LayoutBox's constraints as attributes. + """ + def __init__(self, layout_box): + self._layout_box = layout_box + + def __getattr__(self, name): + return self._layout_box.primitive(name) + + class CoordinateBox(HasTraits): """ Represents a box in screen space, and provides convenience properties to @@ -53,25 +63,28 @@ class CoordinateBox(HasTraits): # Constraints-based layout #------------------------------------------------------------------------ - # The source for constraints variables for this component - layout_box = Instance(LayoutBox) + # A namespace containing the constraints for this + constraints = Instance(_ConstraintsProxy) - # How strongly a component hugs it's width hint. + # How strongly a layout box hugs it's width hint. hug_width = ConstraintPolicyEnum('strong') - # How strongly a component hugs it's height hint. + # How strongly a layout box hugs it's height hint. hug_height = ConstraintPolicyEnum('strong') - # How strongly a component resists clipping its contents. + # How strongly a layout box resists clipping its contents. resist_width = ConstraintPolicyEnum('strong') - # How strongly a component resists clipping its contents. + # How strongly a layout box resists clipping its contents. resist_height = ConstraintPolicyEnum('strong') - # The list of hard constraints which must be applied to the widget. + # The source for constraints variables for this object + _layout_box = Instance(LayoutBox) + + # The list of hard constraints which must be applied to the object. _hard_constraints = Property - # The list of size constraints to apply to the widget. + # The list of size constraints to apply to the object. _size_constraints = Property #------------------------------------------------------------------------ @@ -173,17 +186,20 @@ def _old_set_y2(self, val): self.bounds[1] = new_height return - def _layout_box_default(self): + def _constraints_default(self): + return _ConstraintsProxy(self._layout_box) + + def __layout_box_default(self): return LayoutBox(type(self).__name__, uuid4().hex) def _get__hard_constraints(self): """ Generate the constraints which must always be applied. """ - primitive = self.layout_box.primitive - left = primitive('left') - bottom = primitive('bottom') - width = primitive('width') - height = primitive('height') + constraints = self.constraints + left = constraints.left + bottom = constraints.bottom + width = constraints.width + height = constraints.height cns = [left >= 0, bottom >= 0, width >= 0, height >= 0] return cns @@ -193,9 +209,9 @@ def _get__size_constraints(self): cns = [] push = cns.append width_hint, height_hint = self.bounds - primitive = self.layout_box.primitive - width = primitive('width') - height = primitive('height') + constraints = self.constraints + width = constraints.width + height = constraints.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: diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index e81eff085..12b920f80 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -19,36 +19,37 @@ class Demo(HasTraits): ) def _canvas_default(self): - container = ConstraintsContainer(bounds=(500,500)) + parent = ConstraintsContainer(bounds=(500,500)) hugs = {'hug_width':'weak', 'hug_height':'weak'} - container.add(Component(id="one", bgcolor=0xFF0000, **hugs)) - container.add(Component(id="two", bgcolor=0x00FF00, **hugs)) - container.add(Component(id="three", bgcolor=0x0000FF, **hugs)) - container.add(Component(id="four", bgcolor=0x000000, **hugs)) - - container.layout_constraints = [ - "parent.top == one.top", - "parent.left == one.left", - "parent.top == two.top", - "parent.right == two.right", - "parent.bottom == three.bottom", - "parent.left == three.left", - "parent.bottom == four.bottom", - "parent.right == four.right", - "one.right == two.left", - "three.right == four.left", - "one.bottom == three.top", - "two.bottom == four.top", - "one.width == two.width", - "one.width == three.width", - "one.width == four.width", - "one.height == three.height", - "one.height == two.height", - "one.height == four.height", + one = Component(id="one", bgcolor=0xFF0000, **hugs) + two = Component(id="two", bgcolor=0x00FF00, **hugs) + three = Component(id="three", bgcolor=0x0000FF, **hugs) + four = Component(id="four", bgcolor=0x000000, **hugs) + + parent.add(one, two, three, four) + parent.layout_constraints = [ + parent.constraints.top == one.constraints.top, + parent.constraints.left == one.constraints.left, + parent.constraints.top == two.constraints.top, + parent.constraints.right == two.constraints.right, + parent.constraints.bottom == three.constraints.bottom, + parent.constraints.left == three.constraints.left, + parent.constraints.bottom == four.constraints.bottom, + parent.constraints.right == four.constraints.right, + one.constraints.right == two.constraints.left, + three.constraints.right == four.constraints.left, + one.constraints.bottom == three.constraints.top, + two.constraints.bottom == four.constraints.top, + one.constraints.width == two.constraints.width, + one.constraints.width == three.constraints.width, + one.constraints.width == four.constraints.width, + one.constraints.height == three.constraints.height, + one.constraints.height == two.constraints.height, + one.constraints.height == four.constraints.height, ] - return container + return parent if __name__ == "__main__": From 98a8c0aa32ee6eec93ecae4a3e74fd420308a121 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Wed, 13 Feb 2013 14:47:03 -0600 Subject: [PATCH 27/57] Remove more unused code. --- enable/component_layout_category.py | 33 ------ enable/component_render_category.py | 175 ---------------------------- 2 files changed, 208 deletions(-) delete mode 100644 enable/component_layout_category.py delete mode 100644 enable/component_render_category.py 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 From dc7ddd363d4d4e1e0f49279fff6f7f8463a994fe Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Wed, 13 Feb 2013 17:55:57 -0600 Subject: [PATCH 28/57] Allow the layout_constraints trait to be a callable or a list. --- enable/constraints_container.py | 49 +++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 06d9cc20d..5eb1208e2 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -4,7 +4,7 @@ #------------------------------------------------------------------------------ # traits imports -from traits.api import Dict, Instance, List, Str +from traits.api import Callable, Dict, Either, Instance, List, Str # local imports from container import Container @@ -22,7 +22,13 @@ class ConstraintsContainer(Container): id = "parent" # The layout constraints for this container. - layout_constraints = List + # 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 copy of the layout constraints used when the constraints change + _layout_constraints = List # A dictionary of components added to this container _component_map = Dict @@ -42,6 +48,12 @@ class ConstraintsContainer(Container): # Public methods #------------------------------------------------------------------------ + def refresh_layout_constraints(self): + """ Explicitly regenerate the container's constraints and refresh the + layout. + """ + self._layout_constraints_changed() + def relayout(self): """ Re-run the constraints solver in response to a resize or constraints modification. @@ -69,23 +81,25 @@ def _bounds_changed(self, old, new): super(ConstraintsContainer, self)._bounds_changed(old, new) self.relayout() - def _layout_constraints_changed(self, name, old, new): - """ Invalidate the layout when constraints change + def _layout_constraints_changed(self): + """ React to changes of the user controlled constraints. """ - self._layout_manager.replace_constraints(old, new) - self.relayout() + if self.layout_constraints is None: + return - def _layout_constraints_items_changed(self, event): - """ Invalidate the layout when constraints change - """ - self._layout_manager.replace_constraints(event.removed, event.added) - self.relayout() + if callable(self.layout_constraints): + new = self.layout_constraints(self) + else: + new = self.layout_constraints + + # Update the private constraints list. This will trigger the relayout. + self._layout_constraints = new - def __component_map_default(self): - """ The default component map should include this container so that - name collisions are avoided. + def __layout_constraints_changed(self, name, old, new): + """ Invalidate the layout when the private constraints list changes. """ - return {} + self._layout_manager.replace_constraints(old, new) + self.relayout() def __components_items_changed(self, event): """ Make sure components that are added can be used with constraints. @@ -104,7 +118,7 @@ def __components_changed(self, new): """ Make sure components that are added can be used with constraints. """ # Clear the component maps - self._component_map = self.__component_map_default() + self._component_map = {} self._child_hard_constraints_map = {} self._child_size_constraints_map = {} @@ -163,4 +177,5 @@ def _update_fixed_constraints(self): setattr(self, list_name, new_cns) self._layout_manager.replace_constraints(old_cns, all_new_cns) - + # Possibly regenerate the user-specified constraints + self.refresh_layout_constraints() From f5afcf4b003bd98b96f403a970929792744387cf Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Thu, 14 Feb 2013 10:09:21 -0600 Subject: [PATCH 29/57] Replace LayoutBox with ConstraintsNamespace. --- enable/coordinate_box.py | 34 ++++----- enable/layout/constraints_namespace.py | 74 ++++++++++++++++++++ enable/layout/layout_box.py | 95 -------------------------- 3 files changed, 92 insertions(+), 111 deletions(-) create mode 100644 enable/layout/constraints_namespace.py delete mode 100644 enable/layout/layout_box.py diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index f4f6ae73e..bdcd99c4d 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -6,20 +6,26 @@ # Local, relative imports from enable_traits import bounds_trait, coordinate_trait -from layout.layout_box import LayoutBox +from layout.constraints_namespace import ConstraintsNamespace ConstraintPolicyEnum = Enum('ignore', 'weak', 'medium', 'strong', 'required') -class _ConstraintsProxy(object): - """ A simple wrapper for accessing a LayoutBox's constraints as attributes. +def add_symbolic_constraints(namespace): + """ Add constraints to a namespace that are LinearExpressions of basic + constraints. + """ - def __init__(self, layout_box): - self._layout_box = layout_box + bottom = namespace.bottom + left = namespace.left + width = namespace.width + height = namespace.height - def __getattr__(self, name): - return self._layout_box.primitive(name) + namespace.right = left + width + namespace.top = bottom + height + namespace.h_center = left + 0.5 * width + namespace.v_center = bottom + 0.5 * height class CoordinateBox(HasTraits): @@ -63,8 +69,8 @@ class CoordinateBox(HasTraits): # Constraints-based layout #------------------------------------------------------------------------ - # A namespace containing the constraints for this - constraints = Instance(_ConstraintsProxy) + # A namespace containing the constraints for this CoordinateBox + constraints = Instance(ConstraintsNamespace) # How strongly a layout box hugs it's width hint. hug_width = ConstraintPolicyEnum('strong') @@ -78,9 +84,6 @@ class CoordinateBox(HasTraits): # How strongly a layout box resists clipping its contents. resist_height = ConstraintPolicyEnum('strong') - # The source for constraints variables for this object - _layout_box = Instance(LayoutBox) - # The list of hard constraints which must be applied to the object. _hard_constraints = Property @@ -187,10 +190,9 @@ def _old_set_y2(self, val): return def _constraints_default(self): - return _ConstraintsProxy(self._layout_box) - - def __layout_box_default(self): - return LayoutBox(type(self).__name__, uuid4().hex) + cns_names = ConstraintsNamespace(type(self).__name__, uuid4().hex) + add_symbolic_constraints(cns_names) + return cns_names def _get__hard_constraints(self): """ Generate the constraints which must always be applied. 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/layout_box.py b/enable/layout/layout_box.py deleted file mode 100644 index 14960ff03..000000000 --- a/enable/layout/layout_box.py +++ /dev/null @@ -1,95 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2013, Enthought, Inc. -# All rights reserved. -#------------------------------------------------------------------------------ - -from collections import Callable -from operator import add, mul - -from casuarius import ConstraintVariable - - -SYMBOLIC_CONSTRAINTS = { - 'right': ['left', 'width', add], - 'top': ['bottom', 'height', add], - 'h_center': ['left', 'width', 0.5, mul, add], - 'v_center': ['bottom', 'height', 0.5, mul, add], -} - - -class LayoutBox(object): - """ A class which encapsulates a layout box using 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 LayoutBox. - - Parameters - ---------- - name : str - A name to use in the label for the constraint variables in - this layout box. - - owner : str - The owner id to use in the label for the constraint variables - in this layout box. - - """ - self._name = name - self._owner = owner - self._primitives = {} - - def primitive(self, name): - """ Returns a primitive casuarius constraint variable for the - given name. - - Parameters - ---------- - name : str - The name of the constraint variable to return. - - """ - primitives = self._primitives - if name in primitives: - res = primitives[name] - elif name in SYMBOLIC_CONSTRAINTS: - res = primitives[name] = self._compose_symbolic(name) - else: - label = '{0}|{1}|{2}'.format(self._name, self._owner, name) - res = primitives[name] = ConstraintVariable(label) - return res - - def _compose_symbolic(self, name): - """ Returns a casuarius constraint variable for the given symbolic - constraint name. - - Parameters - ---------- - name : str - The name of the symbolic constraint variable to return. - - """ - symbolic_desc = SYMBOLIC_CONSTRAINTS[name] - operands = [] - push = operands.append - pop = operands.pop - - # RPN evaluation - for part in symbolic_desc: - if isinstance(part, Callable): - op2 = pop() - op1 = pop() - push(part(op1, op2)) - elif isinstance(part, basestring): - push(self.primitive(part)) - elif isinstance(part, (float, int, long)): - push(part) - - assert len(operands) == 1 - return pop() - From 698aac7a9f4f8f1292b71cf076514c1852a6c19a Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Thu, 14 Feb 2013 10:26:24 -0600 Subject: [PATCH 30/57] Add a contents box to the ConstraintsContainer. --- enable/constraints_container.py | 23 ++++++++++++++++++++--- enable/coordinate_box.py | 4 ++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 5eb1208e2..9b068a771 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -17,8 +17,7 @@ class ConstraintsContainer(Container): """ - # The ID for this component. This ID can be used by the layout constraints - # when referencing the container. + # The ID for this component. id = "parent" # The layout constraints for this container. @@ -130,7 +129,7 @@ def __layout_manager_default(self): """ lm = LayoutManager() - constraints = self._hard_constraints + constraints = self._hard_constraints + self._content_box_constraints() lm.initialize(constraints) return lm @@ -161,6 +160,24 @@ def _check_and_add_components(self, components): # Update the fixed constraints self._update_fixed_constraints() + def _content_box_constraints(self): + """ Return the constraints which define the content box of this + container. + + """ + cns = self.constraints + contents_left = cns.contents_left + contents_right = cns.contents_right + contents_top = cns.contents_top + contents_bottom = cns.contents_bottom + cns.contents_width = contents_right - contents_left + cns.contents_height = contents_bottom - contents_top + cns.contents_v_center = contents_top + cns.contents_height / 2.0 + cns.contents_h_center = contents_left + cns.contents_width / 2.0 + + return [contents_left >= 0, contents_right >= 0, + contents_top >= 0, contents_bottom >= 0] + def _update_fixed_constraints(self): """ Resolve the differences between the list of constraints and the map of child component constraints for both types of fixed constraints. diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index bdcd99c4d..8508cd9fb 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -24,8 +24,8 @@ def add_symbolic_constraints(namespace): namespace.right = left + width namespace.top = bottom + height - namespace.h_center = left + 0.5 * width - namespace.v_center = bottom + 0.5 * height + namespace.h_center = left + width / 2.0 + namespace.v_center = bottom + height / 2.0 class CoordinateBox(HasTraits): From 1cd642fd90e8efef525b618b1e8cf8d1ac06030a Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Thu, 14 Feb 2013 11:40:36 -0600 Subject: [PATCH 31/57] Copy layout helpers from Enaml. Some changes were needed to make it work with the way constraints are currently working in Enable. --- enable/coordinate_box.py | 19 +- enable/layout/ab_constrainable.py | 18 + enable/layout/geometry.py | 388 +++++++++ enable/layout/layout_helpers.py | 1278 +++++++++++++++++++++++++++++ enable/layout/utils.py | 24 + 5 files changed, 1710 insertions(+), 17 deletions(-) create mode 100644 enable/layout/ab_constrainable.py create mode 100644 enable/layout/geometry.py create mode 100644 enable/layout/layout_helpers.py create mode 100644 enable/layout/utils.py diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index 8508cd9fb..915130e0b 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -7,25 +7,10 @@ # Local, relative imports from enable_traits import bounds_trait, coordinate_trait from layout.constraints_namespace import ConstraintsNamespace +from layout.utils import add_symbolic_constraints, STRENGTHS -ConstraintPolicyEnum = Enum('ignore', 'weak', 'medium', 'strong', 'required') - - -def add_symbolic_constraints(namespace): - """ Add constraints to a namespace that are LinearExpressions of basic - constraints. - - """ - bottom = namespace.bottom - left = namespace.left - width = namespace.width - height = namespace.height - - namespace.right = left + width - namespace.top = bottom + height - namespace.h_center = left + width / 2.0 - namespace.v_center = bottom + height / 2.0 +ConstraintPolicyEnum = Enum('ignore', *STRENGTHS) class CoordinateBox(HasTraits): diff --git a/enable/layout/ab_constrainable.py b/enable/layout/ab_constrainable.py new file mode 100644 index 000000000..40362c460 --- /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`, + `width`, `height`, `v_center` and `h_center` attributes which are + `LinearSymbolic` instances. + + """ + __metaclass__ = ABCMeta + 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..25cd0381f --- /dev/null +++ b/enable/layout/layout_helpers.py @@ -0,0 +1,1278 @@ +#------------------------------------------------------------------------------ +# 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 +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: + 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 + 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) + width = property(lambda self: self._namespace.width) + height = property(lambda self: self._namespace.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 + top_items = (self.top, EqSpacer(margins.top), row_vars[0]) + bottom_items = (row_vars[-1], EqSpacer(margins.bottom), self.bottom) + left_items = (self.left, EqSpacer(margins.left), col_vars[0]) + right_items = (col_vars[-1], EqSpacer(margins.right), self.right) + helpers = [ + AbutmentHelper('vertical', *top_items), + AbutmentHelper('vertical', *bottom_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': ('bottom', 'top'), + } + + @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 + 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/utils.py b/enable/layout/utils.py new file mode 100644 index 000000000..2a1719e4c --- /dev/null +++ b/enable/layout/utils.py @@ -0,0 +1,24 @@ +#------------------------------------------------------------------------------ +# 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.width + height = namespace.height + + namespace.right = left + width + namespace.top = bottom + height + namespace.h_center = left + width / 2.0 + namespace.v_center = bottom + height / 2.0 + From a1aba8500f35e0c16935be8c8d99507df82a98c0 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Thu, 14 Feb 2013 15:16:45 -0600 Subject: [PATCH 32/57] Fix some basic problems with the layout helpers. - Implement contents constraints in the container - Expand layout helpers in the user-specified constraints --- enable/constraints_container.py | 12 ++++++++++-- enable/coordinate_box.py | 2 +- enable/layout/layout_helpers.py | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 9b068a771..8b427927a 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -8,6 +8,7 @@ # local imports from container import Container +from layout.layout_helpers import expand_constraints from layout.layout_manager import LayoutManager @@ -92,7 +93,9 @@ def _layout_constraints_changed(self): new = self.layout_constraints # Update the private constraints list. This will trigger the relayout. - self._layout_constraints = new + expand = expand_constraints + constraints = self.constraints + self._layout_constraints = [cns for cns in expand(constraints, new)] def __layout_constraints_changed(self, name, old, new): """ Invalidate the layout when the private constraints list changes. @@ -176,7 +179,12 @@ def _content_box_constraints(self): cns.contents_h_center = contents_left + cns.contents_width / 2.0 return [contents_left >= 0, contents_right >= 0, - contents_top >= 0, contents_bottom >= 0] + contents_top >= 0, contents_bottom >= 0, + contents_left == cns.left + self.padding_left, + contents_bottom == cns.bottom + self.padding_bottom, + contents_right == cns.right - self.padding_right, + contents_top == cns.top - self.padding_top, + ] def _update_fixed_constraints(self): """ Resolve the differences between the list of constraints and the diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index 915130e0b..40c39927b 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -175,7 +175,7 @@ def _old_set_y2(self, val): return def _constraints_default(self): - cns_names = ConstraintsNamespace(type(self).__name__, uuid4().hex) + cns_names = ConstraintsNamespace(type(self).__name__, uuid4().hex[:8]) add_symbolic_constraints(cns_names) return cns_names diff --git a/enable/layout/layout_helpers.py b/enable/layout/layout_helpers.py index 25cd0381f..660570ba7 100644 --- a/enable/layout/layout_helpers.py +++ b/enable/layout/layout_helpers.py @@ -340,7 +340,7 @@ def __init__(self, name): """ super(BoxHelper, self).__init__() - owner = uuid4().hex + owner = uuid4().hex[:8] self.constraints_id = name + '|' + owner self._namespace = ConstraintsNamespace(name, owner) add_symbolic_constraints(self._namespace) @@ -356,6 +356,7 @@ def __init__(self, name): ABConstrainable.register(BoxHelper) +ABConstrainable.register(ConstraintsNamespace) class LinearBoxHelper(BoxHelper): From 9e671a9d4a878e2ba419f5f9b91f335bb2d3bd00 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Thu, 14 Feb 2013 15:56:23 -0600 Subject: [PATCH 33/57] Get some of the layout helpers working, more or less... --- enable/constraints_container.py | 6 +++--- examples/enable/constraints_demo.enaml | 21 +++------------------ examples/enable/constraints_demo.py | 24 ++++++------------------ 3 files changed, 12 insertions(+), 39 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 8b427927a..55e817a28 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -173,14 +173,14 @@ def _content_box_constraints(self): contents_right = cns.contents_right contents_top = cns.contents_top contents_bottom = cns.contents_bottom + + # Add these to the namespace, but don't use them cns.contents_width = contents_right - contents_left cns.contents_height = contents_bottom - contents_top cns.contents_v_center = contents_top + cns.contents_height / 2.0 cns.contents_h_center = contents_left + cns.contents_width / 2.0 - return [contents_left >= 0, contents_right >= 0, - contents_top >= 0, contents_bottom >= 0, - contents_left == cns.left + self.padding_left, + return [contents_left == cns.left + self.padding_left, contents_bottom == cns.bottom + self.padding_bottom, contents_right == cns.right - self.padding_right, contents_top == cns.top - self.padding_top, diff --git a/examples/enable/constraints_demo.enaml b/examples/enable/constraints_demo.enaml index e52e34fec..f7167ba96 100644 --- a/examples/enable/constraints_demo.enaml +++ b/examples/enable/constraints_demo.enaml @@ -4,30 +4,15 @@ #------------------------------------------------------------------------------ """ 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 = [ - self.top == one.top, - self.left == one.left, - self.top == two.top, - self.right == two.right, - self.bottom == three.bottom, - self.left == three.left, - self.bottom == four.bottom, - self.right == four.right, - one.right == two.left, - three.right == four.left, - one.bottom == three.top, - two.bottom == four.top, - one.width == two.width, - one.width == three.width, - one.width == four.width, - one.height == three.height, - one.height == two.height, - one.height == four.height, + hbox(one, two, three, four), + align('width', one, two, three, four), ] Html: id: one diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 12b920f80..6c66d5e4e 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -1,5 +1,6 @@ from enable.api import Component, ComponentEditor, ConstraintsContainer +from enable.layout.layout_helpers import hbox, align, grid from traits.api import HasTraits, Instance from traitsui.api import Item, View @@ -29,24 +30,11 @@ def _canvas_default(self): parent.add(one, two, three, four) parent.layout_constraints = [ - parent.constraints.top == one.constraints.top, - parent.constraints.left == one.constraints.left, - parent.constraints.top == two.constraints.top, - parent.constraints.right == two.constraints.right, - parent.constraints.bottom == three.constraints.bottom, - parent.constraints.left == three.constraints.left, - parent.constraints.bottom == four.constraints.bottom, - parent.constraints.right == four.constraints.right, - one.constraints.right == two.constraints.left, - three.constraints.right == four.constraints.left, - one.constraints.bottom == three.constraints.top, - two.constraints.bottom == four.constraints.top, - one.constraints.width == two.constraints.width, - one.constraints.width == three.constraints.width, - one.constraints.width == four.constraints.width, - one.constraints.height == three.constraints.height, - one.constraints.height == two.constraints.height, - one.constraints.height == four.constraints.height, + hbox(one.constraints, two.constraints, + three.constraints, four.constraints), + align('width', one.constraints, two.constraints, + three.constraints, four.constraints), + ] return parent From b734c11f5259c58200034510b931704f4f06f567 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 15 Feb 2013 09:45:05 -0600 Subject: [PATCH 34/57] Fix the definition of some contents traits. --- enable/constraints_container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 55e817a28..fba794253 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -176,8 +176,8 @@ def _content_box_constraints(self): # Add these to the namespace, but don't use them cns.contents_width = contents_right - contents_left - cns.contents_height = contents_bottom - contents_top - cns.contents_v_center = contents_top + cns.contents_height / 2.0 + cns.contents_height = contents_top - contents_bottom + cns.contents_v_center = contents_bottom + cns.contents_height / 2.0 cns.contents_h_center = contents_left + cns.contents_width / 2.0 return [contents_left == cns.left + self.padding_left, From 173cfe41d4bf338934005a00012a0c8f9995d0e6 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 15 Feb 2013 11:50:09 -0600 Subject: [PATCH 35/57] Steal some of Robert's enaml debugging code for visualizing constraints. --- enable/constraints_container.py | 29 ++++++- enable/layout/debug_constraints.py | 117 +++++++++++++++++++++++++++++ enable/layout/layout_manager.py | 12 ++- 3 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 enable/layout/debug_constraints.py diff --git a/enable/constraints_container.py b/enable/constraints_container.py index fba794253..51e4d7476 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -4,10 +4,11 @@ #------------------------------------------------------------------------------ # traits imports -from traits.api import Callable, Dict, Either, Instance, List, Str +from traits.api import Bool, Callable, Dict, Either, Instance, List, Str # local imports from container import Container +from layout.debug_constraints import DebugConstraintsOverlay from layout.layout_helpers import expand_constraints from layout.layout_manager import LayoutManager @@ -44,6 +45,25 @@ class ConstraintsContainer(Container): # The casuarius solver _layout_manager = Instance(LayoutManager) + + #------------------------------------------------------------------------ + # Debugging bits + #------------------------------------------------------------------------ + + # Whether or not debugging info should be shown. + debug = Bool(False) + + # The overlay that draws the debugging info + _debug_overlay = Instance(DebugConstraintsOverlay) + + def __init__(self, **traits): + super(ConstraintsContainer, self).__init__(**traits) + + if self.debug: + dbg = DebugConstraintsOverlay() + self.overlays.append(dbg) + self._debug_overlay = dbg + #------------------------------------------------------------------------ # Public methods #------------------------------------------------------------------------ @@ -68,6 +88,9 @@ def layout(): cns = component.constraints component.position = (cns.left.value, cns.bottom.value) component.bounds = (cns.width.value, cns.height.value) + if self._debug_overlay: + layout_mgr = self._layout_manager + self._debug_overlay.update_from_constraints(layout_mgr) mgr_layout(layout, width_var, height_var, (width, height)) self.invalidate_draw() @@ -101,6 +124,8 @@ def __layout_constraints_changed(self, name, old, new): """ Invalidate the layout when the private constraints list changes. """ self._layout_manager.replace_constraints(old, new) + if self.debug: + self._debug_overlay.selected_constraints = new self.relayout() def __components_items_changed(self, event): @@ -130,7 +155,7 @@ def __components_changed(self, new): def __layout_manager_default(self): """ Create the layout manager. """ - lm = LayoutManager() + lm = LayoutManager(debug=self.debug) constraints = self._hard_constraints + self._content_box_constraints() lm.initialize(constraints) diff --git a/enable/layout/debug_constraints.py b/enable/layout/debug_constraints.py new file mode 100644 index 000000000..152e68955 --- /dev/null +++ b/enable/layout/debug_constraints.py @@ -0,0 +1,117 @@ +from collections import defaultdict + +from enable.abstract_overlay import AbstractOverlay +from enable.colors import ColorTrait +from enable.enable_traits import LineStyle +from traits.api import Any, Bool, Float, HasTraits, Instance, List, Property + + +class Coords(HasTraits): + """ Simple holder of box-related data. + + """ + top = Float() + bottom = Float() + left = Float() + right = Float() + width = Float() + height = Float() + + v_center = Property() + _v_center = Any() + def _get_v_center(self): + if self._v_center is None: + return self.bottom + 0.5 * self.height + else: + return self._v_center + def _set_v_center(self, value): + self._v_center = value + + h_center = Property() + _h_center = Any() + def _get_h_center(self): + if self._h_center is None: + return self.left + 0.5 * self.width + else: + return self._h_center + def _set_h_center(self, value): + self._h_center = value + + +class DebugConstraintsOverlay(AbstractOverlay): + """ Highlight the selected constraints on the outline view. + + """ + + selected_constraints = List() + + # Map from box name to Coords. + boxes = Any() + + # Style options for the lines. + term_color = ColorTrait('lightblue') + term_line_style = LineStyle('solid') + + def update_from_constraints(self, layout_mgr): + """ Update the constraints boxes. + + """ + self.boxes = defaultdict(Coords) + if layout_mgr is not None: + for constraint in layout_mgr._constraints: + for expr in (constraint.lhs, constraint.rhs): + for term in expr.terms: + name, attr = self.split_var_name(term.var.name) + setattr(self.boxes[name], attr, term.var.value) + self.request_redraw() + + def split_var_name(self, var_name): + class_name, hexid, attr = var_name.rsplit('|', 2) + name = '{}|{}'.format(class_name, hexid) + return name, attr + + def overlay(self, other_component, gc, view_bounds=None, mode="normal"): + """ Draws this component overlaid on another component. + + """ + if len(self.selected_constraints) == 0: + return + with gc: + gc.set_stroke_color(self.term_color_) + gc.set_line_dash(self.term_line_style_) + gc.set_line_width(3) + term_attrs = set() + for constraint in self.selected_constraints: + for expr in (constraint.lhs, constraint.rhs): + for term in expr.terms: + term_attrs.add(self.split_var_name(term.var.name)) + for name, attr in sorted(term_attrs): + box = self.boxes[name] + if attr == 'top': + self.hline(gc, box.left, box.top, box.width) + elif attr == 'left': + self.vline(gc, box.left, box.bottom, box.height) + elif attr == 'width': + self.hline(gc, box.left, box.v_center, box.width) + elif attr == 'height': + self.vline(gc, box.h_center, box.top, box.height) + elif attr == 'bottom': + self.hline(gc, box.left, box.bottom, box.right - box.left) + elif attr == 'right': + self.vline(gc, box.right, box.bottom, box.top - box.bottom) + gc.stroke_path() + + def vline(self, gc, x, y0, length): + """ Draw a vertical line. + + """ + gc.move_to(x, y0) + gc.line_to(x, y0+length) + + def hline(self, gc, x0, y, length): + """ Draw a horizontal line. + + """ + gc.move_to(x0, y) + gc.line_to(x0+length, y) + diff --git a/enable/layout/layout_manager.py b/enable/layout/layout_manager.py index 33778d5d4..bcd81480e 100644 --- a/enable/layout/layout_manager.py +++ b/enable/layout/layout_manager.py @@ -10,11 +10,14 @@ class LayoutManager(object): of constraints. """ - def __init__(self): + def __init__(self, debug=False): self._solver = Solver(autosolve=False) self._initialized = False self._running = False + self._debug = debug + self._constraints = None + def initialize(self, constraints): """ Initialize the solver with the given constraints. @@ -25,6 +28,9 @@ def initialize(self, constraints): solvers. """ + if self._debug: + self._constraints = set(constraints) + if self._initialized: raise RuntimeError('Solver already initialized') solver = self._solver @@ -47,6 +53,10 @@ def replace_constraints(self, old_cns, new_cns): The list of casuarius constraints to add to the solver. """ + if self._debug: + self._constraints.difference_update(set(old_cns)) + self._constraints.update(set(new_cns)) + if not self._initialized: raise RuntimeError('Solver not yet initialized') solver = self._solver From f9b277826bd44f35d2c148c6917fe036083f7fc5 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 15 Feb 2013 12:18:04 -0600 Subject: [PATCH 36/57] Flip the debug flag on the constraints demo. --- examples/enable/constraints_demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 6c66d5e4e..753f963f2 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -20,7 +20,7 @@ class Demo(HasTraits): ) def _canvas_default(self): - parent = ConstraintsContainer(bounds=(500,500)) + parent = ConstraintsContainer(bounds=(500,500), debug=True) hugs = {'hug_width':'weak', 'hug_height':'weak'} one = Component(id="one", bgcolor=0xFF0000, **hugs) From e8b78f198732ada6ba4e6aa657cdff8d88ca6a29 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 15 Feb 2013 14:11:18 -0600 Subject: [PATCH 37/57] Add a constraint list with selection to the constraints demo. This should help with the layout helper debugging... --- enable/constraints_container.py | 2 -- enable/layout/debug_constraints.py | 4 ++- examples/enable/constraints_demo.py | 51 +++++++++++++++++++++++++---- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 51e4d7476..32cd5ff5c 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -124,8 +124,6 @@ def __layout_constraints_changed(self, name, old, new): """ Invalidate the layout when the private constraints list changes. """ self._layout_manager.replace_constraints(old, new) - if self.debug: - self._debug_overlay.selected_constraints = new self.relayout() def __components_items_changed(self, event): diff --git a/enable/layout/debug_constraints.py b/enable/layout/debug_constraints.py index 152e68955..54b99356c 100644 --- a/enable/layout/debug_constraints.py +++ b/enable/layout/debug_constraints.py @@ -49,7 +49,7 @@ class DebugConstraintsOverlay(AbstractOverlay): boxes = Any() # Style options for the lines. - term_color = ColorTrait('lightblue') + term_color = ColorTrait('orange') term_line_style = LineStyle('solid') def update_from_constraints(self, layout_mgr): @@ -76,7 +76,9 @@ def overlay(self, other_component, gc, view_bounds=None, mode="normal"): """ if len(self.selected_constraints) == 0: return + origin = other_component.position with gc: + gc.translate_ctm(*origin) gc.set_stroke_color(self.term_color_) gc.set_line_dash(self.term_line_style_) gc.set_line_width(3) diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 753f963f2..7d0c9797c 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -1,26 +1,51 @@ from enable.api import Component, ComponentEditor, ConstraintsContainer from enable.layout.layout_helpers import hbox, align, grid -from traits.api import HasTraits, Instance -from traitsui.api import Item, View +from traits.api import HasTraits, Any, Instance, List, Property +from traitsui.api import Item, View, HGroup, TabularEditor +from traitsui.tabular_adapter import TabularAdapter + + +class ConstraintAdapter(TabularAdapter): + """ Display Constraints in a TabularEditor. + """ + columns = [('Constraint', 'id')] + id_text = Property + def _get_id_text(self): + return self.item.__repr__() class Demo(HasTraits): canvas = Instance(Component) + constraints = Property(List) + + selected_constraints = Any + traits_view = View( - Item('canvas', - editor=ComponentEditor(), - show_label=False, + HGroup( + Item('constraints', + editor=TabularEditor( + adapter=ConstraintAdapter(), + editable=False, + multi_select=True, + selected='selected_constraints', + ), + show_label=False, + ), + Item('canvas', + editor=ComponentEditor(), + show_label=False, + ), ), resizable=True, title="Constraints Demo", - width=500, + width=1000, height=500, ) def _canvas_default(self): - parent = ConstraintsContainer(bounds=(500,500), debug=True) + parent = ConstraintsContainer(bounds=(500,500), padding=20, debug=True) hugs = {'hug_width':'weak', 'hug_height':'weak'} one = Component(id="one", bgcolor=0xFF0000, **hugs) @@ -39,6 +64,18 @@ def _canvas_default(self): return parent + def _get_constraints(self): + return list(self.canvas._layout_manager._constraints) + + def _selected_constraints_changed(self, new): + if new is None or new == []: + return + + if self.canvas.debug: + canvas = self.canvas + canvas._debug_overlay.selected_constraints = new + canvas.request_redraw() + if __name__ == "__main__": Demo().configure_traits() From f0420bd6a9218a5a89e3dd579976ca02ad25a20b Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 15 Feb 2013 16:56:23 -0600 Subject: [PATCH 38/57] Fix the grid layout helper. --- enable/constraints_container.py | 10 +++++----- enable/layout/debug_constraints.py | 2 +- enable/layout/layout_helpers.py | 12 +++++++----- examples/enable/constraints_demo.py | 13 ++++++++----- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 32cd5ff5c..0de8b1f8a 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -197,16 +197,16 @@ def _content_box_constraints(self): contents_top = cns.contents_top contents_bottom = cns.contents_bottom - # Add these to the namespace, but don't use them + # Add these to the namespace, but don't use them here cns.contents_width = contents_right - contents_left cns.contents_height = contents_top - contents_bottom cns.contents_v_center = contents_bottom + cns.contents_height / 2.0 cns.contents_h_center = contents_left + cns.contents_width / 2.0 - return [contents_left == cns.left + self.padding_left, - contents_bottom == cns.bottom + self.padding_bottom, - contents_right == cns.right - self.padding_right, - contents_top == cns.top - self.padding_top, + return [contents_left == cns.left, + contents_bottom == cns.bottom, + contents_right == cns.left + cns.width, + contents_top == cns.bottom + cns.height, ] def _update_fixed_constraints(self): diff --git a/enable/layout/debug_constraints.py b/enable/layout/debug_constraints.py index 54b99356c..bbae3657b 100644 --- a/enable/layout/debug_constraints.py +++ b/enable/layout/debug_constraints.py @@ -57,7 +57,7 @@ def update_from_constraints(self, layout_mgr): """ self.boxes = defaultdict(Coords) - if layout_mgr is not None: + if layout_mgr is not None and layout_mgr._constraints: for constraint in layout_mgr._constraints: for expr in (constraint.lhs, constraint.rhs): for term in expr.terms: diff --git a/enable/layout/layout_helpers.py b/enable/layout/layout_helpers.py index 660570ba7..1214d3116 100644 --- a/enable/layout/layout_helpers.py +++ b/enable/layout/layout_helpers.py @@ -670,19 +670,19 @@ def _get_constraints(self, component): # 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) + 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 - top_items = (self.top, EqSpacer(margins.top), row_vars[0]) - bottom_items = (row_vars[-1], EqSpacer(margins.bottom), self.bottom) + 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', *top_items), AbutmentHelper('vertical', *bottom_items), + AbutmentHelper('vertical', *top_items), AbutmentHelper('horizontal', *left_items), AbutmentHelper('horizontal', *right_items), ] @@ -976,7 +976,7 @@ class AbutmentConstraintFactory(SequenceConstraintFactory): #: lookup for a pair of items in order to make the constraint. orientation_map = { 'horizontal': ('right', 'left'), - 'vertical': ('bottom', 'top'), + 'vertical': ('top', 'bottom'), } @classmethod @@ -1018,6 +1018,8 @@ def from_items(cls, items, orientation, spacing): "'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) diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 7d0c9797c..25cc4d667 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -1,6 +1,6 @@ from enable.api import Component, ComponentEditor, ConstraintsContainer -from enable.layout.layout_helpers import hbox, align, grid +from enable.layout.layout_helpers import hbox, vbox, align, grid, horizontal from traits.api import HasTraits, Any, Instance, List, Property from traitsui.api import Item, View, HGroup, TabularEditor from traitsui.tabular_adapter import TabularAdapter @@ -55,17 +55,20 @@ def _canvas_default(self): parent.add(one, two, three, four) parent.layout_constraints = [ - hbox(one.constraints, two.constraints, - three.constraints, four.constraints), + grid([one.constraints, two.constraints], + [three.constraints, four.constraints]), + align('height', one.constraints, two.constraints, + three.constraints, four.constraints), align('width', one.constraints, two.constraints, three.constraints, four.constraints), - ] return parent def _get_constraints(self): - return list(self.canvas._layout_manager._constraints) + if self.canvas._layout_manager._constraints: + return list(self.canvas._layout_manager._constraints) + return [] def _selected_constraints_changed(self, new): if new is None or new == []: From 5ab3f93ea81df0084ba94ff8e78f4f23367e05d6 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 15 Feb 2013 17:49:05 -0600 Subject: [PATCH 39/57] Don't require passing a constraints namespace to layout helpers. --- enable/layout/layout_helpers.py | 6 ++++++ examples/enable/constraints_demo.py | 11 ++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/enable/layout/layout_helpers.py b/enable/layout/layout_helpers.py index 1214d3116..61a0955ba 100644 --- a/enable/layout/layout_helpers.py +++ b/enable/layout/layout_helpers.py @@ -1234,6 +1234,7 @@ def horizontal(*items, **config): abutments for the given sequence of items. """ + items = [item.constraints for item in items] return AbutmentHelper('horizontal', *items, **config) @@ -1242,6 +1243,7 @@ def vertical(*items, **config): abutments for the given sequence of items. """ + items = [item.constraints for item in items] return AbutmentHelper('vertical', *items, **config) @@ -1250,6 +1252,7 @@ def hbox(*items, **config): abutments for a given sequence of items. """ + items = [item.constraints for item in items] return LinearBoxHelper('horizontal', *items, **config) @@ -1258,6 +1261,7 @@ def vbox(*items, **config): for a given sequence of items. """ + items = [item.constraints for item in items] return LinearBoxHelper('vertical', *items, **config) @@ -1266,6 +1270,7 @@ def align(anchor, *items, **config): spacing is allowed. """ + items = [item.constraints for item in items] return AlignmentHelper(anchor, *items, **config) @@ -1274,6 +1279,7 @@ def grid(*rows, **config): grid. """ + rows = [[item.constraints for item in items] for items in rows] return GridHelper(*rows, **config) diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 25cc4d667..575f2eb49 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -1,6 +1,6 @@ from enable.api import Component, ComponentEditor, ConstraintsContainer -from enable.layout.layout_helpers import hbox, vbox, align, grid, horizontal +from enable.layout.layout_helpers import hbox, vbox, align, grid, vertical from traits.api import HasTraits, Any, Instance, List, Property from traitsui.api import Item, View, HGroup, TabularEditor from traitsui.tabular_adapter import TabularAdapter @@ -55,12 +55,9 @@ def _canvas_default(self): parent.add(one, two, three, four) parent.layout_constraints = [ - grid([one.constraints, two.constraints], - [three.constraints, four.constraints]), - align('height', one.constraints, two.constraints, - three.constraints, four.constraints), - align('width', one.constraints, two.constraints, - three.constraints, four.constraints), + grid([one, two], [three, four]), + align('height', one, two, three, four), + align('width', one, two, three, four), ] return parent From e4d0f006aaa965817bb54f6963f946929ec35043 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 15 Feb 2013 18:09:57 -0600 Subject: [PATCH 40/57] Fix a drawing bug in the constraints debug overlay. --- enable/layout/debug_constraints.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/enable/layout/debug_constraints.py b/enable/layout/debug_constraints.py index bbae3657b..11938576c 100644 --- a/enable/layout/debug_constraints.py +++ b/enable/layout/debug_constraints.py @@ -91,16 +91,16 @@ def overlay(self, other_component, gc, view_bounds=None, mode="normal"): box = self.boxes[name] if attr == 'top': self.hline(gc, box.left, box.top, box.width) + elif attr == 'bottom': + self.hline(gc, box.left, box.bottom, box.width) elif attr == 'left': self.vline(gc, box.left, box.bottom, box.height) + elif attr == 'right': + self.vline(gc, box.right, box.bottom, box.height) elif attr == 'width': self.hline(gc, box.left, box.v_center, box.width) elif attr == 'height': - self.vline(gc, box.h_center, box.top, box.height) - elif attr == 'bottom': - self.hline(gc, box.left, box.bottom, box.right - box.left) - elif attr == 'right': - self.vline(gc, box.right, box.bottom, box.top - box.bottom) + self.vline(gc, box.h_center, box.bottom, box.height) gc.stroke_path() def vline(self, gc, x, y0, length): From 901dc5e545cc630a5240ea2a06c7616ac614dceb Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Mon, 18 Feb 2013 11:21:14 -0600 Subject: [PATCH 41/57] Undo the constraints helper simplification for the meantime. --- enable/layout/layout_helpers.py | 6 ------ examples/enable/constraints_demo.py | 9 ++++++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/enable/layout/layout_helpers.py b/enable/layout/layout_helpers.py index 61a0955ba..1214d3116 100644 --- a/enable/layout/layout_helpers.py +++ b/enable/layout/layout_helpers.py @@ -1234,7 +1234,6 @@ def horizontal(*items, **config): abutments for the given sequence of items. """ - items = [item.constraints for item in items] return AbutmentHelper('horizontal', *items, **config) @@ -1243,7 +1242,6 @@ def vertical(*items, **config): abutments for the given sequence of items. """ - items = [item.constraints for item in items] return AbutmentHelper('vertical', *items, **config) @@ -1252,7 +1250,6 @@ def hbox(*items, **config): abutments for a given sequence of items. """ - items = [item.constraints for item in items] return LinearBoxHelper('horizontal', *items, **config) @@ -1261,7 +1258,6 @@ def vbox(*items, **config): for a given sequence of items. """ - items = [item.constraints for item in items] return LinearBoxHelper('vertical', *items, **config) @@ -1270,7 +1266,6 @@ def align(anchor, *items, **config): spacing is allowed. """ - items = [item.constraints for item in items] return AlignmentHelper(anchor, *items, **config) @@ -1279,7 +1274,6 @@ def grid(*rows, **config): grid. """ - rows = [[item.constraints for item in items] for items in rows] return GridHelper(*rows, **config) diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 575f2eb49..b3fe6fdb0 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -55,9 +55,12 @@ def _canvas_default(self): parent.add(one, two, three, four) parent.layout_constraints = [ - grid([one, two], [three, four]), - align('height', one, two, three, four), - align('width', one, two, three, four), + grid([one.constraints, two.constraints], + [three.constraints, four.constraints]), + align('height', one.constraints, two.constraints, + three.constraints, four.constraints), + align('width', one.constraints, two.constraints, + three.constraints, four.constraints), ] return parent From 70e34d31ef7572257f7cd9668eeeac5aa9efe3d2 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Mon, 18 Feb 2013 11:44:08 -0600 Subject: [PATCH 42/57] Don't require components to have their id explicitly set. --- enable/component.py | 12 ++++++++++-- enable/constraints_container.py | 3 --- enable/coordinate_box.py | 5 ++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/enable/component.py b/enable/component.py index efca0a09e..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, \ @@ -314,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 @@ -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/constraints_container.py b/enable/constraints_container.py index 0de8b1f8a..b0716730c 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -19,9 +19,6 @@ class ConstraintsContainer(Container): """ - # The ID for this component. - id = "parent" - # 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 diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index 40c39927b..d03ed899e 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -1,6 +1,4 @@ -from uuid import uuid4 - # Enthought library imports from traits.api import HasTraits, Enum, Instance, Property @@ -175,7 +173,8 @@ def _old_set_y2(self, val): return def _constraints_default(self): - cns_names = ConstraintsNamespace(type(self).__name__, uuid4().hex[:8]) + 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 From 5fd04a50dbf866dbde86032977d6aef1ee6f9852 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Mon, 18 Feb 2013 17:43:15 -0600 Subject: [PATCH 43/57] Add constraints variables as Property traits on CoordinateBox. This means that layout helpers can accept components directly instead of a reference to the constraints namespace of the component. --- enable/constraints_container.py | 78 ++++++++++++++++++++--------- enable/coordinate_box.py | 67 ++++++++++++++++++++----- enable/layout/layout_helpers.py | 5 +- enable/layout/utils.py | 20 +++++++- examples/enable/constraints_demo.py | 11 ++-- 5 files changed, 132 insertions(+), 49 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index b0716730c..764259419 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -4,13 +4,16 @@ #------------------------------------------------------------------------------ # traits imports -from traits.api import Bool, Callable, Dict, Either, Instance, List, Str +from traits.api import Bool, Callable, Dict, Either, Instance, List, \ + Property, Str # local imports from container import Container +from coordinate_box import get_from_constraints_namespace from layout.debug_constraints import DebugConstraintsOverlay from layout.layout_helpers import expand_constraints from layout.layout_manager import LayoutManager +from layout.utils import add_symbolic_contents_constraints class ConstraintsContainer(Container): @@ -18,6 +21,37 @@ class ConstraintsContainer(Container): 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 @@ -76,15 +110,15 @@ def relayout(self): constraints modification. """ mgr_layout = self._layout_manager.layout - constraints = self.constraints - width_var = constraints.width - height_var = constraints.height + width_var = self.layout_width + height_var = self.layout_height width, height = self.bounds def layout(): for component in self._component_map.itervalues(): - cns = component.constraints - component.position = (cns.left.value, cns.bottom.value) - component.bounds = (cns.width.value, cns.height.value) + component.position = (component.left.value, + component.bottom.value) + component.bounds = (component.layout_width.value, + component.layout_height.value) if self._debug_overlay: layout_mgr = self._layout_manager self._debug_overlay.update_from_constraints(layout_mgr) @@ -114,8 +148,7 @@ def _layout_constraints_changed(self): # Update the private constraints list. This will trigger the relayout. expand = expand_constraints - constraints = self.constraints - self._layout_constraints = [cns for cns in expand(constraints, new)] + self._layout_constraints = [cns for cns in expand(self, new)] def __layout_constraints_changed(self, name, old, new): """ Invalidate the layout when the private constraints list changes. @@ -188,22 +221,17 @@ def _content_box_constraints(self): container. """ - cns = self.constraints - contents_left = cns.contents_left - contents_right = cns.contents_right - contents_top = cns.contents_top - contents_bottom = cns.contents_bottom - - # Add these to the namespace, but don't use them here - cns.contents_width = contents_right - contents_left - cns.contents_height = contents_top - contents_bottom - cns.contents_v_center = contents_bottom + cns.contents_height / 2.0 - cns.contents_h_center = contents_left + cns.contents_width / 2.0 - - return [contents_left == cns.left, - contents_bottom == cns.bottom, - contents_right == cns.left + cns.width, - contents_top == cns.bottom + cns.height, + 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 _update_fixed_constraints(self): diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index d03ed899e..766b27652 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -4,6 +4,7 @@ # 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 @@ -11,6 +12,14 @@ 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): """ Represents a box in screen space, and provides convenience properties to @@ -52,8 +61,37 @@ class CoordinateBox(HasTraits): # Constraints-based layout #------------------------------------------------------------------------ - # A namespace containing the constraints for this CoordinateBox - constraints = Instance(ConstraintsNamespace) + # 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) # How strongly a layout box hugs it's width hint. hug_width = ConstraintPolicyEnum('strong') @@ -67,6 +105,9 @@ class CoordinateBox(HasTraits): # 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 @@ -172,7 +213,7 @@ def _old_set_y2(self, val): self.bounds[1] = new_height return - def _constraints_default(self): + 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) @@ -181,11 +222,10 @@ def _constraints_default(self): def _get__hard_constraints(self): """ Generate the constraints which must always be applied. """ - constraints = self.constraints - left = constraints.left - bottom = constraints.bottom - width = constraints.width - height = constraints.height + 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 @@ -195,9 +235,8 @@ def _get__size_constraints(self): cns = [] push = cns.append width_hint, height_hint = self.bounds - constraints = self.constraints - width = constraints.width - height = constraints.height + 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: @@ -218,4 +257,8 @@ def _get__size_constraints(self): return cns -# EOF +# Register with ABConstrainable so that layout helpers will recognize +# CoordinateBox instances. +ABConstrainable.register(CoordinateBox) + +# EOF \ No newline at end of file diff --git a/enable/layout/layout_helpers.py b/enable/layout/layout_helpers.py index 1214d3116..514b2aa03 100644 --- a/enable/layout/layout_helpers.py +++ b/enable/layout/layout_helpers.py @@ -349,14 +349,13 @@ def __init__(self, name): top = property(lambda self: self._namespace.top) right = property(lambda self: self._namespace.right) bottom = property(lambda self: self._namespace.bottom) - width = property(lambda self: self._namespace.width) - height = property(lambda self: self._namespace.height) + 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) -ABConstrainable.register(ConstraintsNamespace) class LinearBoxHelper(BoxHelper): diff --git a/enable/layout/utils.py b/enable/layout/utils.py index 2a1719e4c..808f6ab70 100644 --- a/enable/layout/utils.py +++ b/enable/layout/utils.py @@ -14,11 +14,27 @@ def add_symbolic_constraints(namespace): """ bottom = namespace.bottom left = namespace.left - width = namespace.width - height = namespace.height + 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 = left - right + 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/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index b3fe6fdb0..bc66ed1a6 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -1,6 +1,6 @@ from enable.api import Component, ComponentEditor, ConstraintsContainer -from enable.layout.layout_helpers import hbox, vbox, align, grid, vertical +from enable.layout.layout_helpers import hbox, vbox, align, grid, vertical, spacer from traits.api import HasTraits, Any, Instance, List, Property from traitsui.api import Item, View, HGroup, TabularEditor from traitsui.tabular_adapter import TabularAdapter @@ -55,12 +55,9 @@ def _canvas_default(self): parent.add(one, two, three, four) parent.layout_constraints = [ - grid([one.constraints, two.constraints], - [three.constraints, four.constraints]), - align('height', one.constraints, two.constraints, - three.constraints, four.constraints), - align('width', one.constraints, two.constraints, - three.constraints, four.constraints), + grid([one, two], [three, four]), + align('layout_height', one, two, three, four), + align('layout_width', one, two, three, four), ] return parent From f2ca75d6f92f83dc6a4c1e2a4c961e6f216e6c3f Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 19 Feb 2013 14:43:57 -0600 Subject: [PATCH 44/57] Fix a bug where do_layout() was not being called on children. --- enable/constraints_container.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 764259419..a414e163b 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -99,6 +99,13 @@ def __init__(self, **traits): # 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) + return + def refresh_layout_constraints(self): """ Explicitly regenerate the container's constraints and refresh the layout. From 95ee85bcd3a5ca69ba905fff5c67dc4bb755cd14 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 19 Feb 2013 19:18:19 -0600 Subject: [PATCH 45/57] Add live constraints editing to the constraints demo. --- enable/layout/layout_helpers.py | 4 +- examples/enable/constraints_demo.py | 69 +++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/enable/layout/layout_helpers.py b/enable/layout/layout_helpers.py index 514b2aa03..e40725a5b 100644 --- a/enable/layout/layout_helpers.py +++ b/enable/layout/layout_helpers.py @@ -6,7 +6,7 @@ from collections import defaultdict from uuid import uuid4 -from casuarius import ConstraintVariable, LinearSymbolic +from casuarius import ConstraintVariable, LinearSymbolic, LinearConstraint from traits.api import HasTraits, Instance, Range from .ab_constrainable import ABConstrainable @@ -67,7 +67,7 @@ def expand_constraints(component, constraints): if item is not None: yield item else: - if cn is not None: + if cn is not None and isinstance(cn, LinearConstraint): yield cn diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index bc66ed1a6..7fa7e45ab 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -1,8 +1,9 @@ from enable.api import Component, ComponentEditor, ConstraintsContainer -from enable.layout.layout_helpers import hbox, vbox, align, grid, vertical, spacer -from traits.api import HasTraits, Any, Instance, List, Property -from traitsui.api import Item, View, HGroup, TabularEditor +from enable.layout.layout_helpers import (expand_constraints, hbox, vbox, + align, grid, horizontal, vertical, spacer) +from traits.api import HasTraits, Any, Instance, List, Property, Str +from traitsui.api import Item, View, HGroup, VGroup, TabularEditor, CodeEditor from traitsui.tabular_adapter import TabularAdapter @@ -20,18 +21,27 @@ class Demo(HasTraits): constraints = Property(List) + constraints_def = Str + selected_constraints = Any traits_view = View( HGroup( - Item('constraints', - editor=TabularEditor( - adapter=ConstraintAdapter(), - editable=False, - multi_select=True, - selected='selected_constraints', - ), - show_label=False, + VGroup( + Item('constraints', + editor=TabularEditor( + adapter=ConstraintAdapter(), + editable=False, + multi_select=True, + selected='selected_constraints', + ), + show_label=False, + ), + Item('constraints_def', + editor=CodeEditor(), + height=100, + show_label=False, + ), ), Item('canvas', editor=ComponentEditor(), @@ -54,12 +64,6 @@ def _canvas_default(self): four = Component(id="four", bgcolor=0x000000, **hugs) parent.add(one, two, three, four) - parent.layout_constraints = [ - grid([one, two], [three, four]), - align('layout_height', one, two, three, four), - align('layout_width', one, two, three, four), - ] - return parent def _get_constraints(self): @@ -67,6 +71,33 @@ def _get_constraints(self): return list(self.canvas._layout_manager._constraints) return [] + def _constraints_def_default(self): + return """[ + grid([one, two], [three, four]), + align('layout_height', one, two, three, four), + align('layout_width', one, two, three, four), +]""" + + def _constraints_def_changed(self): + if self.canvas is None: + return + + canvas = self.canvas + components = canvas._components + one = components[0] + two = components[1] + three = components[2] + four = components[3] + + try: + new_cns = eval(self.constraints_def) + except Exception, ex: + return + + self.selected_constraints = [] + self.canvas.layout_constraints = new_cns + self.canvas.request_redraw() + def _selected_constraints_changed(self, new): if new is None or new == []: return @@ -78,4 +109,6 @@ def _selected_constraints_changed(self, new): if __name__ == "__main__": - Demo().configure_traits() + demo = Demo() + demo._constraints_def_changed() + demo.configure_traits() From 73696c77daa515467a1aa5d3826eaf65e046baf3 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 19 Feb 2013 22:34:00 -0600 Subject: [PATCH 46/57] Fix debug rendering for constraints and tweak the constraints demo --- enable/layout/debug_constraints.py | 24 ++++++++++++------------ examples/enable/constraints_demo.py | 14 ++++++++------ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/enable/layout/debug_constraints.py b/enable/layout/debug_constraints.py index 11938576c..1dc61d6d3 100644 --- a/enable/layout/debug_constraints.py +++ b/enable/layout/debug_constraints.py @@ -14,14 +14,14 @@ class Coords(HasTraits): bottom = Float() left = Float() right = Float() - width = Float() - height = Float() + layout_width = Float() + layout_height = Float() v_center = Property() _v_center = Any() def _get_v_center(self): if self._v_center is None: - return self.bottom + 0.5 * self.height + return self.bottom + 0.5 * self.layout_height else: return self._v_center def _set_v_center(self, value): @@ -31,7 +31,7 @@ def _set_v_center(self, value): _h_center = Any() def _get_h_center(self): if self._h_center is None: - return self.left + 0.5 * self.width + return self.left + 0.5 * self.layout_width else: return self._h_center def _set_h_center(self, value): @@ -90,17 +90,17 @@ def overlay(self, other_component, gc, view_bounds=None, mode="normal"): for name, attr in sorted(term_attrs): box = self.boxes[name] if attr == 'top': - self.hline(gc, box.left, box.top, box.width) + self.hline(gc, box.left, box.top, box.layout_width) elif attr == 'bottom': - self.hline(gc, box.left, box.bottom, box.width) + self.hline(gc, box.left, box.bottom, box.layout_width) elif attr == 'left': - self.vline(gc, box.left, box.bottom, box.height) + self.vline(gc, box.left, box.bottom, box.layout_height) elif attr == 'right': - self.vline(gc, box.right, box.bottom, box.height) - elif attr == 'width': - self.hline(gc, box.left, box.v_center, box.width) - elif attr == 'height': - self.vline(gc, box.h_center, box.bottom, box.height) + self.vline(gc, box.right, box.bottom, box.layout_height) + elif attr == 'layout_width': + self.hline(gc, box.left, box.v_center, box.layout_width) + elif attr == 'layout_height': + self.vline(gc, box.h_center, box.bottom, box.layout_height) gc.stroke_path() def vline(self, gc, x, y0, length): diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 7fa7e45ab..106983a0a 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -1,8 +1,8 @@ from enable.api import Component, ComponentEditor, ConstraintsContainer -from enable.layout.layout_helpers import (expand_constraints, hbox, vbox, - align, grid, horizontal, vertical, spacer) -from traits.api import HasTraits, Any, Instance, List, Property, Str +from enable.layout.layout_helpers import (align, grid, horizontal, hbox, vbox, + spacer, vertical) +from traits.api import HasTraits, Any, Event, Instance, List, Property, Str from traitsui.api import Item, View, HGroup, VGroup, TabularEditor, CodeEditor from traitsui.tabular_adapter import TabularAdapter @@ -19,7 +19,8 @@ def _get_id_text(self): class Demo(HasTraits): canvas = Instance(Component) - constraints = Property(List) + constraints = Property(List, depends_on='constraints_changed') + constraints_changed = Event constraints_def = Str @@ -94,12 +95,13 @@ def _constraints_def_changed(self): except Exception, ex: return - self.selected_constraints = [] self.canvas.layout_constraints = new_cns + self.selected_constraints = [] + self.constraints_changed = True self.canvas.request_redraw() def _selected_constraints_changed(self, new): - if new is None or new == []: + if new is None: return if self.canvas.debug: From 52600630ff9b6018b22ea5f9eeca7432468d5425 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 19 Feb 2013 22:53:03 -0600 Subject: [PATCH 47/57] Fix a typo in the contents constraints creation. --- enable/layout/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enable/layout/utils.py b/enable/layout/utils.py index 808f6ab70..37f3a4202 100644 --- a/enable/layout/utils.py +++ b/enable/layout/utils.py @@ -33,7 +33,7 @@ def add_symbolic_contents_constraints(namespace): top = namespace.contents_top bottom = namespace.contents_bottom - namespace.contents_width = left - right + 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 From 48ef8705c2b6d41572581f1a72e5614f5e741d46 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 19 Feb 2013 22:58:15 -0600 Subject: [PATCH 48/57] Fix a small docstring bug. --- enable/layout/ab_constrainable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enable/layout/ab_constrainable.py b/enable/layout/ab_constrainable.py index 40362c460..cea36d892 100644 --- a/enable/layout/ab_constrainable.py +++ b/enable/layout/ab_constrainable.py @@ -10,8 +10,8 @@ class ABConstrainable(object): layout helpers. Minimally, instances need to have `top`, `bottom`, `left`, `right`, - `width`, `height`, `v_center` and `h_center` attributes which are - `LinearSymbolic` instances. + `layout_width`, `layout_height`, `v_center` and `h_center` attributes + which are `LinearSymbolic` instances. """ __metaclass__ = ABCMeta From 23b4c05d254cd4f1cb8b21d0ff0cce0b0e6775ef Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Thu, 21 Feb 2013 11:23:33 -0600 Subject: [PATCH 49/57] Fix a typo in an exception message. --- enable/layout/layout_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enable/layout/layout_helpers.py b/enable/layout/layout_helpers.py index e40725a5b..175af9258 100644 --- a/enable/layout/layout_helpers.py +++ b/enable/layout/layout_helpers.py @@ -792,7 +792,7 @@ def item_test(item): 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])] + args = [type(items[0]), type(items[-1])] raise TypeError(msg % args) if not all(map(item_test, items)): From 4dd81fd1a70e428519c3e0eba0b8a77bbaa67f9c Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Thu, 21 Feb 2013 17:53:50 -0600 Subject: [PATCH 50/57] Steal layout sharing from Enaml. Remove debug rendering. --- enable/constraints_container.py | 375 +++++++++++++++++++--------- enable/coordinate_box.py | 7 +- enable/layout/layout_manager.py | 12 +- examples/enable/constraints_demo.py | 68 +---- 4 files changed, 279 insertions(+), 183 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index a414e163b..0d88aba3a 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -2,15 +2,15 @@ # Copyright (c) 2013, Enthought, Inc. # All rights reserved. #------------------------------------------------------------------------------ +from collections import deque # traits imports -from traits.api import Bool, Callable, Dict, Either, Instance, List, \ +from traits.api import Any, Bool, Callable, Dict, Either, Instance, List, \ Property, Str # local imports from container import Container -from coordinate_box import get_from_constraints_namespace -from layout.debug_constraints import DebugConstraintsOverlay +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 @@ -59,41 +59,33 @@ class ConstraintsContainer(Container): # to return a list of constraints. layout_constraints = Either(List, Callable) - # A copy of the layout constraints used when the constraints change - _layout_constraints = List + # 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) - # A dictionary of components added to this container - _component_map = Dict - - # All the hard constraints for child components - _child_hard_constraints_map = Dict(Str, List) - _child_hard_constraints = List - - # The size constraints for child components - _child_size_constraints_map = Dict(Str, List) - _child_size_constraints = List - - # The casuarius solver - _layout_manager = Instance(LayoutManager) - - - #------------------------------------------------------------------------ - # Debugging bits - #------------------------------------------------------------------------ + # Sharing related private traits + _owns_layout = Bool(True) + _layout_owner = Any - # Whether or not debugging info should be shown. - debug = Bool(False) + # The contents box constraints for this container + _contents_constraints = Property - # The overlay that draws the debugging info - _debug_overlay = Instance(DebugConstraintsOverlay) + # The user-specified layout constraints, with layout helpers expanded + _layout_constraints = Property - def __init__(self, **traits): - super(ConstraintsContainer, self).__init__(**traits) + # A dictionary of components added to this container + _component_map = Dict - if self.debug: - dbg = DebugConstraintsOverlay() - self.overlays.append(dbg) - self._debug_overlay = dbg + # The casuarius solver + _layout_manager = Instance(LayoutManager, allow_none=True) + _offset_table = List + _layout_table = List #------------------------------------------------------------------------ # Public methods @@ -106,32 +98,89 @@ def do_layout(self, size=None, force=False): component.do_layout(size=size, force=force) return - def refresh_layout_constraints(self): + 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. """ - self._layout_constraints_changed() + self._init_layout() + self.refresh() + + #------------------------------------------------------------------------ + # 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. - def relayout(self): - """ Re-run the constraints solver in response to a resize or - constraints modification. """ - mgr_layout = self._layout_manager.layout - width_var = self.layout_width - height_var = self.layout_height - width, height = self.bounds - def layout(): - for component in self._component_map.itervalues(): - component.position = (component.left.value, - component.bottom.value) - component.bounds = (component.layout_width.value, - component.layout_height.value) - if self._debug_overlay: - layout_mgr = self._layout_manager - self._debug_overlay.update_from_constraints(layout_mgr) - mgr_layout(layout, width_var, height_var, (width, height)) - - self.invalidate_draw() + 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 @@ -140,38 +189,51 @@ def _bounds_changed(self, old, new): """ Run the solver when the container's bounds change. """ super(ConstraintsContainer, self)._bounds_changed(old, new) - self.relayout() + 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 + return [] if callable(self.layout_constraints): new = self.layout_constraints(self) else: new = self.layout_constraints - # Update the private constraints list. This will trigger the relayout. - expand = expand_constraints - self._layout_constraints = [cns for cns in expand(self, new)] - - def __layout_constraints_changed(self, name, old, new): - """ Invalidate the layout when the private constraints list changes. - """ - self._layout_manager.replace_constraints(old, new) - self.relayout() + # 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: - key = item.id - del self._child_hard_constraints_map[key] - del self._child_size_constraints_map[key] - del self._component_map[key] + del self._component_map[item.id] # Check the added components self._check_and_add_components(event.added) @@ -181,25 +243,80 @@ def __components_changed(self, new): """ # Clear the component maps self._component_map = {} - self._child_hard_constraints_map = {} - self._child_size_constraints_map = {} # Check the new components self._check_and_add_components(new) - def __layout_manager_default(self): - """ Create the layout manager. - """ - lm = LayoutManager(debug=self.debug) - - constraints = self._hard_constraints + self._content_box_constraints() - lm.initialize(constraints) - return lm - #------------------------------------------------------------------------ # 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. """ @@ -216,46 +333,74 @@ def _check_and_add_components(self, components): msg = "Can't add a Component with the same id as its parent." raise ValueError(msg) - self._child_hard_constraints_map[key] = item._hard_constraints - self._child_size_constraints_map[key] = item._size_constraints self._component_map[key] = item - # Update the fixed constraints - self._update_fixed_constraints() + # Update the layout + self.relayout() - def _content_box_constraints(self): - """ Return the constraints which define the content box of this - container. + def _generate_constraints(self, layout_table): + """ Creates the list of casuarius LinearConstraint objects for + the widgets for which this container owns the layout. - """ - add_symbolic_contents_constraints(self._constraints_vars) + 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. - contents_left = self.contents_left - contents_right = self.contents_right - contents_top = self.contents_top - contents_bottom = self.contents_bottom + Parameters + ---------- + layout_table : list + The layout table created by a call to _build_layout_table. - return [contents_left == self.left, - contents_bottom == self.bottom, - contents_right == self.left + self.layout_width, - contents_top == self.bottom + self.layout_height, - ] + 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. - def _update_fixed_constraints(self): - """ Resolve the differences between the list of constraints and the - map of child component constraints for both types of fixed constraints. """ - old_cns, all_new_cns = [], [] - for name in ('hard', 'size'): - map_attr = getattr(self, '_child_{0}_constraints_map'.format(name)) - list_name = '_child_{0}_constraints'.format(name) - old_cns.extend(getattr(self, list_name)) - new_cns = [] - for item in map_attr.itervalues(): - new_cns.extend(item) - all_new_cns.extend(new_cns) - setattr(self, list_name, new_cns) - - self._layout_manager.replace_constraints(old_cns, all_new_cns) - # Possibly regenerate the user-specified constraints - self.refresh_layout_constraints() + # 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/coordinate_box.py b/enable/coordinate_box.py index 766b27652..b38cb6ad1 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -1,6 +1,6 @@ # Enthought library imports -from traits.api import HasTraits, Enum, Instance, Property +from traits.api import HasTraits, Enum, Instance, Property, Tuple # Local, relative imports from enable_traits import bounds_trait, coordinate_trait @@ -93,6 +93,9 @@ class CoordinateBox(HasTraits): # 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('strong') @@ -234,7 +237,7 @@ def _get__size_constraints(self): """ cns = [] push = cns.append - width_hint, height_hint = self.bounds + 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 diff --git a/enable/layout/layout_manager.py b/enable/layout/layout_manager.py index bcd81480e..33778d5d4 100644 --- a/enable/layout/layout_manager.py +++ b/enable/layout/layout_manager.py @@ -10,14 +10,11 @@ class LayoutManager(object): of constraints. """ - def __init__(self, debug=False): + def __init__(self): self._solver = Solver(autosolve=False) self._initialized = False self._running = False - self._debug = debug - self._constraints = None - def initialize(self, constraints): """ Initialize the solver with the given constraints. @@ -28,9 +25,6 @@ def initialize(self, constraints): solvers. """ - if self._debug: - self._constraints = set(constraints) - if self._initialized: raise RuntimeError('Solver already initialized') solver = self._solver @@ -53,10 +47,6 @@ def replace_constraints(self, old_cns, new_cns): The list of casuarius constraints to add to the solver. """ - if self._debug: - self._constraints.difference_update(set(old_cns)) - self._constraints.update(set(new_cns)) - if not self._initialized: raise RuntimeError('Solver not yet initialized') solver = self._solver diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 106983a0a..5465732ec 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -2,47 +2,21 @@ from enable.api import Component, ComponentEditor, ConstraintsContainer from enable.layout.layout_helpers import (align, grid, horizontal, hbox, vbox, spacer, vertical) -from traits.api import HasTraits, Any, Event, Instance, List, Property, Str -from traitsui.api import Item, View, HGroup, VGroup, TabularEditor, CodeEditor -from traitsui.tabular_adapter import TabularAdapter - - -class ConstraintAdapter(TabularAdapter): - """ Display Constraints in a TabularEditor. - """ - columns = [('Constraint', 'id')] - id_text = Property - def _get_id_text(self): - return self.item.__repr__() +from traits.api import HasTraits, Instance, Str +from traitsui.api import Item, View, HGroup, CodeEditor class Demo(HasTraits): canvas = Instance(Component) - constraints = Property(List, depends_on='constraints_changed') - constraints_changed = Event - constraints_def = Str - selected_constraints = Any - traits_view = View( HGroup( - VGroup( - Item('constraints', - editor=TabularEditor( - adapter=ConstraintAdapter(), - editable=False, - multi_select=True, - selected='selected_constraints', - ), - show_label=False, - ), - Item('constraints_def', - editor=CodeEditor(), - height=100, - show_label=False, - ), + Item('constraints_def', + editor=CodeEditor(), + height=100, + show_label=False, ), Item('canvas', editor=ComponentEditor(), @@ -56,7 +30,7 @@ class Demo(HasTraits): ) def _canvas_default(self): - parent = ConstraintsContainer(bounds=(500,500), padding=20, debug=True) + parent = ConstraintsContainer(bounds=(500,500), padding=20) hugs = {'hug_width':'weak', 'hug_height':'weak'} one = Component(id="one", bgcolor=0xFF0000, **hugs) @@ -67,18 +41,6 @@ def _canvas_default(self): parent.add(one, two, three, four) return parent - def _get_constraints(self): - if self.canvas._layout_manager._constraints: - return list(self.canvas._layout_manager._constraints) - return [] - - def _constraints_def_default(self): - return """[ - grid([one, two], [three, four]), - align('layout_height', one, two, three, four), - align('layout_width', one, two, three, four), -]""" - def _constraints_def_changed(self): if self.canvas is None: return @@ -96,18 +58,14 @@ def _constraints_def_changed(self): return self.canvas.layout_constraints = new_cns - self.selected_constraints = [] - self.constraints_changed = True self.canvas.request_redraw() - def _selected_constraints_changed(self, new): - if new is None: - return - - if self.canvas.debug: - canvas = self.canvas - canvas._debug_overlay.selected_constraints = new - canvas.request_redraw() + def _constraints_def_default(self): + return """[ + grid([one, two], [three, four]), + align('layout_height', one, two, three, four), + align('layout_width', one, two, three, four), +]""" if __name__ == "__main__": From 8d051063c3f62e2c87734ebce36dea986c68f149 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Thu, 21 Feb 2013 23:18:04 -0600 Subject: [PATCH 51/57] Add layout sharing to the constraints demo. --- enable/constraints_container.py | 7 +- examples/enable/constraints_demo.py | 99 +++++++++++++++++++++++------ 2 files changed, 85 insertions(+), 21 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 0d88aba3a..4f00273b6 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -131,8 +131,11 @@ def relayout(self): """ Explicitly regenerate the container's constraints and refresh the layout. """ - self._init_layout() - self.refresh() + if not self.share_layout: + self._init_layout() + self.refresh() + elif self._layout_owner is not None: + self._layout_owner.relayout() #------------------------------------------------------------------------ # Layout Sharing diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index 5465732ec..eee03c456 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -2,21 +2,32 @@ from enable.api import Component, ComponentEditor, ConstraintsContainer from enable.layout.layout_helpers import (align, grid, horizontal, hbox, vbox, spacer, vertical) -from traits.api import HasTraits, Instance, Str -from traitsui.api import Item, View, HGroup, CodeEditor +from traits.api import HasTraits, Bool, Instance, Str +from traitsui.api import Item, View, HGroup, VGroup, CodeEditor class Demo(HasTraits): - canvas = Instance(Component) + canvas = Instance(ConstraintsContainer) + child_canvas = Instance(ConstraintsContainer) constraints_def = Str + child_constraints_def = Str + share_layout = Bool(False) traits_view = View( HGroup( - Item('constraints_def', - editor=CodeEditor(), - height=100, - show_label=False, + 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(), @@ -33,10 +44,21 @@ def _canvas_default(self): parent = ConstraintsContainer(bounds=(500,500), padding=20) hugs = {'hug_width':'weak', 'hug_height':'weak'} - one = Component(id="one", bgcolor=0xFF0000, **hugs) - two = Component(id="two", bgcolor=0x00FF00, **hugs) - three = Component(id="three", bgcolor=0x0000FF, **hugs) - four = Component(id="four", bgcolor=0x000000, **hugs) + one = Component(id="r", bgcolor=0xFF0000, **hugs) + two = Component(id="g", bgcolor=0x00FF00, **hugs) + three = Component(id="b", bgcolor=0x0000FF, **hugs) + + parent.add(one, two, three, self.child_canvas) + return parent + + def _child_canvas_default(self): + parent = ConstraintsContainer(id="child", share_layout=self.share_layout) + + hugs = {'hug_width':'weak', 'hug_height':'weak'} + one = Component(id="c", bgcolor=0x00FFFF, **hugs) + two = Component(id="m", bgcolor=0xFF00FF, **hugs) + three = Component(id="y", bgcolor=0xFFFF00, **hugs) + four = Component(id="k", bgcolor=0x000000, **hugs) parent.add(one, two, three, four) return parent @@ -47,28 +69,67 @@ def _constraints_def_changed(self): canvas = self.canvas components = canvas._components - one = components[0] - two = components[1] - three = components[2] - four = components[3] + 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 - self.canvas.layout_constraints = new_cns + 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 """[ - grid([one, two], [three, four]), - align('layout_height', one, two, three, four), - align('layout_width', one, two, three, four), + 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() From 25dbd676b0afcb3a2718fda195318e08f5cc3fdf Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Fri, 22 Feb 2013 11:41:03 -0600 Subject: [PATCH 52/57] Add an api.py to layout that includes the layout helpers --- enable/layout/api.py | 8 ++++++++ examples/enable/constraints_demo.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 enable/layout/api.py diff --git a/enable/layout/api.py b/enable/layout/api.py new file mode 100644 index 000000000..db673c30d --- /dev/null +++ b/enable/layout/api.py @@ -0,0 +1,8 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Enthought, Inc. +# All rights reserved. +#------------------------------------------------------------------------------ + +from .layout_helpers import (expand_constraints, horizontal, vertical, hbox, + vbox, align, grid, spacer) + diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index eee03c456..f71e357ef 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -1,7 +1,7 @@ from enable.api import Component, ComponentEditor, ConstraintsContainer -from enable.layout.layout_helpers import (align, grid, horizontal, hbox, vbox, - spacer, vertical) +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 From 3c2ba1d7a336ff209fbcd00714c6308237b71664 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Mon, 25 Feb 2013 09:46:38 -0600 Subject: [PATCH 53/57] Make 'weak' the default for Component hug preferences. We can revisit this once Components have a more robust way of supplying their size preferences. --- enable/coordinate_box.py | 4 ++-- examples/enable/constraints_demo.py | 17 +++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index b38cb6ad1..f0f942b5b 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -97,10 +97,10 @@ class CoordinateBox(HasTraits): layout_size_hint = Tuple(0.0, 0.0) # How strongly a layout box hugs it's width hint. - hug_width = ConstraintPolicyEnum('strong') + hug_width = ConstraintPolicyEnum('weak') # How strongly a layout box hugs it's height hint. - hug_height = ConstraintPolicyEnum('strong') + hug_height = ConstraintPolicyEnum('weak') # How strongly a layout box resists clipping its contents. resist_width = ConstraintPolicyEnum('strong') diff --git a/examples/enable/constraints_demo.py b/examples/enable/constraints_demo.py index f71e357ef..62bb38722 100644 --- a/examples/enable/constraints_demo.py +++ b/examples/enable/constraints_demo.py @@ -43,10 +43,9 @@ class Demo(HasTraits): def _canvas_default(self): parent = ConstraintsContainer(bounds=(500,500), padding=20) - hugs = {'hug_width':'weak', 'hug_height':'weak'} - one = Component(id="r", bgcolor=0xFF0000, **hugs) - two = Component(id="g", bgcolor=0x00FF00, **hugs) - three = Component(id="b", bgcolor=0x0000FF, **hugs) + 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 @@ -54,11 +53,10 @@ def _canvas_default(self): def _child_canvas_default(self): parent = ConstraintsContainer(id="child", share_layout=self.share_layout) - hugs = {'hug_width':'weak', 'hug_height':'weak'} - one = Component(id="c", bgcolor=0x00FFFF, **hugs) - two = Component(id="m", bgcolor=0xFF00FF, **hugs) - three = Component(id="y", bgcolor=0xFFFF00, **hugs) - four = Component(id="k", bgcolor=0x000000, **hugs) + 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 @@ -80,7 +78,6 @@ def _constraints_def_changed(self): y = components[2] k = components[3] - try: new_cns = eval(self.constraints_def) except Exception, ex: From 3aa030c69147fd038a47aec9e3953178545c9a83 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Mon, 25 Feb 2013 13:53:29 -0600 Subject: [PATCH 54/57] Add is_spacer to the layout api.py --- enable/layout/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enable/layout/api.py b/enable/layout/api.py index db673c30d..c0a243eef 100644 --- a/enable/layout/api.py +++ b/enable/layout/api.py @@ -3,6 +3,6 @@ # All rights reserved. #------------------------------------------------------------------------------ -from .layout_helpers import (expand_constraints, horizontal, vertical, hbox, - vbox, align, grid, spacer) +from .layout_helpers import (horizontal, vertical, hbox, vbox, align, grid, + spacer, expand_constraints, is_spacer) From 1302607a04c694f46cb66db09b94ca1827959686 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Wed, 6 Mar 2013 21:39:21 -0600 Subject: [PATCH 55/57] Update the layout when a child component's size hint changes. --- enable/constraints_container.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/enable/constraints_container.py b/enable/constraints_container.py index 4f00273b6..612f65931 100644 --- a/enable/constraints_container.py +++ b/enable/constraints_container.py @@ -6,7 +6,7 @@ # traits imports from traits.api import Any, Bool, Callable, Dict, Either, Instance, List, \ - Property, Str + Property # local imports from container import Container @@ -96,7 +96,6 @@ def do_layout(self, size=None, force=False): """ for component in self.components: component.do_layout(size=size, force=force) - return def refresh(self): """ Re-run the constraints solver in response to a resize or @@ -111,12 +110,13 @@ def refresh(self): 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.position = (nx - dx, ny - dy) item.bounds = (item.layout_width.value, item.layout_height.value) offset_table[running_index] = (nx, ny) @@ -236,6 +236,8 @@ def __components_items_changed(self, event): """ # 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 @@ -245,11 +247,19 @@ 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 #------------------------------------------------------------------------ @@ -337,6 +347,8 @@ def _check_and_add_components(self, components): 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() @@ -406,4 +418,3 @@ def _init_layout(self): self._offset_table = offset_table self._layout_table = layout_table self._layout_manager = manager - From 23f84fbc314af4864ae4b0ac033fdd351ce82562 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Wed, 6 Mar 2013 21:39:56 -0600 Subject: [PATCH 56/57] PEP8 --- enable/coordinate_box.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/enable/coordinate_box.py b/enable/coordinate_box.py index f0f942b5b..f010a27d9 100644 --- a/enable/coordinate_box.py +++ b/enable/coordinate_box.py @@ -133,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 @@ -166,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): @@ -180,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): @@ -194,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 @@ -211,7 +212,7 @@ 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 @@ -263,5 +264,3 @@ def _get__size_constraints(self): # Register with ABConstrainable so that layout helpers will recognize # CoordinateBox instances. ABConstrainable.register(CoordinateBox) - -# EOF \ No newline at end of file From 775d182e02abd789f8997a456e52ca5e5defc085 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Wed, 6 Mar 2013 22:30:32 -0600 Subject: [PATCH 57/57] Remove constraints debug rendering. - Supporting code was already removed from the layout manager. --- enable/layout/debug_constraints.py | 119 ----------------------------- 1 file changed, 119 deletions(-) delete mode 100644 enable/layout/debug_constraints.py diff --git a/enable/layout/debug_constraints.py b/enable/layout/debug_constraints.py deleted file mode 100644 index 1dc61d6d3..000000000 --- a/enable/layout/debug_constraints.py +++ /dev/null @@ -1,119 +0,0 @@ -from collections import defaultdict - -from enable.abstract_overlay import AbstractOverlay -from enable.colors import ColorTrait -from enable.enable_traits import LineStyle -from traits.api import Any, Bool, Float, HasTraits, Instance, List, Property - - -class Coords(HasTraits): - """ Simple holder of box-related data. - - """ - top = Float() - bottom = Float() - left = Float() - right = Float() - layout_width = Float() - layout_height = Float() - - v_center = Property() - _v_center = Any() - def _get_v_center(self): - if self._v_center is None: - return self.bottom + 0.5 * self.layout_height - else: - return self._v_center - def _set_v_center(self, value): - self._v_center = value - - h_center = Property() - _h_center = Any() - def _get_h_center(self): - if self._h_center is None: - return self.left + 0.5 * self.layout_width - else: - return self._h_center - def _set_h_center(self, value): - self._h_center = value - - -class DebugConstraintsOverlay(AbstractOverlay): - """ Highlight the selected constraints on the outline view. - - """ - - selected_constraints = List() - - # Map from box name to Coords. - boxes = Any() - - # Style options for the lines. - term_color = ColorTrait('orange') - term_line_style = LineStyle('solid') - - def update_from_constraints(self, layout_mgr): - """ Update the constraints boxes. - - """ - self.boxes = defaultdict(Coords) - if layout_mgr is not None and layout_mgr._constraints: - for constraint in layout_mgr._constraints: - for expr in (constraint.lhs, constraint.rhs): - for term in expr.terms: - name, attr = self.split_var_name(term.var.name) - setattr(self.boxes[name], attr, term.var.value) - self.request_redraw() - - def split_var_name(self, var_name): - class_name, hexid, attr = var_name.rsplit('|', 2) - name = '{}|{}'.format(class_name, hexid) - return name, attr - - def overlay(self, other_component, gc, view_bounds=None, mode="normal"): - """ Draws this component overlaid on another component. - - """ - if len(self.selected_constraints) == 0: - return - origin = other_component.position - with gc: - gc.translate_ctm(*origin) - gc.set_stroke_color(self.term_color_) - gc.set_line_dash(self.term_line_style_) - gc.set_line_width(3) - term_attrs = set() - for constraint in self.selected_constraints: - for expr in (constraint.lhs, constraint.rhs): - for term in expr.terms: - term_attrs.add(self.split_var_name(term.var.name)) - for name, attr in sorted(term_attrs): - box = self.boxes[name] - if attr == 'top': - self.hline(gc, box.left, box.top, box.layout_width) - elif attr == 'bottom': - self.hline(gc, box.left, box.bottom, box.layout_width) - elif attr == 'left': - self.vline(gc, box.left, box.bottom, box.layout_height) - elif attr == 'right': - self.vline(gc, box.right, box.bottom, box.layout_height) - elif attr == 'layout_width': - self.hline(gc, box.left, box.v_center, box.layout_width) - elif attr == 'layout_height': - self.vline(gc, box.h_center, box.bottom, box.layout_height) - gc.stroke_path() - - def vline(self, gc, x, y0, length): - """ Draw a vertical line. - - """ - gc.move_to(x, y0) - gc.line_to(x, y0+length) - - def hline(self, gc, x0, y, length): - """ Draw a horizontal line. - - """ - gc.move_to(x0, y) - gc.line_to(x0+length, y) -