From d674ff53c2df5f24a7b38ae20dc043a3c306c4e4 2014-06-25 15:51:55 From: Gordon Ball Date: 2014-06-25 15:51:55 Subject: [PATCH] Add initial implementation of 2-handle range sliders for integers. --- diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js index f7a6c6e..0c609fd 100644 --- a/IPython/html/static/widgets/js/widget_int.js +++ b/IPython/html/static/widgets/js/widget_int.js @@ -25,35 +25,35 @@ define(["widgets/js/widget"], function(WidgetManager){ .appendTo(this.$el) .addClass('widget-hlabel') .hide(); - + this.$slider = $('
') .slider({}) .addClass('slider'); - // Put the slider in a container + // Put the slider in a container this.$slider_container = $('
') .addClass('widget-hslider') .append(this.$slider); this.$el_to_style = this.$slider_container; // Set default element to style this.$el.append(this.$slider_container); - + this.$readout = $('
') .appendTo(this.$el) .addClass('widget-hreadout') .hide(); - + // Set defaults. this.update(); }, - + update : function(options){ // Update the contents of this view // - // Called when the model is changed. The model may have been + // Called when the model is changed. The model may have been // changed by another view or by a state update from the back-end. if (options === undefined || options.updated_view != this) { // JQuery slider option keys. These keys happen to have a // one-to-one mapping with the corrosponding keys of the model. - var jquery_slider_keys = ['step', 'max', 'min', 'disabled']; + var jquery_slider_keys = ['step', 'max', 'min', 'disabled', 'range']; var that = this; _.each(jquery_slider_keys, function(key, i) { var model_value = that.model.get(key); @@ -68,15 +68,25 @@ define(["widgets/js/widget"], function(WidgetManager){ // of orientation change. Before applying the new // workaround, we set the value to the minimum to // make sure that the horizontal placement of the - // handle in the vertical slider is always + // handle in the vertical slider is always // consistent. var orientation = this.model.get('orientation'); var value = this.model.get('min'); - this.$slider.slider('option', 'value', value); + if (this.model.get('range')) { + this.$slider.slider('option', 'values', [value, value]); + } else { + this.$slider.slider('option', 'value', value); + } this.$slider.slider('option', 'orientation', orientation); value = this.model.get('value'); - this.$slider.slider('option', 'value', value); - this.$readout.text(value); + if (this.model.get('range')) { + this.$slider.slider('option', 'values', value); + this.$readout.text(value.join("-")); + } else { + this.$slider.slider('option', 'value', value); + this.$readout.text(value); + } + // Use the right CSS classes for vertical & horizontal sliders if (orientation=='vertical') { @@ -116,7 +126,7 @@ define(["widgets/js/widget"], function(WidgetManager){ MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]); this.$label.show(); } - + var readout = this.model.get('readout'); if (readout) { this.$readout.show(); @@ -126,21 +136,27 @@ define(["widgets/js/widget"], function(WidgetManager){ } return IntSliderView.__super__.update.apply(this); }, - + events: { // Dictionary of events and their handlers. "slide" : "handleSliderChange" - }, + }, - handleSliderChange: function(e, ui) { + handleSliderChange: function(e, ui) { // Called when the slider value is changed. - // Calling model.set will trigger all of the other views of the + // Calling model.set will trigger all of the other views of the // model to update. - var actual_value = this._validate_slide_value(ui.value); + if (this.model.get("range")) { + var actual_value = ui.values.map(this._validate_slide_value); + this.$readout.text(actual_value.join("-")); + } else { + var actual_value = this._validate_slide_value(ui.value); + this.$readout.text(actual_value); + } this.model.set('value', actual_value, {updated_view: this}); - this.$readout.text(actual_value); this.touch(); + }, _validate_slide_value: function(x) { @@ -154,7 +170,7 @@ define(["widgets/js/widget"], function(WidgetManager){ WidgetManager.register_widget_view('IntSliderView', IntSliderView); - var IntTextView = IPython.DOMWidgetView.extend({ + var IntTextView = IPython.DOMWidgetView.extend({ render : function(){ // Called when view is rendered. this.$el @@ -170,18 +186,18 @@ define(["widgets/js/widget"], function(WidgetManager){ this.$el_to_style = this.$textbox; // Set default element to style this.update(); // Set defaults. }, - + update : function(options){ // Update the contents of this view // - // Called when the model is changed. The model may have been + // Called when the model is changed. The model may have been // changed by another view or by a state update from the back-end. if (options === undefined || options.updated_view != this) { var value = this.model.get('value'); if (this._parse_value(this.$textbox.val()) != value) { this.$textbox.val(value); } - + if (this.model.get('disabled')) { this.$textbox.attr('disabled','disabled'); } else { @@ -208,20 +224,20 @@ define(["widgets/js/widget"], function(WidgetManager){ // Fires only when control is validated or looses focus. "change input" : "handleChanged" - }, - - handleChanging: function(e) { + }, + + handleChanging: function(e) { // Handles and validates user input. - + // Try to parse value as a int. var numericalValue = 0; if (e.target.value !== '') { var trimmed = e.target.value.trim(); if (!(['-', '-.', '.', '+.', '+'].indexOf(trimmed) >= 0)) { - numericalValue = this._parse_value(e.target.value); - } + numericalValue = this._parse_value(e.target.value); + } } - + // If parse failed, reset value to value stored in model. if (isNaN(numericalValue)) { e.target.value = this.model.get('value'); @@ -232,18 +248,18 @@ define(["widgets/js/widget"], function(WidgetManager){ if (this.model.get('min') !== undefined) { numericalValue = Math.max(this.model.get('min'), numericalValue); } - + // Apply the value if it has changed. if (numericalValue != this.model.get('value')) { - - // Calling model.set will trigger all of the other views of the + + // Calling model.set will trigger all of the other views of the // model to update. this.model.set('value', numericalValue, {updated_view: this}); this.touch(); } } }, - + handleChanged: function(e) { // Applies validated input. if (this.model.get('value') != e.target.value) { @@ -279,18 +295,18 @@ define(["widgets/js/widget"], function(WidgetManager){ .appendTo(this.$progress); this.update(); // Set defaults. }, - + update : function(){ // Update the contents of this view // - // Called when the model is changed. The model may have been + // Called when the model is changed. The model may have been // changed by another view or by a state update from the back-end. var value = this.model.get('value'); var max = this.model.get('max'); var min = this.model.get('min'); var percent = 100.0 * (value - min) / (max - min); this.$bar.css('width', percent + '%'); - + var description = this.model.get('description'); if (description.length === 0) { this.$label.hide(); @@ -300,7 +316,7 @@ define(["widgets/js/widget"], function(WidgetManager){ this.$label.show(); } return ProgressView.__super__.update.apply(this); - }, + }, }); WidgetManager.register_widget_view('ProgressView', ProgressView); diff --git a/IPython/html/widgets/__init__.py b/IPython/html/widgets/__init__.py index 7b6d0a8..803e28e 100644 --- a/IPython/html/widgets/__init__.py +++ b/IPython/html/widgets/__init__.py @@ -5,7 +5,7 @@ from .widget_button import ButtonWidget from .widget_container import ContainerWidget, PopupWidget from .widget_float import FloatTextWidget, BoundedFloatTextWidget, FloatSliderWidget, FloatProgressWidget from .widget_image import ImageWidget -from .widget_int import IntTextWidget, BoundedIntTextWidget, IntSliderWidget, IntProgressWidget +from .widget_int import IntTextWidget, BoundedIntTextWidget, IntSliderWidget, IntProgressWidget, IntRangeSliderWidget from .widget_selection import RadioButtonsWidget, ToggleButtonsWidget, DropdownWidget, SelectWidget from .widget_selectioncontainer import TabWidget, AccordionWidget from .widget_string import HTMLWidget, LatexWidget, TextWidget, TextareaWidget diff --git a/IPython/html/widgets/interaction.py b/IPython/html/widgets/interaction.py index 1b59de8..6378682 100644 --- a/IPython/html/widgets/interaction.py +++ b/IPython/html/widgets/interaction.py @@ -23,7 +23,7 @@ from inspect import getcallargs from IPython.core.getipython import get_ipython from IPython.html.widgets import (Widget, TextWidget, FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget, - ContainerWidget, DOMWidget) + ContainerWidget, DOMWidget, IntRangeSliderWidget) from IPython.display import display, clear_output from IPython.utils.py3compat import string_types, unicode_type from IPython.utils.traitlets import HasTraits, Any, Unicode @@ -107,6 +107,15 @@ def _widget_abbrev(o): else: cls = FloatSliderWidget return cls(value=value, min=min, max=max, step=step) + elif _matches(o, [float_or_int]*4): + min, low, high, max = o + if not min <= low <= high <= max: + raise ValueError("Range input expects min <= low <= high <= max, got %r" % o) + if all(isinstance(_, int) for _ in o): + cls = IntRangeSliderWidget + else: + cls = FloatRangeSliderWidget + return cls(value=(low, high), min=min, max=max) else: return _widget_abbrev_single_value(o) diff --git a/IPython/html/widgets/widget_int.py b/IPython/html/widgets/widget_int.py index 4c9aa2c..824e28a 100644 --- a/IPython/html/widgets/widget_int.py +++ b/IPython/html/widgets/widget_int.py @@ -1,4 +1,4 @@ -"""IntWidget class. +"""IntWidget class. Represents an unbounded int using a widget. """ @@ -14,13 +14,13 @@ Represents an unbounded int using a widget. # Imports #----------------------------------------------------------------------------- from .widget import DOMWidget -from IPython.utils.traitlets import Unicode, CInt, Bool, Enum +from IPython.utils.traitlets import Unicode, CInt, Bool, Enum, Tuple #----------------------------------------------------------------------------- # Classes #----------------------------------------------------------------------------- class _IntWidget(DOMWidget): - value = CInt(0, help="Int value", sync=True) + value = CInt(0, help="Int value", sync=True) disabled = Bool(False, help="Enable or disable user changes", sync=True) description = Unicode(help="Description of the value this widget represents", sync=True) @@ -51,10 +51,40 @@ class BoundedIntTextWidget(_BoundedIntWidget): class IntSliderWidget(_BoundedIntWidget): _view_name = Unicode('IntSliderView', sync=True) - orientation = Enum([u'horizontal', u'vertical'], u'horizontal', + orientation = Enum([u'horizontal', u'vertical'], u'horizontal', help="Vertical or horizontal.", sync=True) + range = Bool(False, help="Display a range selector", sync=True) readout = Bool(True, help="Display the current value of the slider next to it.", sync=True) class IntProgressWidget(_BoundedIntWidget): _view_name = Unicode('ProgressView', sync=True) + +class _IntRangeWidget(_IntWidget): + value = Tuple(CInt, CInt, default_value=(0, 1), help="Low and high int values", sync=True) + +class _BoundedIntRangeWidget(_IntRangeWidget): + step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True) + max = CInt(100, help="Max value", sync=True) + min = CInt(0, help="Min value", sync=True) + + def __init__(self, *pargs, **kwargs): + """Constructor""" + DOMWidget.__init__(self, *pargs, **kwargs) + self.on_trait_change(self._validate, ['value', 'min', 'max']) + + def _validate(self, name, old, new): + """Validate min <= low <= high <= max""" + if name == "value": + low, high = new + low = max(low, self.min) + high = min(high, self.max) + self.value = (min(low, high), max(low, high)) + + +class IntRangeSliderWidget(_BoundedIntRangeWidget): + _view_name = Unicode('IntSliderView', sync=True) + orientation = Enum([u'horizontal', u'vertical'], u'horizontal', + help="Vertical or horizontal.", sync=True) + range = Bool(True, help="Display a range selector", sync=True) + readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)