1
from __future__ import with_statement
5
from chaco.abstract_overlay import AbstractOverlay
6
from enable.api import ColorTrait, KeySpec
7
from traits.api import Bool, Enum, Trait, Int, Float, Tuple, \
8
Instance, DelegatesTo, Property
9
from traits.util.deprecated import deprecated
11
from better_zoom import BetterZoom, ZoomState
12
from tool_states import SelectedZoomState
14
class BetterSelectingZoom(AbstractOverlay, BetterZoom):
15
""" Zooming tool which allows the user to draw a box which defines the
16
desired region to zoom in to
22
# Select a range across a single index or value axis.
24
# Perform a "box" selection on two axes.
25
tool_mode = Enum("box", "range")
27
# Is the tool always "on"? If True, left-clicking always initiates
28
# a zoom operation; if False, the user must press a key to enter zoom mode.
29
always_on = Bool(False)
31
# Defines a meta-key, that works with always_on to set the zoom mode. This
32
# is useful when the zoom tool is used in conjunction with the pan tool.
33
always_on_modifier = Enum('control', 'shift', 'control', 'alt')
35
# The mouse button that initiates the drag. If "None", then the tool
36
# will not respond to drag. (It can still respond to mousewheel events.)
37
drag_button = Enum("left", "right", None)
39
# The minimum amount of screen space the user must select in order for
40
# the tool to actually take effect.
41
minimum_screen_delta = Int(10)
44
#-------------------------------------------------------------------------
45
# deprecated interaction controls, used for API compatability with
47
#-------------------------------------------------------------------------
49
# Conversion ratio from wheel steps to zoom factors.
50
wheel_zoom_step = Property(Float, depends_on='zoom_factor')
52
# The key press to enter zoom mode, if **always_on** is False. Has no effect
53
# if **always_on** is True.
54
enter_zoom_key = Instance(KeySpec, args=("z",))
56
# The key press to leave zoom mode, if **always_on** is False. Has no effect
57
# if **always_on** is True.
58
exit_zoom_key = Instance(KeySpec, args=("z",))
60
# Disable the tool after the zoom is completed?
61
disable_on_complete = Property()
64
#-------------------------------------------------------------------------
65
# Appearance properties (for Box mode)
66
#-------------------------------------------------------------------------
68
# The pointer to use when drawing a zoom box.
71
# The color of the selection box.
72
color = ColorTrait("lightskyblue")
74
# The alpha value to apply to **color** when filling in the selection
75
# region. Because it is almost certainly useless to have an opaque zoom
76
# rectangle, but it's also extremely useful to be able to use the normal
77
# named colors from Enable, this attribute allows the specification of a
78
# separate alpha value that replaces the alpha value of **color** at draw
80
alpha = Trait(0.4, None, Float)
82
# The color of the outside selection rectangle.
83
border_color = ColorTrait("dodgerblue")
85
# The thickness of selection rectangle border.
88
# The possible event states of this zoom tool.
89
event_state = Enum("normal", "selecting", "pre_selecting")
91
# The (x,y) screen point where the mouse went down.
92
_screen_start = Trait(None, None, Tuple)
94
# The (x,,y) screen point of the last seen mouse move event.
95
_screen_end = Trait(None, None, Tuple)
97
# If **always_on** is False, this attribute indicates whether the tool
98
# is currently enabled.
99
_enabled = Bool(False)
101
def __init__(self, component=None, *args, **kw):
102
# Since this class uses multiple inheritance (eek!), lets be
103
# explicit about the order of the parent class constructors
104
AbstractOverlay.__init__(self, component, *args, **kw)
105
BetterZoom.__init__(self, component, *args, **kw)
107
def reset(self, event=None):
108
""" Resets the tool to normal state, with no start or end position.
110
self.event_state = "normal"
111
self._screen_start = None
112
self._screen_end = None
114
#--------------------------------------------------------------------------
115
# BetterZoom interface
116
#--------------------------------------------------------------------------
118
def normal_key_pressed(self, event):
119
""" Handles a key being pressed when the tool is in the 'normal'
122
if not self.always_on:
123
if self.enter_zoom_key.match(event) and not self._enabled:
124
self.event_state = 'pre_selecting'
125
event.window.set_pointer(self.pointer)
126
event.window.set_mouse_owner(self, event.net_transform())
129
elif self.exit_zoom_key.match(event) and self._enabled:
130
self.state = 'normal'
131
self._end_select(event)
134
if not event.handled:
135
super(BetterSelectingZoom, self).normal_key_pressed(event)
137
def normal_left_down(self, event):
138
""" Handles the left mouse button being pressed while the tool is
139
in the 'normal' state.
141
If the tool is enabled or always on, it starts selecting.
143
if self._is_enabling_event(event):
144
self._start_select(event)
149
def normal_right_down(self, event):
150
""" Handles the right mouse button being pressed while the tool is
151
in the 'normal' state.
153
If the tool is enabled or always on, it starts selecting.
155
if self._is_enabling_event(event):
156
self._start_select(event)
161
def pre_selecting_left_down(self, event):
162
""" the use pressed the key to turn on the zoom mode,
163
now handle the click to start the select mode
165
self._start_select(event)
168
def pre_selecting_key_pressed(self, event):
169
""" Handle key presses, specifically the exit zoom key
171
if self.exit_zoom_key.match(event) and self._enabled:
172
self._end_selecting(event)
174
def selecting_key_pressed(self, event):
175
""" Handle key presses, specifically the exit zoom key
177
if self.exit_zoom_key.match(event) and self._enabled:
178
self._end_selecting(event)
180
def selecting_mouse_move(self, event):
181
""" Handles the mouse moving when the tool is in the 'selecting' state.
183
The selection is extended to the current mouse position.
185
self._screen_end = (event.x, event.y)
186
self.component.request_redraw()
190
def selecting_left_up(self, event):
191
""" Handles the left mouse button being released when the tool is in
192
the 'selecting' state.
194
Finishes selecting and does the zoom.
196
if self.drag_button == "left":
197
self._end_select(event)
200
def selecting_right_up(self, event):
201
""" Handles the right mouse button being released when the tool is in
202
the 'selecting' state.
204
Finishes selecting and does the zoom.
206
if self.drag_button == "right":
207
self._end_select(event)
210
def selecting_mouse_leave(self, event):
211
""" Handles the mouse leaving the plot when the tool is in the
214
Ends the selection operation without zooming.
216
self._end_selecting(event)
219
#--------------------------------------------------------------------------
220
# AbstractOverlay interface
221
#--------------------------------------------------------------------------
223
def overlay(self, component, gc, view_bounds=None, mode="normal"):
224
""" Draws this component overlaid on another component.
226
Overrides AbstractOverlay.
228
if self.event_state == "selecting":
229
if self.tool_mode == "range":
230
self._overlay_range(component, gc)
232
self._overlay_box(component, gc)
235
#--------------------------------------------------------------------------
237
#--------------------------------------------------------------------------
240
def _get_disable_on_complete(self):
244
def _set_disable_on_complete(self, value):
248
def _get_wheel_zoom_step(self):
249
return self.zoom_factor - 1.0
252
def _set_wheel_zoom_step(self, value):
253
self.zoom_factor = value + 1.0
255
def _is_enabling_event(self, event):
259
if self.always_on_modifier == 'shift':
260
enabled = event.shift_down
261
elif self.always_on_modifier == 'control':
262
enabled = event.control_down
263
elif self.always_on_modifier == 'alt':
264
enabled = event.alt_down
267
if event.right_down and self.drag_button == 'right':
269
if event.left_down and self.drag_button == 'left':
274
def _start_select(self, event):
275
""" Starts selecting the zoom region
277
if self.component.active_tool in (None, self):
278
self.component.active_tool = self
280
self._enabled = False
281
self._screen_start = (event.x, event.y)
282
self._screen_end = None
283
self.event_state = "selecting"
284
event.window.set_pointer(self.pointer)
285
event.window.set_mouse_owner(self, event.net_transform())
286
self.selecting_mouse_move(event)
289
def _end_select(self, event):
290
""" Ends selection of the zoom region, adds the new zoom range to
291
the zoom stack, and does the zoom.
293
self._screen_end = (event.x, event.y)
295
start = numpy.array(self._screen_start)
296
end = numpy.array(self._screen_end)
298
if sum(abs(end - start)) < self.minimum_screen_delta:
299
self._end_selecting(event)
303
low, high = self._map_coordinate_box(self._screen_start, self._screen_end)
305
x_range = self._get_x_mapper().range
306
y_range = self._get_y_mapper().range
308
prev = (x_range.low, x_range.high, y_range.low, y_range.high)
310
if self.tool_mode == 'range':
311
axis = self._determine_axis()
314
next = (x_range.low, x_range.high, low[1], high[1])
317
next = (low[0], high[0], y_range.low, y_range.high)
320
next = (low[0], high[0], low[1], high[1])
322
zoom_state = SelectedZoomState(prev, next)
323
zoom_state.apply(self)
324
self._append_state(zoom_state)
326
self._end_selecting(event)
330
def _end_selecting(self, event=None):
331
""" Ends selection of zoom region, without zooming.
334
self._enabled = False
335
if self.component.active_tool == self:
336
self.component.active_tool = None
337
if event and event.window:
338
event.window.set_pointer("arrow")
340
self.component.request_redraw()
341
if event and event.window.mouse_owner == self:
342
event.window.set_mouse_owner(None)
345
def _overlay_box(self, component, gc):
346
""" Draws the overlay as a box.
348
if self._screen_start and self._screen_end:
351
gc.set_line_width(self.border_size)
352
gc.set_stroke_color(self.border_color_)
353
gc.clip_to_rect(component.x, component.y, component.width, component.height)
354
x, y = self._screen_start
355
x2, y2 = self._screen_end
356
rect = (x, y, x2-x+1, y2-y+1)
357
if self.color != "transparent":
359
color = list(self.color_)
361
color[3] = self.alpha
363
color += [self.alpha]
366
gc.set_fill_color(color)
373
def _overlay_range(self, component, gc):
374
""" Draws the overlay as a range.
376
axis_ndx = self._determine_axis()
379
lower_left[axis_ndx] = self._screen_start[axis_ndx]
380
lower_left[1-axis_ndx] = self.component.position[1-axis_ndx]
381
upper_right[axis_ndx] = self._screen_end[axis_ndx] - self._screen_start[axis_ndx]
382
upper_right[1-axis_ndx] = self.component.bounds[1-axis_ndx]
386
gc.set_alpha(self.alpha)
387
gc.set_fill_color(self.color_)
388
gc.set_stroke_color(self.border_color_)
389
gc.clip_to_rect(component.x, component.y, component.width, component.height)
390
gc.draw_rect((lower_left[0], lower_left[1], upper_right[0], upper_right[1]))
394
def _determine_axis(self):
395
""" Determines whether the index of the coordinate along the axis of
396
interest is the first or second element of an (x,y) coordinate tuple.
398
if self.axis == "index":
399
if self.component.orientation == "h":
404
if self.component.orientation == "h":
409
def _map_coordinate_box(self, start, end):
410
""" Given start and end points in screen space, returns corresponding
411
low and high points in data space.
415
for axis_index, mapper in [(0, self.component.x_mapper), \
416
(1, self.component.y_mapper)]:
417
low_val = mapper.map_data(start[axis_index])
418
high_val = mapper.map_data(end[axis_index])
420
if low_val > high_val:
421
low_val, high_val = high_val, low_val
422
low[axis_index] = low_val
423
high[axis_index] = high_val