1
#------------------------------------------------------------------------------
2
# Copyright (c) 2008, Riverbank Computing Limited
5
# This software is provided without warranty under the terms of the BSD license.
6
# However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply
9
# Author: Riverbank Computing Limited
10
#------------------------------------------------------------------------------
12
""" Defines the various range editors and the range editor factory, for the
13
PyQt user interface toolkit.
16
#-------------------------------------------------------------------------------
18
#-------------------------------------------------------------------------------
23
from pyface.qt import QtCore, QtGui
26
import TraitError, Str, Float, Any, Bool
28
# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward
29
# compatibility. The class has been moved to the
30
# traitsui.editors.range_editor file.
31
from traitsui.editors.range_editor \
32
import ToolkitEditorFactory
41
import OKColor, ErrorColor
46
#-------------------------------------------------------------------------------
47
# 'BaseRangeEditor' class:
48
#-------------------------------------------------------------------------------
50
class BaseRangeEditor ( Editor ):
51
""" The base class for Range editors. Using an evaluate trait, if specified,
52
when assigning numbers the object trait.
55
#---------------------------------------------------------------------------
57
#---------------------------------------------------------------------------
59
# Function to evaluate floats/ints
62
#---------------------------------------------------------------------------
63
# Sets the associated object trait's value:
64
#---------------------------------------------------------------------------
66
def _set_value ( self, value ):
67
if self.evaluate is not None:
68
value = self.evaluate( value )
69
Editor._set_value( self, value )
71
#-------------------------------------------------------------------------------
72
# 'SimpleSliderEditor' class:
73
#-------------------------------------------------------------------------------
75
class SimpleSliderEditor ( BaseRangeEditor ):
76
""" Simple style of range editor that displays a slider and a text field.
78
The user can set a value either by moving the slider or by typing a value
82
#---------------------------------------------------------------------------
84
#---------------------------------------------------------------------------
86
# Low value for the slider range
89
# High value for the slider range
92
# Formatting string used to format value and labels
95
#---------------------------------------------------------------------------
96
# Finishes initializing the editor by creating the underlying toolkit
98
#---------------------------------------------------------------------------
100
def init ( self, parent ):
101
""" Finishes initializing the editor by creating the underlying toolkit
104
factory = self.factory
105
if not factory.low_name:
106
self.low = factory.low
108
if not factory.high_name:
109
self.high = factory.high
111
self.format = factory.format
113
self.evaluate = factory.evaluate
114
self.sync_value( factory.evaluate_name, 'evaluate', 'from' )
116
self.sync_value( factory.low_name, 'low', 'from' )
117
self.sync_value( factory.high_name, 'high', 'from' )
119
self.control = QtGui.QWidget()
120
panel = QtGui.QHBoxLayout(self.control)
121
panel.setContentsMargins(0, 0, 0, 0)
126
fvalue_text = self.format % fvalue
127
1 / (self.low <= fvalue <= self.high)
132
ivalue = self._convert_to_slider(fvalue)
134
self._label_lo = QtGui.QLabel()
135
self._label_lo.setAlignment(QtCore.Qt.AlignRight |
136
QtCore.Qt.AlignVCenter)
137
if factory.label_width > 0:
138
self._label_lo.setMinimumWidth(factory.label_width)
139
panel.addWidget(self._label_lo)
141
self.control.slider = slider = QtGui.QSlider(QtCore.Qt.Horizontal)
142
slider.setTracking(factory.auto_set)
144
slider.setMaximum(10000)
145
slider.setPageStep(1000)
146
slider.setSingleStep(100)
147
slider.setValue(ivalue)
148
QtCore.QObject.connect(slider, QtCore.SIGNAL('valueChanged(int)'),
149
self.update_object_on_scroll)
150
panel.addWidget(slider)
152
self._label_hi = QtGui.QLabel()
153
panel.addWidget(self._label_hi)
154
if factory.label_width > 0:
155
self._label_hi.setMinimumWidth(factory.label_width)
157
self.control.text = text = QtGui.QLineEdit(fvalue_text)
158
QtCore.QObject.connect(text, QtCore.SIGNAL('editingFinished()'),
159
self.update_object_on_enter)
161
# The default size is a bit too big and probably doesn't need to grow.
163
sh.setWidth(sh.width() / 2)
164
text.setMaximumSize(sh)
166
panel.addWidget(text)
168
low_label = factory.low_label
169
if factory.low_name != '':
170
low_label = self.format % self.low
172
high_label = factory.high_label
173
if factory.high_name != '':
174
high_label = self.format % self.high
176
self._label_lo.setText(low_label)
177
self._label_hi.setText(high_label)
179
self.set_tooltip(slider)
180
self.set_tooltip(self._label_lo)
181
self.set_tooltip(self._label_hi)
182
self.set_tooltip(text)
184
#---------------------------------------------------------------------------
185
# Handles the user changing the current slider value:
186
#---------------------------------------------------------------------------
188
def update_object_on_scroll(self, pos):
189
""" Handles the user changing the current slider value.
191
value = self._convert_from_slider(pos)
192
self.control.text.setText(self.format % value)
195
#---------------------------------------------------------------------------
196
# Handle the user pressing the 'Enter' key in the edit control:
197
#---------------------------------------------------------------------------
199
def update_object_on_enter (self):
200
""" Handles the user pressing the Enter key in the text field.
204
value = eval(unicode(self.control.text.text()).strip())
205
except Exception, ex:
206
# The entered something that didn't eval as a number, (e.g.,
207
# 'foo') pretend it didn't happen
209
self.control.text.setText(unicode(value))
211
if not self.factory.is_float:
215
self.control.slider.setValue(self._convert_to_slider(self.value))
216
except TraitError, excp:
219
#---------------------------------------------------------------------------
220
# Updates the editor when the object trait changes external to the editor:
221
#---------------------------------------------------------------------------
223
def update_editor ( self ):
224
""" Updates the editor when the object trait changes externally to the
231
text = self.format % value
232
1 / (low <= value <= high)
237
ivalue = self._convert_to_slider(value)
239
self.control.text.setText(text)
241
blocked = self.control.slider.blockSignals(True)
242
self.control.slider.setValue(ivalue)
243
self.control.slider.blockSignals(blocked)
245
#---------------------------------------------------------------------------
246
# Returns the editor's control for indicating error status:
247
#---------------------------------------------------------------------------
249
def get_error_control ( self ):
250
""" Returns the editor's control for indicating error status.
252
return self.control.text
254
#---------------------------------------------------------------------------
255
# Handles the 'low'/'high' traits being changed:
256
#---------------------------------------------------------------------------
258
def _low_changed ( self, low ):
260
if self.factory.is_float:
261
self.value = float( low )
263
self.value = int( low )
265
if self._label_lo is not None:
266
self._label_lo.setText(self.format % low)
269
def _high_changed ( self, high ):
270
if self.value > high:
271
if self.factory.is_float:
272
self.value = float( high )
274
self.value = int( high )
276
if self._label_hi is not None:
277
self._label_hi.setText(self.format % high)
280
def _convert_to_slider(self, value):
281
""" Returns the slider setting corresponding to the user-supplied value.
283
if self.high > self.low:
284
ivalue = int( (float( value - self.low ) /
285
(self.high - self.low)) * 10000.0 )
293
def _convert_from_slider(self, ivalue):
294
""" Returns the float or integer value corresponding to the slider
297
value = self.low + ((float( ivalue ) / 10000.0) *
298
(self.high - self.low))
299
if not self.factory.is_float:
300
value = int(round(value))
304
#-------------------------------------------------------------------------------
305
class LogRangeSliderEditor ( SimpleSliderEditor ):
306
#-------------------------------------------------------------------------------
307
""" A slider editor for log-spaced values
310
def _convert_to_slider(self, value):
311
""" Returns the slider setting corresponding to the user-supplied value.
313
value = max(value, self.low)
314
ivalue = int( (log10(value) - log10(self.low)) /
315
(log10(self.high) - log10(self.low)) * 10000.0)
318
def _convert_from_slider(self, ivalue):
319
""" Returns the float or integer value corresponding to the slider
322
value = float( ivalue ) / 10000.0 * (log10(self.high) -log10(self.low))
323
# Do this to handle floating point errors, where fvalue may exceed
325
fvalue = min(self.low*10**(value), self.high)
326
if not self.factory.is_float:
327
fvalue = int(round(fvalue))
330
#-------------------------------------------------------------------------------
331
# 'LargeRangeSliderEditor' class:
332
#-------------------------------------------------------------------------------
334
class LargeRangeSliderEditor ( BaseRangeEditor ):
335
""" A slider editor for large ranges.
337
The editor displays a slider and a text field. A subset of the total range
338
is displayed in the slider; arrow buttons at each end of the slider let
339
the user move the displayed range higher or lower.
341
#---------------------------------------------------------------------------
343
#---------------------------------------------------------------------------
345
# Low value for the slider range
348
# High value for the slider range
351
# Low end of displayed range
354
# High end of displayed range
357
# Flag indicating that the UI is in the process of being updated
358
ui_changing = Bool(False)
360
#---------------------------------------------------------------------------
361
# Finishes initializing the editor by creating the underlying toolkit
363
#---------------------------------------------------------------------------
365
def init ( self, parent ):
366
""" Finishes initializing the editor by creating the underlying toolkit
369
factory = self.factory
371
# Initialize using the factory range defaults:
372
self.low = factory.low
373
self.high = factory.high
374
self.evaluate = factory.evaluate
376
# Hook up the traits to listen to the object.
377
self.sync_value( factory.low_name, 'low', 'from' )
378
self.sync_value( factory.high_name, 'high', 'from' )
379
self.sync_value( factory.evaluate_name, 'evaluate', 'from' )
387
self.control = QtGui.QWidget()
388
panel = QtGui.QHBoxLayout(self.control)
389
panel.setContentsMargins(0, 0, 0, 0)
394
fvalue_text = self._format % fvalue
395
1 / (low <= fvalue <= high)
401
ivalue = int( (float( fvalue - low ) / (high - low)) * 10000 )
406
self.control.label_lo = label_lo = QtGui.QLabel()
407
label_lo.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
408
panel.addWidget(label_lo)
410
# Lower limit button:
411
self.control.button_lo = IconButton(QtGui.QStyle.SP_ArrowLeft,
413
panel.addWidget(self.control.button_lo)
416
self.control.slider = slider = QtGui.QSlider(QtCore.Qt.Horizontal)
417
slider.setTracking(factory.auto_set)
419
slider.setMaximum(10000)
420
slider.setPageStep(1000)
421
slider.setSingleStep(100)
422
slider.setValue(ivalue)
423
QtCore.QObject.connect(slider, QtCore.SIGNAL('valueChanged(int)'),
424
self.update_object_on_scroll)
425
panel.addWidget(slider)
427
# Upper limit button:
428
self.control.button_hi = IconButton(QtGui.QStyle.SP_ArrowRight,
430
panel.addWidget(self.control.button_hi)
433
self.control.label_hi = label_hi = QtGui.QLabel()
434
panel.addWidget(label_hi)
437
self.control.text = text = QtGui.QLineEdit(fvalue_text)
438
QtCore.QObject.connect(text, QtCore.SIGNAL('editingFinished()'),
439
self.update_object_on_enter)
441
# The default size is a bit too big and probably doesn't need to grow.
443
sh.setWidth(sh.width() / 2)
444
text.setMaximumSize(sh)
446
panel.addWidget(text)
448
label_lo.setText(str(low))
449
label_hi.setText(str(high))
450
self.set_tooltip(slider)
451
self.set_tooltip(label_lo)
452
self.set_tooltip(label_hi)
453
self.set_tooltip(text)
455
# Update the ranges and button just in case.
456
self.update_range_ui()
458
#---------------------------------------------------------------------------
459
# Handles the user changing the current slider value:
460
#---------------------------------------------------------------------------
462
def update_object_on_scroll(self, pos):
463
""" Handles the user changing the current slider value.
465
value = self.cur_low + ((float(pos) / 10000.0) * (self.cur_high - self.cur_low))
467
self.control.text.setText(self._format % value)
469
if self.factory.is_float:
472
self.value = int(value)
474
#---------------------------------------------------------------------------
475
# Handle the user pressing the 'Enter' key in the edit control:
476
#---------------------------------------------------------------------------
478
def update_object_on_enter(self):
479
""" Handles the user pressing the Enter key in the text field.
482
self.value = eval(unicode(self.control.text.text()).strip())
483
except TraitError, excp:
486
#---------------------------------------------------------------------------
487
# Updates the editor when the object trait changes external to the editor:
488
#---------------------------------------------------------------------------
490
def update_editor ( self ):
491
""" Updates the editor when the object trait changes externally to the
498
text = self._format % value
499
1 / (low <= value <= high)
504
if not self.ui_changing:
506
self.update_range_ui()
508
def update_range_ui ( self ):
509
""" Updates the slider range controls.
511
low, high = self.cur_low, self.cur_high
514
self.control.label_lo.setText( self._format % low )
515
self.control.label_hi.setText( self._format % high )
518
ivalue = int( (float( value - low ) / (high - low)) * 10000.0 )
522
blocked = self.control.slider.blockSignals(True)
523
self.control.slider.setValue( ivalue )
524
self.control.slider.blockSignals(blocked)
526
text = self._format % self.value
527
self.control.text.setText( text )
528
self.control.button_lo.setEnabled(low != self.low)
529
self.control.button_hi.setEnabled(high != self.high)
531
def init_range ( self ):
532
""" Initializes the slider range controls.
535
low, high = self.low, self.high
536
if (high is None) and (low is not None):
541
cur_low = max( value - 10, low )
542
cur_high = min( value + 10, high )
544
d = 0.5 * (10**int( log10( mag ) + 1 ))
545
cur_low = max( low, value - d )
546
cur_high = min( high, value + d )
548
self.cur_low, self.cur_high = cur_low, cur_high
550
def reduce_range(self):
551
""" Reduces the extent of the displayed range.
553
low, high = self.low, self.high
554
if abs( self.cur_low ) < 10:
555
self.cur_low = max( -10, low )
556
self.cur_high = min( 10, high )
557
elif self.cur_low > 0:
558
self.cur_high = self.cur_low
559
self.cur_low = max( low, self.cur_low / 10 )
561
self.cur_high = self.cur_low
562
self.cur_low = max( low, self.cur_low * 10 )
564
self.ui_changing = True
565
self.value = min( max( self.value, self.cur_low ), self.cur_high )
566
self.ui_changing = False
567
self.update_range_ui()
569
def increase_range(self):
570
""" Increased the extent of the displayed range.
572
low, high = self.low, self.high
573
if abs( self.cur_high ) < 10:
574
self.cur_low = max( -10, low )
575
self.cur_high = min( 10, high )
576
elif self.cur_high > 0:
577
self.cur_low = self.cur_high
578
self.cur_high = min( high, self.cur_high * 10 )
580
self.cur_low = self.cur_high
581
self.cur_high = min( high, self.cur_high / 10 )
583
self.ui_changing = True
584
self.value = min( max( self.value, self.cur_low ), self.cur_high )
585
self.ui_changing = False
586
self.update_range_ui()
588
def _set_format ( self ):
590
factory = self.factory
591
low, high = self.cur_low, self.cur_high
595
self._format = '%.2g'
597
self._format = '%%.%df' % max( 0, 4 -
598
int( log10( high - low ) ) )
600
self._format = '%.3f'
602
#---------------------------------------------------------------------------
603
# Returns the editor's control for indicating error status:
604
#---------------------------------------------------------------------------
606
def get_error_control ( self ):
607
""" Returns the editor's control for indicating error status.
609
return self.control.text
611
#---------------------------------------------------------------------------
612
# Handles the 'low'/'high' traits being changed:
613
#---------------------------------------------------------------------------
615
def _low_changed ( self, low ):
616
if self.control is not None:
618
if self.factory.is_float:
619
self.value = float( low )
621
self.value = int( low )
625
def _high_changed ( self, high ):
626
if self.control is not None:
627
if self.value > high:
628
if self.factory.is_float:
629
self.value = float( high )
631
self.value = int( high )
635
#-------------------------------------------------------------------------------
636
# 'SimpleSpinEditor' class:
637
#-------------------------------------------------------------------------------
639
class SimpleSpinEditor ( BaseRangeEditor ):
640
""" A simple style of range editor that displays a spin box control.
642
#---------------------------------------------------------------------------
644
#---------------------------------------------------------------------------
646
# Low value for the slider range
649
# High value for the slider range
652
#---------------------------------------------------------------------------
653
# Finishes initializing the editor by creating the underlying toolkit
655
#---------------------------------------------------------------------------
657
def init ( self, parent ):
658
""" Finishes initializing the editor by creating the underlying toolkit
661
factory = self.factory
662
if not factory.low_name:
663
self.low = factory.low
665
if not factory.high_name:
666
self.high = factory.high
668
self.sync_value( factory.low_name, 'low', 'from' )
669
self.sync_value( factory.high_name, 'high', 'from' )
673
self.control = QtGui.QSpinBox()
674
self.control.setMinimum(low)
675
self.control.setMaximum(high)
676
self.control.setValue(self.value)
677
QtCore.QObject.connect(self.control,
678
QtCore.SIGNAL('valueChanged(int)'), self.update_object)
681
#---------------------------------------------------------------------------
682
# Handle the user selecting a new value from the spin control:
683
#---------------------------------------------------------------------------
685
def update_object(self, value):
686
""" Handles the user selecting a new value in the spin box.
694
#---------------------------------------------------------------------------
695
# Updates the editor when the object trait changes external to the editor:
696
#---------------------------------------------------------------------------
698
def update_editor ( self ):
699
""" Updates the editor when the object trait changes externally to the
704
self.control.setValue(int(self.value))
708
#---------------------------------------------------------------------------
709
# Handles the 'low'/'high' traits being changed:
710
#---------------------------------------------------------------------------
712
def _low_changed ( self, low ):
714
if self.factory.is_float:
715
self.value = float( low )
717
self.value = int( low )
720
self.control.setMinimum(low)
721
self.control.setValue(int(self.value))
723
def _high_changed ( self, high ):
724
if self.value > high:
725
if self.factory.is_float:
726
self.value = float( high )
728
self.value = int( high )
731
self.control.setMaximum(high)
732
self.control.setValue(int(self.value))
734
#-------------------------------------------------------------------------------
735
# 'RangeTextEditor' class:
736
#-------------------------------------------------------------------------------
738
class RangeTextEditor ( TextEditor ):
739
""" Editor for ranges that displays a text field. If the user enters a
740
value that is outside the allowed range, the background of the field
741
changes color to indicate an error.
744
#---------------------------------------------------------------------------
746
#---------------------------------------------------------------------------
748
# Function to evaluate floats/ints
751
#---------------------------------------------------------------------------
752
# Finishes initializing the editor by creating the underlying toolkit
754
#---------------------------------------------------------------------------
756
def init ( self, parent ):
757
""" Finishes initializing the editor by creating the underlying toolkit
760
TextEditor.init( self, parent )
761
self.evaluate = self.factory.evaluate
762
self.sync_value( self.factory.evaluate_name, 'evaluate', 'from' )
764
#---------------------------------------------------------------------------
765
# Handles the user entering input data in the edit control:
766
#---------------------------------------------------------------------------
768
def update_object (self):
769
""" Handles the user entering input data in the edit control.
772
value = eval(unicode(self.control.text()))
773
if self.evaluate is not None:
774
value = self.evaluate(value)
780
pal = QtGui.QPalette(self.control.palette())
781
pal.setColor(QtGui.QPalette.Base, col)
782
self.control.setPalette(pal)
784
#-------------------------------------------------------------------------------
785
# 'SimpleEnumEditor' factory adaptor:
786
#-------------------------------------------------------------------------------
788
def SimpleEnumEditor ( parent, factory, ui, object, name, description ):
789
return CustomEnumEditor( parent, factory, ui, object, name, description,
792
#-------------------------------------------------------------------------------
793
# 'CustomEnumEditor' factory adaptor:
794
#-------------------------------------------------------------------------------
796
def CustomEnumEditor ( parent, factory, ui, object, name, description,
798
""" Factory adapter that returns a enumeration editor of the specified
801
if factory._enum is None:
802
import traitsui.editors.enum_editor as enum_editor
803
factory._enum = enum_editor.ToolkitEditorFactory(
804
values = range( factory.low, factory.high + 1 ),
805
cols = factory.cols )
807
if style == 'simple':
808
return factory._enum.simple_editor( ui, object, name, description,
811
return factory._enum.custom_editor( ui, object, name, description, parent )
813
#-------------------------------------------------------------------------------
814
# Defines the mapping between editor factory 'mode's and Editor classes:
815
#-------------------------------------------------------------------------------
817
# Mapping between editor factory modes and simple editor classes
819
'slider': SimpleSliderEditor,
820
'xslider': LargeRangeSliderEditor,
821
'spinner': SimpleSpinEditor,
822
'enum': SimpleEnumEditor,
823
'text': RangeTextEditor,
824
'low': LogRangeSliderEditor
826
# Mapping between editor factory modes and custom editor classes
828
'slider': SimpleSliderEditor,
829
'xslider': LargeRangeSliderEditor,
830
'spinner': SimpleSpinEditor,
831
'enum': CustomEnumEditor,
832
'text': RangeTextEditor,
833
'low': LogRangeSliderEditor