Source code for enaml.wx.wx_spin_box

#------------------------------------------------------------------------------
#  Copyright (c) 2012, Enthought, Inc.
#  All rights reserved.
#------------------------------------------------------------------------------
import wx
import wx.lib.newevent

from .wx_control import WxControl


#: The changed event for the custom spin box
wxSpinBoxEvent, EVT_SPIN_BOX = wx.lib.newevent.NewEvent()


class wxProperSpinBox(wx.SpinCtrl):
    """ A custom wx spin control that acts more like QSpinBox.

    The standard wx.SpinCtrl doesn't support too many features, and
    the ones it does support are (like wrapping) are limited. So,
    this custom control hard codes the internal range to the maximum
    range of the wx.SpinCtrl and implements wrapping manually.

    For changed events, users should bind to EVT_SPIN_BOX rather than
    EVT_SPINCTRL.

    See the method docstrings for supported functionality.

    This control is really a god awful hack and needs to be rewritten
    using a combination wx.SpinButton and wx.TextCtrl.

    """
    def __init__(self, *args, **kwargs):
        """ CustomSpinCtrl constructor.

        Parameters
        ----------
        *args, **kwargs
            The positional and keyword arguments for initializing a
            wx.SpinCtrl.

        """
        # The max range of the wx.SpinCtrl is the range of a signed
        # 32bit integer. We don't care about wx's internal value of
        # the control, since we maintain our own internal counter.
        # and because the internal value of the widget gets reset to
        # the minimum of the range whenever SetValueString is called.
        self._hard_min = -(1 << 31)
        self._hard_max = (1 << 31) - 1
        self._internal_value = 0
        self._low = 0
        self._high = 100
        self._step = 1
        self._prefix = u''
        self._suffix = u''
        self._special_value_text = u''
        self._value_string = unicode(self._low)
        self._wrap = False
        self._read_only = False

        # Stores whether spin-up or spin-down was pressed.
        self._spin_state = None

        super(wxProperSpinBox, self).__init__(*args, **kwargs)
        super(wxProperSpinBox, self).SetRange(self._hard_min, self._hard_max)

        # Setting the spin control to process the enter key removes
        # its processing of the Tab key. This is desired for two reasons:
        # 1) It is consistent with the Qt version of the control.
        # 2) The default tab processing is kinda wacky in that when
        #    tab is pressed, it emits a text event with the string
        #    representation of the integer value of the control,
        #    regardless of the value of the user supplied string.
        #    This is definitely not correct and so processing on
        #    Enter allows us to avoid the issue entirely.
        self.WindowStyle |= wx.TE_PROCESS_ENTER

        self.Bind(wx.EVT_SPIN_UP, self.OnSpinUp)
        self.Bind(wx.EVT_SPIN_DOWN, self.OnSpinDown)
        self.Bind(wx.EVT_SPINCTRL, self.OnSpinCtrl)
        self.Bind(wx.EVT_TEXT, self.OnText)
        self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
        self.Bind(wx.EVT_TEXT_ENTER, self.OnEnterPressed)

    #--------------------------------------------------------------------------
    # Event Handlers
    #--------------------------------------------------------------------------
    def OnEnterPressed(self, event):
        """ The event handler for an enter key press. It forces an
        interpretation of the current text control value.

        """
        self.InterpretText()

    def OnKillFocus(self, event):
        """ Handles evaluating the text in the control when the control
        loses focus.

        """
        # The spin control doesn't emit a spin event when losing focus
        # to process typed input change unless it results in a different
        # value, so we have to handle it manually and update the control
        # again after the event. It must be invoked on a CallAfter or it
        # doesn't work properly. The lambda avoids a DeadObjectError if
        # the app is exited before the callback executes.
        wx.CallAfter(lambda: self.InterpretText() if self else None)

    def OnText(self, event):
        """ Handles the text event of the spin control to store away the
        user typed text for later conversion.

        """
        if self._read_only:
            return
        # Do not be tempted to try to implement the 'tracking' feature
        # by adding logic to this method. Wx emits this event at weird
        # times such as ctrl-a select all as well as when SetValueString
        # is called. Granted, this can be avoided with a recursion guard,
        # however, there is no way to get/set the caret position on the
        # control and every call to SetValueString resets the caret
        # position to Zero. So, there is really no possible way to
        # implement 'tracking' without creating an entirely new custom
        # control. So for now, the wx backend just lacks that feature.
        self._value_string = event.GetString()

    def OnSpinUp(self, event):
        """ The event handler for the spin up event. We veto the spin
        event to prevent the control from changing it's internal value.
        Instead, we maintain complete control of the value.

        """
        event.Veto()
        if self._read_only:
            return
        self._spin_state = 'up'
        self.OnSpinCtrl(event)
        self._spin_state = None

    def OnSpinDown(self, event):
        """ The event handler for the spin down event. We veto the spin
        event to prevent the control from changing it's internal value.
        Instead, we maintain complete control of the value.

        """
        event.Veto()
        if self._read_only:
            return
        self._spin_state = 'down'
        self.OnSpinCtrl(event)
        self._spin_state = None

    def OnSpinCtrl(self, event):
        """ Handles the spin control being changed by user interaction.
        All of the manual stepping and wrapping logic is computed by
        this method.

        """
        if self._read_only:
            return
        last = self._internal_value
        low = self._low
        high = self._high
        step = self._step
        wrap = self._wrap
        spin_state = self._spin_state
        if spin_state == 'down':
            if last == low:
                if wrap:
                    computed = high
                else:
                    computed = low
            else:
                computed = last - step
                if computed < low:
                    computed = low
            self.SetValue(computed)
        elif spin_state == 'up':
            if last == high:
                if wrap:
                    computed = low
                else:
                    computed = high
            else:
                computed = last + step
                if computed > high:
                    computed = high
            self.SetValue(computed)
        else:
            # A suprious spin event generated by wx when the widget loses
            # focus. We can safetly ignore it.
            pass

    #--------------------------------------------------------------------------
    # Getters/Setters
    #--------------------------------------------------------------------------
    def GetLow(self):
        """ Returns the minimum value of the control.

        """
        return self._low

    def GetMin(self):
        """ Equivalent to GetLow().

        """
        return self._low

    def SetLow(self, low):
        """ Sets the minimum value of the control and changes the
        value to the min if the current value would be out of range.

        """
        if low < self._hard_min:
            raise ValueError('%s is too low for wxProperSpinBox.' % low)
        self._low = low
        if self.GetValue() < low:
            self.SetValue(low)

    def GetHigh(self):
        """ Returns the maximum value of the control.

        """
        return self._high

    def GetMax(self):
        """ Equivalent to GetHigh().

        """
        return self._high

    def SetHigh(self, high):
        """ Sets the maximum value of the control and changes the
        value to the max if the current value would be out of range.

        """
        if high > self._hard_max:
            raise ValueError('%s is too high for wxProperSpinBox.' % high)
        self._high = high
        if self.GetValue() > high:
            self.SetValue(high)

    def SetRange(self, low, high):
        """ Sets the low and high values of the control.

        """
        self.SetLow(low)
        self.SetHigh(high)

    def GetStep(self):
        """ Returns the step size of the control.

        """
        return self._step

    def SetStep(self, step):
        """ Sets the step size of the control.

        """
        self._step = step

    def GetWrap(self):
        """ Gets the wrap flag of the control.

        """
        return self._wrap

    def SetWrap(self, wrap):
        """ Sets the wrap flag of the control.

        """
        self._wrap = wrap

    def GetPrefix(self):
        """ Get the prefix text for the control.

        Returns
        -------
        result : unicode
            The unicode prefix text.

        """
        return self._prefix

    def SetPrefix(self, prefix):
        """ Set the prefix text for the control.

        Parameters
        ----------
        prefix : unicode
            The unicode prefix text for the control.

        """
        self._prefix = prefix

    def GetSuffix(self):
        """ Get the suffix text for the control.

        Returns
        -------
        result : unicode
            The unicode suffix text.

        """
        return self._suffix

    def SetSuffix(self, suffix):
        """ Set the suffix text for the control.

        Parameters
        ----------
        suffix : unicode
            The unicode suffix text for the control.

        """
        self._suffix = suffix

    def GetSpecialValueText(self):
        """ Returns the special value text for the spin box.

        Returns
        -------
        result : unicode
            The unicode special value text.

        """
        return self._special_value_text

    def SetSpecialValueText(self, text):
        """ Set the special value text for the control.

        Parameters
        ----------
        text : unicode
            The unicode special value text for the control.

        """
        self._special_value_text = text

    def GetReadOnly(self):
        """ Get the read only flag for the control.

        Returns
        -------
        result : bool
            True if the control is read only, False otherwise.

        """
        return self._suffix

    def SetReadOnly(self, read_only):
        """ Set the read only flag for the control

        Parameters
        ----------
        read_only : bool
            True if the control should be read only, False otherwise.

        """
        self._read_only = read_only

    def GetValue(self):
        """ Returns the internal integer value of the control.

        """
        return self._internal_value

    def SetValue(self, value):
        """ Sets the value of the control to the given value, provided
        that the value is within the range of the control. If the
        given value is within range, and is different from the current
        value of the control, an EVT_SPIN_BOX will be emitted.

        """
        different = False
        if self._low <= value <= self._high:
            different = (self._internal_value != value)
            self._internal_value = value

        # Always set the value string, just to be overly
        # safe that we don't fall out of sync.
        self._value_string = self.TextFromValue(self._internal_value)
        self.SetValueString(self._value_string)

        if different:
            evt = wxSpinBoxEvent()
            wx.PostEvent(self, evt)

    #--------------------------------------------------------------------------
    # Support Methods
    #--------------------------------------------------------------------------
    def InterpretText(self):
        """ Interprets the user supplied text and updates the control.

        """
        prefix = self._prefix
        suffix = self._suffix
        svt = self._special_value_text
        text = self._value_string
        if svt and text == svt:
            self.SetValue(self._low)
            return
        if prefix and text.startswith(prefix):
            text = text[len(prefix):]
        if suffix and text.endswith(suffix):
            text = text[:-len(suffix)]
        try:
            value = int(text)
        except ValueError:
            value = self._internal_value
        self.SetValue(value)

    def TextFromValue(self, value):
        """ Converts the given integer to a string for display.

        """
        prefix = self._prefix
        suffix = self._suffix
        svt = self._special_value_text
        if value == self._low and svt:
            return svt
        text = unicode(value)
        if prefix:
            text = '%s%s' % (prefix, text)
        if suffix:
            text = '%s%s' % (text, suffix)
        return text


