~ubuntu-branches/ubuntu/utopic/python-traitsui/utopic

« back to all changes in this revision

Viewing changes to traitsui/wx/scrubber_editor.py

  • Committer: Bazaar Package Importer
  • Author(s): Varun Hiremath
  • Date: 2011-07-09 13:57:39 UTC
  • Revision ID: james.westby@ubuntu.com-20110709135739-x5u20q86huissmn1
Tags: upstream-4.0.0
ImportĀ upstreamĀ versionĀ 4.0.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#-------------------------------------------------------------------------------
 
2
#
 
3
#  Copyright (c) 2007, Enthought, Inc.
 
4
#  All rights reserved.
 
5
#
 
6
#  This software is provided without warranty under the terms of the BSD
 
7
#  license included in enthought/LICENSE.txt and may be redistributed only
 
8
#  under the conditions described in the aforementioned license.  The license
 
9
#  is also available online at http://www.enthought.com/licenses/BSD.txt
 
10
#
 
11
#  Thanks for using Enthought open source!
 
12
#
 
13
#  Author: David C. Morrill
 
14
#  Date:   07/14/2008
 
15
#
 
16
#-------------------------------------------------------------------------------
 
17
 
 
18
""" Traits UI simple, scrubber-based integer or float value editor.
 
19
"""
 
20
 
 
21
#-------------------------------------------------------------------------------
 
22
#  Imports:
 
23
#-------------------------------------------------------------------------------
 
24
 
 
25
import wx
 
26
 
 
27
from math \
 
28
   import log10, pow
 
29
 
 
30
from traits.api \
 
31
    import Any, BaseRange, BaseEnum, Str, Float, TraitError, \
 
32
           on_trait_change
 
33
 
 
34
from traitsui.api \
 
35
    import View, Item, EnumEditor
 
36
 
 
37
# FIXME: ScrubberEditor is a proxy class defined here just for backward
 
38
# compatibility (represents the editor factory for scrubber editors).
 
39
# The class has been moved to traitsui.editors.scrubber_editor
 
40
from traitsui.editors.scrubber_editor \
 
41
    import ScrubberEditor
 
42
 
 
43
from traitsui.wx.editor \
 
44
    import Editor
 
45
 
 
46
from pyface.timer.api \
 
47
    import do_after
 
48
 
 
49
from constants \
 
50
    import ErrorColor
 
51
 
 
52
from image_slice \
 
53
    import paint_parent
 
54
 
 
55
from helper \
 
56
    import disconnect, disconnect_no_id, BufferDC
 
57
 
 
58
#-------------------------------------------------------------------------------
 
59
#  '_ScrubberEditor' class:
 
60
#-------------------------------------------------------------------------------
 
61
 
 
62
class _ScrubberEditor ( Editor ):
 
63
    """ Traits UI simple, scrubber-based integer or float value editor.
 
64
    """
 
65
 
 
66
    # The low end of the slider range:
 
67
    low = Any
 
68
 
 
69
    # The high end of the slider range:
 
70
    high = Any
 
71
 
 
72
    # The smallest allowed increment:
 
73
    increment = Float
 
74
 
 
75
    # The current text being displayed:
 
76
    text = Str
 
77
 
 
78
    # The mapping to use (only for Enum's):
 
79
    mapping = Any
 
80
 
 
81
    #-- Class Variables --------------------------------------------------------
 
82
 
 
83
    text_styles = {
 
84
        'left':   wx.TE_LEFT,
 
85
        'center': wx.TE_CENTRE,
 
86
        'right':  wx.TE_RIGHT
 
87
    }
 
88
 
 
89
    #---------------------------------------------------------------------------
 
90
    #  Finishes initializing the editor by creating the underlying toolkit
 
91
    #  widget:
 
92
    #---------------------------------------------------------------------------
 
93
 
 
94
    def init ( self, parent ):
 
95
        """ Finishes initializing the editor by creating the underlying toolkit
 
96
            widget.
 
97
        """
 
98
        factory = self.factory
 
99
 
 
100
        # Establish the range of the slider:
 
