diff --git a/traitsui/editor.py b/traitsui/editor.py index 4aaab2850..cd6781ef0 100644 --- a/traitsui/editor.py +++ b/traitsui/editor.py @@ -44,6 +44,7 @@ from .item import Item +UNITTESTING = False # Reference to an EditorFactory object factory_trait = Instance(EditorFactory) @@ -164,7 +165,8 @@ def error(self, excp): excp : Exception The exception which occurred. """ - pass + if UNITTESTING: + raise excp def set_focus(self): """Assigns focus to the editor's underlying toolkit widget. diff --git a/traitsui/editor_factory.py b/traitsui/editor_factory.py index 8c971aab5..7cae4d579 100644 --- a/traitsui/editor_factory.py +++ b/traitsui/editor_factory.py @@ -76,6 +76,9 @@ class EditorFactory(HasPrivateTraits): #: The editor class to use for 'readonly' style views. readonly_editor_class = Property() + #: Show the error dialog when an error occur. + show_error_dialog = Bool(True) + def __init__(self, *args, **traits): """Initializes the factory object.""" HasPrivateTraits.__init__(self, **traits) diff --git a/traitsui/editors/range_editor.py b/traitsui/editors/range_editor.py index 832344298..d9fd6621a 100644 --- a/traitsui/editors/range_editor.py +++ b/traitsui/editors/range_editor.py @@ -10,6 +10,7 @@ """ Defines the range editor factory for all traits user interface toolkits. """ +import ast import warnings from types import CodeType @@ -130,12 +131,6 @@ def init(self, handler=None): self.high = eval(handler._high) else: self.high = handler._high - else: - if (self.low is None) and (self.low_name == ""): - self.low = 0.0 - - if (self.high is None) and (self.high_name == ""): - self.high = 1.0 def _get_low(self): return self._low @@ -217,9 +212,9 @@ def _get_simple_editor_class(self): The type of editor depends on the type and extent of the range being edited: - * One end of range is unspecified: RangeTextEditor * **mode** is specified and not 'auto': editor corresponding to **mode** + * One end of range is unspecified: RangeTextEditor * Floating point range with extent > 100: LargeRangeSliderEditor * Integer range or floating point range with extent <= 100: SimpleSliderEditor @@ -227,15 +222,15 @@ def _get_simple_editor_class(self): """ low, high, is_float = self._low_value, self._high_value, self.is_float + if self.mode != "auto": + return toolkit_object("range_editor:SimpleEditorMap")[self.mode] + if (low is None) or (high is None): return toolkit_object("range_editor:RangeTextEditor") if (not is_float) and (abs(high - low) > 1000000000): return toolkit_object("range_editor:RangeTextEditor") - if self.mode != "auto": - return toolkit_object("range_editor:SimpleEditorMap")[self.mode] - if is_float and (abs(high - low) > 100): return toolkit_object("range_editor:LargeRangeSliderEditor") @@ -250,21 +245,21 @@ def _get_custom_editor_class(self): The type of editor depends on the type and extent of the range being edited: - * One end of range is unspecified: RangeTextEditor * **mode** is specified and not 'auto': editor corresponding to **mode** + * One end of range is unspecified: RangeTextEditor * Floating point range: Same as "simple" style * Integer range with extent > 15: Same as "simple" style * Integer range with extent <= 15: CustomEnumEditor """ low, high, is_float = self._low_value, self._high_value, self.is_float - if (low is None) or (high is None): - return toolkit_object("range_editor:RangeTextEditor") - if self.mode != "auto": return toolkit_object("range_editor:CustomEditorMap")[self.mode] + if (low is None) or (high is None): + return toolkit_object("range_editor:RangeTextEditor") + if is_float or (abs(high - low) > 15): return self.simple_editor_class diff --git a/traitsui/examples/demo/Standard_Editors/RangeEditor_demo.py b/traitsui/examples/demo/Standard_Editors/RangeEditor_demo.py index 59b9cd778..01a9b78d3 100644 --- a/traitsui/examples/demo/Standard_Editors/RangeEditor_demo.py +++ b/traitsui/examples/demo/Standard_Editors/RangeEditor_demo.py @@ -28,18 +28,48 @@ .. _RangeEditor API docs: https://docs.enthought.com/traitsui/api/traitsui.editors.range_editor.html#traitsui.editors.range_editor.RangeEditor """ -from traits.api import HasTraits, Range +from traits.api import HasTraits, Range as _Range from traitsui.api import Item, Group, View +# TODO: Update traits.api.Range with the following in order to avoid the popup-error-dialog. +# TODO: Remove redefinition of Range here once the traits.api.Range is updated. +class Range(_Range): + def create_editor(self): + """ Returns the default UI editor for the trait. + """ + # fixme: Needs to support a dynamic range editor. + + auto_set = self.auto_set + if auto_set is None: + auto_set = True + show_error_dialog = self.show_error_dialog + if show_error_dialog is None: + show_error_dialog = True + + from traitsui.api import RangeEditor + return RangeEditor( + self, + mode=self.mode or "auto", + cols=self.cols or 3, + auto_set=auto_set, + enter_set=self.enter_set or False, + low_label=self.low or "", + high_label=self.high or "", + low_name=self._low_name, + high_name=self._high_name, + show_error_dialog=show_error_dialog + ) + + class RangeEditorDemo(HasTraits): """Defines the RangeEditor demo class.""" # Define a trait for each of four range variants: - small_int_range = Range(1, 16) - medium_int_range = Range(1, 25) - large_int_range = Range(1, 150) - float_range = Range(0.0, 150.0) + small_int_range = Range(1, 16, show_error_dialog=False) + medium_int_range = Range(1, 25, show_error_dialog=False) + large_int_range = Range(1, 150, show_error_dialog=False) + float_range = Range(0.0, 150.0, show_error_dialog=False) # RangeEditor display for narrow integer Range traits (< 17 wide): int_range_group1 = Group( diff --git a/traitsui/examples/demo/Standard_Editors/tests/test_RangeEditor_demo.py b/traitsui/examples/demo/Standard_Editors/tests/test_RangeEditor_demo.py index ebf90266d..5a225cfff 100644 --- a/traitsui/examples/demo/Standard_Editors/tests/test_RangeEditor_demo.py +++ b/traitsui/examples/demo/Standard_Editors/tests/test_RangeEditor_demo.py @@ -129,7 +129,8 @@ def test_run_demo(self): simple_float_slider.perform(KeyClick("Page Up")) self.assertEqual(demo.float_range, 1.000) simple_float_text = simple_float.locate(Textbox()) - for _ in range(3): + displayed = simple_float_text.inspect(DisplayedText()) + for _ in range(len(displayed) - 2): simple_float_text.perform(KeyClick("Backspace")) simple_float_text.perform(KeyClick("5")) simple_float_text.perform(KeyClick("Enter")) @@ -137,10 +138,10 @@ def test_run_demo(self): custom_float_slider = custom_float.locate(Slider()) # after the trait is set to 1.5 above, the active range shown by - # the LargeRangeSliderEditor for the custom style is [0,11.500] - # so a page down is now a decrement of 1.15 + # the LargeRangeSliderEditor for the custom style is [0,10.00] + # so a page down is now a decrement of 1.0 custom_float_slider.perform(KeyClick("Page Down")) - self.assertEqual(round(demo.float_range, 2), 0.35) + self.assertEqual(round(demo.float_range, 2), 0.5) custom_float_text = custom_float.locate(Textbox()) for _ in range(5): custom_float_text.perform(KeyClick("Backspace")) diff --git a/traitsui/qt4/editor.py b/traitsui/qt4/editor.py index a73e65158..bd17eabfd 100644 --- a/traitsui/qt4/editor.py +++ b/traitsui/qt4/editor.py @@ -67,22 +67,24 @@ def update_editor(self): def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" - # Make sure the control is a widget rather than a layout. - if isinstance(self.control, QtGui.QLayout): - control = self.control.parentWidget() - else: - control = self.control - - message_box = QtGui.QMessageBox( - QtGui.QMessageBox.Information, - self.description + " value error", - str(excp), - buttons=QtGui.QMessageBox.Ok, - parent=control, - ) - message_box.setTextFormat(QtCore.Qt.PlainText) - message_box.setEscapeButton(QtGui.QMessageBox.Ok) - message_box.exec_() + super().error(excp) + if self.factory.show_error_dialog: + # Make sure the control is a widget rather than a layout. + if isinstance(self.control, QtGui.QLayout): + control = self.control.parentWidget() + else: + control = self.control + + message_box = QtGui.QMessageBox( + QtGui.QMessageBox.Information, + self.description + " value error", + str(excp), + buttons=QtGui.QMessageBox.Ok, + parent=control, + ) + message_box.setTextFormat(QtCore.Qt.PlainText) + message_box.setEscapeButton(QtGui.QMessageBox.Ok) + message_box.exec_() def set_tooltip_text(self, control, text): """Sets the tooltip for a specified control.""" diff --git a/traitsui/qt4/range_editor.py b/traitsui/qt4/range_editor.py index d247e894a..249adb27f 100644 --- a/traitsui/qt4/range_editor.py +++ b/traitsui/qt4/range_editor.py @@ -25,17 +25,16 @@ """ +import ast from math import log10 from pyface.qt import QtCore, QtGui from traits.api import TraitError, Str, Float, Any, Bool -from .editor_factory import TextEditor - from .editor import Editor -from .constants import OKColor, ErrorColor +from .constants import OKColor from .helper import IconButton @@ -62,31 +61,27 @@ def _set_value(self, value): value = self.evaluate(value) Editor._set_value(self, value) - -class SimpleSliderEditor(BaseRangeEditor): - """Simple style of range editor that displays a slider and a text field. - - The user can set a value either by moving the slider or by typing a value - in the text field. - """ - - # ------------------------------------------------------------------------- - # Trait definitions: - # ------------------------------------------------------------------------- - - #: Low value for the slider range + #: Low value for the range low = Any() - #: High value for the slider range + #: High value for the range high = Any() #: Deprecated: This trait is no longer used. See enthought/traitsui#1704 format = Str() + #: Flag indicating that the UI is in the process of being updated + ui_changing = Bool(False) + def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ + self._init_with_factory_defaults() + self.control = self._make_control() + self._do_layout(self.control) + + def _init_with_factory_defaults(self): factory = self.factory if not factory.low_name: self.low = factory.low @@ -100,81 +95,59 @@ def init(self, parent): self.sync_value(factory.low_name, "low", "from") self.sync_value(factory.high_name, "high", "from") - self.control = QtGui.QWidget() - panel = QtGui.QHBoxLayout(self.control) - panel.setContentsMargins(0, 0, 0, 0) + def _make_control(self): + raise NotImplementedError - fvalue = self.value + def _do_layout(self, control): + raise NotImplementedError + + def _clip(self, fvalue, low, high): + """Returns fvalue clipped between low and high""" try: - if not (self.low <= fvalue <= self.high): - fvalue = self.low - fvalue_text = self.string_value(fvalue) + if low is not None and fvalue < low: + return low + if high is not None and high < fvalue: + return high except: - fvalue_text = "" - fvalue = self.low - - ivalue = self._convert_to_slider(fvalue) - - self._label_lo = QtGui.QLabel() - self._label_lo.setAlignment( - QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter - ) - if factory.label_width > 0: - self._label_lo.setMinimumWidth(factory.label_width) - panel.addWidget(self._label_lo) + return low + return fvalue - self.control.slider = slider = QtGui.QSlider(QtCore.Qt.Horizontal) - slider.setTracking(factory.auto_set) - slider.setMinimum(0) - slider.setMaximum(10000) - slider.setPageStep(1000) - slider.setSingleStep(100) - slider.setValue(ivalue) - slider.valueChanged.connect(self.update_object_on_scroll) - panel.addWidget(slider) + def _make_text_entry(self, fvalue_text): - self._label_hi = QtGui.QLabel() - panel.addWidget(self._label_hi) - if factory.label_width > 0: - self._label_hi.setMinimumWidth(factory.label_width) + text = QtGui.QLineEdit(fvalue_text) + # text.installEventFilter(text) + if self.factory.enter_set: + text.returnPressed.connect(self.update_object_on_enter) - self.control.text = text = QtGui.QLineEdit(fvalue_text) text.editingFinished.connect(self.update_object_on_enter) + if self.factory.auto_set: + text.textChanged.connect(self.update_object_on_enter) # The default size is a bit too big and probably doesn't need to grow. sh = text.sizeHint() sh.setWidth(sh.width() // 2) text.setMaximumSize(sh) - - panel.addWidget(text) - - low_label = factory.low_label - if factory.low_name != "": - low_label = self.string_value(self.low) - - high_label = factory.high_label - if factory.high_name != "": - high_label = self.string_value(self.high) - - self._label_lo.setText(low_label) - self._label_hi.setText(high_label) - - self.set_tooltip(slider) - self.set_tooltip(self._label_lo) - self.set_tooltip(self._label_hi) self.set_tooltip(text) - def update_object_on_scroll(self, pos): - """Handles the user changing the current slider value.""" - value = self._convert_from_slider(pos) - self.control.text.setText(self.string_value(value)) - try: - self.value = value - except Exception as exc: - from traitsui.api import raise_to_debug + return text - raise_to_debug() + def _validate(self, value): + if self.low is not None and value < self.low: + message = "The value ({}) must be larger than {}!" + raise ValueError(message.format(value, self.low)) + if self.high is not None and value > self.high: + message = "The value ({}) must be smaller than {}!" + raise ValueError(message.format(value, self.high)) + if not self.factory.is_float and isinstance(value, float): + message = "The value must be an integer, but a value of {} was specified." + raise ValueError(message.format(value)) + + def _set_color(self, color): + if self.control is not None: + pal = QtGui.QPalette(self.control.text.palette()) + pal.setColor(QtGui.QPalette.Base, color) + self.control.text.setPalette(pal) def update_object_on_enter(self): """Handles the user pressing the Enter key in the text field.""" @@ -184,54 +157,49 @@ def update_object_on_enter(self): return try: - try: - value = eval(str(self.control.text.text()).strip()) - except Exception as ex: - # The entered something that didn't eval as a number, (e.g., - # 'foo') pretend it didn't happen - value = self.value - self.control.text.setText(str(value)) - # for compound editor, value may be non-numeric - if not isinstance(value, (int, float)): - return - - if not self.factory.is_float: - value = int(value) - + value = ast.literal_eval(self.control.text.text()) + self._validate(value) self.value = value - blocked = self.control.slider.blockSignals(True) - try: - self.control.slider.setValue( - self._convert_to_slider(self.value) - ) - finally: - self.control.slider.blockSignals(blocked) - except TraitError as excp: - pass + except Exception as excp: + self.error(excp) + return + + if not self.ui_changing: + self._set_slider(value) + + self._set_color(OKColor) + if self._error is not None: + self._error = None + self.ui.errors -= 1 + + def error(self, excp): + """ Handles an error that occurs while setting the object's trait value. + """ + if self._error is None: + self._error = True + self.ui.errors += 1 + super().error(excp) + self.set_error_state(True) def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ - value = self.value - low = self.low - high = self.high - try: - text = self.string_value(value) - 1 / (low <= value <= high) - except: - text = "" - value = low - - ivalue = self._convert_to_slider(value) + fvalue = self._clip(self.value, self.low, self.high) + text = self.string_value(fvalue) + self.ui_changing = True self.control.text.setText(text) + self.ui_changing = False + self._set_slider(fvalue) - blocked = self.control.slider.blockSignals(True) - try: - self.control.slider.setValue(ivalue) - finally: - self.control.slider.blockSignals(blocked) + def _set_slider(self, value): + """Updates the slider range controls.""" + # Do nothing for non-sliders. + + def _get_current_range(self): + low, high = self.low, self.high + return low, high def get_error_control(self): """Returns the editor's control for indicating error status.""" @@ -239,44 +207,137 @@ def get_error_control(self): def _low_changed(self, low): if self.value < low: - if self.factory.is_float: - self.value = float(low) - else: - self.value = int(low) - - if self._label_lo is not None: - self._label_lo.setText(self.string_value(low)) + self.value = float(low) if self.factory.is_float else int(low) + if self.control is not None: self.update_editor() def _high_changed(self, high): if self.value > high: - if self.factory.is_float: - self.value = float(high) - else: - self.value = int(high) - - if self._label_hi is not None: - self._label_hi.setText(self.string_value(high)) + self.value = float(high) if self.factory.is_float else int(high) + if self.control is not None: self.update_editor() + +class SimpleSliderEditor(BaseRangeEditor): + """ Simple style of range editor that displays a slider and a text field. + + The user can set a value either by moving the slider or by typing a value + in the text field. + """ + + # ------------------------------------------------------------------------- + # Trait definitions: See BaseRangeEditor + # ------------------------------------------------------------------------- + + def _make_control(self): + + low, high = self._get_current_range() + fvalue = self._clip(self.value, low, high) + fvalue_text = self.string_value(fvalue) + + width = self._get_default_width() + + control = QtGui.QWidget() + control.label_lo = self._make_label_low(low, width) + control.slider = self._make_slider(fvalue) + control.label_hi = self._make_label_high(high, width) + control.text = self._make_text_entry(fvalue_text) + return control + + @staticmethod + def _do_layout(control): + layout = QtGui.QHBoxLayout(control) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(control.label_lo) + layout.addWidget(control.slider) + layout.addWidget(control.label_hi) + layout.addWidget(control.text) + + def _get_default_width(self): + return self.factory.label_width + + def _get_label_high(self, high): + if self.factory.high_name != "": + return self.string_value(high) + return self.factory.high_label + + def _get_label_low(self, low): + if self.factory.low_name != "": + return self.string_value(low) + return self.factory.low_label + + def _make_label_low(self, low, width): + low_label = self._get_label_low(low) + label_lo = QtGui.QLabel(low_label) + label_lo.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + if width > 0: + label_lo.setMinimumWidth(width) + self.set_tooltip(label_lo) + + return label_lo + + def _make_slider(self, fvalue): + ivalue = self._convert_to_slider(fvalue) + slider = QtGui.QSlider(QtCore.Qt.Horizontal) + slider.setTracking(self.factory.auto_set) + slider.setMinimum(0) + slider.setMaximum(10000) + slider.setPageStep(1000) + slider.setSingleStep(100) + slider.setValue(ivalue) + slider.valueChanged.connect(self.update_object_on_scroll) + self.set_tooltip(slider) + + return slider + + def _make_label_high(self, high, width): + high_label = self._get_label_high(high) + label_hi = QtGui.QLabel(high_label) + if width > 0: + label_hi.setMinimumWidth(width) + self.set_tooltip(label_hi) + + return label_hi + + def update_object_on_scroll(self, pos): + """Handles the user changing the current slider value.""" + value = self._convert_from_slider(pos) + try: + self.ui_changing = True + self.control.text.setText(self.string_value(value)) + self.value = value + except TraitError: + pass + finally: + self.ui_changing = False + + def _set_slider(self, value): + """Updates the slider range controls.""" + low, high = self._get_current_range() + self.control.label_lo.setText(self.string_value(low)) + self.control.label_hi.setText(self.string_value(high)) + blocked = self.control.slider.blockSignals(True) + try: + ivalue = self._convert_to_slider(value) + self.control.slider.setValue(ivalue) + finally: + self.control.slider.blockSignals(blocked) + def _convert_to_slider(self, value): """Returns the slider setting corresponding to the user-supplied value.""" - if self.high > self.low: - ivalue = int( - (float(value - self.low) / (self.high - self.low)) * 10000.0 - ) - else: - ivalue = self.low - - if ivalue is None: - ivalue = 0 - return ivalue + low, high = self._get_current_range() + if high > low: + return int(float(value - low) / (high - low) * 10000.0) + if low is None: + return 0 + return low def _convert_from_slider(self, ivalue): """Returns the float or integer value corresponding to the slider setting. """ - value = self.low + ((float(ivalue) / 10000.0) * (self.high - self.low)) + low, high = self._get_current_range() + value = low + ((float(ivalue) / 10000.0) * (high - low)) if not self.factory.is_float: value = int(round(value)) return value @@ -289,448 +350,340 @@ class LogRangeSliderEditor(SimpleSliderEditor): def _convert_to_slider(self, value): """Returns the slider setting corresponding to the user-supplied value.""" - value = max(value, self.low) - ivalue = int( - (log10(value) - log10(self.low)) - / (log10(self.high) - log10(self.low)) - * 10000.0 - ) - return ivalue + low, high = self._get_current_range() + value = max(value, low) + return int((log10(value) - log10(low)) / (log10(high) - log10(low)) * 10000.0) def _convert_from_slider(self, ivalue): """Returns the float or integer value corresponding to the slider setting. """ - value = float(ivalue) / 10000.0 * (log10(self.high) - log10(self.low)) + low, high = self._get_current_range() + value = float(ivalue) / 10000.0 * (log10(high) - log10(low)) # Do this to handle floating point errors, where fvalue may exceed # self.high. - fvalue = min(self.low * 10 ** (value), self.high) + fvalue = min(low * 10 ** (value), high) if not self.factory.is_float: fvalue = int(round(fvalue)) return fvalue -class LargeRangeSliderEditor(BaseRangeEditor): +class LargeRangeSliderEditor(SimpleSliderEditor): """A slider editor for large ranges. - The editor displays a slider and a text field. A subset of the total range - is displayed in the slider; arrow buttons at each end of the slider let - the user move the displayed range higher or lower. + The editor displays a slider and a text field. A subset of the total + range is displayed in the slider; arrow buttons at each end of the + slider let the user move the displayed range higher or lower. """ # ------------------------------------------------------------------------- - # Trait definitions: + # Trait definitions: See BaseRangeEditor # ------------------------------------------------------------------------- - #: Low value for the slider range - low = Any(0) - - #: High value for the slider range - high = Any(1) - - #: Low end of displayed range + #: Low end of displayed slider range cur_low = Float() - #: High end of displayed range + #: High end of displayed slider range cur_high = Float() - #: Flag indicating that the UI is in the process of being updated - ui_changing = Bool(False) - def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ - factory = self.factory - - # Initialize using the factory range defaults: - self.low = factory.low - self.high = factory.high - self.evaluate = factory.evaluate - - # Hook up the traits to listen to the object. - self.sync_value(factory.low_name, "low", "from") - self.sync_value(factory.high_name, "high", "from") - self.sync_value(factory.evaluate_name, "evaluate", "from") - - self.init_range() - low = self.cur_low - high = self.cur_high - - self._set_format() - - self.control = QtGui.QWidget() - panel = QtGui.QHBoxLayout(self.control) - panel.setContentsMargins(0, 0, 0, 0) - - fvalue = self.value - - try: - fvalue_text = self._format % fvalue - 1 / (low <= fvalue <= high) - except: - fvalue_text = "" - fvalue = low - - if high > low: - ivalue = int((float(fvalue - low) / (high - low)) * 10000) - else: - ivalue = low - - # Lower limit label: - self.control.label_lo = label_lo = QtGui.QLabel() - label_lo.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - panel.addWidget(label_lo) - - # Lower limit button: - self.control.button_lo = IconButton( - QtGui.QStyle.SP_ArrowLeft, self.reduce_range - ) - panel.addWidget(self.control.button_lo) - - # Slider: - self.control.slider = slider = QtGui.QSlider(QtCore.Qt.Horizontal) - slider.setTracking(factory.auto_set) - slider.setMinimum(0) - slider.setMaximum(10000) - slider.setPageStep(1000) - slider.setSingleStep(100) - slider.setValue(ivalue) - slider.valueChanged.connect(self.update_object_on_scroll) - panel.addWidget(slider) - - # Upper limit button: - self.control.button_hi = IconButton( - QtGui.QStyle.SP_ArrowRight, self.increase_range - ) - panel.addWidget(self.control.button_hi) - - # Upper limit label: - self.control.label_hi = label_hi = QtGui.QLabel() - panel.addWidget(label_hi) - - # Text entry: - self.control.text = text = QtGui.QLineEdit(fvalue_text) - text.editingFinished.connect(self.update_object_on_enter) - - # The default size is a bit too big and probably doesn't need to grow. - sh = text.sizeHint() - sh.setWidth(sh.width() // 2) - text.setMaximumSize(sh) - - panel.addWidget(text) - - label_lo.setText(str(low)) - label_hi.setText(str(high)) - self.set_tooltip(slider) - self.set_tooltip(label_lo) - self.set_tooltip(label_hi) - self.set_tooltip(text) - - # Update the ranges and button just in case. - self.update_range_ui() - - def update_object_on_scroll(self, pos): - """Handles the user changing the current slider value.""" - value = self.cur_low + ( - (float(pos) / 10000.0) * (self.cur_high - self.cur_low) - ) - - self.control.text.setText(self._format % value) - - if self.factory.is_float: - self.value = value - else: - self.value = int(value) - - def update_object_on_enter(self): - """Handles the user pressing the Enter key in the text field.""" - # It is possible the event is processed after the control is removed - # from the editor - if self.control is None: - return - try: - self.value = eval(str(self.control.text.text()).strip()) - except TraitError as excp: - pass - - def update_editor(self): - """Updates the editor when the object trait changes externally to the - editor. - """ - value = self.value - low = self.low - high = self.high - try: - text = self._format % value - 1 / (low <= value <= high) - except: - value = low - self.value = value - - if not self.ui_changing: - self.init_range() - self.update_range_ui() - - def update_range_ui(self): + self._init_with_factory_defaults() + self.init_current_range(self.value) + self.control = self._make_control(parent) + # Set-up the layout: + self._do_layout(self.control) + + def _make_control(self, parent): + control = super()._make_control() + low, high = self._get_current_range() + + control.button_lo = self._make_button_low(low) + control.button_hi = self._make_button_high(high) + return control + + @staticmethod + def _do_layout(control): + layout = QtGui.QHBoxLayout(control) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(control.label_lo) + layout.addWidget(control.button_lo) + layout.addWidget(control.slider) + layout.addWidget(control.button_hi) + layout.addWidget(control.label_hi) + layout.addWidget(control.text) + + def _make_button_low(self, low): + button_lo = IconButton(QtGui.QStyle.SP_ArrowLeft, self.reduce_range) + button_lo.setEnabled(self.low is None or low != self.low) + return button_lo + + def _make_button_high(self, high): + button_hi = IconButton(QtGui.QStyle.SP_ArrowRight, self.increase_range) + button_hi.setEnabled(self.high is None or high != self.high) + return button_hi + + def _set_slider(self, value): """Updates the slider range controls.""" - low, high = self.cur_low, self.cur_high - value = self.value - self._set_format() - self.control.label_lo.setText(self._format % low) - self.control.label_hi.setText(self._format % high) - - if high > low: - ivalue = int((float(value - low) / (high - low)) * 10000.0) - else: - ivalue = low - + low, high = self._get_current_range() + if not low <= value <= high: + low, high = self.init_current_range(value) + ivalue = self._convert_to_slider(value) blocked = self.control.slider.blockSignals(True) - self.control.slider.setValue(ivalue) - self.control.slider.blockSignals(blocked) + try: + self.control.slider.setValue(ivalue) + finally: + self.control.slider.blockSignals(blocked) - text = self._format % self.value - self.control.text.setText(text) - self.control.button_lo.setEnabled(low != self.low) - self.control.button_hi.setEnabled(high != self.high) + fmt = self._get_format() + self.control.label_lo.setText(fmt % low) + self.control.label_hi.setText(fmt % high) - def init_range(self): - """Initializes the slider range controls.""" - value = self.value + self.control.button_lo.setEnabled(self.low is None or low != self.low) + self.control.button_hi.setEnabled(self.high is None or high != self.high) + + def init_current_range(self, value): + """Initializes the current slider range controls, cur_low and cur_high.""" low, high = self.low, self.high - if (high is None) and (low is not None): - high = -low - mag = abs(value) - if mag <= 10.0: - cur_low = max(value - 10, low) - cur_high = min(value + 10, high) - else: - d = 0.5 * (10 ** int(log10(mag) + 1)) - cur_low = max(low, value - d) - cur_high = min(high, value + d) + mag = max(abs(value), 1) + rounded_value = 10 ** int(log10(mag)) + fact_hi, fact_lo = (10, 1) if value >= 0 else (-1, -10) + cur_low = rounded_value * fact_lo + cur_high = rounded_value * fact_hi + if mag <= 10: + if value >= 0: + cur_low *= -1 + else: + cur_high *= -1 + + if low is not None and cur_low < low: + cur_low = low + if high is not None and high < cur_high: + cur_high = high self.cur_low, self.cur_high = cur_low, cur_high + return self.cur_low, self.cur_high def reduce_range(self): """Reduces the extent of the displayed range.""" - low, high = self.low, self.high + value = self.value + low = self.low + + old_cur_low = self.cur_low if abs(self.cur_low) < 10: - self.cur_low = max(-10, low) - self.cur_high = min(10, high) - elif self.cur_low > 0: - self.cur_high = self.cur_low - self.cur_low = max(low, self.cur_low / 10) + value = value - 10 + self.cur_low = max(-10, low) if low is not None else -10 + if old_cur_low - self.cur_low > 9: + self.cur_high = old_cur_low else: - self.cur_high = self.cur_low - self.cur_low = max(low, self.cur_low * 10) + fact = 0.1 if self.cur_low > 0 else 10 + value = value * fact + new_cur_low = self.cur_low * fact + self.cur_low = max(low, new_cur_low) if low is not None else new_cur_low + if self.cur_low == new_cur_low: + self.cur_high = old_cur_low - self.ui_changing = True - self.value = min(max(self.value, self.cur_low), self.cur_high) - self.ui_changing = False - self.update_range_ui() + value = min(max(value, self.cur_low), self.cur_high) + + if self.factory.is_float is False: + value = int(value) + self.value = value + self.update_editor() def increase_range(self): """Increased the extent of the displayed range.""" - low, high = self.low, self.high + value = self.value + high = self.high + old_cur_high = self.cur_high if abs(self.cur_high) < 10: - self.cur_low = max(-10, low) - self.cur_high = min(10, high) - elif self.cur_high > 0: - self.cur_low = self.cur_high - self.cur_high = min(high, self.cur_high * 10) + value = value + 10 + self.cur_high = min(10, high) if high is not None else 10 + if self.cur_high - old_cur_high > 9: + self.cur_low = old_cur_high else: - self.cur_low = self.cur_high - self.cur_high = min(high, self.cur_high / 10) - - self.ui_changing = True - self.value = min(max(self.value, self.cur_low), self.cur_high) - self.ui_changing = False - self.update_range_ui() - - def _set_format(self): - self._format = "%d" - factory = self.factory - low, high = self.cur_low, self.cur_high - diff = high - low - if factory.is_float: - if diff > 99999: - self._format = "%.2g" - elif diff > 1: - self._format = "%%.%df" % max(0, 4 - int(log10(high - low))) - else: - self._format = "%.3f" + fact = 10 if self.cur_high > 0 else 0.1 + value = value * fact + new_cur_high = self.cur_high * fact + self.cur_high = min(high, new_cur_high) if high is not None else new_cur_high + if self.cur_high == new_cur_high: + self.cur_low = old_cur_high - def get_error_control(self): - """Returns the editor's control for indicating error status.""" - return self.control.text + value = min(max(value, self.cur_low), self.cur_high) - def _low_changed(self, low): - if self.control is not None: - if self.value < low: - if self.factory.is_float: - self.value = float(low) - else: - self.value = int(low) + if self.factory.is_float is False: + value = int(value) - self.update_editor() + self.value = value + self.update_editor() - def _high_changed(self, high): - if self.control is not None: - if self.value > high: - if self.factory.is_float: - self.value = float(high) - else: - self.value = int(high) + def _get_format(self): + if self.factory.is_float: + low, high = self._get_current_range() + diff = high - low + if diff > 99999: + return "%.2g" + elif diff > 1: + return "%%.%df" % max(0, 4 - int(log10(diff))) + return "%.3f" + return "%d" - self.update_editor() + def _get_current_range(self): + return self.cur_low, self.cur_high class SimpleSpinEditor(BaseRangeEditor): - """A simple style of range editor that displays a spin box control.""" - - # ------------------------------------------------------------------------- - # Trait definitions: - # ------------------------------------------------------------------------- - - # Low value for the slider range - low = Any() - - # High value for the slider range - high = Any() - - def init(self, parent): - """Finishes initializing the editor by creating the underlying toolkit - widget. - """ - factory = self.factory - if not factory.low_name: - self.low = factory.low - - if not factory.high_name: - self.high = factory.high - - self.sync_value(factory.low_name, "low", "from") - self.sync_value(factory.high_name, "high", "from") - low = self.low - high = self.high - - self.control = QtGui.QSpinBox() - self.control.setMinimum(low) - self.control.setMaximum(high) - self.control.setValue(self.value) - self.control.valueChanged.connect(self.update_object) - self.set_tooltip() - - def update_object(self, value): - """Handles the user selecting a new value in the spin box.""" - self._locked = True - try: - self.value = value - finally: - self._locked = False - - def update_editor(self): - """Updates the editor when the object trait changes externally to the - editor. - """ - if not self._locked: - blocked = self.control.blockSignals(True) - try: - self.control.setValue(int(self.value)) - except Exception: - from traitsui.api import raise_to_debug - - raise_to_debug() - finally: - self.control.blockSignals(blocked) - - def _low_changed(self, low): - if self.value < low: - if self.factory.is_float: - self.value = float(low) - else: - self.value = int(low) - - if self.control: - self.control.setMinimum(low) - self.control.setValue(int(self.value)) - - def _high_changed(self, high): - if self.value > high: - if self.factory.is_float: - self.value = float(high) - else: - self.value = int(high) - - if self.control: - self.control.setMaximum(high) - self.control.setValue(int(self.value)) - - -class RangeTextEditor(TextEditor): - """Editor for ranges that displays a text field. If the user enters a - value that is outside the allowed range, the background of the field - changes color to indicate an error. + """A simple style of range editor that displays a spin box control. + + The SimpleSpinEditor catches 3 different types of events that will increase/decrease + the value of the class: + 1) Spin event generated by pushing the up/down spinbutton; + 2) Key pressed event generated by pressing the arrow- or page-up/down of the keyboard. + 3) Mouse wheel event generated by rolling the mouse wheel up or down. + + In addition, there are some other functionalities: + + - ``Shift`` + arrow = 2 * increment (or ``Shift`` + mouse wheel); + - ``Ctrl`` + arrow = 10 * increment (or ``Ctrl`` + mouse wheel); + - ``Alt`` + arrow = 100 * increment (or ``Alt`` + mouse wheel); + - Combinations of ``Shift``, ``Ctrl``, ``Alt`` increment the + step value by the product of the factors; + - ``PgUp`` & ``PgDn`` = 10 * increment * the product of the ``Shift``, ``Ctrl``, ``Alt`` + factors; """ # ------------------------------------------------------------------------- - # Trait definitions: + # Trait definitions: See BaseRangeEditor # ------------------------------------------------------------------------- - #: Low value for the slider range - low = Any() - - #: High value for the slider range - high = Any() - - #: Function to evaluate floats/ints - evaluate = Any() - - def init(self, parent): - """Finishes initializing the editor by creating the underlying toolkit - widget. - """ - TextEditor.init(self, parent) - - factory = self.factory - if not factory.low_name: - self.low = factory.low - - if not factory.high_name: - self.high = factory.high - - self.evaluate = factory.evaluate - self.sync_value(factory.evaluate_name, "evaluate", "from") + #: Step value for the spinner + step = Any(1) + + def _make_control(self): + + low, high = self._get_current_range() + fvalue = self._clip(self.value, low, high) + fvalue_text = self.string_value(fvalue) + + spin_up_or_down = self._spin + + class Control(QtGui.QWidget): + def wheelEvent(self, event): + delta = event.angleDelta() + y = delta.y() + x = delta.x() + sign = 1 if x + y > 0 else -1 + spin_up_or_down(sign) + + def keyPressEvent(self, event): + key = event.key() + scale = 1 if key in {QtCore.Qt.Key_Up, QtCore.Qt.Key_Down} else 10 + if key in {QtCore.Qt.Key_Up, QtCore.Qt.Key_PageUp}: + spin_up_or_down(scale) + elif key in {QtCore.Qt.Key_Down, QtCore.Qt.Key_PageDown}: + spin_up_or_down(-scale) + + control = Control() + control.text = self._make_text_entry(fvalue_text) + control.button_lo = self._make_button_low(fvalue) + control.button_hi = self._make_button_high(fvalue) + return control + + @staticmethod + def _do_layout(control): + height = control.text.minimumSizeHint().height() + width = height // 2 - 1 + control.button_lo.setFixedSize(width, width) + control.button_hi.setFixedSize(width, width) + + layout = QtGui.QHBoxLayout(control) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(control.text) + vwidget = QtGui.QWidget() + vlayout = QtGui.QVBoxLayout(vwidget) + vlayout.setContentsMargins(0, 0, 0, 0) + vlayout.addWidget(control.button_hi) + vlayout.addWidget(control.button_lo) + layout.addWidget(vwidget) + # layout.addStretch(1) + # vwidget.resize(width, height) + + def _make_button_low(self, value): + # icon = QtGui.QStyle.SP_ArrowDown + icon = QtGui.QStyle.SP_TitleBarUnshadeButton + button_lo = IconButton(icon, self.spin_down) + button_lo.setEnabled(self.low is None or self.low < value) + return button_lo + + def _make_button_high(self, value): + #icon = QtGui.QStyle.SP_ArrowUp + icon = QtGui.QStyle.SP_TitleBarShadeButton + button_hi = IconButton(icon, self.spin_up) + button_hi.setEnabled(self.high is None or value < self.high) + return button_hi + + def _spin(self, sign): + step = sign * self._get_modifier() + value = self.value + low, high = self._get_current_range() - self.sync_value(factory.low_name, "low", "from") - self.sync_value(factory.high_name, "high", "from") + value = self._clip(value + step, low, high) - # force value to start in range - if not (self.low <= self.value <= self.high): - self.value = self.low + if self.factory.is_float is False: + value = int(value) + self.value = value + self.update_editor() + + def _get_modifier(self): + QModifiers = QtGui.QApplication.keyboardModifiers() + modifier = self.step + if (QModifiers & QtCore.Qt.ShiftModifier) == QtCore.Qt.ShiftModifier: + modifier = modifier * 2 + if (QModifiers & QtCore.Qt.ControlModifier) == QtCore.Qt.ControlModifier: + modifier = modifier * 10 + if (QModifiers & QtCore.Qt.AltModifier) == QtCore.Qt.AltModifier: + modifier = modifier * 100 + return modifier + + def spin_down(self): + """Reduces the value.""" + self._spin(sign=-1) + + def spin_up(self): + """Increases the value.""" + self._spin(sign=1) + + def _set_slider(self, value): + """Updates the spinbutton controls.""" + low, high = self._get_current_range() + self.control.button_lo.setEnabled(low is None or low < value) + self.control.button_hi.setEnabled(high is None or value < high) + + +class RangeTextEditor(BaseRangeEditor): + """Editor for ranges that displays a text field. + + If the user enters a value that is outside the allowed range, + the background of the field changes color to indicate an error. + """ - def update_object(self): - """Handles the user entering input data in the edit control.""" - try: - value = eval(str(self.control.text())) - if self.evaluate is not None: - value = self.evaluate(value) + def _make_control(self): - if not (self.low <= value <= self.high): - value = self.low - col = ErrorColor - else: - col = OKColor + low, high = self._get_current_range() + fvalue = self._clip(self.value, low, high) + fvalue_text = self.string_value(fvalue) - self.value = value - except Exception: - col = ErrorColor + control = QtGui.QWidget() + control.text = self._make_text_entry(fvalue_text) + return control - if self.control is not None: - pal = QtGui.QPalette(self.control.palette()) - pal.setColor(QtGui.QPalette.Base, col) - self.control.setPalette(pal) + @staticmethod + def _do_layout(control): + layout = QtGui.QHBoxLayout(control) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(control.text) # ------------------------------------------------------------------------- diff --git a/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/range_editor.py b/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/range_editor.py index 8c3aab604..e7e3fe560 100644 --- a/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/range_editor.py +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/range_editor.py @@ -13,6 +13,7 @@ LogRangeSliderEditor, RangeTextEditor, SimpleSliderEditor, + SimpleSpinEditor ) from traitsui.testing.tester.command import KeyClick @@ -100,6 +101,8 @@ def register(registry): SimpleSliderEditor, LogRangeSliderEditor, LargeRangeSliderEditor, + SimpleSpinEditor, + RangeTextEditor ] for target_class in targets: registry.register_location( @@ -109,6 +112,12 @@ def register(registry): textbox=wrapper._target.control.text ), ) + _registry_helper.register_editable_textbox_handlers( + registry=registry, + target_class=target_class, + widget_getter=lambda wrapper: wrapper._target.control.text, + ) + for target_class in targets[:3]: registry.register_location( target_class=target_class, locator_class=Slider, @@ -116,11 +125,6 @@ def register(registry): slider=wrapper._target.control.slider ), ) - _registry_helper.register_editable_textbox_handlers( - registry=registry, - target_class=RangeTextEditor, - widget_getter=lambda wrapper: wrapper._target.control, - ) LocatedTextbox.register(registry) diff --git a/traitsui/testing/tester/_ui_tester_registry/wx/_interaction_helpers.py b/traitsui/testing/tester/_ui_tester_registry/wx/_interaction_helpers.py index a0c3acd48..2b1e637e4 100644 --- a/traitsui/testing/tester/_ui_tester_registry/wx/_interaction_helpers.py +++ b/traitsui/testing/tester/_ui_tester_registry/wx/_interaction_helpers.py @@ -41,7 +41,7 @@ def _create_event(event_type, control): def mouse_click(func): """Decorator function for mouse clicks. Decorated functions will return if they are not enabled. Additionally, this handles the delay for the - click. + click as well as set the focus to the control if not already set. Parameters ---------- @@ -70,6 +70,8 @@ def mouse_click_handler(*, control, delay, **kwargs): "Nothing was performed." ) return + if not control.HasFocus(): + control.SetFocus() wx.MilliSleep(delay) func(control=control, delay=delay, **kwargs) @@ -78,7 +80,7 @@ def mouse_click_handler(*, control, delay, **kwargs): @mouse_click def mouse_click_button(control, delay): - """Performs a mouce click on a wx button. + """Performs a mouse click on a wx button. Parameters ---------- @@ -93,7 +95,7 @@ def mouse_click_button(control, delay): @mouse_click def mouse_click_checkbox(control, delay): - """Performs a mouce click on a wx check box. + """Performs a mouse click on a wx check box. Parameters ---------- @@ -102,6 +104,7 @@ def mouse_click_checkbox(control, delay): delay: int Time delay (in ms) in which click will be performed. """ + click_event = _create_event(wx.wxEVT_COMMAND_CHECKBOX_CLICKED, control) control.SetValue(not control.GetValue()) control.ProcessWindowEvent(click_event) @@ -109,7 +112,7 @@ def mouse_click_checkbox(control, delay): @mouse_click def mouse_click_combobox_or_choice(control, index, delay): - """Performs a mouce click on either a wx combo box or a wx choice on the + """Performs a mouse click on either a wx combo box or a wx choice on the entry at the given index. Parameters @@ -141,7 +144,7 @@ def mouse_click_combobox_or_choice(control, index, delay): @mouse_click def mouse_click_listbox(control, index, delay): - """Performs a mouce click on a wx list box on the entry at + """Performs a mouse click on a wx list box on the entry at the given index. Parameters @@ -176,7 +179,7 @@ def mouse_click_radiobutton(control, delay): @mouse_click def mouse_click_object(control, delay): - """Performs a mouce click on a wxTextCtrl. + """Performs a mouse click on a wxTextCtrl. Parameters ---------- @@ -185,8 +188,6 @@ def mouse_click_object(control, delay): delay: int Time delay (in ms) in which click will be performed. """ - if not control.HasFocus(): - control.SetFocus() click_event = _create_event(wx.wxEVT_COMMAND_LEFT_CLICK, control) control.ProcessWindowEvent(click_event) @@ -292,6 +293,7 @@ def key_click_text_entry( Useful for when the TextEntry.GetSelection is overridden by a subclass that does not conform to the common API. """ + if not (control.IsEnabled() and control.IsEditable()): raise Disabled("{!r} is disabled.".format(control)) if not control.HasFocus(): @@ -312,6 +314,13 @@ def key_click_text_entry( else: pos = control.GetInsertionPoint() control.Remove(max(0, pos - 1), pos) + elif interaction.key in {"Right", "Left"}: + wx.MilliSleep(delay) + n = len(control.GetValue()) + pos = control.GetInsertionPoint() + step = 1 if interaction.key == "Right" else -1 + newpos = max(0, min(pos + step, n)) + control.SetInsertionPoint(newpos) else: check_key_compat(interaction.key) wx.MilliSleep(delay) diff --git a/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/range_editor.py b/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/range_editor.py index 59e7dbe8b..2b9bb0d14 100644 --- a/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/range_editor.py +++ b/traitsui/testing/tester/_ui_tester_registry/wx/_traitsui/range_editor.py @@ -13,6 +13,7 @@ LogRangeSliderEditor, RangeTextEditor, SimpleSliderEditor, + SimpleSpinEditor ) from traitsui.testing.tester.command import KeyClick @@ -100,6 +101,8 @@ def register(registry): SimpleSliderEditor, LogRangeSliderEditor, LargeRangeSliderEditor, + SimpleSpinEditor, + RangeTextEditor ] for target_class in targets: registry.register_location( @@ -109,6 +112,12 @@ def register(registry): textbox=wrapper._target.control.text ), ) + _registry_helper.register_editable_textbox_handlers( + registry=registry, + target_class=target_class, + widget_getter=lambda wrapper: wrapper._target.control.text, + ) + for target_class in targets[:3]: registry.register_location( target_class=target_class, locator_class=Slider, @@ -116,11 +125,6 @@ def register(registry): slider=wrapper._target.control.slider ), ) - _registry_helper.register_editable_textbox_handlers( - registry=registry, - target_class=RangeTextEditor, - widget_getter=lambda wrapper: wrapper._target.control, - ) LocatedTextbox.register(registry) diff --git a/traitsui/tests/editors/test_range_editor.py b/traitsui/tests/editors/test_range_editor.py index 7b500d9e4..0c125efb1 100644 --- a/traitsui/tests/editors/test_range_editor.py +++ b/traitsui/tests/editors/test_range_editor.py @@ -8,6 +8,9 @@ # # Thanks for using Enthought open source! +# from traits.etsconfig.api import ETSConfig +# ETSConfig.toolkit = 'wx' + import platform import unittest @@ -18,7 +21,6 @@ KeyClick, KeySequence, Slider, - TargetRegistry, Textbox, UITester, ) @@ -32,32 +34,6 @@ is_windows = platform.system() == "Windows" -def _register_simple_spin(registry): - """Register interactions for the given registry for a SimpleSpinEditor. - - If there are any conflicts, an error will occur. - - This is kept separate from the below register function because the - SimpleSpinEditor is not yet implemented on wx. This function can be used - with a local reigstry for tests. - - Parameters - ---------- - registry : TargetRegistry - The registry being registered to. - """ - from traitsui.testing.tester._ui_tester_registry.qt4 import ( - _registry_helper, - ) - from traitsui.qt4.range_editor import SimpleSpinEditor - - _registry_helper.register_editable_textbox_handlers( - registry=registry, - target_class=SimpleSpinEditor, - widget_getter=lambda wrapper: wrapper._target.control.lineEdit(), - ) - - class RangeModel(HasTraits): value = Int(1) @@ -83,6 +59,7 @@ def check_range_enum_editor_format_func(self, style): high=3, format_func=lambda v: "{:02d}".format(v), mode="enum", + show_error_dialog=False ), style=style, ) @@ -150,16 +127,14 @@ def test_range_text_editor_set_with_text_valid(self): self.assertEqual(model.value, 10) self.assertEqual(displayed, str(model.value)) - # the tester support code is not yet implemented for Wx SimpleSpinEditor - @requires_toolkit([ToolkitName.qt]) + @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_simple_spin_editor_set_with_text_valid(self): model = RangeModel() view = View( Item("value", editor=RangeEditor(low=1, high=12, mode="spinner")) ) - LOCAL_REGISTRY = TargetRegistry() - _register_simple_spin(LOCAL_REGISTRY) - tester = UITester(registries=[LOCAL_REGISTRY]) + + tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: # sanity check self.assertEqual(model.value, 1) @@ -175,7 +150,7 @@ def test_simple_spin_editor_set_with_text_valid(self): def check_slider_set_with_text_after_empty(self, mode): model = RangeModel() view = View( - Item("value", editor=RangeEditor(low=1, high=12, mode=mode)) + Item("value", editor=RangeEditor(low=1, high=12, mode=mode, show_error_dialog=False)) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: @@ -199,13 +174,12 @@ def test_large_range_slider_editor_set_with_text_after_empty(self): def test_log_range_slider_editor_set_with_text_after_empty(self): return self.check_slider_set_with_text_after_empty(mode='logslider') - # on wx the text style editor gives an error whenever the textbox - # is empty, even if enter has not been pressed. - @requires_toolkit([ToolkitName.qt]) + @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_range_text_editor_set_with_text_after_empty(self): model = RangeModel() view = View( - Item("value", editor=RangeEditor(low=1, high=12, mode="text")) + Item("value", editor=RangeEditor(low=1, high=12, mode="text", + show_error_dialog=False)) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: @@ -219,16 +193,15 @@ def test_range_text_editor_set_with_text_after_empty(self): self.assertEqual(model.value, 11) self.assertEqual(displayed, str(model.value)) - # the tester support code is not yet implemented for Wx SimpleSpinEditor - @requires_toolkit([ToolkitName.qt]) + @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_simple_spin_editor_set_with_text_after_empty(self): model = RangeModel() view = View( - Item("value", editor=RangeEditor(low=1, high=12, mode="spinner")) + Item("value", editor=RangeEditor(low=1, high=12, mode="spinner", + show_error_dialog=False)) ) - LOCAL_REGISTRY = TargetRegistry() - _register_simple_spin(LOCAL_REGISTRY) - tester = UITester(registries=[LOCAL_REGISTRY]) + + tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: number_field_text = tester.find_by_name(ui, "value") number_field_text.perform(KeyClick("Right")) @@ -314,7 +287,8 @@ def num_to_time(num): model = RangeModel() view = View( - Item("float_value", editor=RangeEditor(format_func=num_to_time)) + Item("float_value", editor=RangeEditor(format_func=num_to_time, + show_error_dialog=False)) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: @@ -332,7 +306,8 @@ def test_editor_factory_format(self): model = RangeModel() with self.assertWarns(DeprecationWarning): view = View( - Item("float_value", editor=RangeEditor(format="%s ...")) + Item("float_value", editor=RangeEditor(format="%s ...", + show_error_dialog=False)) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: @@ -350,7 +325,8 @@ def test_editor_format(self): model = RangeModel() with self.assertWarns(DeprecationWarning): view = View( - Item("float_value", editor=RangeEditor(format="%s ...")) + Item("float_value", editor=RangeEditor(format="%s ...", + show_error_dialog=False)) ) tester = UITester() with tester.create_ui(model, dict(view=view)) as ui: @@ -367,7 +343,8 @@ def test_set_text_out_of_range(self): model = RangeModel() view = View( Item( - 'float_value', editor=RangeEditor(mode='text', low=0.0, high=1) + 'float_value', editor=RangeEditor(mode='text', low=0.0, high=1, + show_error_dialog=False) ), ) tester = UITester() diff --git a/traitsui/tests/editors/test_range_editor_text.py b/traitsui/tests/editors/test_range_editor_text.py index b20688165..d39ef0db2 100644 --- a/traitsui/tests/editors/test_range_editor_text.py +++ b/traitsui/tests/editors/test_range_editor_text.py @@ -13,16 +13,21 @@ A RangeEditor in mode 'text' for an Int allows values out of range. """ + +# from traits.etsconfig.api import ETSConfig +# ETSConfig.toolkit = 'wx' + import unittest from traits.has_traits import HasTraits from traits.trait_types import Float, Int from traitsui.item import Item from traitsui.view import View +from traitsui import editor from traitsui.editors.range_editor import RangeEditor from traitsui.testing.tester import command -from traitsui.testing.tester.ui_tester import UITester +from traitsui.testing.api import UITester, DisplayedText, Textbox from traitsui.tests._tools import ( BaseTestMixin, requires_toolkit, @@ -37,8 +42,11 @@ class NumberWithRangeEditor(HasTraits): traits_view = View( Item(label="Range should be 3 to 8. Enter 1, then press OK"), - Item("number", editor=RangeEditor(low=3, high=8, mode="text")), - buttons=["OK"], + Item("number", editor=RangeEditor(low=3, high=8, mode="text", + show_error_dialog=False, + enter_set=True, + auto_set=False)), + buttons=["OK", "Cancel"], ) @@ -46,40 +54,115 @@ class FloatWithRangeEditor(HasTraits): """Dialog containing a RangeEditor in 'spinner' mode for an Int.""" number = Float(5.0) + number2 = int(3) traits_view = View( - Item("number", editor=RangeEditor(low=0.0, high=12.0)), buttons=["OK"] + Item("number", editor=RangeEditor(low=-1.0, high=None, mode='spinner', + enter_set=True, + auto_set=False)), + Item("number2", editor=RangeEditor(low=-1, high=1100000, mode='text', + enter_set=True, + auto_set=False)), + Item('number2', style='readonly'), + Item('number', style='readonly'), + buttons=["OK", "Cancel"] ) class TestRangeEditorText(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) + editor.UNITTESTING = True # if True raise error on all errors. Makes it easier to test def tearDown(self): BaseTestMixin.tearDown(self) + editor.UNITTESTING = False # Reset so it does not affect the other test. @requires_toolkit([ToolkitName.wx]) - def test_wx_text_editing(self): - # behavior: when editing the text part of a spin control box, pressing + def test_text_editing(self): + # behavior: when editing the text part of a range control box, pressing # the OK button should update the value of the HasTraits class # (tests a bug where this fails with an AttributeError) num = NumberWithRangeEditor() tester = UITester() + with tester.create_ui(num) as ui: + # the following is equivalent to setting the text in the text + # control, then pressing OK + number_field = tester.find_by_name(ui, "number") + text = number_field.locate(Textbox()) + # text = number_field + + displayed = text.inspect(DisplayedText()) + assert displayed == '3' + assert num.number == 3 + text.perform(command.KeyClick("Backspace")) # make sure to delete 3 + text.perform(command.KeyClick("6")) + + displayed = text.inspect(DisplayedText()) + assert displayed == '6' + assert num.number == 3 # num.number is not updated yet + text.perform(command.KeyClick("Enter")) # Update num.number + displayed = text.inspect(DisplayedText()) + assert displayed == '6' + assert num.number == 6 + text.perform(command.KeyClick("Backspace")) # make sure to delete 6 + text.perform(command.KeyClick("7")) + + displayed = text.inspect(DisplayedText()) + assert displayed == '7' + assert num.number == 6 + + ok_button = tester.find_by_id(ui, "OK") + ok_button.perform(command.MouseClick()) + + # the number traits should be 7 + assert num.number == 7 + + @requires_toolkit([ToolkitName.wx]) + def test_text_editing_out_of_bounds(self): + # behavior: + # When the value entered is out of bounds an error should be raised + # when editing the text part of a range control box, pressing + # the Cancel button should update the value of the HasTraits class + # since the focus is changed. + # (tests a bug where this fails with an AttributeError) + + num = NumberWithRangeEditor() + assert num.number == 0 + tester = UITester() with tester.create_ui(num) as ui: # the following is equivalent to setting the text in the text # control, then pressing OK text = tester.find_by_name(ui, "number") + displayed = text.inspect(DisplayedText()) + assert displayed == '3' + assert num.number == 3 + text.perform(command.KeyClick("Backspace")) # make sure to delete 3 text.perform(command.KeyClick("1")) - text.perform(command.KeyClick("Enter")) + displayed = text.inspect(DisplayedText()) + assert displayed == '1' + with self.assertRaises(RuntimeError) as context: + text.perform(command.KeyClick("Enter")) + self.assertTrue("The value (1) must be larger than 3!" in str(context.exception)) + # the number traits should be 3 + assert num.number == 3 + text.perform(command.KeyClick("Backspace")) + text.perform(command.KeyClick("4")) + displayed = text.inspect(DisplayedText()) + assert displayed == '4' + assert num.number == 3 + + cancel_button = tester.find_by_id(ui, "Cancel") + cancel_button.perform(command.MouseClick()) - # the number traits should be between 3 and 8 - self.assertTrue(3 <= num.number <= 8) + assert num.number == 4 if __name__ == "__main__": + editor.UNITTESTING = False # if False show errordialog on all errors. Makes it easier to test manually # Executing the file opens the dialog for manual testing - num = NumberWithRangeEditor() + # num = NumberWithRangeEditor() + num = FloatWithRangeEditor() num.configure_traits() print(num.number) diff --git a/traitsui/tests/test_editor.py b/traitsui/tests/test_editor.py index 1e8cf4551..2321fad13 100644 --- a/traitsui/tests/test_editor.py +++ b/traitsui/tests/test_editor.py @@ -22,6 +22,7 @@ Range, Undefined, ) +from traitsui.api import View, RangeEditor, Item from traits.trait_base import xgetattr from traitsui.context_value import ContextValue, CVFloat, CVInt @@ -948,7 +949,9 @@ def test_editor_error_msg(self): from pyface.qt import QtCore, QtGui class Foo(HasTraits): - x = Range(low=0.0, high=1.0, value=0.5, exclude_low=True) + x = Range(low=0.0, high=1.0, value=0.5, exclude_low=True, + enter_set=True, auto_set=False) + # TODO: Range must be modified to have a show_error_dialog attribute too! foo = Foo() tester = UITester(auto_process_events=False) diff --git a/traitsui/wx/editor.py b/traitsui/wx/editor.py index 53ee5e1a5..34c2c2484 100644 --- a/traitsui/wx/editor.py +++ b/traitsui/wx/editor.py @@ -63,14 +63,16 @@ def update_editor(self): def error(self, excp): """Handles an error that occurs while setting the object's trait value.""" - dlg = wx.MessageDialog( - self.control, - str(excp), - self.description + " value error", - wx.OK | wx.ICON_INFORMATION, - ) - dlg.ShowModal() - dlg.Destroy() + super().error(excp) + if self.factory.show_error_dialog: + dlg = wx.MessageDialog( + self.control, + str(excp), + self.description + " value error", + wx.OK | wx.ICON_INFORMATION, + ) + dlg.ShowModal() + dlg.Destroy() def set_tooltip_text(self, control, text): """Sets the tooltip for a specified control.""" diff --git a/traitsui/wx/range_editor.py b/traitsui/wx/range_editor.py index 0b610ba2c..5bb1fe982 100644 --- a/traitsui/wx/range_editor.py +++ b/traitsui/wx/range_editor.py @@ -12,26 +12,24 @@ """ -import sys -import wx - +import ast from math import log10 -from traits.api import TraitError, Str, Float, Any, Bool +import wx -from .editor_factory import TextEditor +from traits.api import TraitError, Str, Float, Any, Bool from .editor import Editor -from .constants import OKColor, ErrorColor +from .constants import OKColor from .helper import TraitsUIPanel, Slider - +# pylint: disable=no-member if not hasattr(wx, "wx.wxEVT_SCROLL_ENDSCROLL"): wxEVT_SCROLL_ENDSCROLL = wx.wxEVT_SCROLL_CHANGED else: - wxEVT_SCROLL_ENDSCROLL = wx.wxEVT_SCROLL_ENDSCROLL + wxEVT_SCROLL_ENDSCROLL = wx.wxEVT_SCROLL_ENDSCROLL # @UndefinedVariable # ------------------------------------------------------------------------- @@ -56,22 +54,10 @@ def _set_value(self, value): value = self.evaluate(value) Editor._set_value(self, value) - -class SimpleSliderEditor(BaseRangeEditor): - """Simple style of range editor that displays a slider and a text field. - - The user can set a value either by moving the slider or by typing a value - in the text field. - """ - - # ------------------------------------------------------------------------- - # Trait definitions: - # ------------------------------------------------------------------------- - - #: Low value for the slider range + #: Low value for the range low = Any() - #: High value for the slider range + #: High value for the range high = Any() #: Deprecated: This trait is no longer used. See enthought/traitsui#1704 @@ -84,6 +70,11 @@ def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ + self._init_with_factory_defaults() + self.control = self._make_control(parent) + self._do_layout(self.control) + + def _init_with_factory_defaults(self): factory = self.factory if not factory.low_name: self.low = factory.low @@ -94,150 +85,91 @@ def init(self, parent): self.evaluate = factory.evaluate self.sync_value(factory.evaluate_name, "evaluate", "from") - size = wx.DefaultSize - if factory.label_width > 0: - size = wx.Size(factory.label_width, 20) - self.sync_value(factory.low_name, "low", "from") self.sync_value(factory.high_name, "high", "from") - self.control = panel = TraitsUIPanel(parent, -1) - sizer = wx.BoxSizer(wx.HORIZONTAL) - fvalue = self.value - - if not (self.low <= fvalue <= self.high): - fvalue_text = "" - fvalue = self.low - else: - try: - fvalue_text = self.string_value(fvalue) - except (ValueError, TypeError) as e: - fvalue_text = "" - ivalue = self._convert_to_slider(fvalue) + def _make_control(self, parent): + raise NotImplementedError - self._label_lo = wx.StaticText( - panel, - -1, - "999999", - size=size, - style=wx.ALIGN_RIGHT | wx.ST_NO_AUTORESIZE, - ) - sizer.Add(self._label_lo, 0, wx.ALIGN_CENTER) - panel.slider = slider = Slider( - panel, - -1, - ivalue, - 0, - 10000, - size=wx.Size(80, 20), - style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS, - ) - slider.SetTickFreq(1000) - slider.SetValue(1) - slider.SetPageSize(1000) - slider.SetLineSize(100) - slider.Bind(wx.EVT_SCROLL, self.update_object_on_scroll) - sizer.Add(slider, 1, wx.EXPAND) - self._label_hi = wx.StaticText(panel, -1, "999999", size=size) - sizer.Add(self._label_hi, 0, wx.ALIGN_CENTER) - - panel.text = text = wx.TextCtrl( - panel, - -1, - fvalue_text, - size=wx.Size(56, 20), - style=wx.TE_PROCESS_ENTER, - ) - panel.Bind( - wx.EVT_TEXT_ENTER, self.update_object_on_enter, id=text.GetId() - ) - text.Bind(wx.EVT_KILL_FOCUS, self.update_object_on_enter) + def _do_layout(self, control): + raise NotImplementedError - sizer.Add(text, 0, wx.LEFT | wx.EXPAND, 4) + def _clip(self, fvalue, low, high): + """Returns fvalue clipped between low and high""" - low_label = factory.low_label - if factory.low_name != "": - low_label = self.string_value(self.low) + try: + if low is not None and fvalue < low: + return low + if high is not None and high < fvalue: + return high + except: + return low + return fvalue - high_label = factory.high_label - if factory.high_name != "": - high_label = self.string_value(self.high) + def _make_text_entry(self, control, fvalue_text, size=wx.Size(56, 20)): + if self.factory.enter_set: + text = wx.TextCtrl(control, + -1, + fvalue_text, + size=size, + style=wx.TE_PROCESS_ENTER) + control.Bind(wx.EVT_TEXT_ENTER, self.update_object_on_enter, id=text.GetId()) + else: + text = wx.TextCtrl(control, -1, fvalue_text, size=size) - self._label_lo.SetLabel(low_label) - self._label_hi.SetLabel(high_label) - self.set_tooltip(slider) - self.set_tooltip(self._label_lo) - self.set_tooltip(self._label_hi) + text.Bind(wx.EVT_KILL_FOCUS, self.update_object_on_enter) + if self.factory.auto_set: + control.Bind(wx.EVT_TEXT, self.update_object_on_enter, id=text.GetId()) self.set_tooltip(text) - # Set-up the layout: - panel.SetSizerAndFit(sizer) + return text - def update_object_on_scroll(self, event): - """Handles the user changing the current slider value.""" - value = self._convert_from_slider(event.GetPosition()) - event_type = event.GetEventType() - if ( - (event_type == wxEVT_SCROLL_ENDSCROLL) - or ( - self.factory.auto_set - and (event_type == wx.wxEVT_SCROLL_THUMBTRACK) - ) - or ( - self.factory.enter_set - and (event_type == wx.wxEVT_SCROLL_THUMBRELEASE) - ) - ): - try: - self.ui_changing = True - self.control.text.SetValue(self.string_value(value)) - self.value = value - except TraitError: - pass - finally: - self.ui_changing = False + def _validate(self, value): + if self.low is not None and value < self.low: + message = "The value ({}) must be larger than {}!" + raise ValueError(message.format(value, self.low)) + if self.high is not None and value > self.high: + message = "The value ({}) must be smaller than {}!" + raise ValueError(message.format(value, self.high)) + if not self.factory.is_float and isinstance(value, float): + message = "The value must be an integer, but a value of {} was specified." + raise ValueError(message.format(value)) + + def _set_color(self, color): + if self.control is not None: + self.control.text.SetBackgroundColour(color) + self.control.text.Refresh() def update_object_on_enter(self, event): """Handles the user pressing the Enter key in the text field.""" if isinstance(event, wx.FocusEvent): event.Skip() - # There are cases where this method is called with self.control == - # None. + # It is possible the event is processed after the control is removed + # from the editor if self.control is None: return try: - try: - value = self.control.text.GetValue().strip() - if self.factory.is_float: - value = float(value) - else: - value = int(value) - except Exception as ex: - # The user entered something that didn't eval as a number (e.g., 'foo'). - # Pretend it didn't happen (i.e. do not change self.value). - value = self.value - self.control.text.SetValue(str(value)) - # for compound editor, value may be non-numeric - if not isinstance(value, (int, float)): - return - + value = ast.literal_eval(self.control.text.GetValue()) + self._validate(value) self.value = value - if not self.ui_changing: - self.control.slider.SetValue( - self._convert_to_slider(self.value) - ) - self.control.text.SetBackgroundColour(OKColor) - self.control.text.Refresh() - if self._error is not None: - self._error = None - self.ui.errors -= 1 - except TraitError: - pass + + except Exception as excp: + self.error(excp) + return + + if not self.ui_changing: + self._set_slider(value) + + self._set_color(OKColor) + if self._error is not None: + self._error = None + self.ui.errors -= 1 def error(self, excp): - """Handles an error that occurs while setting the object's trait value.""" + """ Handles an error that occurs while setting the object's trait value. + """ if self._error is None: self._error = True self.ui.errors += 1 @@ -248,36 +180,21 @@ def update_editor(self): """Updates the editor when the object trait changes externally to the editor. """ - value = self.value - try: - text = self.string_value(value) - 1 // (self.low <= value <= self.high) - except: - text = "" - value = self.low + fvalue = self._clip(self.value, self.low, self.high) + text = self.string_value(fvalue) - ivalue = self._convert_to_slider(value) + self.ui_changing = True self.control.text.SetValue(text) - self.control.slider.SetValue(ivalue) + self.ui_changing = False + self._set_slider(fvalue) - def _convert_to_slider(self, value): - """Returns the slider setting corresponding to the user-supplied value.""" - if self.high > self.low: - ivalue = int( - (float(value - self.low) / (self.high - self.low)) * 10000.0 - ) - else: - ivalue = self.low - return ivalue + def _set_slider(self, value): + """Updates the slider range controls.""" + # Do nothing for non-sliders. - def _convert_from_slider(self, ivalue): - """Returns the float or integer value corresponding to the slider - setting. - """ - value = self.low + ((float(ivalue) / 10000.0) * (self.high - self.low)) - if not self.factory.is_float: - value = int(round(value)) - return value + def _get_current_range(self): + low, high = self.low, self.high + return low, high def get_error_control(self): """Returns the editor's control for indicating error status.""" @@ -285,27 +202,151 @@ def get_error_control(self): def _low_changed(self, low): if self.value < low: - if self.factory.is_float: - self.value = float(low) - else: - self.value = int(low) - - if self._label_lo is not None: - self._label_lo.SetLabel(self.string_value(low)) + self.value = float(low) if self.factory.is_float else int(low) + if self.control is not None: self.update_editor() def _high_changed(self, high): if self.value > high: - if self.factory.is_float: - self.value = float(high) - else: - self.value = int(high) - - if self._label_hi is not None: - self._label_hi.SetLabel(self.string_value(high)) + self.value = float(high) if self.factory.is_float else int(high) + if self.control is not None: self.update_editor() +class SimpleSliderEditor(BaseRangeEditor): + """ Simple style of range editor that displays a slider and a text field. + + The user can set a value either by moving the slider or by typing a value + in the text field. + """ + + # ------------------------------------------------------------------------- + # Trait definitions: See BaseRangeEditor + # ------------------------------------------------------------------------- + + def _make_control(self, parent): + + low, high = self._get_current_range() + fvalue = self._clip(self.value, low, high) + fvalue_text = self.string_value(fvalue) + + size = self._get_default_size() + + control = TraitsUIPanel(parent, -1) + control.label_lo = self._make_label_low(control, low, size) + control.slider = self._make_slider(control, fvalue) + control.label_hi = self._make_label_high(control, high, size) + control.text = self._make_text_entry(control, fvalue_text) + return control + + @staticmethod + def _do_layout(control): + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(control.label_lo, 0, wx.ALIGN_CENTER) + sizer.Add(control.slider, 1, wx.EXPAND) + sizer.Add(control.label_hi, 0, wx.ALIGN_CENTER) + sizer.Add(control.text, 0, wx.LEFT | wx.EXPAND, 4) + control.SetSizerAndFit(sizer) + + def _get_default_size(self): + if self.factory.label_width > 0: + return wx.Size(self.factory.label_width, 20) + return wx.DefaultSize + + def _get_label_high(self, high): + if self.factory.high_name != "": + return self.string_value(high) + return self.factory.high_label + + def _get_label_low(self, low): + if self.factory.low_name != "": + return self.string_value(low) + return self.factory.low_label + + def _make_label_low(self, control, low, size): + low_label = self._get_label_low(low) + style = wx.ALIGN_RIGHT | wx.ST_NO_AUTORESIZE + label_lo = wx.StaticText(control, -1, low_label, size=size, style=style) + self.set_tooltip(label_lo) + + return label_lo + + def _make_slider(self, control, fvalue): + ivalue = self._convert_to_slider(fvalue) + slider = Slider(control, + -1, + value=ivalue, + minValue=0, + maxValue=10000, + size=wx.Size(80, 20), + style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS) + slider.SetTickFreq(1000) + slider.SetValue(1) + slider.SetPageSize(1000) + slider.SetLineSize(100) + slider.Bind(wx.EVT_SCROLL, self.update_object_on_scroll) + self.set_tooltip(slider) + + return slider + + def _make_label_high(self, control, high, size): + high_label = self._get_label_high(high) + label_hi = wx.StaticText(control, -1, high_label, size=size) + self.set_tooltip(label_hi) + return label_hi + + def update_object_on_scroll(self, event): + """Handles the user changing the current slider value.""" + event_type = event.GetEventType() + if ( + (event_type == wxEVT_SCROLL_ENDSCROLL) + or ( + self.factory.auto_set + and (event_type == wx.wxEVT_SCROLL_THUMBTRACK) + ) + or ( + self.factory.enter_set + and (event_type == wx.wxEVT_SCROLL_THUMBRELEASE) + ) + ): + value = self._convert_from_slider(event.GetPosition()) + try: + self.ui_changing = True + self.control.text.SetValue(self.string_value(value)) + self.value = value + except TraitError: + pass + finally: + self.ui_changing = False + + def _set_slider(self, value): + """Updates the slider range controls.""" + low, high = self._get_current_range() + self.control.label_lo.SetLabel(self.string_value(low)) + self.control.label_hi.SetLabel(self.string_value(high)) + ivalue = self._convert_to_slider(value) + self.control.slider.SetValue(ivalue) + + def _convert_to_slider(self, value): + """Returns the slider setting corresponding to the user-supplied value.""" + low, high = self._get_current_range() + if high > low: + return int(float(value - low) / (high - low) * 10000.0) + if low is None: + return 0 + return low + + def _convert_from_slider(self, ivalue): + """Returns the float or integer value corresponding to the slider + setting. + """ + low, high = self._get_current_range() + value = low + ((float(ivalue) / 10000.0) * (high - low)) + if not self.factory.is_float: + value = int(round(value)) + return value + + # ------------------------------------------------------------------------- class LogRangeSliderEditor(SimpleSliderEditor): # ------------------------------------------------------------------------- @@ -313,28 +354,25 @@ class LogRangeSliderEditor(SimpleSliderEditor): def _convert_to_slider(self, value): """Returns the slider setting corresponding to the user-supplied value.""" - value = max(value, self.low) - ivalue = int( - (log10(value) - log10(self.low)) - / (log10(self.high) - log10(self.low)) - * 10000.0 - ) - return ivalue + low, high = self._get_current_range() + value = max(value, low) + return int((log10(value) - log10(low)) / (log10(high) - log10(low)) * 10000.0) def _convert_from_slider(self, ivalue): """Returns the float or integer value corresponding to the slider setting. """ - value = float(ivalue) / 10000.0 * (log10(self.high) - log10(self.low)) + low, high = self._get_current_range() + value = float(ivalue) / 10000.0 * (log10(high) - log10(low)) # Do this to handle floating point errors, where fvalue may exceed # self.high. - fvalue = min(self.low * 10 ** (value), self.high) + fvalue = min(low * 10 ** (value), high) if not self.factory.is_float: fvalue = int(round(fvalue)) return fvalue -class LargeRangeSliderEditor(BaseRangeEditor): +class LargeRangeSliderEditor(SimpleSliderEditor): """A slider editor for large ranges. The editor displays a slider and a text field. A subset of the total @@ -343,552 +381,340 @@ class LargeRangeSliderEditor(BaseRangeEditor): """ # ------------------------------------------------------------------------- - # Trait definitions: + # Trait definitions: See BaseRangeEditor # ------------------------------------------------------------------------- - #: Low value for the slider range - low = Any(0) - - #: High value for the slider range - high = Any(1) - - #: Low end of displayed range + #: Low end of displayed slider range cur_low = Float() - #: High end of displayed range + #: High end of displayed slider range cur_high = Float() - #: Flag indicating that the UI is in the process of being updated - ui_changing = Bool(False) - def init(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ - factory = self.factory - - # Initialize using the factory range defaults: - self.low = factory.low - self.high = factory.high - self.evaluate = factory.evaluate + self._init_with_factory_defaults() + self.init_current_range(self.value) + self.control = self._make_control(parent) + # Set-up the layout: + self._do_layout(self.control) - # Hook up the traits to listen to the object. - self.sync_value(factory.low_name, "low", "from") - self.sync_value(factory.high_name, "high", "from") - self.sync_value(factory.evaluate_name, "evaluate", "from") + def _make_control(self, parent): + control = super()._make_control(parent) + low, high = self._get_current_range() - self.init_range() - low = self.cur_low - high = self.cur_high + control.button_lo = self._make_button_low(control, low) + control.button_hi = self._make_button_high(control, high) + return control - self._set_format() - self.control = panel = TraitsUIPanel(parent, -1) + @staticmethod + def _do_layout(control): sizer = wx.BoxSizer(wx.HORIZONTAL) - fvalue = self.value - try: - fvalue_text = self._format % fvalue - 1 // (low <= fvalue <= high) - except: - fvalue_text = "" - fvalue = low - - if high > low: - ivalue = int((float(fvalue - low) / (high - low)) * 10000) - else: - ivalue = low - - # Lower limit label: - label_lo = wx.StaticText(panel, -1, "999999") - panel.label_lo = label_lo - sizer.Add(label_lo, 2, wx.ALIGN_CENTER) - - # Lower limit button: + sizer.Add(control.label_lo, 2, wx.ALIGN_CENTER) + sizer.Add(control.button_lo, 1, wx.ALIGN_CENTER) + sizer.Add(control.slider, 6, wx.EXPAND) + sizer.Add(control.button_hi, 1, wx.ALIGN_CENTER) + sizer.Add(control.label_hi, 2, wx.ALIGN_CENTER) + sizer.Add(control.text, 0, wx.LEFT | wx.EXPAND, 4) + control.SetSizerAndFit(sizer) + + def _make_button_low(self, control, low): bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_BACK, size=(15, 15)) - button_lo = wx.BitmapButton( - panel, - -1, - bitmap=bmp, - size=(-1, 20), - style=wx.BU_EXACTFIT | wx.NO_BORDER, - ) - panel.button_lo = button_lo + button_lo = wx.BitmapButton(control, + -1, + bitmap=bmp, + size=(-1, 20), + style=wx.BU_EXACTFIT | wx.NO_BORDER, + name="button_lo") button_lo.Bind(wx.EVT_BUTTON, self.reduce_range) - sizer.Add(button_lo, 1, wx.ALIGN_CENTER) - - # Slider: - panel.slider = slider = Slider( - panel, - -1, - ivalue, - 0, - 10000, - size=wx.Size(80, 20), - style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS, - ) - slider.SetTickFreq(1000) - slider.SetValue(1) - slider.SetPageSize(1000) - slider.SetLineSize(100) - slider.Bind(wx.EVT_SCROLL, self.update_object_on_scroll) - sizer.Add(slider, 6, wx.EXPAND) + button_lo.Enable(self.low is None or low != self.low) + return button_lo + + def _make_button_high(self, control, high): - # Upper limit button: bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_FORWARD, size=(15, 15)) - button_hi = wx.BitmapButton( - panel, - -1, - bitmap=bmp, - size=(-1, 20), - style=wx.BU_EXACTFIT | wx.NO_BORDER, - ) - panel.button_hi = button_hi + button_hi = wx.BitmapButton(control, + -1, + bitmap=bmp, + size=(-1, 20), + style=wx.BU_EXACTFIT | wx.NO_BORDER, + name="button_hi") button_hi.Bind(wx.EVT_BUTTON, self.increase_range) - sizer.Add(button_hi, 1, wx.ALIGN_CENTER) - - # Upper limit label: - label_hi = wx.StaticText(panel, -1, "999999") - panel.label_hi = label_hi - sizer.Add(label_hi, 2, wx.ALIGN_CENTER) - - # Text entry: - panel.text = text = wx.TextCtrl( - panel, - -1, - fvalue_text, - size=wx.Size(56, 20), - style=wx.TE_PROCESS_ENTER, - ) - panel.Bind( - wx.EVT_TEXT_ENTER, self.update_object_on_enter, id=text.GetId() - ) - text.Bind(wx.EVT_KILL_FOCUS, self.update_object_on_enter) - - sizer.Add(text, 0, wx.LEFT | wx.EXPAND, 4) - - # Set-up the layout: - panel.SetSizerAndFit(sizer) - label_lo.SetLabel(str(low)) - label_hi.SetLabel(str(high)) - self.set_tooltip(slider) - self.set_tooltip(label_lo) - self.set_tooltip(label_hi) - self.set_tooltip(text) - - # Update the ranges and button just in case. - self.update_range_ui() - - def update_object_on_scroll(self, event): - """Handles the user changing the current slider value.""" - low = self.cur_low - high = self.cur_high - value = low + ((float(event.GetPosition()) / 10000.0) * (high - low)) - self.control.text.SetValue(self._format % value) - event_type = event.GetEventType() - try: - self.ui_changing = True - if ( - (event_type == wxEVT_SCROLL_ENDSCROLL) - or ( - self.factory.auto_set - and (event_type == wx.wxEVT_SCROLL_THUMBTRACK) - ) - or ( - self.factory.enter_set - and (event_type == wx.wxEVT_SCROLL_THUMBRELEASE) - ) - ): - if self.factory.is_float: - self.value = value - else: - self.value = int(value) - finally: - self.ui_changing = False - - def update_object_on_enter(self, event): - """Handles the user pressing the Enter key in the text field.""" - if isinstance(event, wx.FocusEvent): - event.Skip() - # It is possible the event is processed after the control is removed - # from the editor - if self.control is None: - return - try: - value = self.control.text.GetValue().strip() - try: - if self.factory.is_float: - value = float(value) - else: - value = int(value) - except Exception as ex: - # The user entered something that didn't eval as a number (e.g., 'foo'). - # Pretend it didn't happen (i.e. do not change self.value). - value = self.value - self.control.text.SetValue(str(value)) - - self.value = value - self.control.text.SetBackgroundColour(OKColor) - self.control.text.Refresh() - # Update the slider range. - # Set ui_changing to True to avoid recursion: - # the update_range_ui method will try to set the value in the text - # box, which will again fire this method if auto_set is True. - if not self.ui_changing: - self.ui_changing = True - self.init_range() - self.update_range_ui() - self.ui_changing = False - if self._error is not None: - self._error = None - self.ui.errors -= 1 - except TraitError as excp: - pass - - def error(self, excp): - """Handles an error that occurs while setting the object's trait value.""" - if self._error is None: - self._error = True - self.ui.errors += 1 - super().error(excp) - self.set_error_state(True) - - def update_editor(self): - """Updates the editor when the object trait changes externally to the - editor. - """ - value = self.value - low = self.low - high = self.high - try: - text = self._format % value - 1 / (low <= value <= high) - except: - value = low - self.value = value + button_hi.Enable(self.high is None or high != self.high) + return button_hi - if not self.ui_changing: - self.init_range() - self.update_range_ui() - - def update_range_ui(self): + def _set_slider(self, value): """Updates the slider range controls.""" - low, high = self.cur_low, self.cur_high - value = self.value - self._set_format() - self.control.label_lo.SetLabel(self._format % low) - self.control.label_hi.SetLabel(self._format % high) - if high > low: - ivalue = int((float(value - low) / (high - low)) * 10000.0) - else: - ivalue = low - self.control.slider.SetValue(ivalue) - text = self._format % self.value - self.control.text.SetValue(text) - factory = self.factory - f_low, f_high = self.low, self.high + low, high = self._get_current_range() + if not low <= value <= high: + low, high = self.init_current_range(value) - if low == f_low: - self.control.button_lo.Disable() - else: - self.control.button_lo.Enable() + ivalue = self._convert_to_slider(value) + self.control.slider.SetValue(ivalue) - if high == f_high: - self.control.button_hi.Disable() - else: - self.control.button_hi.Enable() + fmt = self._get_format() + self.control.label_lo.SetLabel(fmt % low) + self.control.label_hi.SetLabel(fmt % high) + self.control.button_lo.Enable(self.low is None or low != self.low) + self.control.button_hi.Enable(self.high is None or high != self.high) - def init_range(self): - """Initializes the slider range controls.""" - value = self.value - factory = self.factory + def init_current_range(self, value): + """Initializes the current slider range controls, cur_low and cur_high.""" low, high = self.low, self.high - if (high is None) and (low is not None): - high = -low - mag = abs(value) - if mag <= 10.0: - cur_low = max(value - 10, low) - cur_high = min(value + 10, high) - else: - d = 0.5 * (10 ** int(log10(mag) + 1)) - cur_low = max(low, value - d) - cur_high = min(high, value + d) + mag = max(abs(value), 1) + rounded_value = 10 ** int(log10(mag)) + fact_hi, fact_lo = (10, 1) if value >= 0 else (-1, -10) + cur_low = rounded_value * fact_lo + cur_high = rounded_value * fact_hi + if mag <= 10: + if value >= 0: + cur_low *= -1 + else: + cur_high *= -1 + + if low is not None and cur_low < low: + cur_low = low + if high is not None and high < cur_high: + cur_high = high self.cur_low, self.cur_high = cur_low, cur_high + return self.cur_low, self.cur_high def reduce_range(self, event): """Reduces the extent of the displayed range.""" - factory = self.factory - low, high = self.low, self.high + value = self.value + low = self.low + + old_cur_low = self.cur_low if abs(self.cur_low) < 10: - self.cur_low = max(-10, low) - self.cur_high = min(10, high) - elif self.cur_low > 0: - self.cur_high = self.cur_low - self.cur_low = max(low, self.cur_low / 10) + value = value - 10 + self.cur_low = max(-10, low) if low is not None else -10 + if old_cur_low - self.cur_low > 9: + self.cur_high = old_cur_low else: - self.cur_high = self.cur_low - self.cur_low = max(low, self.cur_low * 10) + fact = 0.1 if self.cur_low > 0 else 10 + value = value * fact + new_cur_low = self.cur_low * fact + self.cur_low = max(low, new_cur_low) if low is not None else new_cur_low + if self.cur_low == new_cur_low: + self.cur_high = old_cur_low - self.ui_changing = True - self.value = min(max(self.value, self.cur_low), self.cur_high) - self.ui_changing = False - self.update_range_ui() + value = min(max(value, self.cur_low), self.cur_high) + + if self.factory.is_float is False: + value = int(value) + self.value = value + self.update_editor() def increase_range(self, event): """Increased the extent of the displayed range.""" - factory = self.factory - low, high = self.low, self.high + value = self.value + high = self.high + old_cur_high = self.cur_high if abs(self.cur_high) < 10: - self.cur_low = max(-10, low) - self.cur_high = min(10, high) - elif self.cur_high > 0: - self.cur_low = self.cur_high - self.cur_high = min(high, self.cur_high * 10) + value = value + 10 + self.cur_high = min(10, high) if high is not None else 10 + if self.cur_high - old_cur_high > 9: + self.cur_low = old_cur_high else: - self.cur_low = self.cur_high - self.cur_high = min(high, self.cur_high / 10) + fact = 10 if self.cur_high > 0 else 0.1 + value = value * fact + new_cur_high = self.cur_high * fact + self.cur_high = min(high, new_cur_high) if high is not None else new_cur_high + if self.cur_high == new_cur_high: + self.cur_low = old_cur_high - self.ui_changing = True - self.value = min(max(self.value, self.cur_low), self.cur_high) - self.ui_changing = False - self.update_range_ui() + value = min(max(value, self.cur_low), self.cur_high) - def _set_format(self): - self._format = "%d" - factory = self.factory - low, high = self.cur_low, self.cur_high - diff = high - low - if factory.is_float: - if diff > 99999: - self._format = "%.2g" - elif diff > 1: - self._format = "%%.%df" % max(0, 4 - int(log10(high - low))) - else: - self._format = "%.3f" - - def get_error_control(self): - """Returns the editor's control for indicating error status.""" - return self.control.text - - def _low_changed(self, low): - if self.control is not None: - if self.value < low: - if self.factory.is_float: - self.value = float(low) - else: - self.value = int(low) + if self.factory.is_float is False: + value = int(value) - self.update_editor() + self.value = value + self.update_editor() - def _high_changed(self, high): - if self.control is not None: - if self.value > high: - if self.factory.is_float: - self.value = float(high) - else: - self.value = int(high) + def _get_format(self): + if self.factory.is_float: + low, high = self._get_current_range() + diff = high - low + if diff > 99999: + return "%.2g" + elif diff > 1: + return "%%.%df" % max(0, 4 - int(log10(diff))) + return "%.3f" + return "%d" - self.update_editor() + def _get_current_range(self): + return self.cur_low, self.cur_high class SimpleSpinEditor(BaseRangeEditor): - """A simple style of range editor that displays a spin box control.""" - - # ------------------------------------------------------------------------- - # Trait definitions: - # ------------------------------------------------------------------------- - - #: Low value for the slider range - low = Any() - - #: High value for the slider range - high = Any() - - def init(self, parent): - """Finishes initializing the editor by creating the underlying toolkit - widget. - """ - factory = self.factory - if not factory.low_name: - self.low = factory.low - - if not factory.high_name: - self.high = factory.high - - self.sync_value(factory.low_name, "low", "from") - self.sync_value(factory.high_name, "high", "from") - low = self.low - high = self.high - self.control = wx.SpinCtrl( - parent, -1, self.str_value, min=low, max=high, initial=self.value - ) - parent.Bind( - wx.EVT_SPINCTRL, self.update_object, id=self.control.GetId() - ) - self.set_tooltip() - - def update_object(self, event): - """Handles the user selecting a new value in the spin box.""" - if self.control is None: - return - self._locked = True - try: - self.value = self.control.GetValue() - finally: - self._locked = False - - def update_editor(self): - """Updates the editor when the object trait changes externally to the - editor. - """ - if not self._locked: - try: - self.control.SetValue(int(self.value)) - except: - pass - - def _low_changed(self, low): - if self.value < low: - if self.factory.is_float: - self.value = float(low) - else: - self.value = int(low) - if self.control: - self.control.SetRange(self.low, self.high) - self.control.SetValue(int(self.value)) - - def _high_changed(self, high): - if self.value > high: - if self.factory.is_float: - self.value = float(high) - else: - self.value = int(high) - if self.control: - self.control.SetRange(self.low, self.high) - self.control.SetValue(int(self.value)) - - -class RangeTextEditor(TextEditor): - """Editor for ranges that displays a text field. If the user enters a - value that is outside the allowed range, the background of the field - changes color to indicate an error. + """A simple style of range editor that displays a spin box control. + + The SimpleSpinEditor catches 3 different types of events that will increase/decrease + the value of the class: + 1) Spin event generated by pushing the up/down spinbutton; + 2) Key pressed event generated by pressing the arrow- or page-up/down of the keyboard. + 3) Mouse wheel event generated by rolling the mouse wheel up or down. + + In addition, there are some other functionalities: + + - ``Shift`` + arrow = 2 * increment (or ``Shift`` + mouse wheel); + - ``Ctrl`` + arrow = 10 * increment (or ``Ctrl`` + mouse wheel); + - ``Alt`` + arrow = 100 * increment (or ``Alt`` + mouse wheel); + - Combinations of ``Shift``, ``Ctrl``, ``Alt`` increment the + step value by the product of the factors; + - ``PgUp`` & ``PgDn`` = 10 * increment * the product of the ``Shift``, ``Ctrl``, ``Alt`` + factors; """ # ------------------------------------------------------------------------- - # Trait definitions: + # Trait definitions: See BaseRangeEditor # ------------------------------------------------------------------------- - #: Low value for the slider range - low = Any() + #: Step value for the spinner + step = Any(1) - #: High value for the slider range - high = Any() + def _make_control(self, parent): - #: Function to evaluate floats/ints - evaluate = Any() - - def init(self, parent): - """Finishes initializing the editor by creating the underlying toolkit - widget. - """ - if not self.factory.low_name: - self.low = self.factory.low + low, high = self._get_current_range() + fvalue = self._clip(self.value, low, high) + fvalue_text = self.string_value(fvalue) - if not self.factory.high_name: - self.high = self.factory.high + control = TraitsUIPanel(parent, -1) + control.text = self._make_text_entry(control, fvalue_text) + control.button_lo = self._make_button_low(control, fvalue) + control.button_hi = self._make_button_high(control, fvalue) + return control - self.sync_value(self.factory.low_name, "low", "from") - self.sync_value(self.factory.high_name, "high", "from") + def _make_text_entry(self, control, fvalue_text, size=wx.Size(41, 20)): + text = super()._make_text_entry(control, fvalue_text, size) + text.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel) + text.Bind(wx.EVT_CHAR, self.on_char) + return text - if self.factory.enter_set: - control = wx.TextCtrl( - parent, -1, self.str_value, style=wx.TE_PROCESS_ENTER - ) - parent.Bind( - wx.EVT_TEXT_ENTER, self.update_object, id=control.GetId() - ) + @staticmethod + def _do_layout(control): + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(control.text, 12, wx.LEFT | wx.EXPAND) + spinctrl_sizer = wx.BoxSizer(wx.VERTICAL) + spinctrl_sizer.Add(control.button_hi, 1, wx.ALIGN_CENTER) + spinctrl_sizer.Add(control.button_lo, 1, wx.ALIGN_CENTER) + sizer.Add(spinctrl_sizer, 1, wx.RIGHT) + control.SetSizerAndFit(sizer) + + def _make_button_low(self, control, value): + bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, size=(15, 10)) + button_lo = wx.BitmapButton(control, + -1, + bitmap=bmp, + size=(15, 12), + style=wx.BU_EXACTFIT | wx.NO_BORDER, + name="button_lo") + # button_lo.Bind(wx.EVT_BUTTON, self.spin_down) + button_lo.Bind(wx.EVT_LEFT_DOWN, self.spin_down) + button_lo.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel) + button_lo.Enable(self.low is None or self.low < value) + return button_lo + + def _make_button_high(self, control, value): + + bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_UP, size=(15, 10)) + button_hi = wx.BitmapButton(control, + -1, + bitmap=bmp, + size=(15, 12), + style=wx.BU_EXACTFIT | wx.NO_BORDER, + name="button_hi") + # button_hi.Bind(wx.EVT_BUTTON, self.spin_up) + button_hi.Bind(wx.EVT_LEFT_DOWN, self.spin_up) + button_hi.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel) + + button_hi.Enable(self.high is None or value < self.high) + return button_hi + + def _spin(self, step): + + low, high = self._get_current_range() + value = self._clip(self.value + step, low, high) + + if self.factory.is_float is False: + value = int(value) + self.value = value + self.update_editor() + + def _get_modifier(self, event): + + modifier = self.step + if event.ShiftDown(): + modifier = modifier * 2 + if event.ControlDown(): + modifier = modifier * 10 + if event.AltDown(): + modifier = modifier * 100 + return modifier + + def on_char(self, event): + keycode = event.GetKeyCode() + if keycode == wx.WXK_UP: + self.spin_up(event) + elif keycode == wx.WXK_DOWN: + self.spin_down(event) + elif keycode == wx.WXK_PAGEUP: + self.spin_up(event, 10) + elif keycode == wx.WXK_PAGEDOWN: + self.spin_down(event, 10) else: - control = wx.TextCtrl(parent, -1, self.str_value) + event.Skip() - control.Bind(wx.EVT_KILL_FOCUS, self.update_object) + def spin_down(self, event, fact=1): + """Reduces the value.""" + step = -1 * self._get_modifier(event) * fact + self._spin(step) - if self.factory.auto_set: - parent.Bind(wx.EVT_TEXT, self.update_object, id=control.GetId()) + def spin_up(self, event, fact=1): + """Increases the value.""" + step = self._get_modifier(event) * fact + self._spin(step) - self.evaluate = self.factory.evaluate - self.sync_value(self.factory.evaluate_name, "evaluate", "from") + def on_mouse_wheel(self, event): + sign = -1 if event.WheelRotation < 0 else 1 + step = sign * self._get_modifier(event) + self._spin(step) - self.control = control - self.set_tooltip() + def _set_slider(self, value): + """Updates the spinbutton controls.""" + low, high = self._get_current_range() + self.control.button_lo.Enable(low is None or low < value) + self.control.button_hi.Enable(high is None or value < high) - def update_object(self, event): - """Handles the user entering input data in the edit control.""" - if isinstance(event, wx.FocusEvent): - event.Skip() - # It is possible the event is processed after the control is removed - # from the editor - if self.control is None: - return +class RangeTextEditor(BaseRangeEditor): + """Editor for ranges that displays a text field. - value = self.control.GetValue() + If the user enters a value that is outside the allowed range, + the background of the field changes color to indicate an error. + """ - # Try to convert the string value entered by the user to a numerical - # value. - try: - if self.evaluate is not None: - value = self.evaluate(value) - else: - if self.factory.is_float: - value = float(value) - else: - value = int(value) - except Exception as excp: - # The conversion failed. - self.error(excp) - return + def _make_control(self, parent): - if value < self.low or value > self.high: - self.set_error_state(True) - return + low, high = self._get_current_range() + fvalue = self._clip(self.value, low, high) + fvalue_text = self.string_value(fvalue) - # Try to assign the numerical value to the trait. - # This may fail because of constraints on the trait. - try: - self.value = value - self.control.SetBackgroundColour(OKColor) - self.control.Refresh() - if self._error is not None: - self._error = None - self.ui.errors -= 1 - except TraitError as excp: - pass + control = TraitsUIPanel(parent, -1) + control.text = self._make_text_entry(control, fvalue_text) + return control - def error(self, excp): - """Handles an error that occurs while setting the object's trait value.""" - if self._error is None: - self._error = True - self.ui.errors += 1 - super().error(excp) - self.set_error_state(True) + @staticmethod + def _do_layout(control): + pass - def _low_changed(self, low): - if self.value < low: - if self.factory.is_float: - self.value = float(low) - else: - self.value = int(low) - if self.control: - self.control.SetValue(int(self.value)) - def _high_changed(self, high): - if self.value > high: - if self.factory.is_float: - self.value = float(high) - else: - self.value = int(high) - if self.control: - self.control.SetValue(int(self.value)) +# ------------------------------------------------------------------------- +# 'SimpleEnumEditor' factory adaptor: +# ------------------------------------------------------------------------- def SimpleEnumEditor(parent, factory, ui, object, name, description, **kwargs): @@ -906,7 +732,7 @@ def CustomEnumEditor( if factory._enum is None: import traitsui.editors.enum_editor as enum_editor - factory._enum = enum_editor.EnumEditor( + factory._enum = enum_editor.ToolkitEditorFactory( values=list(range(factory.low, factory.high + 1)), cols=factory.cols, )