diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js index 7334f1c..324ba8e 100644 --- a/IPython/html/static/widgets/js/widget_int.js +++ b/IPython/html/static/widgets/js/widget_int.js @@ -64,7 +64,7 @@ define([ 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', 'disabled']; var that = this; that.$slider.slider({}); _.each(jquery_slider_keys, function(key, i) { @@ -73,6 +73,14 @@ define([ that.$slider.slider("option", key, model_value); } }); + + var max = this.model.get('max'); + var min = this.model.get('min'); + if (min <= max) { + if (max !== undefined) this.$slider.slider('option', 'max', max); + if (min !== undefined) this.$slider.slider('option', 'min', min); + } + var range_value = this.model.get("_range"); if (range_value !== undefined) { this.$slider.slider("option", "range", range_value); diff --git a/IPython/html/tests/widgets/widget_int.js b/IPython/html/tests/widgets/widget_int.js index 409dfb6..838ab62 100644 --- a/IPython/html/tests/widgets/widget_int.js +++ b/IPython/html/tests/widgets/widget_int.js @@ -154,4 +154,22 @@ casper.notebook_test(function () { this.test.assertEquals(this.get_output_cell(index).text, '50\n', 'Invalid int textbox characters ignored'); }); + + index = this.append_cell( + 'a = widgets.IntSlider()\n' + + 'display(a)\n' + + 'a.max = -1\n' + + 'print("Success")\n'); + this.execute_cell_then(index, function(index){ + this.test.assertEquals(0, 0, 'Invalid int range max bound does not cause crash.'); + }); + + index = this.append_cell( + 'a = widgets.IntSlider()\n' + + 'display(a)\n' + + 'a.min = 101\n' + + 'print("Success")\n'); + this.execute_cell_then(index, function(index){ + this.test.assertEquals(0, 0, 'Invalid int range min bound does not cause crash.'); + }); }); \ No newline at end of file diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py index eba315f..b8d9936 100644 --- a/IPython/html/widgets/widget.py +++ b/IPython/html/widgets/widget.py @@ -125,7 +125,6 @@ class Widget(LoggingConfigurable): self._model_id = kwargs.pop('model_id', None) super(Widget, self).__init__(**kwargs) - self.on_trait_change(self._handle_property_changed, self.keys) Widget._call_widget_constructed(self) self.open() @@ -323,13 +322,21 @@ class Widget(LoggingConfigurable): def _handle_custom_msg(self, content): """Called when a custom msg is received.""" self._msg_callbacks(self, content) - - def _handle_property_changed(self, name, old, new): + + def _notify_trait(self, name, old_value, new_value): """Called when a property has been changed.""" - # Make sure this isn't information that the front-end just sent us. - if self._should_send_property(name, new): - # Send new state to front-end - self.send_state(key=name) + # Trigger default traitlet callback machinery. This allows any user + # registered validation to be processed prior to allowing the widget + # machinery to handle the state. + LoggingConfigurable._notify_trait(self, name, old_value, new_value) + + # Send the state after the user registered callbacks for trait changes + # have all fired (allows for user to validate values). + if self.comm is not None and name in self.keys: + # Make sure this isn't information that the front-end just sent us. + if self._should_send_property(name, new_value): + # Send new state to front-end + self.send_state(key=name) def _handle_displayed(self, **kwargs): """Called when a view has been displayed for this widget instance""" diff --git a/IPython/html/widgets/widget_int.py b/IPython/html/widgets/widget_int.py index 123a3a3..6cdbf29 100644 --- a/IPython/html/widgets/widget_int.py +++ b/IPython/html/widgets/widget_int.py @@ -37,13 +37,24 @@ class _BoundedInt(_Int): def __init__(self, *pargs, **kwargs): """Constructor""" DOMWidget.__init__(self, *pargs, **kwargs) - self.on_trait_change(self._validate, ['value', 'min', 'max']) + self.on_trait_change(self._validate_value, ['value']) + self.on_trait_change(self._handle_max_changed, ['max']) + self.on_trait_change(self._handle_min_changed, ['min']) - def _validate(self, name, old, new): - """Validate value, max, min.""" + def _validate_value(self, name, old, new): + """Validate value.""" if self.min > new or new > self.max: self.value = min(max(new, self.min), self.max) + def _handle_max_changed(self, name, old, new): + """Make sure the min is always <= the max.""" + if new < self.min: + raise ValueError("setting max < min") + + def _handle_min_changed(self, name, old, new): + """Make sure the max is always >= the min.""" + if new > self.max: + raise ValueError("setting min > max") class IntText(_Int): """Textbox widget that represents a int.""" @@ -134,11 +145,9 @@ class _BoundedIntRange(_IntRange): if name == "min": if new > self.max: raise ValueError("setting min > max") - self.min = new elif name == "max": if new < self.min: raise ValueError("setting max < min") - self.max = new low, high = self.value if name == "value":