101
        low_name  = high_name = ''
 
102
        low, high = factory.low, factory.high
 
103
        if high <= low:
 
104
            low = high = None
 
105
            handler    = self.object.trait( self.name ).handler
 
106
            if isinstance( handler, BaseRange ):
 
107
                low_name, high_name = handler._low_name, handler._high_name
 
108
 
 
109
                if low_name == '':
 
110
                    low = handler._low
 
111
 
 
112
                if high_name == '':
 
113
                    high = handler._high
 
114
 
 
115
            elif isinstance( handler, BaseEnum ):
 
116
                if handler.name == '':
 
117
                    self.mapping = handler.values
 
118
                else:
 
119
                    self.sync_value( handler.name, 'mapping', 'from' )
 
120
 
 
121
                low, high = 0, self.high
 
122
 
 
123
        # Create the control:
 
124
        self.control = control = wx.Window( parent, -1,
 
125
                                            size  = wx.Size( 50, 18 ),
 
126
                                            style = wx.FULL_REPAINT_ON_RESIZE |
 
127
                                                    wx.TAB_TRAVERSAL )
 
128
 
 
129
        # Set up the painting event handlers:
 
130
        wx.EVT_ERASE_BACKGROUND( control, self._erase_background )
 
131
        wx.EVT_PAINT(            control, self._on_paint )
 
132
        wx.EVT_SET_FOCUS(        control, self._set_focus )
 
133
 
 
134
        # Set up mouse event handlers:
 
135
        wx.EVT_LEAVE_WINDOW( control, self._leave_window )
 
136
        wx.EVT_ENTER_WINDOW( control, self._enter_window )
 
137
        wx.EVT_LEFT_DOWN(    control, self._left_down )
 
138
        wx.EVT_LEFT_UP(      control, self._left_up )
 
139
        wx.EVT_MOTION(       control, self._motion )
 
140
        wx.EVT_MOUSEWHEEL(   control, self._mouse_wheel )
 
141
 
 
142
        # Set up the control resize handler:
 
143
        wx.EVT_SIZE( control, self._resize )
 
144
 
 
145
        # Set the tooltip:
 
146
        self._can_set_tooltip = (not self.set_tooltip())
 
147
 
 
148
        # Save the values we calculated:
 
149
        self.set( low = low, high = high )
 
150
        self.sync_value( low_name,  'low',  'from' )
 
151
        self.sync_value( high_name, 'high', 'from' )
 
152
 
 
153
        # Force a reset (in case low = high = None, which won't cause a
 
154
        # notification to fire):
 
155
        self._reset_scrubber()
 
156
 
 
157
    #---------------------------------------------------------------------------
 
158
    #  Disposes of the contents of an editor:
 
159
    #---------------------------------------------------------------------------
 
160
 
 
161
    def dispose ( self ):
 
162
        """ Disposes of the contents of an editor.
 
163
        """
 
164
        # Remove all of the wx event handlers:
 
165
        disconnect_no_id( self.control, wx.EVT_ERASE_BACKGROUND, wx.EVT_PAINT,
 
166
            wx.EVT_SET_FOCUS, wx.EVT_LEAVE_WINDOW, wx.EVT_ENTER_WINDOW,
 
167
            wx.EVT_LEFT_DOWN, wx.EVT_LEFT_UP, wx.EVT_MOTION, wx.EVT_MOUSEWHEEL,
 
168
            wx.EVT_SIZE )
 
169
 
 
170
        # Disconnect the pop-up text event handlers:
 
171
        self._disconnect_text()
 
172
 
 
173
        super( _ScrubberEditor, self ).dispose()
 
174
 
 
175
    #---------------------------------------------------------------------------
 
176
    #  Updates the editor when the object trait changes external to the editor:
 
177
    #---------------------------------------------------------------------------
 
178
 
 
179
    def update_editor ( self ):
 
180
        """ Updates the editor when the object trait changes externally to the
 
181
            editor.
 
182
        """
 
183
        self.text       = self.string_value( self.value )
 
184
        self._text_size = None
 
185
        self._refresh()
 
