1
#-------------------------------------------------------------------------------
3
# Copyright (c) 2007, Enthought, Inc.
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
11
# Thanks for using Enthought open source!
13
# Author: David C. Morrill
16
#-------------------------------------------------------------------------------
18
""" Traits UI simple, scrubber-based integer or float value editor.
21
#-------------------------------------------------------------------------------
23
#-------------------------------------------------------------------------------
31
import Any, BaseRange, BaseEnum, Str, Float, TraitError, \
35
import View, Item, EnumEditor
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 \
43
from traitsui.wx.editor \
46
from pyface.timer.api \
56
import disconnect, disconnect_no_id, BufferDC
58
#-------------------------------------------------------------------------------
59
# '_ScrubberEditor' class:
60
#-------------------------------------------------------------------------------
62
class _ScrubberEditor ( Editor ):
63
""" Traits UI simple, scrubber-based integer or float value editor.
66
# The low end of the slider range:
69
# The high end of the slider range:
72
# The smallest allowed increment:
75
# The current text being displayed:
78
# The mapping to use (only for Enum's):
81
#-- Class Variables --------------------------------------------------------
85
'center': wx.TE_CENTRE,
89
#---------------------------------------------------------------------------
90
# Finishes initializing the editor by creating the underlying toolkit
92
#---------------------------------------------------------------------------
94
def init ( self, parent ):
95
""" Finishes initializing the editor by creating the underlying toolkit
98
factory = self.factory
100
# Establish the range of the slider:
101
low_name = high_name = ''
102
low, high = factory.low, factory.high
105
handler = self.object.trait( self.name ).handler
106
if isinstance( handler, BaseRange ):
107
low_name, high_name = handler._low_name, handler._high_name
115
elif isinstance( handler, BaseEnum ):
116
if handler.name == '':
117
self.mapping = handler.values
119
self.sync_value( handler.name, 'mapping', 'from' )
121
low, high = 0, self.high
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 |
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 )
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 )
142
# Set up the control resize handler:
143
wx.EVT_SIZE( control, self._resize )
146
self._can_set_tooltip = (not self.set_tooltip())
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' )
153
# Force a reset (in case low = high = None, which won't cause a
154
# notification to fire):
155
self._reset_scrubber()
157
#---------------------------------------------------------------------------
158
# Disposes of the contents of an editor:
159
#---------------------------------------------------------------------------
161
def dispose ( self ):
162
""" Disposes of the contents of an editor.
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,
170
# Disconnect the pop-up text event handlers:
171
self._disconnect_text()
173
super( _ScrubberEditor, self ).dispose()
175
#---------------------------------------------------------------------------
176
# Updates the editor when the object trait changes external to the editor:
177
#---------------------------------------------------------------------------
179
def update_editor ( self ):
180
""" Updates the editor when the object trait changes externally to the
183
self.text = self.string_value( self.value )
184
self._text_size = None
187
self._enum_completed()
189
#---------------------------------------------------------------------------
190
# Updates the object when the scrubber value changes:
191
#---------------------------------------------------------------------------
193
def update_object ( self, value ):
194
""" Updates the object when the scrubber value changes.
196
if self.mapping is not None:
197
value = self.mapping[ int( value ) ]
199
if value != self.value:
205
if value != self.value:
209
#---------------------------------------------------------------------------
210
# Handles an error that occurs while setting the object's trait value:
211
#---------------------------------------------------------------------------
213
def error ( self, excp ):
214
""" Handles an error that occurs while setting the object's trait value.
218
#-- Trait Event Handlers ---------------------------------------------------
220
def _mapping_changed ( self, mapping ):
221
""" Handles the Enum mapping being changed.
223
self.high = len( mapping ) - 1
225
#-- Private Methods --------------------------------------------------------
227
@on_trait_change( 'low, high' )
228
def _reset_scrubber ( self ):
229
""" Sets the the current tooltip.
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:
240
tooltip = '[%g..]' % low
242
tooltip = '[..%g]' % high
244
tooltip = '[%g..%g]' % ( low, high )
246
self.control.SetToolTipString( tooltip )
248
# Establish the slider increment:
249
increment = self.factory.increment
251
if (low is None) or (high is None) or isinstance( low, int ):
254
increment = pow( 10, round( log10( (high - low) / 100.0 ) ) )
256
self.increment = increment
260
def _get_text_bounds ( self ):
261
""" Get the window bounds of where the current text should be
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':
270
elif alignment == 'center':
275
return ( tx, ty, tdx, tdy )
277
def _get_text_size ( self ):
278
""" Returns the text size information for the window.
280
if self._text_size is None:
281
self._text_size = self.control.GetFullTextExtent(
282
self.text.strip() or 'M' )
284
return self._text_size
286
def _refresh ( self ):
287
""" Refreshes the contents of the control.
289
if self.control is not None:
290
self.control.Refresh()
292
def _set_scrubber_position ( self, event, delta ):
293
""" Calculates a new scrubber value for a specified mouse position
297
increment = self.increment
298
if event.ShiftDown():
301
elif event.ControlDown():
304
value = self._value + (delta / clicks) * increment
306
if self.low is not None:
307
value = max( value, self.low )
309
if self.high is not None:
310
value = min( value, self.high )
312
self.update_object( value )
314
def _delayed_click ( self ):
315
""" Handle a delayed click response.
317
self._pending = False
319
def _pop_up_editor ( self ):
320
""" Pop-up a text control to allow the user to enter a value using
323
self.control.SetCursor( wx.StockCursor( wx.CURSOR_ARROW ) )
325
if self.mapping is not None:
330
def _pop_up_enum ( self ):
331
self._ui = self.object.edit_traits(
337
editor = EnumEditor( name = 'editor.mapping' ) ),
339
parent = self.control,
340
context = { 'object': self.object, 'editor': self } )
342
dx, dy = self.control.GetSizeTuple()
343
drop_down = self._ui.info.drop_down.control
344
drop_down.SetDimensions( 0, 0, dx, dy )
346
wx.EVT_KILL_FOCUS( drop_down, self._enum_completed )
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 )
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 )
362
def _destroy_text ( self ):
363
""" Destroys the current text control.
365
self._ignore_focus = self._in_text_window
367
self._disconnect_text()
369
self.control.DestroyChildren()
373
def _disconnect_text ( self ):
374
""" Disconnects the event handlers for the pop up text editor.
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 )
381
def _init_value ( self ):
382
""" Initializes the current value when the user begins a drag or moves
385
if self.mapping is not None:
387
self._value = list( self.mapping ).index( self.value )
391
self._value = self.value
393
#--- wxPython Event Handlers -----------------------------------------------
395
def _erase_background ( self, event ):
396
""" Do not erase the background here (do it in the 'on_paint' handler).
400
def _on_paint ( self, event ):
401
""" Paint the background using the associated ImageSlice object.
403
control = self.control
404
dc = BufferDC( control )
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_
413
if factory.hover_color_ is not None:
414
color = factory.hover_color_
417
paint_parent( dc, control )
418
brush = wx.TRANSPARENT_BRUSH
420
brush = wx.Brush( color )
422
color = factory.border_color_
423
if color is not None:
424
pen = wx.Pen( color )
426
pen = wx.TRANSPARENT_PEN
428
if (pen != wx.TRANSPARENT_PEN) or (brush != wx.TRANSPARENT_BRUSH):
429
wdx, wdy = control.GetClientSizeTuple()
432
dc.DrawRectangle( 0, 0, wdx, wdy )
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 )
441
# Copy the buffer contents to the display:
444
def _resize ( self, event ):
445
""" Handles the control being resized.
447
if self._text is not None:
448
self._text.SetSize( self.control.GetSize() )
450
def _set_focus ( self, event ):
451
""" Handle the control getting the keyboard focus.
453
if ((not self._ignore_focus) and
454
(self._x is None) and
455
(self._text is None)):
456
self._pop_up_editor()
460
def _enter_window ( self, event ):
461
""" Handles the mouse entering the window.
465
self.control.SetCursor( wx.StockCursor( wx.CURSOR_HAND ) )
467
if not self._ignore_focus:
468
self._ignore_focus = True
469
self.control.SetFocus()
471
self._ignore_focus = False
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()
479
def _leave_window ( self, event ):
480
""" Handles the mouse leaving the window.
484
if self.factory.hover_color_ != self.factory.color_:
485
self.control.Refresh()
487
def _left_down ( self, event ):
488
""" Handles the left mouse being pressed.
490
self._x, self._y = event.GetX(), event.GetY()
495
self.control.CaptureMouse()
497
if self.factory.active_color_ != self.factory.hover_color_:
498
self.control.Refresh()
500
do_after( 200, self._delayed_click )
502
def _left_up ( self, event ):
503
""" Handles the left mouse button being released.
505
self.control.ReleaseMouse()
507
self._pop_up_editor()
509
self._x = self._y = self._value = self._pending = None
511
if self._hover or (self.factory.active_color_ != self.factory.color_):
512
self.control.Refresh()
514
def _motion ( self, event ):
515
""" Handles the mouse moving.
517
if self._x is not None:
518
x, y = event.GetX(), event.GetY()
526
self._pending = False
533
self._set_scrubber_position( event, delta )
535
def _mouse_wheel ( self, event ):
536
""" Handles the mouse wheel moving.
541
if event.ShiftDown():
543
delta = clicks * (event.GetWheelRotation() / event.GetWheelDelta())
544
self._set_scrubber_position( event, delta )
546
def _update_value ( self, event ):
547
""" Updates the object value from the current text control value.
549
control = event.GetEventObject()
551
self.update_object( float( control.GetValue() ) )
556
control.SetBackgroundColour( ErrorColor )
561
def _enter_text ( self, event ):
562
""" Handles the mouse entering the pop-up text control.
564
self._in_text_window = True
566
def _leave_text ( self, event ):
567
""" Handles the mouse leaving the pop-up text control.
569
self._in_text_window = False
571
def _text_completed ( self, event ):
572
""" Handles the user pressing the 'Enter' key in the text control.
574
if self._update_value( event ):
577
def _enum_completed ( self, event = None ):
578
""" Handles the Enum drop-down control losing focus.
580
if self._ui is not None:
581
self._ignore_focus = True
582
disconnect_no_id( self._ui.info.drop_down.control,
587
def _key_entered ( self, event ):
588
""" Handles individual key strokes while the text control is active.
590
key_code = event.GetKeyCode()
591
if key_code == wx.WXK_ESCAPE:
595
if key_code == wx.WXK_TAB:
596
if self._update_value( event ):
597
if event.ShiftDown():
598
self.control.Navigate( 0 )
600
self.control.Navigate()