##// END OF EJS Templates
Merge pull request #6050 from chronitis/interact-range-widgets...
Jonathan Frederic -
r17707:8ab7bc79 merge
parent child Browse files
Show More
@@ -1365,6 +1365,10 b' div.cell.text_cell.rendered {'
1365 height: 28px !important;
1365 height: 28px !important;
1366 margin-top: -8px !important;
1366 margin-top: -8px !important;
1367 }
1367 }
1368 .widget-hslider .ui-slider .ui-slider-range {
1369 height: 12px !important;
1370 margin-top: -4px !important;
1371 }
1368 .widget-vslider {
1372 .widget-vslider {
1369 /* Vertical jQuery Slider */
1373 /* Vertical jQuery Slider */
1370 /* Fix the padding of the slide track so the ui-slider is sized
1374 /* Fix the padding of the slide track so the ui-slider is sized
@@ -1442,6 +1446,10 b' div.cell.text_cell.rendered {'
1442 height: 14px !important;
1446 height: 14px !important;
1443 margin-left: -9px;
1447 margin-left: -9px;
1444 }
1448 }
1449 .widget-vslider .ui-slider .ui-slider-range {
1450 width: 12px !important;
1451 margin-left: -1px !important;
1452 }
1445 .widget-text {
1453 .widget-text {
1446 /* String Textbox - used for TextBoxView and TextAreaView */
1454 /* String Textbox - used for TextBoxView and TextAreaView */
1447 width: 350px;
1455 width: 350px;
@@ -9137,6 +9137,10 b' div.cell.text_cell.rendered {'
9137 height: 28px !important;
9137 height: 28px !important;
9138 margin-top: -8px !important;
9138 margin-top: -8px !important;
9139 }
9139 }
9140 .widget-hslider .ui-slider .ui-slider-range {
9141 height: 12px !important;
9142 margin-top: -4px !important;
9143 }
9140 .widget-vslider {
9144 .widget-vslider {
9141 /* Vertical jQuery Slider */
9145 /* Vertical jQuery Slider */
9142 /* Fix the padding of the slide track so the ui-slider is sized
9146 /* Fix the padding of the slide track so the ui-slider is sized
@@ -9214,6 +9218,10 b' div.cell.text_cell.rendered {'
9214 height: 14px !important;
9218 height: 14px !important;
9215 margin-left: -9px;
9219 margin-left: -9px;
9216 }
9220 }
9221 .widget-vslider .ui-slider .ui-slider-range {
9222 width: 12px !important;
9223 margin-left: -1px !important;
9224 }
9217 .widget-text {
9225 .widget-text {
9218 /* String Textbox - used for TextBoxView and TextAreaView */
9226 /* String Textbox - used for TextBoxView and TextAreaView */
9219 width: 350px;
9227 width: 350px;
@@ -52,6 +52,10 b' define(['
52 that.$slider.slider("option", key, model_value);
52 that.$slider.slider("option", key, model_value);
53 }
53 }
54 });
54 });
55 var range_value = this.model.get("_range");
56 if (range_value !== undefined) {
57 this.$slider.slider("option", "range", range_value);
58 }
55
59
56 // WORKAROUND FOR JQUERY SLIDER BUG.
60 // WORKAROUND FOR JQUERY SLIDER BUG.
57 // The horizontal position of the slider handle
61 // The horizontal position of the slider handle
@@ -64,17 +68,28 b' define(['
64 var orientation = this.model.get('orientation');
68 var orientation = this.model.get('orientation');
65 var min = this.model.get('min');
69 var min = this.model.get('min');
66 var max = this.model.get('max');
70 var max = this.model.get('max');
67 this.$slider.slider('option', 'value', min);
71 if (this.model.get('_range')) {
72 this.$slider.slider('option', 'values', [min, min]);
73 } else {
74 this.$slider.slider('option', 'value', min);
75 }
68 this.$slider.slider('option', 'orientation', orientation);
76 this.$slider.slider('option', 'orientation', orientation);
69 var value = this.model.get('value');
77 var value = this.model.get('value');
70 if(value > max) {
78 if (this.model.get('_range')) {
71 value = max;
79 // values for the range case are validated python-side in
72 }
80 // _Bounded{Int,Float}RangeWidget._validate
73 else if(value < min){
81 this.$slider.slider('option', 'values', value);
74 value = min;
82 this.$readout.text(value.join("-"));
83 } else {
84 if(value > max) {
85 value = max;
86 }
87 else if(value < min){
88 value = min;
89 }
90 this.$slider.slider('option', 'value', value);
91 this.$readout.text(value);
75 }
92 }
76 this.$slider.slider('option', 'value', value);
77 this.$readout.text(value);
78
93
79 if(this.model.get('value')!=value) {
94 if(this.model.get('value')!=value) {
80 this.model.set('value', value, {updated_view: this});
95 this.model.set('value', value, {updated_view: this});
@@ -140,9 +155,14 b' define(['
140
155
141 // Calling model.set will trigger all of the other views of the
156 // Calling model.set will trigger all of the other views of the
142 // model to update.
157 // model to update.
143 var actual_value = this._validate_slide_value(ui.value);
158 if (this.model.get("_range")) {
159 var actual_value = ui.values.map(this._validate_slide_value);
160 this.$readout.text(actual_value.join("-"));
161 } else {
162 var actual_value = this._validate_slide_value(ui.value);
163 this.$readout.text(actual_value);
164 }
144 this.model.set('value', actual_value, {updated_view: this});
165 this.model.set('value', actual_value, {updated_view: this});
145 this.$readout.text(actual_value);
146 this.touch();
166 this.touch();
147 },
167 },
148
168
@@ -122,6 +122,11 b''
122 height : 28px !important;
122 height : 28px !important;
123 margin-top : -8px !important;
123 margin-top : -8px !important;
124 }
124 }
125
126 .ui-slider-range {
127 height : 12px !important;
128 margin-top : -4px !important;
129 }
125 }
130 }
126 }
131 }
127
132
@@ -160,6 +165,11 b''
160 height : 14px !important;
165 height : 14px !important;
161 margin-left : -9px;
166 margin-left : -9px;
162 }
167 }
168
169 .ui-slider-range {
170 width : 12px !important;
171 margin-left : -1px !important;
172 }
163 }
173 }
164 }
174 }
165
175
@@ -3,9 +3,9 b' from .widget import Widget, DOMWidget, CallbackDispatcher'
3 from .widget_bool import Checkbox, ToggleButton
3 from .widget_bool import Checkbox, ToggleButton
4 from .widget_button import Button
4 from .widget_button import Button
5 from .widget_box import Box, Popup, FlexBox, HBox, VBox
5 from .widget_box import Box, Popup, FlexBox, HBox, VBox
6 from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress
6 from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress, FloatRangeSlider
7 from .widget_image import Image
7 from .widget_image import Image
8 from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress
8 from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress, IntRangeSlider
9 from .widget_selection import RadioButtons, ToggleButtons, Dropdown, Select
9 from .widget_selection import RadioButtons, ToggleButtons, Dropdown, Select
10 from .widget_selectioncontainer import Tab, Accordion
10 from .widget_selectioncontainer import Tab, Accordion
11 from .widget_string import HTML, Latex, Text, Textarea
11 from .widget_string import HTML, Latex, Text, Textarea
@@ -480,3 +480,120 b' def test_custom_description():'
480 value='text',
480 value='text',
481 description='foo',
481 description='foo',
482 )
482 )
483
484 def test_int_range_logic():
485 irsw = widgets.IntRangeSlider
486 w = irsw(value=(2, 4), min=0, max=6)
487 check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
488 w.value = (4, 2)
489 check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
490 w.value = (-1, 7)
491 check_widget(w, cls=irsw, value=(0, 6), min=0, max=6)
492 w.min = 3
493 check_widget(w, cls=irsw, value=(3, 6), min=3, max=6)
494 w.max = 3
495 check_widget(w, cls=irsw, value=(3, 3), min=3, max=3)
496
497 w.min = 0
498 w.max = 6
499 w.lower = 2
500 w.upper = 4
501 check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
502 w.value = (0, 1) #lower non-overlapping range
503 check_widget(w, cls=irsw, value=(0, 1), min=0, max=6)
504 w.value = (5, 6) #upper non-overlapping range
505 check_widget(w, cls=irsw, value=(5, 6), min=0, max=6)
506 w.value = (-1, 4) #semi out-of-range
507 check_widget(w, cls=irsw, value=(0, 4), min=0, max=6)
508 w.lower = 2
509 check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
510 w.value = (-2, -1) #wholly out of range
511 check_widget(w, cls=irsw, value=(0, 0), min=0, max=6)
512 w.value = (7, 8)
513 check_widget(w, cls=irsw, value=(6, 6), min=0, max=6)
514
515 with nt.assert_raises(ValueError):
516 w.min = 7
517 with nt.assert_raises(ValueError):
518 w.max = -1
519 with nt.assert_raises(ValueError):
520 w.lower = 5
521 with nt.assert_raises(ValueError):
522 w.upper = 1
523
524 w = irsw(min=2, max=3)
525 check_widget(w, min=2, max=3)
526 w = irsw(min=100, max=200)
527 check_widget(w, lower=125, upper=175, value=(125, 175))
528
529 with nt.assert_raises(ValueError):
530 irsw(value=(2, 4), lower=3)
531 with nt.assert_raises(ValueError):
532 irsw(value=(2, 4), upper=3)
533 with nt.assert_raises(ValueError):
534 irsw(value=(2, 4), lower=3, upper=3)
535 with nt.assert_raises(ValueError):
536 irsw(min=2, max=1)
537 with nt.assert_raises(ValueError):
538 irsw(lower=5)
539 with nt.assert_raises(ValueError):
540 irsw(upper=5)
541
542
543 def test_float_range_logic():
544 frsw = widgets.FloatRangeSlider
545 w = frsw(value=(.2, .4), min=0., max=.6)
546 check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
547 w.value = (.4, .2)
548 check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
549 w.value = (-.1, .7)
550 check_widget(w, cls=frsw, value=(0., .6), min=0., max=.6)
551 w.min = .3
552 check_widget(w, cls=frsw, value=(.3, .6), min=.3, max=.6)
553 w.max = .3
554 check_widget(w, cls=frsw, value=(.3, .3), min=.3, max=.3)
555
556 w.min = 0.
557 w.max = .6
558 w.lower = .2
559 w.upper = .4
560 check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
561 w.value = (0., .1) #lower non-overlapping range
562 check_widget(w, cls=frsw, value=(0., .1), min=0., max=.6)
563 w.value = (.5, .6) #upper non-overlapping range
564 check_widget(w, cls=frsw, value=(.5, .6), min=0., max=.6)
565 w.value = (-.1, .4) #semi out-of-range
566 check_widget(w, cls=frsw, value=(0., .4), min=0., max=.6)
567 w.lower = .2
568 check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
569 w.value = (-.2, -.1) #wholly out of range
570 check_widget(w, cls=frsw, value=(0., 0.), min=0., max=.6)
571 w.value = (.7, .8)
572 check_widget(w, cls=frsw, value=(.6, .6), min=.0, max=.6)
573
574 with nt.assert_raises(ValueError):
575 w.min = .7
576 with nt.assert_raises(ValueError):
577 w.max = -.1
578 with nt.assert_raises(ValueError):
579 w.lower = .5
580 with nt.assert_raises(ValueError):
581 w.upper = .1
582
583 w = frsw(min=2, max=3)
584 check_widget(w, min=2, max=3)
585 w = frsw(min=1., max=2.)
586 check_widget(w, lower=1.25, upper=1.75, value=(1.25, 1.75))
587
588 with nt.assert_raises(ValueError):
589 frsw(value=(2, 4), lower=3)
590 with nt.assert_raises(ValueError):
591 frsw(value=(2, 4), upper=3)
592 with nt.assert_raises(ValueError):
593 frsw(value=(2, 4), lower=3, upper=3)
594 with nt.assert_raises(ValueError):
595 frsw(min=.2, max=.1)
596 with nt.assert_raises(ValueError):
597 frsw(lower=5)
598 with nt.assert_raises(ValueError):
599 frsw(upper=5)
@@ -14,7 +14,7 b' Represents an unbounded float using a widget.'
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from .widget import DOMWidget
16 from .widget import DOMWidget
17 from IPython.utils.traitlets import Unicode, CFloat, Bool, Enum
17 from IPython.utils.traitlets import Unicode, CFloat, Bool, Enum, Tuple
18 from IPython.utils.warn import DeprecatedClass
18 from IPython.utils.warn import DeprecatedClass
19
19
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
@@ -55,12 +55,113 b' class FloatSlider(_BoundedFloat):'
55 _view_name = Unicode('FloatSliderView', sync=True)
55 _view_name = Unicode('FloatSliderView', sync=True)
56 orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
56 orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
57 help="Vertical or horizontal.", sync=True)
57 help="Vertical or horizontal.", sync=True)
58 _range = Bool(False, help="Display a range selector", sync=True)
58 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
59 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
59
60
60
61
61 class FloatProgress(_BoundedFloat):
62 class FloatProgress(_BoundedFloat):
62 _view_name = Unicode('ProgressView', sync=True)
63 _view_name = Unicode('ProgressView', sync=True)
63
64
65 class _FloatRange(_Float):
66 value = Tuple(CFloat, CFloat, default_value=(0.0, 1.0), help="Tuple of (lower, upper) bounds", sync=True)
67 lower = CFloat(0.0, help="Lower bound", sync=False)
68 upper = CFloat(1.0, help="Upper bound", sync=False)
69
70 def __init__(self, *pargs, **kwargs):
71 value_given = 'value' in kwargs
72 lower_given = 'lower' in kwargs
73 upper_given = 'upper' in kwargs
74 if value_given and (lower_given or upper_given):
75 raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
76 if lower_given != upper_given:
77 raise ValueError("Must specify both 'lower' and 'upper' for range widget")
78
79 DOMWidget.__init__(self, *pargs, **kwargs)
80
81 # ensure the traits match, preferring whichever (if any) was given in kwargs
82 if value_given:
83 self.lower, self.upper = self.value
84 else:
85 self.value = (self.lower, self.upper)
86
87 self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
88
89 def _validate(self, name, old, new):
90 if name == 'value':
91 self.lower, self.upper = min(new), max(new)
92 elif name == 'lower':
93 self.value = (new, self.value[1])
94 elif name == 'upper':
95 self.value = (self.value[0], new)
96
97 class _BoundedFloatRange(_FloatRange):
98 step = CFloat(1.0, help="Minimum step that the value can take (ignored by some views)", sync=True)
99 max = CFloat(100.0, help="Max value", sync=True)
100 min = CFloat(0.0, help="Min value", sync=True)
101
102 def __init__(self, *pargs, **kwargs):
103 any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
104 _FloatRange.__init__(self, *pargs, **kwargs)
105
106 # ensure a minimal amount of sanity
107 if self.min > self.max:
108 raise ValueError("min must be <= max")
109
110 if any_value_given:
111 # if a value was given, clamp it within (min, max)
112 self._validate("value", None, self.value)
113 else:
114 # otherwise, set it to 25-75% to avoid the handles overlapping
115 self.value = (0.75*self.min + 0.25*self.max,
116 0.25*self.min + 0.75*self.max)
117 # callback already set for 'value', 'lower', 'upper'
118 self.on_trait_change(self._validate, ['min', 'max'])
119
120
121 def _validate(self, name, old, new):
122 if name == "min":
123 if new > self.max:
124 raise ValueError("setting min > max")
125 self.min = new
126 elif name == "max":
127 if new < self.min:
128 raise ValueError("setting max < min")
129 self.max = new
130
131 low, high = self.value
132 if name == "value":
133 low, high = min(new), max(new)
134 elif name == "upper":
135 if new < self.lower:
136 raise ValueError("setting upper < lower")
137 high = new
138 elif name == "lower":
139 if new > self.upper:
140 raise ValueError("setting lower > upper")
141 low = new
142
143 low = max(self.min, min(low, self.max))
144 high = min(self.max, max(high, self.min))
145
146 # determine the order in which we should update the
147 # lower, upper traits to avoid a temporary inverted overlap
148 lower_first = high < self.lower
149
150 self.value = (low, high)
151 if lower_first:
152 self.lower = low
153 self.upper = high
154 else:
155 self.upper = high
156 self.lower = low
157
158
159 class FloatRangeSlider(_BoundedFloatRange):
160 _view_name = Unicode('FloatSliderView', sync=True)
161 orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
162 help="Vertical or horizontal.", sync=True)
163 _range = Bool(True, help="Display a range selector", sync=True)
164 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
64
165
65 # Remove in IPython 4.0
166 # Remove in IPython 4.0
66 FloatTextWidget = DeprecatedClass(FloatText, 'FloatTextWidget')
167 FloatTextWidget = DeprecatedClass(FloatText, 'FloatTextWidget')
@@ -14,7 +14,7 b' Represents an unbounded int using a widget.'
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from .widget import DOMWidget
16 from .widget import DOMWidget
17 from IPython.utils.traitlets import Unicode, CInt, Bool, Enum
17 from IPython.utils.traitlets import Unicode, CInt, Bool, Enum, Tuple
18 from IPython.utils.warn import DeprecatedClass
18 from IPython.utils.warn import DeprecatedClass
19
19
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
@@ -60,6 +60,7 b' class IntSlider(_BoundedInt):'
60 _view_name = Unicode('IntSliderView', sync=True)
60 _view_name = Unicode('IntSliderView', sync=True)
61 orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
61 orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
62 help="Vertical or horizontal.", sync=True)
62 help="Vertical or horizontal.", sync=True)
63 _range = Bool(False, help="Display a range selector", sync=True)
63 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
64 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
64
65
65
66
@@ -67,6 +68,104 b' class IntProgress(_BoundedInt):'
67 """Progress bar that represents a int bounded by a minimum and maximum value."""
68 """Progress bar that represents a int bounded by a minimum and maximum value."""
68 _view_name = Unicode('ProgressView', sync=True)
69 _view_name = Unicode('ProgressView', sync=True)
69
70
71 class _IntRange(_Int):
72 value = Tuple(CInt, CInt, default_value=(0, 1), help="Tuple of (lower, upper) bounds", sync=True)
73 lower = CInt(0, help="Lower bound", sync=False)
74 upper = CInt(1, help="Upper bound", sync=False)
75
76 def __init__(self, *pargs, **kwargs):
77 value_given = 'value' in kwargs
78 lower_given = 'lower' in kwargs
79 upper_given = 'upper' in kwargs
80 if value_given and (lower_given or upper_given):
81 raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
82 if lower_given != upper_given:
83 raise ValueError("Must specify both 'lower' and 'upper' for range widget")
84
85 DOMWidget.__init__(self, *pargs, **kwargs)
86
87 # ensure the traits match, preferring whichever (if any) was given in kwargs
88 if value_given:
89 self.lower, self.upper = self.value
90 else:
91 self.value = (self.lower, self.upper)
92
93 self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
94
95 def _validate(self, name, old, new):
96 if name == 'value':
97 self.lower, self.upper = min(new), max(new)
98 elif name == 'lower':
99 self.value = (new, self.value[1])
100 elif name == 'upper':
101 self.value = (self.value[0], new)
102
103 class _BoundedIntRange(_IntRange):
104 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
105 max = CInt(100, help="Max value", sync=True)
106 min = CInt(0, help="Min value", sync=True)
107
108 def __init__(self, *pargs, **kwargs):
109 any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
110 _IntRange.__init__(self, *pargs, **kwargs)
111
112 # ensure a minimal amount of sanity
113 if self.min > self.max:
114 raise ValueError("min must be <= max")
115
116 if any_value_given:
117 # if a value was given, clamp it within (min, max)
118 self._validate("value", None, self.value)
119 else:
120 # otherwise, set it to 25-75% to avoid the handles overlapping
121 self.value = (0.75*self.min + 0.25*self.max,
122 0.25*self.min + 0.75*self.max)
123 # callback already set for 'value', 'lower', 'upper'
124 self.on_trait_change(self._validate, ['min', 'max'])
125
126 def _validate(self, name, old, new):
127 if name == "min":
128 if new > self.max:
129 raise ValueError("setting min > max")
130 self.min = new
131 elif name == "max":
132 if new < self.min:
133 raise ValueError("setting max < min")
134 self.max = new
135
136 low, high = self.value
137 if name == "value":
138 low, high = min(new), max(new)
139 elif name == "upper":
140 if new < self.lower:
141 raise ValueError("setting upper < lower")
142 high = new
143 elif name == "lower":
144 if new > self.upper:
145 raise ValueError("setting lower > upper")
146 low = new
147
148 low = max(self.min, min(low, self.max))
149 high = min(self.max, max(high, self.min))
150
151 # determine the order in which we should update the
152 # lower, upper traits to avoid a temporary inverted overlap
153 lower_first = high < self.lower
154
155 self.value = (low, high)
156 if lower_first:
157 self.lower = low
158 self.upper = high
159 else:
160 self.upper = high
161 self.lower = low
162
163 class IntRangeSlider(_BoundedIntRange):
164 _view_name = Unicode('IntSliderView', sync=True)
165 orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
166 help="Vertical or horizontal.", sync=True)
167 _range = Bool(True, help="Display a range selector", sync=True)
168 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
70
169
71 # Remove in IPython 4.0
170 # Remove in IPython 4.0
72 IntTextWidget = DeprecatedClass(IntText, 'IntTextWidget')
171 IntTextWidget = DeprecatedClass(IntText, 'IntTextWidget')
General Comments 0
You need to be logged in to leave comments. Login now