186
 
 
187
        self._enum_completed()
 
188
 
 
189
    #---------------------------------------------------------------------------
 
190
    #  Updates the object when the scrubber value changes:
 
191
    #---------------------------------------------------------------------------
 
192
 
 
193
    def update_object ( self, value ):
 
194
        """ Updates the object when the scrubber value changes.
 
195
        """
 
196
        if self.mapping is not None:
 
197
            value = self.mapping[ int( value ) ]
 
198
 
 
199
        if value != self.value:
 
200
            try:
 
201
                self.value = value
 
202
                self.update_editor()
 
203
            except TraitError:
 
204
                value = int( value )
 
205
                if value != self.value:
 
206
                    self.value = value
 
207
                    self.update_editor()
 
208
 
 
209
    #---------------------------------------------------------------------------
 
210
    #  Handles an error that occurs while setting the object's trait value:
 
211
    #---------------------------------------------------------------------------
 
212
 
 
213
    def error ( self, excp ):
 
214
        """ Handles an error that occurs while setting the object's trait value.
 
215
        """
 
216
        pass
 
217
 
 
218
    #-- Trait Event Handlers ---------------------------------------------------
 
219
 
 
220
    def _mapping_changed ( self, mapping ):
 
221
        """ Handles the Enum mapping being changed.
 
222
        """
 
223
        self.high = len( mapping ) - 1
 
224
 
 
225
    #-- Private Methods --------------------------------------------------------
 
226
 
 
227
    @on_trait_change( 'low, high' )
 
228
    def _reset_scrubber ( self ):
 
229
        """ Sets the the current tooltip.
 
230
        """
 
231
        low, high = self.low, self.high
 
232
        if self._can_set_tooltip:
 
233
            if self.mapping is not None:
 
234
                tooltip = '[%s]' % (', '.join( self.mapping ))
 
235
                if len( tooltip ) > 80:
 
236
                    tooltip = ''
 
237
            elif high is None:
 
238
                tooltip = ''
 
239
                if low is not None:
 
240
                    tooltip = '[%g..]' % low
 
241
            elif low is None:
 
242
                tooltip = '[..%g]' % high
 
243
            else:
 
244
                tooltip = '[%g..%g]' % ( low, high )
 
245
 
 
246
            self.control.SetToolTipString( tooltip )
 
247
 
 
248
        # Establish the slider increment:
 
249
        increment = self.factory.increment
 
250
        if increment <= 0.0:
 
251
            if (low is None) or (high is None) or isinstance( low, int ):
 
252
                increment = 1.0
 
253
            else:
 
254
                increment = pow( 10, round( log10( (high - low) / 100.0 ) ) )
 
255
 
 
256
        self.increment = increment
 
257
 
 
258
        self.update_editor()
 
259
 
 
260
    def _get_text_bounds ( self ):
 
261
        """ Get the window bounds of where the current text should be
 
262
            displayed.
 
263
        """
 
264
        tdx, tdy, descent, leading = self._get_text_size()
 
265
        wdx, wdy  = self.control.GetClientSizeTuple()
 
266
        ty        = ((wdy - (tdy - descent)) / 2) - 1
 
267
        alignment = self.factory.alignment
 
268
        if alignment == 'left':
 
269
            tx = 0
 
270
        elif alignment == 'center':
 
271
            tx = (wdx - tdx) / 2
 
272
        else:
 
273
            tx = wdx - tdx
 
274
 
 
275
        return ( tx, ty, tdx, tdy )
 
276
 
 
277
    def _get_text_size ( self ):
 
278
        """ Returns the text size information for the window.
 
279
        """
 
280
        if self._text_size is None:
 
281
            self._text_size = self.control.GetFullTextExtent(
 
282
                                               self.text.strip() or 'M' )
 
283
 
 
284
        return self._text_size
 
285
 
 
286
    def _refresh ( self ):
 
287
        """ Refreshes the contents of the control.
 
288
        """
 
289
        if self.control is not None:
 
290
            self.control.Refresh()
 
291
 
 
292
    def _set_scrubber_position ( self, event, delta ):
 
