##// END OF EJS Templates
Fix infinite loop typo
Jonathan Frederic -
Show More
@@ -1,330 +1,338 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "jqueryui",
6 "jqueryui",
7 "bootstrap",
7 "bootstrap",
8 ], function(widget, $){
8 ], function(widget, $){
9
9
10 var IntSliderView = widget.DOMWidgetView.extend({
10 var IntSliderView = widget.DOMWidgetView.extend({
11 render : function(){
11 render : function(){
12 // Called when view is rendered.
12 // Called when view is rendered.
13 this.$el
13 this.$el
14 .addClass('widget-hbox-single');
14 .addClass('widget-hbox-single');
15 this.$label = $('<div />')
15 this.$label = $('<div />')
16 .appendTo(this.$el)
16 .appendTo(this.$el)
17 .addClass('widget-hlabel')
17 .addClass('widget-hlabel')
18 .hide();
18 .hide();
19
19
20 this.$slider = $('<div />')
20 this.$slider = $('<div />')
21 .slider({})
21 .slider({})
22 .addClass('slider');
22 .addClass('slider');
23 // Put the slider in a container
23 // Put the slider in a container
24 this.$slider_container = $('<div />')
24 this.$slider_container = $('<div />')
25 .addClass('widget-hslider')
25 .addClass('widget-hslider')
26 .append(this.$slider);
26 .append(this.$slider);
27 this.$el.append(this.$slider_container);
27 this.$el.append(this.$slider_container);
28
28
29 this.$readout = $('<div/>')
29 this.$readout = $('<div/>')
30 .appendTo(this.$el)
30 .appendTo(this.$el)
31 .addClass('widget-hreadout')
31 .addClass('widget-hreadout')
32 .hide();
32 .hide();
33
33
34 // Set defaults.
34 // Set defaults.
35 this.update();
35 this.update();
36 },
36 },
37
37
38 update : function(options){
38 update : function(options){
39 // Update the contents of this view
39 // Update the contents of this view
40 //
40 //
41 // Called when the model is changed. The model may have been
41 // Called when the model is changed. The model may have been
42 // changed by another view or by a state update from the back-end.
42 // changed by another view or by a state update from the back-end.
43 if (options === undefined || options.updated_view != this) {
43 if (options === undefined || options.updated_view != this) {
44 // JQuery slider option keys. These keys happen to have a
44 // JQuery slider option keys. These keys happen to have a
45 // one-to-one mapping with the corrosponding keys of the model.
45 // one-to-one mapping with the corrosponding keys of the model.
46 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
46 var jquery_slider_keys = ['step', 'disabled'];
47 var that = this;
47 var that = this;
48 that.$slider.slider({});
48 that.$slider.slider({});
49 _.each(jquery_slider_keys, function(key, i) {
49 _.each(jquery_slider_keys, function(key, i) {
50 var model_value = that.model.get(key);
50 var model_value = that.model.get(key);
51 if (model_value !== undefined) {
51 if (model_value !== undefined) {
52 that.$slider.slider("option", key, model_value);
52 that.$slider.slider("option", key, model_value);
53 }
53 }
54 });
54 });
55
56 var max = this.model.get('max');
57 var min = this.model.get('min');
58 if (min <= max) {
59 if (max !== undefined) this.$slider.slider('option', 'max', max);
60 if (min !== undefined) this.$slider.slider('option', 'min', min);
61 }
62
55 var range_value = this.model.get("_range");
63 var range_value = this.model.get("_range");
56 if (range_value !== undefined) {
64 if (range_value !== undefined) {
57 this.$slider.slider("option", "range", range_value);
65 this.$slider.slider("option", "range", range_value);
58 }
66 }
59
67
60 // WORKAROUND FOR JQUERY SLIDER BUG.
68 // WORKAROUND FOR JQUERY SLIDER BUG.
61 // The horizontal position of the slider handle
69 // The horizontal position of the slider handle
62 // depends on the value of the slider at the time
70 // depends on the value of the slider at the time
63 // of orientation change. Before applying the new
71 // of orientation change. Before applying the new
64 // workaround, we set the value to the minimum to
72 // workaround, we set the value to the minimum to
65 // make sure that the horizontal placement of the
73 // make sure that the horizontal placement of the
66 // handle in the vertical slider is always
74 // handle in the vertical slider is always
67 // consistent.
75 // consistent.
68 var orientation = this.model.get('orientation');
76 var orientation = this.model.get('orientation');
69 var min = this.model.get('min');
77 var min = this.model.get('min');
70 var max = this.model.get('max');
78 var max = this.model.get('max');
71 if (this.model.get('_range')) {
79 if (this.model.get('_range')) {
72 this.$slider.slider('option', 'values', [min, min]);
80 this.$slider.slider('option', 'values', [min, min]);
73 } else {
81 } else {
74 this.$slider.slider('option', 'value', min);
82 this.$slider.slider('option', 'value', min);
75 }
83 }
76 this.$slider.slider('option', 'orientation', orientation);
84 this.$slider.slider('option', 'orientation', orientation);
77 var value = this.model.get('value');
85 var value = this.model.get('value');
78 if (this.model.get('_range')) {
86 if (this.model.get('_range')) {
79 // values for the range case are validated python-side in
87 // values for the range case are validated python-side in
80 // _Bounded{Int,Float}RangeWidget._validate
88 // _Bounded{Int,Float}RangeWidget._validate
81 this.$slider.slider('option', 'values', value);
89 this.$slider.slider('option', 'values', value);
82 this.$readout.text(value.join("-"));
90 this.$readout.text(value.join("-"));
83 } else {
91 } else {
84 if(value > max) {
92 if(value > max) {
85 value = max;
93 value = max;
86 }
94 }
87 else if(value < min){
95 else if(value < min){
88 value = min;
96 value = min;
89 }
97 }
90 this.$slider.slider('option', 'value', value);
98 this.$slider.slider('option', 'value', value);
91 this.$readout.text(value);
99 this.$readout.text(value);
92 }
100 }
93
101
94 if(this.model.get('value')!=value) {
102 if(this.model.get('value')!=value) {
95 this.model.set('value', value, {updated_view: this});
103 this.model.set('value', value, {updated_view: this});
96 this.touch();
104 this.touch();
97 }
105 }
98
106
99 // Use the right CSS classes for vertical & horizontal sliders
107 // Use the right CSS classes for vertical & horizontal sliders
100 if (orientation=='vertical') {
108 if (orientation=='vertical') {
101 this.$slider_container
109 this.$slider_container
102 .removeClass('widget-hslider')
110 .removeClass('widget-hslider')
103 .addClass('widget-vslider');
111 .addClass('widget-vslider');
104 this.$el
112 this.$el
105 .removeClass('widget-hbox-single')
113 .removeClass('widget-hbox-single')
106 .addClass('widget-vbox-single');
114 .addClass('widget-vbox-single');
107 this.$label
115 this.$label
108 .removeClass('widget-hlabel')
116 .removeClass('widget-hlabel')
109 .addClass('widget-vlabel');
117 .addClass('widget-vlabel');
110 this.$readout
118 this.$readout
111 .removeClass('widget-hreadout')
119 .removeClass('widget-hreadout')
112 .addClass('widget-vreadout');
120 .addClass('widget-vreadout');
113
121
114 } else {
122 } else {
115 this.$slider_container
123 this.$slider_container
116 .removeClass('widget-vslider')
124 .removeClass('widget-vslider')
117 .addClass('widget-hslider');
125 .addClass('widget-hslider');
118 this.$el
126 this.$el
119 .removeClass('widget-vbox-single')
127 .removeClass('widget-vbox-single')
120 .addClass('widget-hbox-single');
128 .addClass('widget-hbox-single');
121 this.$label
129 this.$label
122 .removeClass('widget-vlabel')
130 .removeClass('widget-vlabel')
123 .addClass('widget-hlabel');
131 .addClass('widget-hlabel');
124 this.$readout
132 this.$readout
125 .removeClass('widget-vreadout')
133 .removeClass('widget-vreadout')
126 .addClass('widget-hreadout');
134 .addClass('widget-hreadout');
127 }
135 }
128
136
129 var description = this.model.get('description');
137 var description = this.model.get('description');
130 if (description.length === 0) {
138 if (description.length === 0) {
131 this.$label.hide();
139 this.$label.hide();
132 } else {
140 } else {
133 this.$label.text(description);
141 this.$label.text(description);
134 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
142 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
135 this.$label.show();
143 this.$label.show();
136 }
144 }
137
145
138 var readout = this.model.get('readout');
146 var readout = this.model.get('readout');
139 if (readout) {
147 if (readout) {
140 this.$readout.show();
148 this.$readout.show();
141 } else {
149 } else {
142 this.$readout.hide();
150 this.$readout.hide();
143 }
151 }
144 }
152 }
145 return IntSliderView.__super__.update.apply(this);
153 return IntSliderView.__super__.update.apply(this);
146 },
154 },
147
155
148 events: {
156 events: {
149 // Dictionary of events and their handlers.
157 // Dictionary of events and their handlers.
150 "slide" : "handleSliderChange"
158 "slide" : "handleSliderChange"
151 },
159 },
152
160
153 handleSliderChange: function(e, ui) {
161 handleSliderChange: function(e, ui) {
154 // Called when the slider value is changed.
162 // Called when the slider value is changed.
155
163
156 // Calling model.set will trigger all of the other views of the
164 // Calling model.set will trigger all of the other views of the
157 // model to update.
165 // model to update.
158 if (this.model.get("_range")) {
166 if (this.model.get("_range")) {
159 var actual_value = ui.values.map(this._validate_slide_value);
167 var actual_value = ui.values.map(this._validate_slide_value);
160 this.$readout.text(actual_value.join("-"));
168 this.$readout.text(actual_value.join("-"));
161 } else {
169 } else {
162 var actual_value = this._validate_slide_value(ui.value);
170 var actual_value = this._validate_slide_value(ui.value);
163 this.$readout.text(actual_value);
171 this.$readout.text(actual_value);
164 }
172 }
165 this.model.set('value', actual_value, {updated_view: this});
173 this.model.set('value', actual_value, {updated_view: this});
166 this.touch();
174 this.touch();
167 },
175 },
168
176
169 _validate_slide_value: function(x) {
177 _validate_slide_value: function(x) {
170 // Validate the value of the slider before sending it to the back-end
178 // Validate the value of the slider before sending it to the back-end
171 // and applying it to the other views on the page.
179 // and applying it to the other views on the page.
172
180
173 // Double bit-wise not truncates the decimel (int cast).
181 // Double bit-wise not truncates the decimel (int cast).
174 return ~~x;
182 return ~~x;
175 },
183 },
176 });
184 });
177
185
178
186
179 var IntTextView = widget.DOMWidgetView.extend({
187 var IntTextView = widget.DOMWidgetView.extend({
180 render : function(){
188 render : function(){
181 // Called when view is rendered.
189 // Called when view is rendered.
182 this.$el
190 this.$el
183 .addClass('widget-hbox-single');
191 .addClass('widget-hbox-single');
184 this.$label = $('<div />')
192 this.$label = $('<div />')
185 .appendTo(this.$el)
193 .appendTo(this.$el)
186 .addClass('widget-hlabel')
194 .addClass('widget-hlabel')
187 .hide();
195 .hide();
188 this.$textbox = $('<input type="text" />')
196 this.$textbox = $('<input type="text" />')
189 .addClass('form-control')
197 .addClass('form-control')
190 .addClass('widget-numeric-text')
198 .addClass('widget-numeric-text')
191 .appendTo(this.$el);
199 .appendTo(this.$el);
192 this.update(); // Set defaults.
200 this.update(); // Set defaults.
193 },
201 },
194
202
195 update : function(options){
203 update : function(options){
196 // Update the contents of this view
204 // Update the contents of this view
197 //
205 //
198 // Called when the model is changed. The model may have been
206 // Called when the model is changed. The model may have been
199 // changed by another view or by a state update from the back-end.
207 // changed by another view or by a state update from the back-end.
200 if (options === undefined || options.updated_view != this) {
208 if (options === undefined || options.updated_view != this) {
201 var value = this.model.get('value');
209 var value = this.model.get('value');
202 if (this._parse_value(this.$textbox.val()) != value) {
210 if (this._parse_value(this.$textbox.val()) != value) {
203 this.$textbox.val(value);
211 this.$textbox.val(value);
204 }
212 }
205
213
206 if (this.model.get('disabled')) {
214 if (this.model.get('disabled')) {
207 this.$textbox.attr('disabled','disabled');
215 this.$textbox.attr('disabled','disabled');
208 } else {
216 } else {
209 this.$textbox.removeAttr('disabled');
217 this.$textbox.removeAttr('disabled');
210 }
218 }
211
219
212 var description = this.model.get('description');
220 var description = this.model.get('description');
213 if (description.length === 0) {
221 if (description.length === 0) {
214 this.$label.hide();
222 this.$label.hide();
215 } else {
223 } else {
216 this.$label.text(description);
224 this.$label.text(description);
217 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
225 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
218 this.$label.show();
226 this.$label.show();
219 }
227 }
220 }
228 }
221 return IntTextView.__super__.update.apply(this);
229 return IntTextView.__super__.update.apply(this);
222 },
230 },
223
231
224 events: {
232 events: {
225 // Dictionary of events and their handlers.
233 // Dictionary of events and their handlers.
226 "keyup input" : "handleChanging",
234 "keyup input" : "handleChanging",
227 "paste input" : "handleChanging",
235 "paste input" : "handleChanging",
228 "cut input" : "handleChanging",
236 "cut input" : "handleChanging",
229
237
230 // Fires only when control is validated or looses focus.
238 // Fires only when control is validated or looses focus.
231 "change input" : "handleChanged"
239 "change input" : "handleChanged"
232 },
240 },
233
241
234 handleChanging: function(e) {
242 handleChanging: function(e) {
235 // Handles and validates user input.
243 // Handles and validates user input.
236
244
237 // Try to parse value as a int.
245 // Try to parse value as a int.
238 var numericalValue = 0;
246 var numericalValue = 0;
239 if (e.target.value !== '') {
247 if (e.target.value !== '') {
240 var trimmed = e.target.value.trim();
248 var trimmed = e.target.value.trim();
241 if (!(['-', '-.', '.', '+.', '+'].indexOf(trimmed) >= 0)) {
249 if (!(['-', '-.', '.', '+.', '+'].indexOf(trimmed) >= 0)) {
242 numericalValue = this._parse_value(e.target.value);
250 numericalValue = this._parse_value(e.target.value);
243 }
251 }
244 }
252 }
245
253
246 // If parse failed, reset value to value stored in model.
254 // If parse failed, reset value to value stored in model.
247 if (isNaN(numericalValue)) {
255 if (isNaN(numericalValue)) {
248 e.target.value = this.model.get('value');
256 e.target.value = this.model.get('value');
249 } else if (!isNaN(numericalValue)) {
257 } else if (!isNaN(numericalValue)) {
250 if (this.model.get('max') !== undefined) {
258 if (this.model.get('max') !== undefined) {
251 numericalValue = Math.min(this.model.get('max'), numericalValue);
259 numericalValue = Math.min(this.model.get('max'), numericalValue);
252 }
260 }
253 if (this.model.get('min') !== undefined) {
261 if (this.model.get('min') !== undefined) {
254 numericalValue = Math.max(this.model.get('min'), numericalValue);
262 numericalValue = Math.max(this.model.get('min'), numericalValue);
255 }
263 }
256
264
257 // Apply the value if it has changed.
265 // Apply the value if it has changed.
258 if (numericalValue != this.model.get('value')) {
266 if (numericalValue != this.model.get('value')) {
259
267
260 // Calling model.set will trigger all of the other views of the
268 // Calling model.set will trigger all of the other views of the
261 // model to update.
269 // model to update.
262 this.model.set('value', numericalValue, {updated_view: this});
270 this.model.set('value', numericalValue, {updated_view: this});
263 this.touch();
271 this.touch();
264 }
272 }
265 }
273 }
266 },
274 },
267
275
268 handleChanged: function(e) {
276 handleChanged: function(e) {
269 // Applies validated input.
277 // Applies validated input.
270 if (this.model.get('value') != e.target.value) {
278 if (this.model.get('value') != e.target.value) {
271 e.target.value = this.model.get('value');
279 e.target.value = this.model.get('value');
272 }
280 }
273 },
281 },
274
282
275 _parse_value: function(value) {
283 _parse_value: function(value) {
276 // Parse the value stored in a string.
284 // Parse the value stored in a string.
277 return parseInt(value);
285 return parseInt(value);
278 },
286 },
279 });
287 });
280
288
281
289
282 var ProgressView = widget.DOMWidgetView.extend({
290 var ProgressView = widget.DOMWidgetView.extend({
283 render : function(){
291 render : function(){
284 // Called when view is rendered.
292 // Called when view is rendered.
285 this.$el
293 this.$el
286 .addClass('widget-hbox-single');
294 .addClass('widget-hbox-single');
287 this.$label = $('<div />')
295 this.$label = $('<div />')
288 .appendTo(this.$el)
296 .appendTo(this.$el)
289 .addClass('widget-hlabel')
297 .addClass('widget-hlabel')
290 .hide();
298 .hide();
291 this.$progress = $('<div />')
299 this.$progress = $('<div />')
292 .addClass('progress')
300 .addClass('progress')
293 .addClass('widget-progress')
301 .addClass('widget-progress')
294 .appendTo(this.$el);
302 .appendTo(this.$el);
295 this.$bar = $('<div />')
303 this.$bar = $('<div />')
296 .addClass('progress-bar')
304 .addClass('progress-bar')
297 .css('width', '50%')
305 .css('width', '50%')
298 .appendTo(this.$progress);
306 .appendTo(this.$progress);
299 this.update(); // Set defaults.
307 this.update(); // Set defaults.
300 },
308 },
301
309
302 update : function(){
310 update : function(){
303 // Update the contents of this view
311 // Update the contents of this view
304 //
312 //
305 // Called when the model is changed. The model may have been
313 // Called when the model is changed. The model may have been
306 // changed by another view or by a state update from the back-end.
314 // changed by another view or by a state update from the back-end.
307 var value = this.model.get('value');
315 var value = this.model.get('value');
308 var max = this.model.get('max');
316 var max = this.model.get('max');
309 var min = this.model.get('min');
317 var min = this.model.get('min');
310 var percent = 100.0 * (value - min) / (max - min);
318 var percent = 100.0 * (value - min) / (max - min);
311 this.$bar.css('width', percent + '%');
319 this.$bar.css('width', percent + '%');
312
320
313 var description = this.model.get('description');
321 var description = this.model.get('description');
314 if (description.length === 0) {
322 if (description.length === 0) {
315 this.$label.hide();
323 this.$label.hide();
316 } else {
324 } else {
317 this.$label.text(description);
325 this.$label.text(description);
318 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
326 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
319 this.$label.show();
327 this.$label.show();
320 }
328 }
321 return ProgressView.__super__.update.apply(this);
329 return ProgressView.__super__.update.apply(this);
322 },
330 },
323 });
331 });
324
332
325 return {
333 return {
326 'IntSliderView': IntSliderView,
334 'IntSliderView': IntSliderView,
327 'IntTextView': IntTextView,
335 'IntTextView': IntTextView,
328 'ProgressView': ProgressView,
336 'ProgressView': ProgressView,
329 };
337 };
330 });
338 });
@@ -1,177 +1,175 b''
1 // Test widget int class
1 // Test widget int class
2 casper.notebook_test(function () {
2 casper.notebook_test(function () {
3 index = this.append_cell(
3 index = this.append_cell(
4 'from IPython.html import widgets\n' +
4 'from IPython.html import widgets\n' +
5 'from IPython.display import display, clear_output\n' +
5 'from IPython.display import display, clear_output\n' +
6 'print("Success")');
6 'print("Success")');
7 this.execute_cell_then(index);
7 this.execute_cell_then(index);
8
8
9 var int_text = {};
9 var int_text = {};
10 int_text.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-int-text';
10 int_text.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-int-text';
11 int_text.index = this.append_cell(
11 int_text.index = this.append_cell(
12 'int_widget = widgets.IntText()\n' +
12 'int_widget = widgets.IntText()\n' +
13 'display(int_widget)\n' +
13 'display(int_widget)\n' +
14 'int_widget.add_class("my-second-int-text", selector="input")\n' +
14 'int_widget.add_class("my-second-int-text", selector="input")\n' +
15 'print(int_widget.model_id)\n');
15 'print(int_widget.model_id)\n');
16 this.execute_cell_then(int_text.index, function(index){
16 this.execute_cell_then(int_text.index, function(index){
17 int_text.model_id = this.get_output_cell(index).text.trim();
17 int_text.model_id = this.get_output_cell(index).text.trim();
18
18
19 this.test.assert(this.cell_element_exists(index,
19 this.test.assert(this.cell_element_exists(index,
20 '.widget-area .widget-subarea'),
20 '.widget-area .widget-subarea'),
21 'Widget subarea exists.');
21 'Widget subarea exists.');
22
22
23 this.test.assert(this.cell_element_exists(index, int_text.query),
23 this.test.assert(this.cell_element_exists(index, int_text.query),
24 'Widget int textbox exists.');
24 'Widget int textbox exists.');
25
25
26 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
26 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
27 this.sendKeys(int_text.query, '1.05');
27 this.sendKeys(int_text.query, '1.05');
28 });
28 });
29
29
30 this.wait_for_widget(int_text);
30 this.wait_for_widget(int_text);
31
31
32 index = this.append_cell('print(int_widget.value)\n');
32 index = this.append_cell('print(int_widget.value)\n');
33 this.execute_cell_then(index, function(index){
33 this.execute_cell_then(index, function(index){
34 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
34 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
35 'Int textbox value set.');
35 'Int textbox value set.');
36 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
36 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
37 this.sendKeys(int_text.query, '123456789');
37 this.sendKeys(int_text.query, '123456789');
38 });
38 });
39
39
40 this.wait_for_widget(int_text);
40 this.wait_for_widget(int_text);
41
41
42 index = this.append_cell('print(int_widget.value)\n');
42 index = this.append_cell('print(int_widget.value)\n');
43 this.execute_cell_then(index, function(index){
43 this.execute_cell_then(index, function(index){
44 this.test.assertEquals(this.get_output_cell(index).text, '123456789\n',
44 this.test.assertEquals(this.get_output_cell(index).text, '123456789\n',
45 'Long int textbox value set (probably triggers throttling).');
45 'Long int textbox value set (probably triggers throttling).');
46 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
46 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
47 this.sendKeys(int_text.query, '12hello');
47 this.sendKeys(int_text.query, '12hello');
48 });
48 });
49
49
50 this.wait_for_widget(int_text);
50 this.wait_for_widget(int_text);
51
51
52 index = this.append_cell('print(int_widget.value)\n');
52 index = this.append_cell('print(int_widget.value)\n');
53 this.execute_cell_then(index, function(index){
53 this.execute_cell_then(index, function(index){
54 this.test.assertEquals(this.get_output_cell(index).text, '12\n',
54 this.test.assertEquals(this.get_output_cell(index).text, '12\n',
55 'Invald int textbox value caught and filtered.');
55 'Invald int textbox value caught and filtered.');
56 });
56 });
57
57
58 index = this.append_cell(
58 index = this.append_cell(
59 'from IPython.html import widgets\n' +
59 'from IPython.html import widgets\n' +
60 'from IPython.display import display, clear_output\n' +
60 'from IPython.display import display, clear_output\n' +
61 'print("Success")');
61 'print("Success")');
62 this.execute_cell_then(index);
62 this.execute_cell_then(index);
63
63
64
64
65 var slider_query = '.widget-area .widget-subarea .widget-hbox-single .slider';
65 var slider_query = '.widget-area .widget-subarea .widget-hbox-single .slider';
66 var int_text2 = {};
66 var int_text2 = {};
67 int_text2.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-num-test-text';
67 int_text2.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-num-test-text';
68 int_text2.index = this.append_cell(
68 int_text2.index = this.append_cell(
69 'intrange = [widgets.BoundedIntTextWidget(),\n' +
69 'intrange = [widgets.BoundedIntTextWidget(),\n' +
70 ' widgets.IntSliderWidget()]\n' +
70 ' widgets.IntSliderWidget()]\n' +
71 '[display(intrange[i]) for i in range(2)]\n' +
71 '[display(intrange[i]) for i in range(2)]\n' +
72 'intrange[0].add_class("my-second-num-test-text", selector="input")\n' +
72 'intrange[0].add_class("my-second-num-test-text", selector="input")\n' +
73 'print(intrange[0].model_id)\n');
73 'print(intrange[0].model_id)\n');
74 this.execute_cell_then(int_text2.index, function(index){
74 this.execute_cell_then(int_text2.index, function(index){
75 int_text2.model_id = this.get_output_cell(index).text.trim();
75 int_text2.model_id = this.get_output_cell(index).text.trim();
76
76
77 this.test.assert(this.cell_element_exists(index,
77 this.test.assert(this.cell_element_exists(index,
78 '.widget-area .widget-subarea'),
78 '.widget-area .widget-subarea'),
79 'Widget subarea exists.');
79 'Widget subarea exists.');
80
80
81 this.test.assert(this.cell_element_exists(index, slider_query),
81 this.test.assert(this.cell_element_exists(index, slider_query),
82 'Widget slider exists.');
82 'Widget slider exists.');
83
83
84 this.test.assert(this.cell_element_exists(index, int_text2.query),
84 this.test.assert(this.cell_element_exists(index, int_text2.query),
85 'Widget int textbox exists.');
85 'Widget int textbox exists.');
86 });
86 });
87
87
88 index = this.append_cell(
88 index = this.append_cell(
89 'for widget in intrange:\n' +
89 'for widget in intrange:\n' +
90 ' widget.max = 50\n' +
90 ' widget.max = 50\n' +
91 ' widget.min = -50\n' +
91 ' widget.min = -50\n' +
92 ' widget.value = 25\n' +
92 ' widget.value = 25\n' +
93 'print("Success")\n');
93 'print("Success")\n');
94 this.execute_cell_then(index, function(index){
94 this.execute_cell_then(index, function(index){
95
95
96 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
96 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
97 'Int range properties cell executed with correct output.');
97 'Int range properties cell executed with correct output.');
98
98
99 this.test.assert(this.cell_element_exists(int_text2.index, slider_query),
99 this.test.assert(this.cell_element_exists(int_text2.index, slider_query),
100 'Widget slider exists.');
100 'Widget slider exists.');
101
101
102 this.test.assert(this.cell_element_function(int_text2.index, slider_query,
102 this.test.assert(this.cell_element_function(int_text2.index, slider_query,
103 'slider', ['value']) == 25,
103 'slider', ['value']) == 25,
104 'Slider set to Python value.');
104 'Slider set to Python value.');
105
105
106 this.test.assert(this.cell_element_function(int_text2.index, int_text2.query,
106 this.test.assert(this.cell_element_function(int_text2.index, int_text2.query,
107 'val') == 25, 'Int textbox set to Python value.');
107 'val') == 25, 'Int textbox set to Python value.');
108
108
109 // Clear the int textbox value and then set it to 1 by emulating
109 // Clear the int textbox value and then set it to 1 by emulating
110 // keyboard presses.
110 // keyboard presses.
111 this.evaluate(function(q){
111 this.evaluate(function(q){
112 var textbox = IPython.notebook.element.find(q);
112 var textbox = IPython.notebook.element.find(q);
113 textbox.val('1');
113 textbox.val('1');
114 textbox.trigger('keyup');
114 textbox.trigger('keyup');
115 }, {q: int_text2.query});
115 }, {q: int_text2.query});
116 });
116 });
117
117
118 this.wait_for_widget(int_text2);
118 this.wait_for_widget(int_text2);
119
119
120 index = this.append_cell('print(intrange[0].value)\n');
120 index = this.append_cell('print(intrange[0].value)\n');
121 this.execute_cell_then(index, function(index){
121 this.execute_cell_then(index, function(index){
122 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
122 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
123 'Int textbox set int range value');
123 'Int textbox set int range value');
124
124
125 // Clear the int textbox value and then set it to 120 by emulating
125 // Clear the int textbox value and then set it to 120 by emulating
126 // keyboard presses.
126 // keyboard presses.
127 this.evaluate(function(q){
127 this.evaluate(function(q){
128 var textbox = IPython.notebook.element.find(q);
128 var textbox = IPython.notebook.element.find(q);
129 textbox.val('120');
129 textbox.val('120');
130 textbox.trigger('keyup');
130 textbox.trigger('keyup');
131 }, {q: int_text2.query});
131 }, {q: int_text2.query});
132 });
132 });
133
133
134 this.wait_for_widget(int_text2);
134 this.wait_for_widget(int_text2);
135
135
136 index = this.append_cell('print(intrange[0].value)\n');
136 index = this.append_cell('print(intrange[0].value)\n');
137 this.execute_cell_then(index, function(index){
137 this.execute_cell_then(index, function(index){
138 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
138 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
139 'Int textbox value bound');
139 'Int textbox value bound');
140
140
141 // Clear the int textbox value and then set it to 'hello world' by
141 // Clear the int textbox value and then set it to 'hello world' by
142 // emulating keyboard presses. 'hello world' should get filtered...
142 // emulating keyboard presses. 'hello world' should get filtered...
143 this.evaluate(function(q){
143 this.evaluate(function(q){
144 var textbox = IPython.notebook.element.find(q);
144 var textbox = IPython.notebook.element.find(q);
145 textbox.val('hello world');
145 textbox.val('hello world');
146 textbox.trigger('keyup');
146 textbox.trigger('keyup');
147 }, {q: int_text2.query});
147 }, {q: int_text2.query});
148 });
148 });
149
149
150 this.wait_for_widget(int_text2);
150 this.wait_for_widget(int_text2);
151
151
152 index = this.append_cell('print(intrange[0].value)\n');
152 index = this.append_cell('print(intrange[0].value)\n');
153 this.execute_cell_then(index, function(index){
153 this.execute_cell_then(index, function(index){
154 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
154 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
155 'Invalid int textbox characters ignored');
155 'Invalid int textbox characters ignored');
156 });
156 });
157
157
158 index = this.append_cell(
158 index = this.append_cell(
159 'a = widgets.IntSlider()\n' +
159 'a = widgets.IntSlider()\n' +
160 'display(a)\n' +
160 'display(a)\n' +
161 'a.max = -1\n' +
161 'a.max = -1\n' +
162 'print("Success")\n');
162 'print("Success")\n');
163 this.execute_cell_then(index, function(index){
163 this.execute_cell_then(index, function(index){
164 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
164 this.test.assertEquals(0, 0, 'Invalid int range max bound does not cause crash.');
165 'Invalid int range max bound does not cause crash.');
166 });
165 });
167
166
168 index = this.append_cell(
167 index = this.append_cell(
169 'a = widgets.IntSlider()\n' +
168 'a = widgets.IntSlider()\n' +
170 'display(a)\n' +
169 'display(a)\n' +
171 'a.min = 101\n' +
170 'a.min = 101\n' +
172 'print("Success")\n');
171 'print("Success")\n');
173 this.execute_cell_then(index, function(index){
172 this.execute_cell_then(index, function(index){
174 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
173 this.test.assertEquals(0, 0, 'Invalid int range min bound does not cause crash.');
175 'Invalid int range min bound does not cause crash.');
176 });
174 });
177 }); No newline at end of file
175 });
@@ -1,478 +1,485 b''
1 """Base Widget class. Allows user to create widgets in the back-end that render
1 """Base Widget class. Allows user to create widgets in the back-end that render
2 in the IPython notebook front-end.
2 in the IPython notebook front-end.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from contextlib import contextmanager
15 from contextlib import contextmanager
16 import collections
16 import collections
17
17
18 from IPython.core.getipython import get_ipython
18 from IPython.core.getipython import get_ipython
19 from IPython.kernel.comm import Comm
19 from IPython.kernel.comm import Comm
20 from IPython.config import LoggingConfigurable
20 from IPython.config import LoggingConfigurable
21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int, Set
21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int, Set
22 from IPython.utils.py3compat import string_types
22 from IPython.utils.py3compat import string_types
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Classes
25 # Classes
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 class CallbackDispatcher(LoggingConfigurable):
27 class CallbackDispatcher(LoggingConfigurable):
28 """A structure for registering and running callbacks"""
28 """A structure for registering and running callbacks"""
29 callbacks = List()
29 callbacks = List()
30
30
31 def __call__(self, *args, **kwargs):
31 def __call__(self, *args, **kwargs):
32 """Call all of the registered callbacks."""
32 """Call all of the registered callbacks."""
33 value = None
33 value = None
34 for callback in self.callbacks:
34 for callback in self.callbacks:
35 try:
35 try:
36 local_value = callback(*args, **kwargs)
36 local_value = callback(*args, **kwargs)
37 except Exception as e:
37 except Exception as e:
38 ip = get_ipython()
38 ip = get_ipython()
39 if ip is None:
39 if ip is None:
40 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
40 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
41 else:
41 else:
42 ip.showtraceback()
42 ip.showtraceback()
43 else:
43 else:
44 value = local_value if local_value is not None else value
44 value = local_value if local_value is not None else value
45 return value
45 return value
46
46
47 def register_callback(self, callback, remove=False):
47 def register_callback(self, callback, remove=False):
48 """(Un)Register a callback
48 """(Un)Register a callback
49
49
50 Parameters
50 Parameters
51 ----------
51 ----------
52 callback: method handle
52 callback: method handle
53 Method to be registered or unregistered.
53 Method to be registered or unregistered.
54 remove=False: bool
54 remove=False: bool
55 Whether to unregister the callback."""
55 Whether to unregister the callback."""
56
56
57 # (Un)Register the callback.
57 # (Un)Register the callback.
58 if remove and callback in self.callbacks:
58 if remove and callback in self.callbacks:
59 self.callbacks.remove(callback)
59 self.callbacks.remove(callback)
60 elif not remove and callback not in self.callbacks:
60 elif not remove and callback not in self.callbacks:
61 self.callbacks.append(callback)
61 self.callbacks.append(callback)
62
62
63 def _show_traceback(method):
63 def _show_traceback(method):
64 """decorator for showing tracebacks in IPython"""
64 """decorator for showing tracebacks in IPython"""
65 def m(self, *args, **kwargs):
65 def m(self, *args, **kwargs):
66 try:
66 try:
67 return(method(self, *args, **kwargs))
67 return(method(self, *args, **kwargs))
68 except Exception as e:
68 except Exception as e:
69 ip = get_ipython()
69 ip = get_ipython()
70 if ip is None:
70 if ip is None:
71 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
71 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
72 else:
72 else:
73 ip.showtraceback()
73 ip.showtraceback()
74 return m
74 return m
75
75
76 class Widget(LoggingConfigurable):
76 class Widget(LoggingConfigurable):
77 #-------------------------------------------------------------------------
77 #-------------------------------------------------------------------------
78 # Class attributes
78 # Class attributes
79 #-------------------------------------------------------------------------
79 #-------------------------------------------------------------------------
80 _widget_construction_callback = None
80 _widget_construction_callback = None
81 widgets = {}
81 widgets = {}
82
82
83 @staticmethod
83 @staticmethod
84 def on_widget_constructed(callback):
84 def on_widget_constructed(callback):
85 """Registers a callback to be called when a widget is constructed.
85 """Registers a callback to be called when a widget is constructed.
86
86
87 The callback must have the following signature:
87 The callback must have the following signature:
88 callback(widget)"""
88 callback(widget)"""
89 Widget._widget_construction_callback = callback
89 Widget._widget_construction_callback = callback
90
90
91 @staticmethod
91 @staticmethod
92 def _call_widget_constructed(widget):
92 def _call_widget_constructed(widget):
93 """Static method, called when a widget is constructed."""
93 """Static method, called when a widget is constructed."""
94 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
94 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
95 Widget._widget_construction_callback(widget)
95 Widget._widget_construction_callback(widget)
96
96
97 #-------------------------------------------------------------------------
97 #-------------------------------------------------------------------------
98 # Traits
98 # Traits
99 #-------------------------------------------------------------------------
99 #-------------------------------------------------------------------------
100 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
100 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
101 registered in the front-end to create and sync this widget with.""")
101 registered in the front-end to create and sync this widget with.""")
102 _view_name = Unicode('WidgetView', help="""Default view registered in the front-end
102 _view_name = Unicode('WidgetView', help="""Default view registered in the front-end
103 to use to represent the widget.""", sync=True)
103 to use to represent the widget.""", sync=True)
104 comm = Instance('IPython.kernel.comm.Comm')
104 comm = Instance('IPython.kernel.comm.Comm')
105
105
106 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
106 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
107 front-end can send before receiving an idle msg from the back-end.""")
107 front-end can send before receiving an idle msg from the back-end.""")
108
108
109 keys = List()
109 keys = List()
110 def _keys_default(self):
110 def _keys_default(self):
111 return [name for name in self.traits(sync=True)]
111 return [name for name in self.traits(sync=True)]
112
112
113 _property_lock = Tuple((None, None))
113 _property_lock = Tuple((None, None))
114 _send_state_lock = Int(0)
114 _send_state_lock = Int(0)
115 _states_to_send = Set(allow_none=False)
115 _states_to_send = Set(allow_none=False)
116 _display_callbacks = Instance(CallbackDispatcher, ())
116 _display_callbacks = Instance(CallbackDispatcher, ())
117 _msg_callbacks = Instance(CallbackDispatcher, ())
117 _msg_callbacks = Instance(CallbackDispatcher, ())
118
118
119 #-------------------------------------------------------------------------
119 #-------------------------------------------------------------------------
120 # (Con/de)structor
120 # (Con/de)structor
121 #-------------------------------------------------------------------------
121 #-------------------------------------------------------------------------
122 def __init__(self, **kwargs):
122 def __init__(self, **kwargs):
123 """Public constructor"""
123 """Public constructor"""
124 self._model_id = kwargs.pop('model_id', None)
124 self._model_id = kwargs.pop('model_id', None)
125 super(Widget, self).__init__(**kwargs)
125 super(Widget, self).__init__(**kwargs)
126
126
127 self.on_trait_change(self._handle_property_changed, self.keys)
128 Widget._call_widget_constructed(self)
127 Widget._call_widget_constructed(self)
129 self.open()
128 self.open()
130
129
131 def __del__(self):
130 def __del__(self):
132 """Object disposal"""
131 """Object disposal"""
133 self.close()
132 self.close()
134
133
135 #-------------------------------------------------------------------------
134 #-------------------------------------------------------------------------
136 # Properties
135 # Properties
137 #-------------------------------------------------------------------------
136 #-------------------------------------------------------------------------
138
137
139 def open(self):
138 def open(self):
140 """Open a comm to the frontend if one isn't already open."""
139 """Open a comm to the frontend if one isn't already open."""
141 if self.comm is None:
140 if self.comm is None:
142 if self._model_id is None:
141 if self._model_id is None:
143 self.comm = Comm(target_name=self._model_name)
142 self.comm = Comm(target_name=self._model_name)
144 self._model_id = self.model_id
143 self._model_id = self.model_id
145 else:
144 else:
146 self.comm = Comm(target_name=self._model_name, comm_id=self._model_id)
145 self.comm = Comm(target_name=self._model_name, comm_id=self._model_id)
147 self.comm.on_msg(self._handle_msg)
146 self.comm.on_msg(self._handle_msg)
148 Widget.widgets[self.model_id] = self
147 Widget.widgets[self.model_id] = self
149
148
150 # first update
149 # first update
151 self.send_state()
150 self.send_state()
152
151
153 @property
152 @property
154 def model_id(self):
153 def model_id(self):
155 """Gets the model id of this widget.
154 """Gets the model id of this widget.
156
155
157 If a Comm doesn't exist yet, a Comm will be created automagically."""
156 If a Comm doesn't exist yet, a Comm will be created automagically."""
158 return self.comm.comm_id
157 return self.comm.comm_id
159
158
160 #-------------------------------------------------------------------------
159 #-------------------------------------------------------------------------
161 # Methods
160 # Methods
162 #-------------------------------------------------------------------------
161 #-------------------------------------------------------------------------
163
162
164 def close(self):
163 def close(self):
165 """Close method.
164 """Close method.
166
165
167 Closes the underlying comm.
166 Closes the underlying comm.
168 When the comm is closed, all of the widget views are automatically
167 When the comm is closed, all of the widget views are automatically
169 removed from the front-end."""
168 removed from the front-end."""
170 if self.comm is not None:
169 if self.comm is not None:
171 Widget.widgets.pop(self.model_id, None)
170 Widget.widgets.pop(self.model_id, None)
172 self.comm.close()
171 self.comm.close()
173 self.comm = None
172 self.comm = None
174
173
175 def send_state(self, key=None):
174 def send_state(self, key=None):
176 """Sends the widget state, or a piece of it, to the front-end.
175 """Sends the widget state, or a piece of it, to the front-end.
177
176
178 Parameters
177 Parameters
179 ----------
178 ----------
180 key : unicode, or iterable (optional)
179 key : unicode, or iterable (optional)
181 A single property's name or iterable of property names to sync with the front-end.
180 A single property's name or iterable of property names to sync with the front-end.
182 """
181 """
183 self._send({
182 self._send({
184 "method" : "update",
183 "method" : "update",
185 "state" : self.get_state(key=key)
184 "state" : self.get_state(key=key)
186 })
185 })
187
186
188 def get_state(self, key=None):
187 def get_state(self, key=None):
189 """Gets the widget state, or a piece of it.
188 """Gets the widget state, or a piece of it.
190
189
191 Parameters
190 Parameters
192 ----------
191 ----------
193 key : unicode or iterable (optional)
192 key : unicode or iterable (optional)
194 A single property's name or iterable of property names to get.
193 A single property's name or iterable of property names to get.
195 """
194 """
196 if key is None:
195 if key is None:
197 keys = self.keys
196 keys = self.keys
198 elif isinstance(key, string_types):
197 elif isinstance(key, string_types):
199 keys = [key]
198 keys = [key]
200 elif isinstance(key, collections.Iterable):
199 elif isinstance(key, collections.Iterable):
201 keys = key
200 keys = key
202 else:
201 else:
203 raise ValueError("key must be a string, an iterable of keys, or None")
202 raise ValueError("key must be a string, an iterable of keys, or None")
204 state = {}
203 state = {}
205 for k in keys:
204 for k in keys:
206 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
205 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
207 value = getattr(self, k)
206 value = getattr(self, k)
208 state[k] = f(value)
207 state[k] = f(value)
209 return state
208 return state
210
209
211 def send(self, content):
210 def send(self, content):
212 """Sends a custom msg to the widget model in the front-end.
211 """Sends a custom msg to the widget model in the front-end.
213
212
214 Parameters
213 Parameters
215 ----------
214 ----------
216 content : dict
215 content : dict
217 Content of the message to send.
216 Content of the message to send.
218 """
217 """
219 self._send({"method": "custom", "content": content})
218 self._send({"method": "custom", "content": content})
220
219
221 def on_msg(self, callback, remove=False):
220 def on_msg(self, callback, remove=False):
222 """(Un)Register a custom msg receive callback.
221 """(Un)Register a custom msg receive callback.
223
222
224 Parameters
223 Parameters
225 ----------
224 ----------
226 callback: callable
225 callback: callable
227 callback will be passed two arguments when a message arrives::
226 callback will be passed two arguments when a message arrives::
228
227
229 callback(widget, content)
228 callback(widget, content)
230
229
231 remove: bool
230 remove: bool
232 True if the callback should be unregistered."""
231 True if the callback should be unregistered."""
233 self._msg_callbacks.register_callback(callback, remove=remove)
232 self._msg_callbacks.register_callback(callback, remove=remove)
234
233
235 def on_displayed(self, callback, remove=False):
234 def on_displayed(self, callback, remove=False):
236 """(Un)Register a widget displayed callback.
235 """(Un)Register a widget displayed callback.
237
236
238 Parameters
237 Parameters
239 ----------
238 ----------
240 callback: method handler
239 callback: method handler
241 Must have a signature of::
240 Must have a signature of::
242
241
243 callback(widget, **kwargs)
242 callback(widget, **kwargs)
244
243
245 kwargs from display are passed through without modification.
244 kwargs from display are passed through without modification.
246 remove: bool
245 remove: bool
247 True if the callback should be unregistered."""
246 True if the callback should be unregistered."""
248 self._display_callbacks.register_callback(callback, remove=remove)
247 self._display_callbacks.register_callback(callback, remove=remove)
249
248
250 #-------------------------------------------------------------------------
249 #-------------------------------------------------------------------------
251 # Support methods
250 # Support methods
252 #-------------------------------------------------------------------------
251 #-------------------------------------------------------------------------
253 @contextmanager
252 @contextmanager
254 def _lock_property(self, key, value):
253 def _lock_property(self, key, value):
255 """Lock a property-value pair.
254 """Lock a property-value pair.
256
255
257 The value should be the JSON state of the property.
256 The value should be the JSON state of the property.
258
257
259 NOTE: This, in addition to the single lock for all state changes, is
258 NOTE: This, in addition to the single lock for all state changes, is
260 flawed. In the future we may want to look into buffering state changes
259 flawed. In the future we may want to look into buffering state changes
261 back to the front-end."""
260 back to the front-end."""
262 self._property_lock = (key, value)
261 self._property_lock = (key, value)
263 try:
262 try:
264 yield
263 yield
265 finally:
264 finally:
266 self._property_lock = (None, None)
265 self._property_lock = (None, None)
267
266
268 @contextmanager
267 @contextmanager
269 def hold_sync(self):
268 def hold_sync(self):
270 """Hold syncing any state until the context manager is released"""
269 """Hold syncing any state until the context manager is released"""
271 # We increment a value so that this can be nested. Syncing will happen when
270 # We increment a value so that this can be nested. Syncing will happen when
272 # all levels have been released.
271 # all levels have been released.
273 self._send_state_lock += 1
272 self._send_state_lock += 1
274 try:
273 try:
275 yield
274 yield
276 finally:
275 finally:
277 self._send_state_lock -=1
276 self._send_state_lock -=1
278 if self._send_state_lock == 0:
277 if self._send_state_lock == 0:
279 self.send_state(self._states_to_send)
278 self.send_state(self._states_to_send)
280 self._states_to_send.clear()
279 self._states_to_send.clear()
281
280
282 def _should_send_property(self, key, value):
281 def _should_send_property(self, key, value):
283 """Check the property lock (property_lock)"""
282 """Check the property lock (property_lock)"""
284 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
283 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
285 if (key == self._property_lock[0]
284 if (key == self._property_lock[0]
286 and to_json(value) == self._property_lock[1]):
285 and to_json(value) == self._property_lock[1]):
287 return False
286 return False
288 elif self._send_state_lock > 0:
287 elif self._send_state_lock > 0:
289 self._states_to_send.add(key)
288 self._states_to_send.add(key)
290 return False
289 return False
291 else:
290 else:
292 return True
291 return True
293
292
294 # Event handlers
293 # Event handlers
295 @_show_traceback
294 @_show_traceback
296 def _handle_msg(self, msg):
295 def _handle_msg(self, msg):
297 """Called when a msg is received from the front-end"""
296 """Called when a msg is received from the front-end"""
298 data = msg['content']['data']
297 data = msg['content']['data']
299 method = data['method']
298 method = data['method']
300 if not method in ['backbone', 'custom']:
299 if not method in ['backbone', 'custom']:
301 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
300 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
302
301
303 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
302 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
304 if method == 'backbone' and 'sync_data' in data:
303 if method == 'backbone' and 'sync_data' in data:
305 sync_data = data['sync_data']
304 sync_data = data['sync_data']
306 self._handle_receive_state(sync_data) # handles all methods
305 self._handle_receive_state(sync_data) # handles all methods
307
306
308 # Handle a custom msg from the front-end
307 # Handle a custom msg from the front-end
309 elif method == 'custom':
308 elif method == 'custom':
310 if 'content' in data:
309 if 'content' in data:
311 self._handle_custom_msg(data['content'])
310 self._handle_custom_msg(data['content'])
312
311
313 def _handle_receive_state(self, sync_data):
312 def _handle_receive_state(self, sync_data):
314 """Called when a state is received from the front-end."""
313 """Called when a state is received from the front-end."""
315 for name in self.keys:
314 for name in self.keys:
316 if name in sync_data:
315 if name in sync_data:
317 json_value = sync_data[name]
316 json_value = sync_data[name]
318 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
317 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
319 with self._lock_property(name, json_value):
318 with self._lock_property(name, json_value):
320 setattr(self, name, from_json(json_value))
319 setattr(self, name, from_json(json_value))
321
320
322 def _handle_custom_msg(self, content):
321 def _handle_custom_msg(self, content):
323 """Called when a custom msg is received."""
322 """Called when a custom msg is received."""
324 self._msg_callbacks(self, content)
323 self._msg_callbacks(self, content)
325
324
326 def _handle_property_changed(self, name, old, new):
325 def _notify_trait(self, name, old_value, new_value):
327 """Called when a property has been changed."""
326 """Called when a property has been changed."""
328 # Make sure this isn't information that the front-end just sent us.
327 # Trigger default traitlet callback machinery. This allows any user
329 if self._should_send_property(name, new):
328 # registered validation to be processed prior to allowing the widget
330 # Send new state to front-end
329 # machinery to handle the state.
331 self.send_state(key=name)
330 super(Widget, self)._notify_trait(name, old_value, new_value)
331
332 # Send the state after the user registered callbacks for trait changes
333 # have all fired (allows for user to validate values).
334 if name in self.keys:
335 # Make sure this isn't information that the front-end just sent us.
336 if self._should_send_property(name, new_value):
337 # Send new state to front-end
338 self.send_state(key=name)
332
339
333 def _handle_displayed(self, **kwargs):
340 def _handle_displayed(self, **kwargs):
334 """Called when a view has been displayed for this widget instance"""
341 """Called when a view has been displayed for this widget instance"""
335 self._display_callbacks(self, **kwargs)
342 self._display_callbacks(self, **kwargs)
336
343
337 def _trait_to_json(self, x):
344 def _trait_to_json(self, x):
338 """Convert a trait value to json
345 """Convert a trait value to json
339
346
340 Traverse lists/tuples and dicts and serialize their values as well.
347 Traverse lists/tuples and dicts and serialize their values as well.
341 Replace any widgets with their model_id
348 Replace any widgets with their model_id
342 """
349 """
343 if isinstance(x, dict):
350 if isinstance(x, dict):
344 return {k: self._trait_to_json(v) for k, v in x.items()}
351 return {k: self._trait_to_json(v) for k, v in x.items()}
345 elif isinstance(x, (list, tuple)):
352 elif isinstance(x, (list, tuple)):
346 return [self._trait_to_json(v) for v in x]
353 return [self._trait_to_json(v) for v in x]
347 elif isinstance(x, Widget):
354 elif isinstance(x, Widget):
348 return "IPY_MODEL_" + x.model_id
355 return "IPY_MODEL_" + x.model_id
349 else:
356 else:
350 return x # Value must be JSON-able
357 return x # Value must be JSON-able
351
358
352 def _trait_from_json(self, x):
359 def _trait_from_json(self, x):
353 """Convert json values to objects
360 """Convert json values to objects
354
361
355 Replace any strings representing valid model id values to Widget references.
362 Replace any strings representing valid model id values to Widget references.
356 """
363 """
357 if isinstance(x, dict):
364 if isinstance(x, dict):
358 return {k: self._trait_from_json(v) for k, v in x.items()}
365 return {k: self._trait_from_json(v) for k, v in x.items()}
359 elif isinstance(x, (list, tuple)):
366 elif isinstance(x, (list, tuple)):
360 return [self._trait_from_json(v) for v in x]
367 return [self._trait_from_json(v) for v in x]
361 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
368 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
362 # we want to support having child widgets at any level in a hierarchy
369 # we want to support having child widgets at any level in a hierarchy
363 # trusting that a widget UUID will not appear out in the wild
370 # trusting that a widget UUID will not appear out in the wild
364 return Widget.widgets[x]
371 return Widget.widgets[x]
365 else:
372 else:
366 return x
373 return x
367
374
368 def _ipython_display_(self, **kwargs):
375 def _ipython_display_(self, **kwargs):
369 """Called when `IPython.display.display` is called on the widget."""
376 """Called when `IPython.display.display` is called on the widget."""
370 # Show view. By sending a display message, the comm is opened and the
377 # Show view. By sending a display message, the comm is opened and the
371 # initial state is sent.
378 # initial state is sent.
372 self._send({"method": "display"})
379 self._send({"method": "display"})
373 self._handle_displayed(**kwargs)
380 self._handle_displayed(**kwargs)
374
381
375 def _send(self, msg):
382 def _send(self, msg):
376 """Sends a message to the model in the front-end."""
383 """Sends a message to the model in the front-end."""
377 self.comm.send(msg)
384 self.comm.send(msg)
378
385
379
386
380 class DOMWidget(Widget):
387 class DOMWidget(Widget):
381 visible = Bool(True, help="Whether the widget is visible.", sync=True)
388 visible = Bool(True, help="Whether the widget is visible.", sync=True)
382 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
389 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
383
390
384 def get_css(self, key, selector=""):
391 def get_css(self, key, selector=""):
385 """Get a CSS property of the widget.
392 """Get a CSS property of the widget.
386
393
387 Note: This function does not actually request the CSS from the
394 Note: This function does not actually request the CSS from the
388 front-end; Only properties that have been set with set_css can be read.
395 front-end; Only properties that have been set with set_css can be read.
389
396
390 Parameters
397 Parameters
391 ----------
398 ----------
392 key: unicode
399 key: unicode
393 CSS key
400 CSS key
394 selector: unicode (optional)
401 selector: unicode (optional)
395 JQuery selector used when the CSS key/value was set.
402 JQuery selector used when the CSS key/value was set.
396 """
403 """
397 if selector in self._css and key in self._css[selector]:
404 if selector in self._css and key in self._css[selector]:
398 return self._css[selector][key]
405 return self._css[selector][key]
399 else:
406 else:
400 return None
407 return None
401
408
402 def set_css(self, dict_or_key, value=None, selector=''):
409 def set_css(self, dict_or_key, value=None, selector=''):
403 """Set one or more CSS properties of the widget.
410 """Set one or more CSS properties of the widget.
404
411
405 This function has two signatures:
412 This function has two signatures:
406 - set_css(css_dict, selector='')
413 - set_css(css_dict, selector='')
407 - set_css(key, value, selector='')
414 - set_css(key, value, selector='')
408
415
409 Parameters
416 Parameters
410 ----------
417 ----------
411 css_dict : dict
418 css_dict : dict
412 CSS key/value pairs to apply
419 CSS key/value pairs to apply
413 key: unicode
420 key: unicode
414 CSS key
421 CSS key
415 value:
422 value:
416 CSS value
423 CSS value
417 selector: unicode (optional, kwarg only)
424 selector: unicode (optional, kwarg only)
418 JQuery selector to use to apply the CSS key/value. If no selector
425 JQuery selector to use to apply the CSS key/value. If no selector
419 is provided, an empty selector is used. An empty selector makes the
426 is provided, an empty selector is used. An empty selector makes the
420 front-end try to apply the css to a default element. The default
427 front-end try to apply the css to a default element. The default
421 element is an attribute unique to each view, which is a DOM element
428 element is an attribute unique to each view, which is a DOM element
422 of the view that should be styled with common CSS (see
429 of the view that should be styled with common CSS (see
423 `$el_to_style` in the Javascript code).
430 `$el_to_style` in the Javascript code).
424 """
431 """
425 if value is None:
432 if value is None:
426 css_dict = dict_or_key
433 css_dict = dict_or_key
427 else:
434 else:
428 css_dict = {dict_or_key: value}
435 css_dict = {dict_or_key: value}
429
436
430 for (key, value) in css_dict.items():
437 for (key, value) in css_dict.items():
431 # First remove the selector/key pair from the css list if it exists.
438 # First remove the selector/key pair from the css list if it exists.
432 # Then add the selector/key pair and new value to the bottom of the
439 # Then add the selector/key pair and new value to the bottom of the
433 # list.
440 # list.
434 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
441 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
435 self._css += [(selector, key, value)]
442 self._css += [(selector, key, value)]
436 self.send_state('_css')
443 self.send_state('_css')
437
444
438 def add_class(self, class_names, selector=""):
445 def add_class(self, class_names, selector=""):
439 """Add class[es] to a DOM element.
446 """Add class[es] to a DOM element.
440
447
441 Parameters
448 Parameters
442 ----------
449 ----------
443 class_names: unicode or list
450 class_names: unicode or list
444 Class name(s) to add to the DOM element(s).
451 Class name(s) to add to the DOM element(s).
445 selector: unicode (optional)
452 selector: unicode (optional)
446 JQuery selector to select the DOM element(s) that the class(es) will
453 JQuery selector to select the DOM element(s) that the class(es) will
447 be added to.
454 be added to.
448 """
455 """
449 class_list = class_names
456 class_list = class_names
450 if isinstance(class_list, (list, tuple)):
457 if isinstance(class_list, (list, tuple)):
451 class_list = ' '.join(class_list)
458 class_list = ' '.join(class_list)
452
459
453 self.send({
460 self.send({
454 "msg_type" : "add_class",
461 "msg_type" : "add_class",
455 "class_list" : class_list,
462 "class_list" : class_list,
456 "selector" : selector
463 "selector" : selector
457 })
464 })
458
465
459 def remove_class(self, class_names, selector=""):
466 def remove_class(self, class_names, selector=""):
460 """Remove class[es] from a DOM element.
467 """Remove class[es] from a DOM element.
461
468
462 Parameters
469 Parameters
463 ----------
470 ----------
464 class_names: unicode or list
471 class_names: unicode or list
465 Class name(s) to remove from the DOM element(s).
472 Class name(s) to remove from the DOM element(s).
466 selector: unicode (optional)
473 selector: unicode (optional)
467 JQuery selector to select the DOM element(s) that the class(es) will
474 JQuery selector to select the DOM element(s) that the class(es) will
468 be removed from.
475 be removed from.
469 """
476 """
470 class_list = class_names
477 class_list = class_names
471 if isinstance(class_list, (list, tuple)):
478 if isinstance(class_list, (list, tuple)):
472 class_list = ' '.join(class_list)
479 class_list = ' '.join(class_list)
473
480
474 self.send({
481 self.send({
475 "msg_type" : "remove_class",
482 "msg_type" : "remove_class",
476 "class_list" : class_list,
483 "class_list" : class_list,
477 "selector" : selector,
484 "selector" : selector,
478 })
485 })
@@ -1,184 +1,183 b''
1 """Int class.
1 """Int class.
2
2
3 Represents an unbounded int using a widget.
3 Represents an unbounded int using a widget.
4 """
4 """
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
6 # Copyright (c) 2013, the IPython Development Team.
7 #
7 #
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9 #
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
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, Tuple
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 #-----------------------------------------------------------------------------
21 # Classes
21 # Classes
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 class _Int(DOMWidget):
23 class _Int(DOMWidget):
24 """Base class used to create widgets that represent an int."""
24 """Base class used to create widgets that represent an int."""
25 value = CInt(0, help="Int value", sync=True)
25 value = CInt(0, help="Int value", sync=True)
26 disabled = Bool(False, help="Enable or disable user changes", sync=True)
26 disabled = Bool(False, help="Enable or disable user changes", sync=True)
27 description = Unicode(help="Description of the value this widget represents", sync=True)
27 description = Unicode(help="Description of the value this widget represents", sync=True)
28
28
29
29
30 class _BoundedInt(_Int):
30 class _BoundedInt(_Int):
31 """Base class used to create widgets that represent a int that is bounded
31 """Base class used to create widgets that represent a int that is bounded
32 by a minium and maximum."""
32 by a minium and maximum."""
33 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
33 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
34 max = CInt(100, help="Max value", sync=True)
34 max = CInt(100, help="Max value", sync=True)
35 min = CInt(0, help="Min value", sync=True)
35 min = CInt(0, help="Min value", sync=True)
36
36
37 def __init__(self, *pargs, **kwargs):
37 def __init__(self, *pargs, **kwargs):
38 """Constructor"""
38 """Constructor"""
39 DOMWidget.__init__(self, *pargs, **kwargs)
39 DOMWidget.__init__(self, *pargs, **kwargs)
40 self.on_trait_change(self._validate_value, ['value'])
40 self.on_trait_change(self._validate_value, ['value'])
41 self.on_trait_change(self._handle_max_changed, ['max'])
41 self.on_trait_change(self._handle_max_changed, ['max'])
42 self.on_trait_change(self._handle_min_changed, ['min'])
42 self.on_trait_change(self._handle_min_changed, ['min'])
43
43
44 def _validate_value(self, name, old, new):
44 def _validate_value(self, name, old, new):
45 """Validate value."""
45 """Validate value."""
46 if self.min > new or new > self.max:
46 if self.min > new or new > self.max:
47 self.value = min(max(new, self.min), self.max)
47 self.value = min(max(new, self.min), self.max)
48
48
49 def _handle_max_changed(self, name, old, new):
49 def _handle_max_changed(self, name, old, new):
50 """Make sure the min is always <= the max."""
50 """Make sure the min is always <= the max."""
51 self.min = min(self.min, new)
51 if new < self.min:
52 raise ValueError("setting max < min")
52
53
53 def _handle_min_changed(self, name, old, new):
54 def _handle_min_changed(self, name, old, new):
54 """Make sure the max is always >= the min."""
55 """Make sure the max is always >= the min."""
55 self.max = max(self.max, new)
56 if new > self.max:
56
57 raise ValueError("setting min > max")
57
58
58 class IntText(_Int):
59 class IntText(_Int):
59 """Textbox widget that represents a int."""
60 """Textbox widget that represents a int."""
60 _view_name = Unicode('IntTextView', sync=True)
61 _view_name = Unicode('IntTextView', sync=True)
61
62
62
63
63 class BoundedIntText(_BoundedInt):
64 class BoundedIntText(_BoundedInt):
64 """Textbox widget that represents a int bounded by a minimum and maximum value."""
65 """Textbox widget that represents a int bounded by a minimum and maximum value."""
65 _view_name = Unicode('IntTextView', sync=True)
66 _view_name = Unicode('IntTextView', sync=True)
66
67
67
68
68 class IntSlider(_BoundedInt):
69 class IntSlider(_BoundedInt):
69 """Slider widget that represents a int bounded by a minimum and maximum value."""
70 """Slider widget that represents a int bounded by a minimum and maximum value."""
70 _view_name = Unicode('IntSliderView', sync=True)
71 _view_name = Unicode('IntSliderView', sync=True)
71 orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
72 orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
72 help="Vertical or horizontal.", sync=True)
73 help="Vertical or horizontal.", sync=True)
73 _range = Bool(False, help="Display a range selector", sync=True)
74 _range = Bool(False, help="Display a range selector", sync=True)
74 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
75 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
75
76
76
77
77 class IntProgress(_BoundedInt):
78 class IntProgress(_BoundedInt):
78 """Progress bar that represents a int bounded by a minimum and maximum value."""
79 """Progress bar that represents a int bounded by a minimum and maximum value."""
79 _view_name = Unicode('ProgressView', sync=True)
80 _view_name = Unicode('ProgressView', sync=True)
80
81
81 class _IntRange(_Int):
82 class _IntRange(_Int):
82 value = Tuple(CInt, CInt, default_value=(0, 1), help="Tuple of (lower, upper) bounds", sync=True)
83 value = Tuple(CInt, CInt, default_value=(0, 1), help="Tuple of (lower, upper) bounds", sync=True)
83 lower = CInt(0, help="Lower bound", sync=False)
84 lower = CInt(0, help="Lower bound", sync=False)
84 upper = CInt(1, help="Upper bound", sync=False)
85 upper = CInt(1, help="Upper bound", sync=False)
85
86
86 def __init__(self, *pargs, **kwargs):
87 def __init__(self, *pargs, **kwargs):
87 value_given = 'value' in kwargs
88 value_given = 'value' in kwargs
88 lower_given = 'lower' in kwargs
89 lower_given = 'lower' in kwargs
89 upper_given = 'upper' in kwargs
90 upper_given = 'upper' in kwargs
90 if value_given and (lower_given or upper_given):
91 if value_given and (lower_given or upper_given):
91 raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
92 raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
92 if lower_given != upper_given:
93 if lower_given != upper_given:
93 raise ValueError("Must specify both 'lower' and 'upper' for range widget")
94 raise ValueError("Must specify both 'lower' and 'upper' for range widget")
94
95
95 DOMWidget.__init__(self, *pargs, **kwargs)
96 DOMWidget.__init__(self, *pargs, **kwargs)
96
97
97 # ensure the traits match, preferring whichever (if any) was given in kwargs
98 # ensure the traits match, preferring whichever (if any) was given in kwargs
98 if value_given:
99 if value_given:
99 self.lower, self.upper = self.value
100 self.lower, self.upper = self.value
100 else:
101 else:
101 self.value = (self.lower, self.upper)
102 self.value = (self.lower, self.upper)
102
103
103 self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
104 self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
104
105
105 def _validate(self, name, old, new):
106 def _validate(self, name, old, new):
106 if name == 'value':
107 if name == 'value':
107 self.lower, self.upper = min(new), max(new)
108 self.lower, self.upper = min(new), max(new)
108 elif name == 'lower':
109 elif name == 'lower':
109 self.value = (new, self.value[1])
110 self.value = (new, self.value[1])
110 elif name == 'upper':
111 elif name == 'upper':
111 self.value = (self.value[0], new)
112 self.value = (self.value[0], new)
112
113
113 class _BoundedIntRange(_IntRange):
114 class _BoundedIntRange(_IntRange):
114 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
115 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
115 max = CInt(100, help="Max value", sync=True)
116 max = CInt(100, help="Max value", sync=True)
116 min = CInt(0, help="Min value", sync=True)
117 min = CInt(0, help="Min value", sync=True)
117
118
118 def __init__(self, *pargs, **kwargs):
119 def __init__(self, *pargs, **kwargs):
119 any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
120 any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
120 _IntRange.__init__(self, *pargs, **kwargs)
121 _IntRange.__init__(self, *pargs, **kwargs)
121
122
122 # ensure a minimal amount of sanity
123 # ensure a minimal amount of sanity
123 if self.min > self.max:
124 if self.min > self.max:
124 raise ValueError("min must be <= max")
125 raise ValueError("min must be <= max")
125
126
126 if any_value_given:
127 if any_value_given:
127 # if a value was given, clamp it within (min, max)
128 # if a value was given, clamp it within (min, max)
128 self._validate("value", None, self.value)
129 self._validate("value", None, self.value)
129 else:
130 else:
130 # otherwise, set it to 25-75% to avoid the handles overlapping
131 # otherwise, set it to 25-75% to avoid the handles overlapping
131 self.value = (0.75*self.min + 0.25*self.max,
132 self.value = (0.75*self.min + 0.25*self.max,
132 0.25*self.min + 0.75*self.max)
133 0.25*self.min + 0.75*self.max)
133 # callback already set for 'value', 'lower', 'upper'
134 # callback already set for 'value', 'lower', 'upper'
134 self.on_trait_change(self._validate, ['min', 'max'])
135 self.on_trait_change(self._validate, ['min', 'max'])
135
136
136 def _validate(self, name, old, new):
137 def _validate(self, name, old, new):
137 if name == "min":
138 if name == "min":
138 if new > self.max:
139 if new > self.max:
139 raise ValueError("setting min > max")
140 raise ValueError("setting min > max")
140 self.min = new
141 elif name == "max":
141 elif name == "max":
142 if new < self.min:
142 if new < self.min:
143 raise ValueError("setting max < min")
143 raise ValueError("setting max < min")
144 self.max = new
145
144
146 low, high = self.value
145 low, high = self.value
147 if name == "value":
146 if name == "value":
148 low, high = min(new), max(new)
147 low, high = min(new), max(new)
149 elif name == "upper":
148 elif name == "upper":
150 if new < self.lower:
149 if new < self.lower:
151 raise ValueError("setting upper < lower")
150 raise ValueError("setting upper < lower")
152 high = new
151 high = new
153 elif name == "lower":
152 elif name == "lower":
154 if new > self.upper:
153 if new > self.upper:
155 raise ValueError("setting lower > upper")
154 raise ValueError("setting lower > upper")
156 low = new
155 low = new
157
156
158 low = max(self.min, min(low, self.max))
157 low = max(self.min, min(low, self.max))
159 high = min(self.max, max(high, self.min))
158 high = min(self.max, max(high, self.min))
160
159
161 # determine the order in which we should update the
160 # determine the order in which we should update the
162 # lower, upper traits to avoid a temporary inverted overlap
161 # lower, upper traits to avoid a temporary inverted overlap
163 lower_first = high < self.lower
162 lower_first = high < self.lower
164
163
165 self.value = (low, high)
164 self.value = (low, high)
166 if lower_first:
165 if lower_first:
167 self.lower = low
166 self.lower = low
168 self.upper = high
167 self.upper = high
169 else:
168 else:
170 self.upper = high
169 self.upper = high
171 self.lower = low
170 self.lower = low
172
171
173 class IntRangeSlider(_BoundedIntRange):
172 class IntRangeSlider(_BoundedIntRange):
174 _view_name = Unicode('IntSliderView', sync=True)
173 _view_name = Unicode('IntSliderView', sync=True)
175 orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
174 orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
176 help="Vertical or horizontal.", sync=True)
175 help="Vertical or horizontal.", sync=True)
177 _range = Bool(True, help="Display a range selector", sync=True)
176 _range = Bool(True, help="Display a range selector", sync=True)
178 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
177 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
179
178
180 # Remove in IPython 4.0
179 # Remove in IPython 4.0
181 IntTextWidget = DeprecatedClass(IntText, 'IntTextWidget')
180 IntTextWidget = DeprecatedClass(IntText, 'IntTextWidget')
182 BoundedIntTextWidget = DeprecatedClass(BoundedIntText, 'BoundedIntTextWidget')
181 BoundedIntTextWidget = DeprecatedClass(BoundedIntText, 'BoundedIntTextWidget')
183 IntSliderWidget = DeprecatedClass(IntSlider, 'IntSliderWidget')
182 IntSliderWidget = DeprecatedClass(IntSlider, 'IntSliderWidget')
184 IntProgressWidget = DeprecatedClass(IntProgress, 'IntProgressWidget')
183 IntProgressWidget = DeprecatedClass(IntProgress, 'IntProgressWidget')
General Comments 0
You need to be logged in to leave comments. Login now