4
from enthought.enable.api import Pointer
5
from enthought.enable.tools.drag_tool import DragTool
6
from enthought.traits.api import Bool, Enum, Float, Tuple
9
class PanTool(DragTool):
10
""" An implementation of a pan tool based on the DragTool instead of
14
# The cursor to use when panning.
15
drag_pointer = Pointer("hand")
17
# Scaling factor on the panning "speed".
20
# The modifier key that, if depressed when the drag is initiated, constrains
21
# the panning to happen in the only direction of largest initial motion.
22
# It is possible to permanently restrict this tool to always drag along one
23
# direction. To do so, set constrain=True, constrain_key=None, and
24
# constrain_direction to the desired direction.
25
constrain_key = Enum(None, "shift", "control", "alt")
27
# Constrain the panning to one direction?
28
constrain = Bool(False)
30
# The direction of constrained draw. A value of None means that the user
31
# has initiated the drag and pressed the constrain_key, but hasn't moved
32
# the mouse yet; the magnitude of the components of the next mouse_move
33
# event will determine the constrain_direction.
34
constrain_direction = Enum(None, "x", "y")
36
# Restrict to the bounds of the plot data
37
restrict_to_data = Bool(False)
39
# (x,y) of the point where the mouse button was pressed.
42
# Data coordinates of **_original_xy**. This may be either (index,value)
43
# or (value,index) depending on the component's orientation.
44
_original_data = Tuple
46
# Was constrain=True triggered by the **contrain_key**? If False, it was
47
# set programmatically.
48
_auto_constrain = Bool(False)
50
#------------------------------------------------------------------------
51
# Inherited BaseTool traits
52
#------------------------------------------------------------------------
54
# The tool does not have a visual representation (overrides
58
# The tool is not visible (overrides BaseTool).
61
# The possible event states of this tool (overrides enable.Interactor).
62
#event_state = Enum("normal", "panning")
65
def drag_start(self, event):
66
""" Called when the drag operation starts """
67
self._start_pan(event)
69
def dragging(self, event):
72
if self._auto_constrain and self.constrain_direction is None:
73
# Determine the constraint direction
74
if abs(event.x - self._original_xy[0]) > abs(event.y - self._original_xy[1]):
75
self.constrain_direction = "x"
77
self.constrain_direction = "y"
79
for direction, bound_name, ndx in [("x","width",0), ("y","height",1)]:
80
if not self.constrain or self.constrain_direction == direction:
81
mapper = getattr(plot, direction + "_mapper")
83
domain_min, domain_max = mapper.domain_limits
84
eventpos = getattr(event, direction)
85
origpos = self._original_xy[ndx]
87
screenlow, screenhigh = mapper.screen_bounds
88
screendelta = self.speed * (eventpos - origpos)
89
#if getattr(plot, direction + "_direction", None) == "flipped":
90
# screendelta = -screendelta
92
newlow = mapper.map_data(screenlow - screendelta)
93
newhigh = mapper.map_data(screenhigh - screendelta)
95
# Don't set the range in this dimension if the panning
96
# would exceed the domain limits.
97
# To do this offset properly, we would need to iteratively
98
# solve for a root using map_data on successive trial
99
# values. As a first approximation, we're just going to
100
# use a linear approximation, which works perfectly for
101
# linear mappers (which is used 99% of the time).
102
if domain_min is None:
103
if self.restrict_to_data:
104
domain_min = min([source.get_data().min() for source in range.sources])
107
if domain_max is None:
108
if self.restrict_to_data:
109
domain_max = max([source.get_data().max() for source in range.sources])
112
if (newlow <= domain_min) and (newhigh >= domain_max):
113
# Don't do anything; effectively, freeze the pan
115
if newlow <= domain_min:
116
delta = newhigh - newlow
118
# Don't let the adjusted newhigh exceed domain_max; this
119
# can happen with a nonlinear mapper.
120
newhigh = min(domain_max, domain_min + delta)
121
elif newhigh >= domain_max:
122
delta = newhigh - newlow
124
# Don't let the adjusted newlow go below domain_min; this
125
# can happen with a nonlinear mapper.
126
newlow = max(domain_min, domain_max - delta)
128
# Use .set_bounds() so that we don't generate two range_changed
129
# events on the DataRange
130
range.set_bounds(newlow, newhigh)
134
self._original_xy = (event.x, event.y)
135
plot.request_redraw()
138
def drag_cancel(self, event):
139
# We don't do anything for "cancelling" of the drag event because its
140
# transient states during the drag are generally valid and useful
141
# terminal states unto themselves.
144
def drag_end(self, event):
145
return self._end_pan(event)
147
def _start_pan(self, event, capture_mouse=True):
148
self._original_xy = (event.x, event.y)
149
if self.constrain_key is not None:
150
if getattr(event, self.constrain_key + "_down"):
151
self.constrain = True
152
self._auto_constrain = True
153
self.constrain_direction = None
154
self.event_state = "panning"
156
event.window.set_pointer(self.drag_pointer)
157
event.window.set_mouse_owner(self, event.net_transform())
161
def _end_pan(self, event):
162
if self._auto_constrain:
163
self.constrain = False
164
self.constrain_direction = None
165
self.event_state = "normal"
166
event.window.set_pointer("arrow")
167
if event.window.mouse_owner == self:
168
event.window.set_mouse_owner(None)