293
        """ Calculates a new scrubber value for a specified mouse position
 
294
            change.
 
295
        """
 
296
        clicks    = 3
 
297
        increment = self.increment
 
298
        if event.ShiftDown():
 
299
            increment *= 10.0
 
300
            clicks     = 7
 
301
        elif event.ControlDown():
 
302
            increment /= 10.0
 
303
 
 
304
        value = self._value + (delta / clicks) * increment
 
305
 
 
306
        if self.low is not None:
 
307
            value = max( value, self.low )
 
308
 
 
309
        if self.high is not None:
 
310
            value = min( value, self.high )
 
311
 
 
312
        self.update_object( value )
 
313
 
 
314
    def _delayed_click ( self ):
 
315
        """ Handle a delayed click response.
 
316
        """
 
317
        self._pending = False
 
318
 
 
319
    def _pop_up_editor ( self ):
 
320
        """ Pop-up a text control to allow the user to enter a value using
 
321
            the keyboard.
 
322
        """
 
323
        self.control.SetCursor( wx.StockCursor( wx.CURSOR_ARROW ) )
 
324
 
 
325
        if self.mapping is not None:
 
326
            self._pop_up_enum()
 
327
        else:
 
328
            self._pop_up_text()
 
329
 
 
330
    def _pop_up_enum ( self ):
 
331
        self._ui = self.object.edit_traits(
 
332
            view = View(
 
333
                Item( self.name,
 
334
                      id         = 'drop_down',
 
335
                      show_label = False,
 
336
                      padding    = -4,
 
337
                      editor     = EnumEditor( name = 'editor.mapping' ) ),
 
338
                kind = 'subpanel' ),
 
339
            parent  = self.control,
 
340
            context = { 'object': self.object, 'editor': self } )
 
341
 
 
342
        dx, dy    = self.control.GetSizeTuple()
 
343
        drop_down = self._ui.info.drop_down.control
 
344
        drop_down.SetDimensions(  0, 0, dx, dy )
 
345
        drop_down.SetFocus()
 
346
        wx.EVT_KILL_FOCUS( drop_down, self._enum_completed )
 
347
 
 
348
    def _pop_up_text ( self ):
 
349
        control = self.control
 
350
        self._text = text = wx.TextCtrl( control, -1, str( self.value ),
 
351
                            size  = control.GetSize(),
 
352
                            style = self.text_styles[ self.factory.alignment ] |
 
353
                                    wx.TE_PROCESS_ENTER )
 
354
        text.SetSelection( -1, -1 )
 
355
        text.SetFocus()
 
356
        wx.EVT_TEXT_ENTER( control, text.GetId(), self._text_completed )
 
357
        wx.EVT_KILL_FOCUS(   text, self._text_completed )
 
358
        wx.EVT_ENTER_WINDOW( text, self._enter_text )
 
359
        wx.EVT_LEAVE_WINDOW( text, self._leave_text )
 
360
        wx.EVT_CHAR( text, self._key_entered )
 
361
 
 
362
    def _destroy_text ( self ):
 
363
        """ Destroys the current text control.
 
364
        """
 
365
        self._ignore_focus = self._in_text_window
 
366
 
 
367
        self._disconnect_text()
 
368
 
 
369
        self.control.DestroyChildren()
 
370
 
 
371
        self._text = None
 
372
 
 
373
    def _disconnect_text ( self ):
 
374
        """ Disconnects the event handlers for the pop up text editor.
 
375
        """
 
376
        if self._text is not None:
 
377
            disconnect( self._text, wx.EVT_TEXT_ENTER )
 
378
            disconnect_no_id( self._text, wx.EVT_KILL_FOCUS,
 
379
                wx.EVT_ENTER_WINDOW, wx.EVT_LEAVE_WINDOW, wx.EVT_CHAR )
 
380
 
 
381
    def _init_value ( self ):
 
382
        """ Initializes the current value when the user begins a drag or moves
 
383
            the mouse wheel.
 
384
        """
 
385
        if self.mapping is not None:
 
386
            try:
 