[docs]class WxSpinBox(WxControl): """ A Wx implementation of an Enaml SpinBox. """ #-------------------------------------------------------------------------- # Setup Methods #--------------------------------------------------------------------------
[docs] def create_widget(self, parent, tree): """ Create the underlying wxProperSpinBox widget. """ return wxProperSpinBox(parent)
[docs] def create(self, tree): """ Create and initialize the slider control. """ super(WxSpinBox, self).create(tree) self.set_maximum(tree['maximum']) self.set_minimum(tree['minimum']) self.set_value(tree['value']) self.set_prefix(tree['prefix']) self.set_suffix(tree['suffix']) self.set_special_value_text(tree['special_value_text']) self.set_single_step(tree['single_step']) self.set_read_only(tree['read_only']) self.set_wrapping(tree['wrapping']) self.widget().Bind(EVT_SPIN_BOX, self.on_value_changed) #-------------------------------------------------------------------------- # Event Handlers #--------------------------------------------------------------------------
[docs] def on_value_changed(self, event): """ The event handler for the 'EVT_SPIN_BOX' event. """ content = {'value': self.widget().GetValue()} self.send_action('value_changed', content) #-------------------------------------------------------------------------- # Message Handlers #--------------------------------------------------------------------------
[docs] def on_action_set_maximum(self, content): """ Handler for the 'set_maximum' action from the Enaml widget. """ self.set_maximum(content['maximum'])
[docs] def on_action_set_minimum(self, content): """ Handler for the 'set_minimum' action from the Enaml widget. """ self.set_minimum(content['minimum'])
[docs] def on_action_set_value(self, content): """ Handler for the 'set_value' action from the Enaml widget. """ self.set_value(content['value'])
[docs] def on_action_set_prefix(self, content): """ Handler for the 'set_prefix' action from the Enaml widget. """ self.set_prefix(content['prefix'])
[docs] def on_action_set_suffix(self, content): """ Handler for the 'set_suffix' action from the Enaml widget. """ self.set_suffix(content['suffix'])
[docs] def on_action_set_special_value_text(self, content): """ Handler for the 'set_special_value_text' action from the Enaml widget. """ self.set_special_value_text(content['special_value_text'])
[docs] def on_action_set_single_step(self, content): """ Handler for the 'set_single_step' action from the Enaml widget. """ self.set_single_step(content['single_step'])
[docs] def on_action_set_read_only(self, content): """ Handler for the 'set_read_only' action from the Enaml widget. """ self.set_read_only(content['read_only'])
[docs] def on_action_set_wrapping(self, content): """ Handler for the 'set_wrapping' action from the Enaml widget. """ self.set_wrapping(content['wrapping']) #-------------------------------------------------------------------------- # Widget Update Methods #--------------------------------------------------------------------------
[docs] def set_maximum(self, maximum): """ Set the widget's maximum value. """ self.widget().SetHigh(maximum)
[docs] def set_minimum(self, minimum): """ Set the widget's minimum value. """ self.widget().SetLow(minimum)
[docs] def set_value(self, value): """ Set the spin box's value. """ self.widget().SetValue(value)
[docs] def set_prefix(self, prefix): """ Set the prefix for the spin box. """ self.widget().SetPrefix(prefix)
[docs] def set_suffix(self, suffix): """ Set the suffix for the spin box. """ self.widget().SetSuffix(suffix)
[docs] def set_special_value_text(self, text): """ Set the special value text for the spin box. """ self.widget().SetSpecialValueText(text)
[docs] def set_single_step(self, step): """ Set the widget's single step value. """ self.widget().SetStep(step)
[docs] def set_read_only(self, read_only): """ Set the widget's read only flag. """ self.widget().SetReadOnly(read_only)
[docs] def set_wrapping(self, wrapping): """ Set the widget's wrapping flag. """ self.widget().SetWrap(wrapping)