Source code for enable.tools.viewport_zoom_tool
# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" Defines the SimpleZoom class.
"""
from numpy import inf
# Enthought library imports
from traits.api import Bool, Enum, Float, Instance, Int, List, Tuple, Union
# Enable imports
from enable.base_tool import KeySpec
from enable.colors import ColorTrait
from enable.abstract_overlay import AbstractOverlay
from .base_zoom_tool import BaseZoomTool
from .tool_history_mixin import ToolHistoryMixin
[docs]class ViewportZoomTool(AbstractOverlay, ToolHistoryMixin, BaseZoomTool):
""" Selects a range along the index or value axis.
The user left-click-drags to select a region to zoom in.
Certain keyboard keys are mapped to performing zoom actions as well.
Implements a basic "zoom stack" so the user move go backwards and forwards
through previous zoom regions.
"""
# The selection mode:
#
# range:
# Select a range across a single index or value axis.
# box:
# Perform a "box" selection on two axes.
tool_mode = Enum("range", "box") # Enum("box", "range")
# Is the tool always "on"? If True, left-clicking always initiates
# a zoom operation; if False, the user must press a key to enter zoom mode.
always_on = Bool(False)
# -------------------------------------------------------------------------
# Zoom control
# -------------------------------------------------------------------------
# The axis to which the selection made by this tool is perpendicular. This
# only applies in 'range' mode.
axis = Enum("x", "y")
# -------------------------------------------------------------------------
# Interaction control
# -------------------------------------------------------------------------
# Enable the mousewheel for zooming?
enable_wheel = Bool(True)
# The mouse button that initiates the drag.
drag_button = Enum("left", "right")
# Conversion ratio from wheel steps to zoom factors.
wheel_zoom_step = Float(0.25)
# The key press to enter zoom mode, if **always_on** is False.
# Has no effect if **always_on** is True.
enter_zoom_key = Instance(KeySpec, args=("z",))
# The key press to leave zoom mode, if **always_on** is False.
# Has no effect if **always_on** is True.
exit_zoom_key = Instance(KeySpec, args=("z",))
# Disable the tool after the zoom is completed?
disable_on_complete = Bool(True)
# The minimum amount of screen space the user must select in order for
# the tool to actually take effect.
minimum_screen_delta = Int(10)
# The most that this tool will zoom in on the target. Since zoom is the
# ratio of the original bounds to the new bounds, a max_zoom value of 2.0
# would make the tool stop once it had zoomed into a region half the size
# of the original bounds.
max_zoom = Float(inf)
# The most that this tool will zoom out from the target. For example,
# a min_zoom of 0.2 would prevent the tool from showing a view zoomed
# out more than 5 times from the original bounds.
min_zoom = Float(-inf)
# -------------------------------------------------------------------------
# Appearance properties (for Box mode)
# -------------------------------------------------------------------------
# The pointer to use when drawing a zoom box.
pointer = "magnifier"
# The color of the selection box.
color = ColorTrait("lightskyblue")
# The alpha value to apply to **color** when filling in the selection
# region. Because it is almost certainly useless to have an opaque zoom
# rectangle, but it's also extremely useful to be able to use the normal
# named colors from Enable, this attribute allows the specification of a
# separate alpha value that replaces the alpha value of **color** at draw
# time.
alpha = Union(None, Float, default_value=0.4)
# The color of the outside selection rectangle.
border_color = ColorTrait("dodgerblue")
# The thickness of selection rectangle border.
border_size = Int(1)
# The possible event states of this zoom tool.
event_state = Enum("normal", "selecting")
# ------------------------------------------------------------------------
# Key mappings
# ------------------------------------------------------------------------
# The key that cancels the zoom and resets the view to the original
# defaults.
cancel_zoom_key = Instance(KeySpec, args=("Esc",))
# ------------------------------------------------------------------------
# Private traits
# ------------------------------------------------------------------------
# If **always_on** is False, this attribute indicates whether the tool
# is currently enabled.
_enabled = Bool(False)
# the original numerical screen ranges
_orig_position = Union(None, List, Float)
_orig_bounds = Union(None, List, Float)
# The (x,y) screen point where the mouse went down.
_screen_start = Union(None, Tuple)
# The (x,,y) screen point of the last seen mouse move event.
_screen_end = Union(None, Tuple)
def __init__(self, component=None, *args, **kw):
# Support [Chaco's] AbstractController-style constructors which allow
# the component as the first positional argument.
if component is not None:
kw["component"] = component
super().__init__(**kw)
self._reset_state_to_current()
if self.tool_mode == "range":
i = self._get_range_index()
self._orig_position = self.component.view_position[i]
self._orig_bounds = self.component.view_bounds[i]
else:
self._orig_position = self.component.view_position
self._orig_bounds = self.component.view_bounds
[docs] def enable(self, event=None):
""" Provides a programmatic way to enable this tool, if
**always_on** is False.
Calling this method has the same effect as if the user pressed the
**enter_zoom_key**.
"""
if self.component.active_tool != self:
self.component.active_tool = self
self._enabled = True
if event and event.window:
event.window.set_pointer(self.pointer)
[docs] def disable(self, event=None):
""" Provides a programmatic way to enable this tool, if **always_on**
is False.
Calling this method has the same effect as if the user pressed the
**exit_zoom_key**.
"""
self.reset()
self._enabled = False
if self.component.active_tool == self:
self.component.active_tool = None
if event and event.window:
event.window.set_pointer("arrow")
[docs] def reset(self, event=None):
""" Resets the tool to normal state, with no start or end position.
"""
self.event_state = "normal"
self._screen_start = None
self._screen_end = None
[docs] def deactivate(self, component):
""" Called when this is no longer the active tool.
"""
# Required as part of the AbstractController interface.
return self.disable()
[docs] def normal_left_down(self, event):
""" Handles the left mouse button being pressed while the tool is
in the 'normal' state.
If the tool is enabled or always on, it starts selecting.
"""
if self.always_on or self._enabled:
# we need to make sure that there isn't another active tool that
# we will interfere with.
if self.drag_button == "left":
self._start_select(event)
[docs] def normal_right_down(self, event):
""" Handles the right mouse button being pressed while the tool is
in the 'normal' state.
If the tool is enabled or always on, it starts selecting.
"""
if self.always_on or self._enabled:
if self.drag_button == "right":
self._start_select(event)
[docs] def normal_mouse_wheel(self, event):
""" Handles the mouse wheel being used when the tool is in the 'normal'
state.
Scrolling the wheel "up" zooms in; scrolling it "down" zooms out.
self.component is the viewport
self.component.component is the canvas
"""
if self.enable_wheel and event.mouse_wheel != 0:
position = self.component.view_position
scale = self.component.zoom
transformed_x = event.x / scale + position[0]
transformed_y = event.y / scale + position[1]
# Calculate zoom
if event.mouse_wheel < 0:
zoom = 1.0 / (1.0 + 0.5 * self.wheel_zoom_step)
new_zoom = self.component.zoom * zoom
elif event.mouse_wheel > 0:
zoom = 1.0 + 0.5 * self.wheel_zoom_step
new_zoom = self.component.zoom * zoom
if new_zoom < self.min_zoom:
new_zoom = self.min_zoom
zoom = new_zoom / self.component.zoom
elif new_zoom > self.max_zoom:
new_zoom = self.max_zoom
zoom = new_zoom / self.component.zoom
self.component.zoom = new_zoom
x_pos = transformed_x - (transformed_x - position[0]) / zoom
y_pos = transformed_y - (transformed_y - position[1]) / zoom
self.component.trait_setq(view_position=[x_pos, y_pos])
bounds = self.component.view_bounds
self.component.view_bounds = [bounds[0] / zoom, bounds[1] / zoom]
event.handled = True
self.component.request_redraw()
def _component_changed(self):
self._reset_state_to_current()
# ------------------------------------------------------------------------
# Implementation of PlotComponent interface
# ------------------------------------------------------------------------
def _activate(self):
""" Called by PlotComponent to set this as the active tool.
"""
self.enable()
# ------------------------------------------------------------------------
# implementations of abstract methods on ToolHistoryMixin
# ------------------------------------------------------------------------
def _reset_state_to_current(self):
""" Clears the tool history, and sets the current state to be the
first state in the history.
"""
if self.tool_mode == "range":
i = self._get_range_index()
self._reset_state(
(
self.component.view_position[i],
self.component.view_bounds[i],
)
)
else:
self._reset_state(
(self.component.view_position, self.component.view_bounds)
)