From 62091c83b73d8de9eb882e9035d015ccc620ab4b Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 21 Aug 2015 10:12:24 +0100 Subject: [PATCH 01/10] [WIP] adding spcific control on how to resize viewports. --- enable/viewport.py | 49 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/enable/viewport.py b/enable/viewport.py index 445f9559e..ebb9de3d8 100644 --- a/enable/viewport.py +++ b/enable/viewport.py @@ -39,7 +39,13 @@ class Viewport(Component): # Whether or not this viewport should stay constrained to the bounds # of the viewed component # TODO: Implement this - stay_inside = Bool(False) + stay_inside = Bool(True) + + # Where to anchor vertically on resizes + vertical_anchor = Enum('top', 'bottom', 'top', 'center') + + # Where to anchor vertically on resizes + horizontal_anchor = Enum('left', 'right', 'center') # Enable Zoom interaction enable_zoom = Bool(False) @@ -70,7 +76,7 @@ class Viewport(Component): def __init__(self, **traits): Component.__init__(self, **traits) - self._update_component_view_bounds() + self._adjust_bounds(0, 0) if 'zoom_tool' not in traits: self.zoom_tool = ViewportZoomTool(self) if self.enable_zoom: @@ -190,7 +196,7 @@ def _draw_mainlayer(self, gc, view_bounds=None, mode="normal"): gc.clip_to_rect(x-0.5, y-0.5, self.width+1, self.height+1) - + # There is a two-step transformation from the viewport's "outer" # coordinates into the coordinates space of the viewed component: # scaling, followed by a translation. @@ -202,7 +208,7 @@ def _draw_mainlayer(self, gc, view_bounds=None, mode="normal"): raise RuntimeError("Viewport zoomed out too far.") else: gc.translate_ctm(x - view_x, y - view_y) - + # Now transform the passed-in view_bounds; this is not the same thing as # self.view_bounds! if view_bounds: @@ -287,10 +293,12 @@ def _component_changed(self, old, new): def _bounds_changed(self, old, new): Component._bounds_changed(self, old, new) - self.set(view_bounds = [new[0]/self.zoom, new[1]/self.zoom], - trait_change_notify=False) - self._update_component_view_bounds() - return + new_w = new[0]/self.zoom + new_h = new[1]/self.zoom + w, h = self.view_bounds + delta_x = new_w - w + delta_y = new_h - h + self._adjust_bounds(delta_x, delta_y) def _bounds_items_changed(self, event): return self._bounds_changed(None, self.bounds) @@ -305,5 +313,26 @@ def _get_position(self): def _get_bounds(self): return self.view_bounds -# EOF - + def _adjust_bounds(self, delta_x, delta_y): + w, h = self.view_bounds + x, y = self.view_position + new_w = w + delta_x + new_h = h + delta_y + + if self.vertical_anchor == 'top': + y -= delta_y + elif self.vertical_anchor == 'center': + y -= delta_y/2 + if self.stay_inside and self.component is not None and \ + new_h <= self.component.height: + y = max(0, y) + + if self.horizontal_anchor == 'right': + x -= delta_x + elif self.horizontal_anchor == 'center': + x -= delta_x/2 + if self.stay_inside and self.component is not None and \ + new_w <= self.component.width: + x = max(0, x) + + self.trait_set(view_bounds=[new_w, new_h], view_position=[x, y]) From f531cb6603b332d26bf1ad46ecb023f5b3b1fc63 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 21 Aug 2015 13:50:10 +0100 Subject: [PATCH 02/10] Get initialization of view position and scroll bounds right. --- enable/scrolled.py | 19 +++++++------------ enable/viewport.py | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/enable/scrolled.py b/enable/scrolled.py index af78d1ef9..a28460974 100644 --- a/enable/scrolled.py +++ b/enable/scrolled.py @@ -258,8 +258,8 @@ def _view_position_items_changed_for_viewport_component(self): self.update_from_viewport() return - def _component_bounds_items_handler(self, object, new): - if new.added != new.removed: + def _component_bounds_items_handler(self, object, event): + if event.added != event.removed: self.update_bounds() def _component_bounds_handler(self, object, name, old, new): @@ -299,9 +299,9 @@ def _inside_padding_width_changed(self): def _viewport_component_changed(self): if self.viewport_component is None: self.viewport_component = Viewport() - self.viewport_component.component = self.component - self.viewport_component.view_position = [0,0] self.viewport_component.view_bounds = self.bounds + self.viewport_component.component = self.component + #self.viewport_component.view_position = [0, 0] self.add(self.viewport_component) def _alternate_vsb_changed(self, old, new): @@ -323,12 +323,10 @@ def _component_update(self, old, new): def _bounds_changed ( self, old, new ): Component._bounds_changed( self, old, new ) self.update_bounds() - return def _bounds_items_changed(self, event): Component._bounds_items_changed(self, event) self.update_bounds() - return #--------------------------------------------------------------------------- @@ -400,6 +398,7 @@ def _do_layout ( self ): range=range_x, enabled=False, ) + self._hsb.scroll_position = self.viewport_component.view_position[0] self._hsb.on_trait_change(self._handle_horizontal_scroll, 'scroll_position') self._hsb.on_trait_change(self._mouse_thumb_changed, @@ -411,8 +410,6 @@ def _do_layout ( self ): self._hsb.position = hsb_position elif self._hsb is not None: self._hsb = self._release_sb(self._hsb) - if not hasattr(self.component, "bounds_offset"): - self.viewport_component.view_position[0] = 0 else: # We don't need to render the horizontal scrollbar, and we don't # have one to update, either. @@ -432,9 +429,9 @@ def _do_layout ( self ): self._vsb = NativeScrollBar(orientation = 'vertical', bounds=bounds, position=vsb_position, - range=range_y + range=range_y, ) - + self._vsb.scroll_position = self.viewport_component.view_position[1] self._vsb.on_trait_change(self._handle_vertical_scroll, 'scroll_position') self._vsb.on_trait_change(self._mouse_thumb_changed, @@ -446,8 +443,6 @@ def _do_layout ( self ): self._vsb.range = range_y elif self._vsb: self._vsb = self._release_sb(self._vsb) - if not hasattr(self.component, "bounds_offset"): - self.viewport_component.view_position[1] = 0 else: # We don't need to render the vertical scrollbar, and we don't # have one to update, either. diff --git a/enable/viewport.py b/enable/viewport.py index ebb9de3d8..03e9f9fb9 100644 --- a/enable/viewport.py +++ b/enable/viewport.py @@ -76,12 +76,12 @@ class Viewport(Component): def __init__(self, **traits): Component.__init__(self, **traits) - self._adjust_bounds(0, 0) + if self.component is not None: + self._initialize_position() if 'zoom_tool' not in traits: self.zoom_tool = ViewportZoomTool(self) if self.enable_zoom: self._enable_zoom_changed(False, True) - return def components_at(self, x, y, add_containers = False): """ @@ -288,6 +288,7 @@ def _component_changed(self, old, new): if (new is not None) and (self not in new.viewports): new.viewports.append(self) + self._initialize_position() self._update_component_view_bounds() return @@ -313,6 +314,19 @@ def _get_position(self): def _get_bounds(self): return self.view_bounds + def _initialize_position(self): + x = 0 + y = 0 + if self.vertical_anchor == 'top': + y = self.component.height-self.view_bounds[1] + elif self.vertical_anchor == 'center': + y = (self.component.height-self.view_bounds[1])/2.0 + if self.horizontal_anchor == 'right': + x = self.component.width-self.view_bounds[0] + elif self.horizontal_anchor == 'center': + x = (self.component.width-self.view_bounds[0])/2.0 + self.trait_set(view_position=[x, y]) + def _adjust_bounds(self, delta_x, delta_y): w, h = self.view_bounds x, y = self.view_position From 2bbf7de0838d03aef438a436682d8e4a225fa8cb Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 21 Aug 2015 15:16:17 +0100 Subject: [PATCH 03/10] Fixes based on getting tests to work right. --- enable/scrolled.py | 22 +- enable/tests/viewport_test_case.py | 261 +++++++++++++++++++++++- enable/viewport.py | 45 ++-- examples/enable/scrolled_canvas_demo.py | 6 +- examples/enable/scrolled_demo.py | 5 +- 5 files changed, 316 insertions(+), 23 deletions(-) diff --git a/enable/scrolled.py b/enable/scrolled.py index a28460974..b40625665 100644 --- a/enable/scrolled.py +++ b/enable/scrolled.py @@ -2,7 +2,7 @@ from __future__ import with_statement # Enthought library imports -from traits.api import Bool, Instance, Int, Any, Float +from traits.api import Any, Bool, DelegatesTo, Float, Instance, Int # Local, relative imports from base import intersect_bounds, empty_rectangle @@ -23,7 +23,17 @@ class Scrolled(Container): component = Instance(Component) # The viewport onto our component - viewport_component = Instance(Viewport) + viewport_component = Instance(Viewport, ()) + + # Whether or not the viewport should stay constrained to the bounds + # of the viewed component + stay_inside = DelegatesTo('viewport_component') + + # Where to anchor vertically on resizes + vertical_anchor = DelegatesTo('viewport_component') + + # Where to anchor vertically on resizes + horizontal_anchor = DelegatesTo('viewport_component') # Inside padding is a background drawn area between the edges or scrollbars # and the scrolled area/left component. @@ -298,10 +308,14 @@ def _inside_padding_width_changed(self): def _viewport_component_changed(self): if self.viewport_component is None: - self.viewport_component = Viewport() + self.viewport_component = Viewport( + stay_inside=self.stay_inside, + vertical_anchor=self.vertical_anchor, + horizontal_anchor=self.horizontal_anchor, + ) self.viewport_component.view_bounds = self.bounds self.viewport_component.component = self.component - #self.viewport_component.view_position = [0, 0] + self.viewport_component._initialize_position() self.add(self.viewport_component) def _alternate_vsb_changed(self, old, new): diff --git a/enable/tests/viewport_test_case.py b/enable/tests/viewport_test_case.py index 89f43bc6e..257ccd2a7 100644 --- a/enable/tests/viewport_test_case.py +++ b/enable/tests/viewport_test_case.py @@ -14,6 +14,9 @@ def test_basic_viewport(self): view_bounds=[50.0, 50.0], position=[0,0], bounds=[50,50]) + + self.assertEqual(view.view_position, [10, 10]) + print view.components_at(0.0, 0.0), view.view_position self.assert_(view.components_at(0.0, 0.0)[0] == component) self.assert_(view.components_at(44.9, 0.0)[0] == component) self.assert_(view.components_at(0.0, 44.9)[0] == component) @@ -23,7 +26,263 @@ def test_basic_viewport(self): self.assert_(view.components_at(46.0, 0.0) == []) self.assert_(view.components_at(45.0, 46.0) == []) self.assert_(view.components_at(0.0, 46.0) == []) - return + + def test_initial_position(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + position=[0,0], + bounds=[50,50]) + self.assertEqual(view.view_position, [0, 0]) + + def test_initial_position_vanchor_top(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + vertical_anchor='top', + view_bounds=[50.0, 50.0], + position=[0,0], + bounds=[50,50]) + self.assertEqual(view.view_position, [0, 50]) + + def test_initial_position_vanchor_center(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + vertical_anchor='center', + view_bounds=[50.0, 50.0], + position=[0,0], + bounds=[50,50]) + self.assertEqual(view.view_position, [0, 25]) + + def test_initial_position_hanchor_right(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + horizontal_anchor='right', + view_bounds=[50.0, 50.0], + position=[0,0], + bounds=[50,50]) + self.assertEqual(view.view_position, [50, 0]) + + def test_initial_position_hanchor_center(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + horizontal_anchor='center', + view_bounds=[50.0, 50.0], + position=[0,0], + bounds=[50,50]) + self.assertEqual(view.view_position, [25, 0]) + + def test_adjust_bounds(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[10,10], + position=[0,0], + bounds=[50,50]) + + # simple resize + view.bounds = [60, 60] + self.assertEqual(view.view_position, [10, 10]) + + def test_adjust_bounds_vanchor_top(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + vertical_anchor='top') + + # simple resize + view.bounds = [60, 60] + self.assertEqual(view.view_position, [20, 10.0]) + + # resize beyond bottom + view.bounds = [80, 80] + self.assertEqual(view.view_position, [20, -10.0]) + + # resize bigger than view + view.bounds = [120, 120] + self.assertEqual(view.view_position, [20, -50.0]) + + def test_adjust_bounds_vanchor_top_stay_inside(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + vertical_anchor='top', + stay_inside=True) + + # simple resize + view.bounds = [60, 60] + self.assertEqual(view.view_position, [20, 10.0]) + + # resize beyond bottom + view.bounds = [80, 80] + self.assertEqual(view.view_position, [20, 0.0]) + + # resize bigger than view + view.bounds = [120, 120] + self.assertEqual(view.view_position, [0, -20.0]) + + def test_adjust_bounds_vanchor_center(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + vertical_anchor='center') + + # simple resize + view.bounds = [60, 60] + self.assertEqual(view.view_position, [20, 15.0]) + + # resize beyond bottom + view.bounds = [95, 95] + self.assertEqual(view.view_position, [20, -2.5]) + + # resize bigger than view + view.bounds = [120, 120] + self.assertEqual(view.view_position, [20, -15.0]) + + def test_adjust_bounds_vanchor_center_stay_inside(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + vertical_anchor='center', + stay_inside=True) + + # simple resize + view.bounds = [60, 60] + self.assertEqual(view.view_position, [20, 15.0]) + + # resize beyond bottom + view.bounds = [95, 95] + self.assertEqual(view.view_position, [5, 0.0]) + + # resize bigger than view + view.bounds = [120, 120] + self.assertEqual(view.view_position, [0, -10.0]) + + def test_adjust_bounds_hanchor_top(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + horizontal_anchor='right') + + # simple resize + view.bounds = [60, 60] + self.assertEqual(view.view_position, [10, 20.0]) + + # resize beyond bottom + view.bounds = [80, 80] + self.assertEqual(view.view_position, [-10, 20.0]) + + # resize bigger than view + view.bounds = [120, 120] + self.assertEqual(view.view_position, [-50, 20.0]) + + def test_adjust_bounds_hanchor_top_stay_inside(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + horizontal_anchor='right', + stay_inside=True) + + # simple resize + view.bounds = [60, 60] + self.assertEqual(view.view_position, [10, 20.0]) + + # resize beyond bottom + view.bounds = [80, 80] + self.assertEqual(view.view_position, [0, 20.0]) + + # resize bigger than view + view.bounds = [120, 120] + self.assertEqual(view.view_position, [-20.0, 0]) + + def test_adjust_bounds_hanchor_center(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + horizontal_anchor='center') + + # simple resize + view.bounds = [60, 60] + self.assertEqual(view.view_position, [15.0, 20]) + + # resize beyond bottom + view.bounds = [95, 95] + self.assertEqual(view.view_position, [-2.5, 20]) + + # resize bigger than view + view.bounds = [120, 120] + self.assertEqual(view.view_position, [-15.0, 20]) + + def test_adjust_bounds_hanchor_center_stay_inside(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + horizontal_anchor='center', + stay_inside=True) + + # simple resize + view.bounds = [60, 60] + self.assertEqual(view.view_position, [15.0, 20]) + + # resize beyond bottom + view.bounds = [95, 95] + self.assertEqual(view.view_position, [0.0, 5]) + + # resize bigger than view + view.bounds = [120, 120] + self.assertEqual(view.view_position, [-10.0, 0]) + if __name__ == "__main__": diff --git a/enable/viewport.py b/enable/viewport.py index 03e9f9fb9..167319281 100644 --- a/enable/viewport.py +++ b/enable/viewport.py @@ -38,11 +38,10 @@ class Viewport(Component): # Whether or not this viewport should stay constrained to the bounds # of the viewed component - # TODO: Implement this - stay_inside = Bool(True) + stay_inside = Bool(False) # Where to anchor vertically on resizes - vertical_anchor = Enum('top', 'bottom', 'top', 'center') + vertical_anchor = Enum('bottom', 'top', 'center') # Where to anchor vertically on resizes horizontal_anchor = Enum('left', 'right', 'center') @@ -76,7 +75,8 @@ class Viewport(Component): def __init__(self, **traits): Component.__init__(self, **traits) - if self.component is not None: + # can't use a default because need bounds to be set first + if 'view_position' not in traits and self.component is not None: self._initialize_position() if 'zoom_tool' not in traits: self.zoom_tool = ViewportZoomTool(self) @@ -288,9 +288,7 @@ def _component_changed(self, old, new): if (new is not None) and (self not in new.viewports): new.viewports.append(self) - self._initialize_position() self._update_component_view_bounds() - return def _bounds_changed(self, old, new): Component._bounds_changed(self, old, new) @@ -314,7 +312,7 @@ def _get_position(self): def _get_bounds(self): return self.view_bounds - def _initialize_position(self): + def _initial_position(self): x = 0 y = 0 if self.vertical_anchor == 'top': @@ -325,7 +323,10 @@ def _initialize_position(self): x = self.component.width-self.view_bounds[0] elif self.horizontal_anchor == 'center': x = (self.component.width-self.view_bounds[0])/2.0 - self.trait_set(view_position=[x, y]) + return [x, y] + + def _initialize_position(self): + self.trait_set(view_position=self._initial_position()) def _adjust_bounds(self, delta_x, delta_y): w, h = self.view_bounds @@ -337,16 +338,32 @@ def _adjust_bounds(self, delta_x, delta_y): y -= delta_y elif self.vertical_anchor == 'center': y -= delta_y/2 - if self.stay_inside and self.component is not None and \ - new_h <= self.component.height: - y = max(0, y) if self.horizontal_anchor == 'right': x -= delta_x elif self.horizontal_anchor == 'center': x -= delta_x/2 - if self.stay_inside and self.component is not None and \ - new_w <= self.component.width: - x = max(0, x) + + if self.stay_inside and self.component is not None: + extra_height = self.component.height - new_h + extra_width = self.component.width - new_w + + if extra_height >= 0: + y = min(max(0, y), extra_height) + elif self.vertical_anchor == 'top': + y = extra_height + elif self.vertical_anchor == 'center': + y = extra_height/2 + else: + y = 0 + + if extra_width >= 0: + x = min(max(0, x), extra_width) + elif self.horizontal_anchor == 'right': + x = extra_width + elif self.horizontal_anchor == 'center': + x = extra_width/2 + else: + x = 0 self.trait_set(view_bounds=[new_w, new_h], view_position=[x, y]) diff --git a/examples/enable/scrolled_canvas_demo.py b/examples/enable/scrolled_canvas_demo.py index d5cc3b184..726cbc83f 100644 --- a/examples/enable/scrolled_canvas_demo.py +++ b/examples/enable/scrolled_canvas_demo.py @@ -32,8 +32,10 @@ def _create_window(self): j*spacing + offset - boxsize/2 + 0.5] canvas.add(box) - viewport = Viewport(component=canvas, enable_zoom=True) - viewport.view_position = [0,0] + viewport = Viewport(component=canvas, enable_zoom=True, + vertical_anchor='center', + horizontal_anchor='center') + #viewport.view_position = [0,0] viewport.tools.append(ViewportPanTool(viewport)) # Uncomment the following to enforce limits on the zoom diff --git a/examples/enable/scrolled_demo.py b/examples/enable/scrolled_demo.py index a25aa1d86..36fe85672 100644 --- a/examples/enable/scrolled_demo.py +++ b/examples/enable/scrolled_demo.py @@ -117,14 +117,15 @@ class MyFrame(DemoFrame): def _create_window(self): - container = Container(bounds=[800,600], bgcolor=(0.9, 0.7, 0.7, 1.0), + container = Container(bounds=[800, 600], bgcolor=(0.9, 0.7, 0.7, 1.0), auto_size=False, fit_window=False) circle1 = Circle(bounds=[75,75], position=[100,100], shadow_type="dashed") container.add(circle1) scr = Scrolled(container, bounds=[200,200], position=[50,50], - fit_window=False) + stay_inside=True, vertical_anchor='top', + horizontal_anchor='left', fit_window=False) return Window(self, -1, component=scr) From c4ba76af2002f474604276e15ba1e6207f7f8366 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 21 Aug 2015 15:27:08 +0100 Subject: [PATCH 04/10] Fix copypasta in comments [skip ci] --- enable/tests/viewport_test_case.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/enable/tests/viewport_test_case.py b/enable/tests/viewport_test_case.py index 257ccd2a7..4b71e452d 100644 --- a/enable/tests/viewport_test_case.py +++ b/enable/tests/viewport_test_case.py @@ -181,7 +181,7 @@ def test_adjust_bounds_vanchor_center_stay_inside(self): view.bounds = [60, 60] self.assertEqual(view.view_position, [20, 15.0]) - # resize beyond bottom + # resize beyond left view.bounds = [95, 95] self.assertEqual(view.view_position, [5, 0.0]) @@ -204,7 +204,7 @@ def test_adjust_bounds_hanchor_top(self): view.bounds = [60, 60] self.assertEqual(view.view_position, [10, 20.0]) - # resize beyond bottom + # resize beyond left view.bounds = [80, 80] self.assertEqual(view.view_position, [-10, 20.0]) @@ -228,7 +228,7 @@ def test_adjust_bounds_hanchor_top_stay_inside(self): view.bounds = [60, 60] self.assertEqual(view.view_position, [10, 20.0]) - # resize beyond bottom + # resize beyond left view.bounds = [80, 80] self.assertEqual(view.view_position, [0, 20.0]) @@ -251,7 +251,7 @@ def test_adjust_bounds_hanchor_center(self): view.bounds = [60, 60] self.assertEqual(view.view_position, [15.0, 20]) - # resize beyond bottom + # resize beyond left view.bounds = [95, 95] self.assertEqual(view.view_position, [-2.5, 20]) @@ -275,7 +275,7 @@ def test_adjust_bounds_hanchor_center_stay_inside(self): view.bounds = [60, 60] self.assertEqual(view.view_position, [15.0, 20]) - # resize beyond bottom + # resize beyond left view.bounds = [95, 95] self.assertEqual(view.view_position, [0.0, 5]) From 8865117ff91e460089a2229b9247e29d1316b8d0 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 25 Aug 2015 11:30:37 +0100 Subject: [PATCH 05/10] Fix handling of component resizes; refactor to make algorithm clearer. --- enable/tests/viewport_test_case.py | 108 +++++++++++++++++++- enable/viewport.py | 159 ++++++++++++++++++++++++----- 2 files changed, 239 insertions(+), 28 deletions(-) diff --git a/enable/tests/viewport_test_case.py b/enable/tests/viewport_test_case.py index 4b71e452d..bde7a7e1d 100644 --- a/enable/tests/viewport_test_case.py +++ b/enable/tests/viewport_test_case.py @@ -276,17 +276,117 @@ def test_adjust_bounds_hanchor_center_stay_inside(self): self.assertEqual(view.view_position, [15.0, 20]) # resize beyond left - view.bounds = [95, 95] + view.height = 95 + view.width = 95 self.assertEqual(view.view_position, [0.0, 5]) # resize bigger than view - view.bounds = [120, 120] + view.bounds[0] = 120 + view.bounds[1] = 120 self.assertEqual(view.view_position, [-10.0, 0]) + def test_adjust_container_bounds_vanchor_top(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + vertical_anchor='top') + + # simple resize bigger + container.bounds = [120, 120] + self.assertEqual(view.view_position, [20, 40.0]) + # simple resize smaller + container.height = 90 + container.width = 90 + self.assertEqual(view.view_position, [20, 10.0]) + + # simple resize much smaller + container.bounds[0] = 40 + container.bounds[1] = 40 + self.assertEqual(view.view_position, [20, -40.0]) + + def test_adjust_container_bounds_vanchor_top_stay_inside(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + vertical_anchor='top', + stay_inside=True) + + # simple resize bigger + container.bounds = [120, 120] + self.assertEqual(view.view_position, [20, 40.0]) + + # simple resize smaller + container.height = 90 + container.width = 90 + self.assertEqual(view.view_position, [20, 10.0]) + + # simple resize much smaller + container.bounds[0] = 40 + container.bounds[1] = 40 + self.assertEqual(view.view_position, [0, -10.0]) + + def test_adjust_container_bounds_hanchor_right(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + horizontal_anchor='right') + + # simple resize bigger + container.bounds = [120, 120] + self.assertEqual(view.view_position, [40, 20.0]) + + # simple resize smaller + container.height = 90 + container.width = 90 + self.assertEqual(view.view_position, [10, 20.0]) + + # simple resize much smaller + container.bounds[0] = 40 + container.bounds[1] = 40 + self.assertEqual(view.view_position, [-40, 20.0]) + + def test_adjust_container_bounds_hanchor_right_stay_inside(self): + container = Container(bounds=[100.0, 100.0]) + component = Component(bounds=[50.0, 50.0], position=[5.0, 5.0]) + container.add(component) + view = Viewport(component=container, + view_bounds=[50.0, 50.0], + view_position=[20, 20], + position=[0,0], + bounds=[50,50], + vertical_anchor='top', + stay_inside=True) + + # simple resize bigger + container.bounds = [120, 120] + self.assertEqual(view.view_position, [20, 40.0]) + + # simple resize smaller + container.height = 90 + container.width = 90 + self.assertEqual(view.view_position, [20, 10.0]) + + # simple resize much smaller + container.bounds[0] = 40 + container.bounds[1] = 40 + self.assertEqual(view.view_position, [0, -10.0]) if __name__ == "__main__": import nose nose.main() - -# EOF diff --git a/enable/viewport.py b/enable/viewport.py index 167319281..8f7bfc6f0 100644 --- a/enable/viewport.py +++ b/enable/viewport.py @@ -290,6 +290,22 @@ def _component_changed(self, old, new): new.viewports.append(self) self._update_component_view_bounds() + @on_trait_change('component:bounds') + def _component_bounds_updated(self, obj, name, old, new): + if name == 'bounds': + delta_x = new[0] - old[0] + delta_y = new[1] - old[1] + elif name == 'bounds_items': + delta_x = 0 + delta_y = 0 + if new.index == 1: + delta_y = new.added[0] - new.removed[0] + else: + delta_x = new.added[0] - new.removed[0] + if len(new.removed) == 2: + delta_y = new.added[1] - new.removed[1] + self._adjust_view_from_component_resize(delta_x, delta_y) + def _bounds_changed(self, old, new): Component._bounds_changed(self, old, new) new_w = new[0]/self.zoom @@ -297,7 +313,7 @@ def _bounds_changed(self, old, new): w, h = self.view_bounds delta_x = new_w - w delta_y = new_h - h - self._adjust_bounds(delta_x, delta_y) + self._adjust_view_from_viewport_resize(delta_x, delta_y) def _bounds_items_changed(self, event): return self._bounds_changed(None, self.bounds) @@ -328,12 +344,80 @@ def _initial_position(self): def _initialize_position(self): self.trait_set(view_position=self._initial_position()) - def _adjust_bounds(self, delta_x, delta_y): + def _adjust_view_from_viewport_resize(self, delta_x, delta_y): + """ The viewport has been resized, so need to adjust view parameters + + This computes the new view bounds, shifts the view position depending + on the horizontal and vertical anchoring, and if we are trying to stay + inside the viewed component, adjusts for that as well. + + Parameters + ---------- + delta_x : float + The change in width of the view_bounds in viewed component + coordinates. + + delta_y : float + The change in height of the view_bounds in viewed component + coordinates. + """ + # resize the view w, h = self.view_bounds - x, y = self.view_position new_w = w + delta_x new_h = h + delta_y + x, y = self._shift_view_position(delta_x, delta_y) + + if self.stay_inside and self.component is not None: + extra_width = self.component.width - new_w + extra_height = self.component.height - new_h + x, y = self._adjust_stay_inside(x, y, extra_width, extra_height) + + self.trait_set(view_bounds=[new_w, new_h], view_position=[x, y]) + + def _adjust_view_from_component_resize(self, delta_x, delta_y): + """ The viewport has been resized, so need to adjust view parameters + + This shifts the view position depending on the horizontal and vertical + anchoring, and if we are trying to stay inside the viewed component, + adjusts for that as well. The view bounds should not change from a + component resize. + + Parameters + ---------- + delta_x : float + The change in width of the viewed component. + + delta_y : float + The change in height of the viewed component. + """ + x, y = self._shift_view_position(-delta_x, -delta_y) + + if self.stay_inside and self.component is not None: + w, h = self.view_bounds + extra_width = self.component.width - w + extra_height = self.component.height - h + x, y = self._adjust_stay_inside(x, y, extra_width, extra_height) + + self.trait_set(view_position=[x, y]) + + def _shift_view_position(self, delta_x, delta_y): + """ Compute new view position, accounting for anchoring + + Parameters + ---------- + delta_x : float + The change in width that needs to be accounted for. + + delta_y : float + The change in height that needs to be accounted for. + + Returns + ------- + position : tuple of float, float + The new x, y coordinates for the view position. + """ + x, y = self.view_position if self.vertical_anchor == 'top': y -= delta_y elif self.vertical_anchor == 'center': @@ -343,27 +427,54 @@ def _adjust_bounds(self, delta_x, delta_y): x -= delta_x elif self.horizontal_anchor == 'center': x -= delta_x/2 + return x, y - if self.stay_inside and self.component is not None: - extra_height = self.component.height - new_h - extra_width = self.component.width - new_w + def _adjust_stay_inside(self, x, y, extra_width, extra_height): + """ Compute new view position, resisting views outside component - if extra_height >= 0: - y = min(max(0, y), extra_height) - elif self.vertical_anchor == 'top': - y = extra_height - elif self.vertical_anchor == 'center': - y = extra_height/2 - else: - y = 0 - - if extra_width >= 0: - x = min(max(0, x), extra_width) - elif self.horizontal_anchor == 'right': - x = extra_width - elif self.horizontal_anchor == 'center': - x = extra_width/2 - else: - x = 0 + The algorithm followed is: - self.trait_set(view_bounds=[new_w, new_h], view_position=[x, y]) + * if the view is smaller than the component, position should be + between 0 and the amount of extra space + * otherwise, expand view outside of component based on anchor point + + Parameters + ---------- + x : float + The proposed x coordinate of the new view position. + + y : float + The proposed y coordinate of the new view position. + + extra_width : float + The difference in width between the viewed component and the view + bounds (may be negative) + + extra_height : float + The difference in height between the viewed component and the view + bounds (may be negative) + + Returns + ------- + position : tuple of float, float + The new x, y coordinates for the view position. + """ + if extra_height >= 0: + y = min(max(0, y), extra_height) + elif self.vertical_anchor == 'top': + y = extra_height + elif self.vertical_anchor == 'center': + y = extra_height/2 + else: + y = 0 + + if extra_width >= 0: + x = min(max(0, x), extra_width) + elif self.horizontal_anchor == 'right': + x = extra_width + elif self.horizontal_anchor == 'center': + x = extra_width/2 + else: + x = 0 + + return x, y From 6af78696e492d830a80b36e62b9ebaacf9bc4e90 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Tue, 25 Aug 2015 11:33:16 +0100 Subject: [PATCH 06/10] Add another test case. --- enable/tests/viewport_test_case.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/enable/tests/viewport_test_case.py b/enable/tests/viewport_test_case.py index bde7a7e1d..0b4efb623 100644 --- a/enable/tests/viewport_test_case.py +++ b/enable/tests/viewport_test_case.py @@ -370,22 +370,22 @@ def test_adjust_container_bounds_hanchor_right_stay_inside(self): view_position=[20, 20], position=[0,0], bounds=[50,50], - vertical_anchor='top', + horizontal_anchor='right', stay_inside=True) # simple resize bigger container.bounds = [120, 120] - self.assertEqual(view.view_position, [20, 40.0]) + self.assertEqual(view.view_position, [40, 20]) # simple resize smaller container.height = 90 container.width = 90 - self.assertEqual(view.view_position, [20, 10.0]) + self.assertEqual(view.view_position, [10, 20]) # simple resize much smaller container.bounds[0] = 40 container.bounds[1] = 40 - self.assertEqual(view.view_position, [0, -10.0]) + self.assertEqual(view.view_position, [-10, 0]) if __name__ == "__main__": import nose From 3f3a2f42b12e7224e1ed350be02ba482e937e260 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Tue, 29 Sep 2015 21:15:47 +0200 Subject: [PATCH 07/10] Add docs and examples to the source distribution --- MANIFEST.in | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 96042441c..f16d67535 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ include kiva/agg/agg.i -include chaco/tests/data/PngSuite/*.png -include chaco/tests/data/PngSuite/LICENSE.txt +include docs/Makefile +include docs/kiva/agg/notes +recursive-include docs *.py *.rst *.txt *.css *.png *.ico *.doc +recursive-include examples *.py *.txt *.gif *.jpg *.enaml From 41134b03b96b741d95554846841c189313052265 Mon Sep 17 00:00:00 2001 From: John Wiggins Date: Thu, 1 Oct 2015 20:40:07 +0200 Subject: [PATCH 08/10] Read the docs version directly from enable._version --- docs/source/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 10b755228..995e315b2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -41,8 +41,8 @@ # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. d = {} -execfile(os.path.join('..', '..', 'enable', '__init__.py'), d) -version = release = d['__version__'] +execfile(os.path.join('..', '..', 'enable', '_version.py'), d) +version = release = d['full_version'] # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: From ca9df9d9f5c9837de703a54a4adea2105e206a3d Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 22 Jan 2016 16:01:41 +0000 Subject: [PATCH 09/10] Fix some old PIL API calls. --- kiva/pdf.py | 10 +++++----- kiva/ps.py | 11 +++++------ kiva/quartz/ABCGI.pyx | 4 ++-- kiva/svg.py | 11 +++++------ 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/kiva/pdf.py b/kiva/pdf.py index 967a73d71..6380c0d25 100644 --- a/kiva/pdf.py +++ b/kiva/pdf.py @@ -575,8 +575,8 @@ def draw_image(self, img, rect=None): # do it nicely (using convert_pixel_format), and if not, we do # it brute-force using Agg. from reportlab.lib.utils import ImageReader - from PIL import Image as PilImage from kiva import agg + from kiva.compat import pilfromstring if type(img) == type(array([])): # Numeric array @@ -596,10 +596,10 @@ def draw_image(self, img, rect=None): return # converted_img now holds an Agg graphics context with the image - pil_img = PilImage.fromstring(format, - (converted_img.width(), - converted_img.height()), - converted_img.bmp_array.tostring()) + pil_img = pilfromstring(format, + (converted_img.width(), + converted_img.height()), + converted_img.bmp_array.tostring()) if rect is None: rect = (0, 0, img.width(), img.height()) diff --git a/kiva/ps.py b/kiva/ps.py index 91ba8786b..463588ef7 100644 --- a/kiva/ps.py +++ b/kiva/ps.py @@ -199,7 +199,7 @@ def device_draw_image(self, img, rect): Requires the Python Imaging Library (PIL). """ - from PIL import Image as PilImage + from kiva.compat import pilfromstring if type(img) == type(array([])): # Numeric array @@ -221,10 +221,10 @@ def device_draw_image(self, img, rect): return # converted_img now holds an Agg graphics context with the image - pil_img = PilImage.fromstring(format, - (converted_img.width(), - converted_img.height()), - converted_img.bmp_array.tostring()) + pil_img = pilfromstring(format, + (converted_img.width(), + converted_img.height()), + converted_img.bmp_array.tostring()) if rect == None: rect = (0, 0, img.width(), img.height()) @@ -339,4 +339,3 @@ def device_update_line_state(self): def device_update_fill_state(self): pass - diff --git a/kiva/quartz/ABCGI.pyx b/kiva/quartz/ABCGI.pyx index d8f3ea6aa..cf52b2029 100644 --- a/kiva/quartz/ABCGI.pyx +++ b/kiva/quartz/ABCGI.pyx @@ -1604,7 +1604,7 @@ cdef class CGBitmapContext(CGContext): """ try: - from PIL import Image + from kiva.compat import pilfromstring except ImportError: raise ImportError("need PIL (or Pillow) to save images") @@ -1623,7 +1623,7 @@ cdef class CGBitmapContext(CGContext): if file_format is None: file_format = '' - img = Image.fromstring(mode, (self.width(), self.height()), self) + img = pilfromstring(mode, (self.width(), self.height()), self) if 'A' in mode: # Check the output format to see if it can handle an alpha channel. no_alpha_formats = ('jpg', 'bmp', 'eps', 'jpeg') diff --git a/kiva/svg.py b/kiva/svg.py index 895124e96..3a66b6864 100644 --- a/kiva/svg.py +++ b/kiva/svg.py @@ -221,7 +221,7 @@ def device_draw_image(self, img, rect): Requires the Python Imaging Library (PIL). """ - from PIL import Image as PilImage + from kiva.compat import pilfromstring, Image as PilImage # We turn img into a PIL object, since that is what ReportLab # requires. To do this, we first determine if the input image @@ -250,10 +250,10 @@ def device_draw_image(self, img, rect): return # converted_img now holds an Agg graphics context with the image - pil_img = PilImage.fromstring(format, - (converted_img.width(), - converted_img.height()), - converted_img.bmp_array.tostring()) + pil_img = pilfromstring(format, + (converted_img.width(), + converted_img.height()), + converted_img.bmp_array.tostring()) if rect == None: rect = (0, 0, img.width(), img.height()) @@ -439,4 +439,3 @@ def font_metrics_provider(): SVGGC = GraphicsContext # for b/w compatibility - From 372af8b11d5c58f16dc952dad87279c33895faad Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Fri, 22 Jan 2016 16:04:10 +0000 Subject: [PATCH 10/10] And one more in savage. --- enable/savage/svg/document.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/enable/savage/svg/document.py b/enable/savage/svg/document.py index e899352ae..ba356551b 100644 --- a/enable/savage/svg/document.py +++ b/enable/savage/svg/document.py @@ -205,11 +205,12 @@ def open_image(self, path): fin = open(path) from PIL import Image + from kiva.compat import piltostring import numpy pil_img = Image.open(fin) if pil_img.mode not in ('RGB', 'RGBA'): pil_img = pil_img.convert('RGBA') - img = numpy.fromstring(pil_img.tostring(), numpy.uint8) + img = numpy.fromstring(piltostring(pil_img), numpy.uint8) shape = (pil_img.size[1],pil_img.size[0],len(pil_img.mode)) img.shape = shape return img