387
                self._value = list( self.mapping ).index( self.value )
 
388
            except:
 
389
                self._value = 0
 
390
        else:
 
391
            self._value = self.value
 
392
 
 
393
    #--- wxPython Event Handlers -----------------------------------------------
 
394
 
 
395
    def _erase_background ( self, event ):
 
396
        """ Do not erase the background here (do it in the 'on_paint' handler).
 
397
        """
 
398
        pass
 
399
 
 
400
    def _on_paint ( self, event ):
 
401
        """ Paint the background using the associated ImageSlice object.
 
402
        """
 
403
        control = self.control
 
404
        dc      = BufferDC( control )
 
405
 
 
406
        # Draw the background:
 
407
        factory  = self.factory
 
408
        color    = factory.color_
 
409
        if self._x is not None:
 
410
            if factory.active_color_ is not None:
 
411
                color = factory.active_color_
 
412
        elif self._hover:
 
413
            if factory.hover_color_ is not None:
 
414
                color = factory.hover_color_
 
415
 
 
416
        if color is None:
 
417
            paint_parent( dc, control )
 
418
            brush = wx.TRANSPARENT_BRUSH
 
419
        else:
 
420
            brush = wx.Brush( color )
 
421
 
 
422
        color = factory.border_color_
 
423
        if color is not None:
 
424
            pen = wx.Pen( color )
 
425
        else:
 
426
            pen = wx.TRANSPARENT_PEN
 
427
 
 
428
        if (pen != wx.TRANSPARENT_PEN) or (brush != wx.TRANSPARENT_BRUSH):
 
429
            wdx, wdy = control.GetClientSizeTuple()
 
430
            dc.SetBrush( brush )
 
431
            dc.SetPen( pen )
 
432
            dc.DrawRectangle( 0, 0, wdx, wdy )
 
433
 
 
434
        # Draw the current text value:
 
435
        dc.SetBackgroundMode( wx.TRANSPARENT )
 
436
        dc.SetTextForeground( factory.text_color_ )
 
437
        dc.SetFont( control.GetFont() )
 
438
        tx, ty, tdx, tdy = self._get_text_bounds()
 
439
        dc.DrawText( self.text, tx, ty )
 
440
 
 
441
        # Copy the buffer contents to the display:
 
442
        dc.copy()
 
443
 
 
444
    def _resize ( self, event ):
 
445
        """ Handles the control being resized.
 
446
        """
 
447
        if self._text is not None:
 
448
            self._text.SetSize( self.control.GetSize() )
 
449
 
 
450
    def _set_focus ( self, event ):
 
451
        """ Handle the control getting the keyboard focus.
 
452
        """
 
453
        if ((not self._ignore_focus) and
 
454
            (self._x is None)        and
 
455
            (self._text is None)):
 
456
            self._pop_up_editor()
 
457
 
 
458
        event.Skip()
 
459
 
 
460
    def _enter_window ( self, event ):
 
461
        """ Handles the mouse entering the window.
 
462
        """
 
463
        self._hover = True
 
464
 
 
465
        self.control.SetCursor( wx.StockCursor( wx.CURSOR_HAND ) )
 
466
 
 
467
        if not self._ignore_focus:
 
468
            self._ignore_focus = True
 
469
            self.control.SetFocus()
 
470
 
 
471
        self._ignore_focus = False
 
472
 
 
473
        if self._x is not None:
 
474
            if self.factory.active_color_ != self.factory.color_:
 
475
                self.control.Refresh()
 
476
        elif self.factory.hover_color_ != self.factory.color_:
 
477
            self.control.Refresh()
 
478
 
 
479
    def _leave_window ( self, event ):
 
480
        """ Handles the mouse leaving the window.
 
481
        """
 
482
        self._hover = False
 
483
 
 
484
        if self.factory.hover_color_ != self.factory.color_:
 
485
            self.control.Refresh()
 
486
 
 
487
    def _left_down ( self, event ):
 
488
        """ Handles the left mouse being pressed.
 
489
        """
 
490
        self._x, self._y = event.GetX(), event.GetY()
 
