~ubuntu-branches/ubuntu/utopic/python-chaco/utopic

« back to all changes in this revision

Viewing changes to chaco/tools/better_selecting_zoom.py

  • Committer: Bazaar Package Importer
  • Author(s): Varun Hiremath
  • Date: 2011-07-08 20:38:02 UTC
  • mfrom: (7.2.3 sid)
  • Revision ID: james.westby@ubuntu.com-20110708203802-5t32e0ldv441yh90
Tags: 4.0.0-1
* New upstream release
* debian/control:
  - Depend on python-traitsui (Closes: #633604)
  - Bump Standards-Version to 3.9.2
* Update debian/watch file
* Remove debian/patches/* -- no longer needed

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from __future__ import with_statement
 
2
 
 
3
import numpy
 
4
 
 
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
 
10
 
 
11
from better_zoom import BetterZoom, ZoomState
 
12
from tool_states import SelectedZoomState
 
13
 
 
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
 
17
    """
 
18
 
 
19
    # The selection mode:
 
20
    #
 
21
    # range:
 
22
    #   Select a range across a single index or value axis.
 
23
    # box:
 
24
    #   Perform a "box" selection on two axes.
 
25
    tool_mode = Enum("box", "range")
 
26
 
 
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)
 
30
 
 
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')
 
34
 
 
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)
 
38
 
 
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)
 
42
 
 
43
 
 
44
    #-------------------------------------------------------------------------
 
45
    # deprecated interaction controls, used for API compatability with
 
46
    # SimpleZoom
 
47
    #-------------------------------------------------------------------------
 
48
 
 
49
    # Conversion ratio from wheel steps to zoom factors.
 
50
    wheel_zoom_step = Property(Float, depends_on='zoom_factor')
 
51
 
 
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",))
 
55
 
 
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",))
 
59
 
 
60
    # Disable the tool after the zoom is completed?
 
61
    disable_on_complete = Property()
 
62
 
 
63
 
 
64
    #-------------------------------------------------------------------------
 
65
    # Appearance properties (for Box mode)
 
66
    #-------------------------------------------------------------------------
 
67
 
 
68
    # The pointer to use when drawing a zoom box.
 
69
    pointer = "magnifier"
 
70
 
 
71
    # The color of the selection box.
 
72
    color = ColorTrait("lightskyblue")
 
73
 
 
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
 
79
    # time.
 
80
    alpha = Trait(0.4, None, Float)
 
81
 
 
82
    # The color of the outside selection rectangle.
 
83
    border_color = ColorTrait("dodgerblue")
 
84
 
 
85
    # The thickness of selection rectangle border.
 
86
    border_size = Int(1)
 
87
 
 
88
    # The possible event states of this zoom tool.
 
89
    event_state = Enum("normal", "selecting", "pre_selecting")
 
90
 
 
91
    # The (x,y) screen point where the mouse went down.
 
92
    _screen_start = Trait(None, None, Tuple)
 
93
 
 
94
    # The (x,,y) screen point of the last seen mouse move event.
 
95
    _screen_end = Trait(None, None, Tuple)
 
96
 
 
97
    # If **always_on** is False, this attribute indicates whether the tool
 
98
    # is currently enabled.
 
99
    _enabled = Bool(False)
 
100
 
 
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)
 
106
 
 
107
    def reset(self, event=None):
 
108
        """ Resets the tool to normal state, with no start or end position.
 
109
        """
 
110
        self.event_state = "normal"
 
111
        self._screen_start = None
 
112
        self._screen_end = None
 
113
 
 
114
    #--------------------------------------------------------------------------
 
115
    #  BetterZoom interface
 
116
    #--------------------------------------------------------------------------
 
117
 
 
118
    def normal_key_pressed(self, event):
 
119
        """ Handles a key being pressed when the tool is in the 'normal'
 
120
        state.
 
121
        """
 
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())
 
127
                self._enabled = True
 
128
                event.handled = True
 
129
            elif self.exit_zoom_key.match(event) and self._enabled:
 
130
                self.state = 'normal'
 
131
                self._end_select(event)
 
132
                event.handled = True
 
133
 
 
134
        if not event.handled:
 
135
            super(BetterSelectingZoom, self).normal_key_pressed(event)
 
136
 
 
137
    def normal_left_down(self, event):
 
138
        """ Handles the left mouse button being pressed while the tool is
 
139
        in the 'normal' state.
 
140
 
 
141
        If the tool is enabled or always on, it starts selecting.
 
142
        """
 
143
        if self._is_enabling_event(event):
 
144
            self._start_select(event)
 
145
            event.handled = True
 
146
 
 
147
        return
 
148
 
 
149
    def normal_right_down(self, event):
 
150
        """ Handles the right mouse button being pressed while the tool is
 
151
        in the 'normal' state.
 
152
 
 
153
        If the tool is enabled or always on, it starts selecting.
 
154
        """
 
155
        if self._is_enabling_event(event):
 
156
            self._start_select(event)
 
157
            event.handled = True
 
158
 
 
159
        return
 
160
 
 
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
 
164
        """
 
165
        self._start_select(event)
 
166
        event.handled = True
 
167
 
 
168
    def pre_selecting_key_pressed(self, event):
 
169
        """ Handle key presses, specifically the exit zoom key
 
170
        """
 
171
        if self.exit_zoom_key.match(event) and self._enabled:
 
172
            self._end_selecting(event)
 
173
 
 
174
    def selecting_key_pressed(self, event):
 
175
        """ Handle key presses, specifically the exit zoom key
 
176
        """
 
177
        if self.exit_zoom_key.match(event) and self._enabled:
 
178
            self._end_selecting(event)
 
179
 
 
180
    def selecting_mouse_move(self, event):
 
181
        """ Handles the mouse moving when the tool is in the 'selecting' state.
 
182
 
 
183
        The selection is extended to the current mouse position.
 
184
        """
 
185
        self._screen_end = (event.x, event.y)
 
186
        self.component.request_redraw()
 
187
        event.handled = True
 
188
        return
 
189
 
 
190
    def selecting_left_up(self, event):
 
191
        """ Handles the left mouse button being released when the tool is in
 
192
        the 'selecting' state.
 
193
 
 
194
        Finishes selecting and does the zoom.
 
195
        """
 
196
        if self.drag_button == "left":
 
197
            self._end_select(event)
 
198
        return
 
199
 
 
200
    def selecting_right_up(self, event):
 
201
        """ Handles the right mouse button being released when the tool is in
 
202
        the 'selecting' state.
 
203
 
 
204
        Finishes selecting and does the zoom.
 
205
        """
 
206
        if self.drag_button == "right":
 
207
            self._end_select(event)
 
208
        return
 
209
 
 
210
    def selecting_mouse_leave(self, event):
 
211
        """ Handles the mouse leaving the plot when the tool is in the
 
212
        'selecting' state.
 
213
 
 
214
        Ends the selection operation without zooming.
 
215
        """
 
216
        self._end_selecting(event)
 
217
        return
 
218
 
 
219
    #--------------------------------------------------------------------------
 
220
    #  AbstractOverlay interface
 
221
    #--------------------------------------------------------------------------
 
222
 
 
223
    def overlay(self, component, gc, view_bounds=None, mode="normal"):
 
224
        """ Draws this component overlaid on another component.
 
225
 
 
226
        Overrides AbstractOverlay.
 
227
        """
 
228
        if self.event_state == "selecting":
 
229
            if self.tool_mode == "range":
 
230
                self._overlay_range(component, gc)
 
231
            else:
 
232
                self._overlay_box(component, gc)
 
233
        return
 
234
 
 
235
    #--------------------------------------------------------------------------
 
236
    #  private interface
 
237
    #--------------------------------------------------------------------------
 
238
 
 
239
    @deprecated
 
240
    def _get_disable_on_complete(self):
 
241
        return True
 
242
 
 
243
    @deprecated
 
244
    def _set_disable_on_complete(self, value):
 
245
        return
 
246
 
 
247
    @deprecated
 
248
    def _get_wheel_zoom_step(self):
 
249
        return self.zoom_factor - 1.0
 
250
 
 
251
    @deprecated
 
252
    def _set_wheel_zoom_step(self, value):
 
253
        self.zoom_factor = value + 1.0
 
254
 
 
255
    def _is_enabling_event(self, event):
 
256
        if self.always_on:
 
257
            enabled = True
 
258
        else:
 
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
 
265
 
 
266
        if enabled:
 
267
            if event.right_down and self.drag_button == 'right':
 
268
                return True
 
269
            if event.left_down and self.drag_button == 'left':
 
270
                return True
 
271
 
 
272
        return False
 
273
 
 
274
    def _start_select(self, event):
 
275
        """ Starts selecting the zoom region
 
276
        """
 
277
        if self.component.active_tool in (None, self):
 
278
            self.component.active_tool = self
 
279
        else:
 
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)
 
287
        return
 
288
 
 
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.
 
292
        """
 
293
        self._screen_end = (event.x, event.y)
 
294
 
 
295
        start = numpy.array(self._screen_start)
 
296
        end = numpy.array(self._screen_end)
 
297
 
 
298
        if sum(abs(end - start)) < self.minimum_screen_delta:
 
299
            self._end_selecting(event)
 
300
            event.handled = True
 
301
            return
 
302
 
 
303
        low, high = self._map_coordinate_box(self._screen_start, self._screen_end)
 
304
 
 
305
        x_range = self._get_x_mapper().range
 
306
        y_range = self._get_y_mapper().range
 
307
 
 
308
        prev = (x_range.low, x_range.high, y_range.low, y_range.high)
 
309
 
 
310
        if self.tool_mode == 'range':
 
311
            axis = self._determine_axis()
 
312
            if axis == 1:
 
313
                # vertical
 
314
                next = (x_range.low, x_range.high, low[1], high[1])
 
315
            else:
 
316
                # horizontal
 
317
                next = (low[0], high[0], y_range.low, y_range.high)
 
318
 
 
319
        else:
 
320
            next = (low[0], high[0], low[1], high[1])
 
321
 
 
322
        zoom_state = SelectedZoomState(prev, next)
 
323
        zoom_state.apply(self)
 
324
        self._append_state(zoom_state)
 
325
 
 
326
        self._end_selecting(event)
 
327
        event.handled = True
 
328
        return
 
329
 
 
330
    def _end_selecting(self, event=None):
 
331
        """ Ends selection of zoom region, without zooming.
 
332
        """
 
333
        self.reset()
 
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")
 
339
 
 
340
        self.component.request_redraw()
 
341
        if event and event.window.mouse_owner == self:
 
342
            event.window.set_mouse_owner(None)
 
343
        return
 
344
 
 
345
    def _overlay_box(self, component, gc):
 
346
        """ Draws the overlay as a box.
 
347
        """
 
348
        if self._screen_start and self._screen_end:
 
349
            with gc:
 
350
                gc.set_antialias(0)
 
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":
 
358
                    if self.alpha:
 
359
                        color = list(self.color_)
 
360
                        if len(color) == 4:
 
361
                            color[3] = self.alpha
 
362
                        else:
 
363
                            color += [self.alpha]
 
364
                    else:
 
365
                        color = self.color_
 
366
                    gc.set_fill_color(color)
 
367
                    gc.draw_rect(rect)
 
368
                else:
 
369
                    gc.rect(*rect)
 
370
                    gc.stroke_path()
 
371
        return
 
372
 
 
373
    def _overlay_range(self, component, gc):
 
374
        """ Draws the overlay as a range.
 
375
        """
 
376
        axis_ndx = self._determine_axis()
 
377
        lower_left = [0,0]
 
378
        upper_right = [0,0]
 
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]
 
383
 
 
384
        with gc:
 
385
            gc.set_antialias(0)
 
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]))
 
391
 
 
392
        return
 
393
 
 
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.
 
397
        """
 
398
        if self.axis == "index":
 
399
            if self.component.orientation == "h":
 
400
                return 0
 
401
            else:
 
402
                return 1
 
403
        else:
 
404
            if self.component.orientation == "h":
 
405
                return 1
 
406
            else:
 
407
                return 0
 
408
 
 
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.
 
412
        """
 
413
        low = [0,0]
 
414
        high = [0,0]
 
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])
 
419
 
 
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
 
424
        return low, high
 
425