Source code for cytoflowgui.editors.value_bounds_editor

#!/usr/bin/env python3.8
# coding: latin-1

# (c) Massachusetts Institute of Technology 2015-2018
# (c) Brian Teague 2018-2022
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
cytoflowgui.editors.value_bounds_editor
---------------------------------------

A `traitsui.editors.range_editor.RangeEditor` that allows the user to 
select a range of values from a list (specified in **values**, naturally).

Uses `RangeSlider` for the widget.
"""

if __name__ == '__main__':
    import os
    os.environ['TRAITS_DEBUG'] = "1"

from pyface.qt import QtGui, QtCore

from traits.api import Any, Str, List, Bool
from traitsui.editors.api import RangeEditor
from traitsui.editor_factory import EditorWithListFactory
from traitsui.qt4.editor import EditorWithList

from .range_slider import RangeSlider

class _ValueBoundsEditor(EditorWithList):
    """
    Creates a "range editor" over a specified set of values (instead of a
    defined range.)  Adapted from traitsui.qt4.extra.bounds_editor.
    """

    evaluate = Any

    values = List
    
    # the synchronized values
    low = Any
    high = Any
    
    # a format string for the text boxes
    format = Str

    # the slider positions.  either synchronized to low, high immediately or 
    # when the slider is released, depending on whether auto_set is True or not
    slider_low = Any
    slider_high = Any
    
    low_invalid = Bool(False)
    high_invalid = Bool(False)
    
    # quash slider jiggle
    _updating = Bool(False)

    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.format = factory.format

        self.evaluate = factory.evaluate
        self.sync_value( factory.evaluate_name, 'evaluate', 'from' )
        
        self.sync_value( factory.low_name,  'low',  'both' )
        self.sync_value( factory.high_name, 'high', 'both' )

        self.control = QtGui.QWidget()
        panel = QtGui.QHBoxLayout(self.control)
        panel.setContentsMargins(0, 0, 0, 0)

        self._label_lo = QtGui.QLineEdit(self.format % self.low)
        self._label_lo.editingFinished.connect(self.update_low_on_enter)

        panel.addWidget(self._label_lo)

        # The default size is a bit too big and probably doesn't need to grow.
        sh = self._label_lo.sizeHint()
        sh.setWidth(sh.width() / 2)
        self._label_lo.setMaximumSize(sh)

        self.control.slider = slider = RangeSlider(QtCore.Qt.Horizontal)
        slider.setTracking(True)
        slider.setMinimum(0)
        slider.setMaximum(10000)
        slider.setPageStep(1000)
        slider.setSingleStep(100)

        slider.sliderMoved.connect(self._slider_moved)
        slider.sliderReleased.connect(self._slider_released)
        
        panel.addWidget(slider)

        self._label_hi = QtGui.QLineEdit(self.format % self.high)
        self._label_hi.editingFinished.connect(self.update_high_on_enter)

        panel.addWidget(self._label_hi)

        # The default size is a bit too big and probably doesn't need to grow.
        sh = self._label_hi.sizeHint()
        sh.setWidth(sh.width() / 2)
        self._label_hi.setMaximumSize(sh)

        self.set_tooltip(slider)
        self.set_tooltip(self._label_lo)
        self.set_tooltip(self._label_hi)

        super(_ValueBoundsEditor, self).init(parent)
        
    def list_updated ( self, values ):
        """ Handles the monitored list being updated.
        """
        
        self.values = sorted(values)
        
        if self.high not in self.values:
            self.high = min(self.values, key = lambda x: abs(x - self.high))
            
        if self.low not in self.values:
            self.low = min(self.values, key = lambda x: abs(x - self.low))
        
        slider = self.control.slider
        
        if len(self.values) > 1:
            slider.setLow(self._convert_to_slider(self.low))
            slider.setHigh(self._convert_to_slider(self.high))
        else:
            slider.setLow(slider.minimum())
            slider.setHigh(slider.maximum())
            self.control.setEnabled(False)
        
        
    def update_editor(self):
        # all this is taken care of in other handlers
        pass

    def update_low_on_enter(self):
        try:
            try:
                low = eval(str(self._label_lo.text()).strip())
                if self.evaluate is not None:
                    low = self.evaluate(low)
            except:
                low = self.low
                self._label_lo.setText(self.format % self.low)

            if low > self.high:
                low = self.high
                self._label_lo.setText(self.format % low)

            if low not in self.values:
                low = min(self.values, key = lambda x: abs(x - low))
            
            self.low = low
            self.low_invalid = False

        except:
            self.low_invalid = True

    def update_high_on_enter(self):
        try:
            try:
                high = eval(str(self._label_hi.text()).strip())
                if self.evaluate is not None:
                    high = self.evaluate(high)
            except:
                high = self.high
                self._label_hi.setText(self.format % self.high)

            if high < self.low:
                high = self.low
                self._label_hi.setText(self.format % high)

            if high not in self.values:
                high = min(self.values, key = lambda x: abs(x - high))
                
            self.high = high
            self.high_invalid = False

        except:
            self.high_invalid = True

    def _slider_moved(self, pos = 0):
        self._updating = True
        self.slider_low = self._convert_from_slider(self.control.slider.low())
        self.slider_high = self._convert_from_slider(self.control.slider.high())
        self._updating = False
        
        if self.factory.auto_set:
            if self.low != self.slider_low:
                self.low = self.slider_low
            if self.high != self.slider_high:
                self.high = self.slider_high
            
    def _slider_released(self):
        if not self.factory.auto_set:
            if self.low != self.slider_low:
                self.low = self.slider_low
            if self.high != self.slider_high:
                self.high = self.slider_high
            

    def _step_width(self):
        return ((self.control.slider.maximum() - self.control.slider.minimum()) / float(len(self.values) - 1))

    def _convert_from_slider(self, slider_val):
        if len(self.values) == 1:
            return self.values[0]
        
        idx = int((slider_val / self._step_width()) + 0.5)

        return self.values[idx]

    def _convert_to_slider(self, value):
        # first we have to find the value in the list.  let's assume, for the
        # sake of argument, that the list is sorted and pretty small, so we can
        # do something costly like a binary search (implemented recursively!)
        
        if value < self.values[0]:
            value = self.values[0]
            
        if value > self.values[-1]:
            value = self.values[-1]
        
        def find_idx(start, end):
            # binary search
            if start == end:
                return start
                
            cut = ((start + end) // 2)

            if value == self.values[cut]:
                return cut
            if value < self.values[cut]:
                return find_idx(start, cut - 1)
            else:
                return find_idx(cut + 1, end)
            
        idx = find_idx(0, len(self.values) - 1)
        
        return float(idx) * self._step_width() 
    
    def _low_changed(self, low):
        self.slider_low = low

    def _slider_low_changed(self, low):
        if self.control is None:
            return
        
        if self._label_lo is not None:
            self._label_lo.setText(self.format % low)

        self.low_invalid = False

        if not self._updating:
            self.control.slider.setLow(self._convert_to_slider(low))
            
            slider_high = self._convert_to_slider(self.slider_high)
            if slider_high < (self.control.slider.minimum() + 
                              self.control.slider.singleStep()):
                self.control.slider.setHigh(slider_high)
                
    def _low_invalid_changed(self, invalid):
        self.set_error_state(invalid, self._label_lo)
                
    def _high_changed(self, high):
        self.slider_high = high

    def _slider_high_changed(self, high):
        if self.control is None:
            return
        
        if self._label_hi is not None:
            self._label_hi.setText(self.format % high)
            
        self.high_invalid = False

        if not self._updating:
            self.control.slider.setHigh(self._convert_to_slider(high))
            
            slider_low = self._convert_to_slider(self.slider_low)
            if slider_low > (self.control.slider.maximum() - 
                             self.control.slider.singleStep()):
                self.control.slider.setLow(slider_low) 
                
    def _high_invalid_changed(self, invalid):
        self.set_error_state(invalid, self._label_hi)
            
    
[docs]class ValuesBoundsEditor(EditorWithListFactory, RangeEditor): """ A `traitsui.editors.range_editor.RangeEditor` that uses a list of values instead of low & high range. """ def _get_simple_editor_class(self): return _ValueBoundsEditor def _get_custom_editor_class(self): return _ValueBoundsEditor
if __name__ == "__main__": from traits.api import Int, HasTraits from traitsui.api import View, Item class T(HasTraits): lo = Int(2) hi = Int(4) def _anytrait_changed(self, name, old, new): print("{0} changed to {1}".format(name, new)) values = [1,2,3,4,5] view = View(Item('lo', editor = ValuesBoundsEditor(values = values, low_name = 'lo', high_name = 'hi', auto_set = False))) t = T() t.configure_traits(view = view) print(t.lo) print(t.hi)