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