491
        self._pending    = True
 
492
 
 
493
        self._init_value()
 
494
 
 
495
        self.control.CaptureMouse()
 
496
 
 
497
        if self.factory.active_color_ != self.factory.hover_color_:
 
498
            self.control.Refresh()
 
499
 
 
500
        do_after( 200, self._delayed_click )
 
501
 
 
502
    def _left_up ( self, event ):
 
503
        """ Handles the left mouse button being released.
 
504
        """
 
505
        self.control.ReleaseMouse()
 
506
        if self._pending:
 
507
            self._pop_up_editor()
 
508
 
 
509
        self._x = self._y = self._value = self._pending = None
 
510
 
 
511
        if self._hover or (self.factory.active_color_ != self.factory.color_):
 
512
            self.control.Refresh()
 
513
 
 
514
    def _motion ( self, event ):
 
515
        """ Handles the mouse moving.
 
516
        """
 
517
        if self._x is not None:
 
518
            x, y = event.GetX(), event.GetY()
 
519
            dx   = x - self._x
 
520
            adx  = abs( dx )
 
521
            dy   = y - self._y
 
522
            ady  = abs( dy )
 
523
            if self._pending:
 
524
                if (adx + ady) < 3:
 
525
                    return
 
526
                self._pending = False
 
527
 
 
528
            if adx > ady:
 
529
                delta = dx
 
530
            else:
 
531
                delta = -dy
 
532
 
 
533
            self._set_scrubber_position( event, delta )
 
534
 
 
535
    def _mouse_wheel ( self, event ):
 
536
        """ Handles the mouse wheel moving.
 
537
        """
 
538
        if self._hover:
 
539
            self._init_value()
 
540
            clicks = 3
 
541
            if event.ShiftDown():
 
542
                clicks = 7
 
543
            delta = clicks * (event.GetWheelRotation() / event.GetWheelDelta())
 
544
            self._set_scrubber_position( event, delta )
 
545
 
 
546
    def _update_value ( self, event ):
 
547
        """ Updates the object value from the current text control value.
 
548
        """
 
549
        control = event.GetEventObject()
 
550
        try:
 
551
            self.update_object( float( control.GetValue() ) )
 
552
 
 
553
            return True
 
554
 
 
555
        except TraitError:
 
556
            control.SetBackgroundColour( ErrorColor )
 
557
            control.Refresh()
 
558
 
 
559
            return False
 
560
 
 
561
    def _enter_text ( self, event ):
 
562
        """ Handles the mouse entering the pop-up text control.
 
563
        """
 
564
        self._in_text_window = True
 
565
 
 
566
    def _leave_text ( self, event ):
 
567
        """ Handles the mouse leaving the pop-up text control.
 
568
        """
 
569
        self._in_text_window = False
 
570
 
 
571
    def _text_completed ( self, event ):
 
572
        """ Handles the user pressing the 'Enter' key in the text control.
 
573
        """
 
574
        if self._update_value( event ):
 
575
            self._destroy_text()
 
576
 
 
577
    def _enum_completed ( self, event = None ):
 
578
        """ Handles the Enum drop-down control losing focus.
 
579
        """
 
580
        if self._ui is not None:
 
581
            self._ignore_focus = True
 
582
            disconnect_no_id( self._ui.info.drop_down.control,
 
583
                              wx.EVT_KILL_FOCUS )
 
584
            self._ui.dispose()
 
585
            del self._ui
 
586
 
 
587
    def _key_entered ( self, event ):
 
588
        """ Handles individual key strokes while the text control is active.
 
589
        """
 
590
        key_code = event.GetKeyCode()
 
591
        if key_code == wx.WXK_ESCAPE:
 
592
            self._destroy_text()
 
593
            return
 
594
 
 
595
        if key_code == wx.WXK_TAB:
 
596
            if self._update_value( event ):
 
597
                if event.ShiftDown():
 
598
                    self.control.Navigate( 0 )
 
599
                else:
 
600
                    self.control.Navigate()
 
601
            return
 
602
 
 
603
        event.Skip()
 
604