Show More
@@ -3,6 +3,7 b'' | |||||
3 |
|
3 | |||
4 | define([ |
|
4 | define([ | |
5 | "widgets/js/manager", |
|
5 | "widgets/js/manager", | |
|
6 | "widgets/js/widget", | |||
6 | "widgets/js/widget_link", |
|
7 | "widgets/js/widget_link", | |
7 | "widgets/js/widget_bool", |
|
8 | "widgets/js/widget_bool", | |
8 | "widgets/js/widget_button", |
|
9 | "widgets/js/widget_button", | |
@@ -14,21 +15,20 b' define([' | |||||
14 | "widgets/js/widget_selection", |
|
15 | "widgets/js/widget_selection", | |
15 | "widgets/js/widget_selectioncontainer", |
|
16 | "widgets/js/widget_selectioncontainer", | |
16 | "widgets/js/widget_string", |
|
17 | "widgets/js/widget_string", | |
17 |
], function(widgetmanager, |
|
18 | ], function(widgetmanager, widget) { | |
18 | for (var target_name in linkModels) { |
|
19 | // Register all of the loaded models and views with the widget manager. | |
19 | if (linkModels.hasOwnProperty(target_name)) { |
|
|||
20 | widgetmanager.WidgetManager.register_widget_model(target_name, linkModels[target_name]); |
|
|||
21 | } |
|
|||
22 | } |
|
|||
23 |
|
||||
24 | // Register all of the loaded views with the widget manager. |
|
|||
25 | for (var i = 2; i < arguments.length; i++) { |
|
20 | for (var i = 2; i < arguments.length; i++) { | |
26 |
|
|
21 | var module = arguments[i]; | |
27 | if (arguments[i].hasOwnProperty(target_name)) { |
|
22 | for (var target_name in module) { | |
28 | widgetmanager.WidgetManager.register_widget_view(target_name, arguments[i][target_name]); |
|
23 | if (module.hasOwnProperty(target_name)) { | |
|
24 | var target = module[target_name]; | |||
|
25 | if (target.prototype instanceof widget.WidgetModel) { | |||
|
26 | widgetmanager.WidgetManager.register_widget_model(target_name, target); | |||
|
27 | } else if (target.prototype instanceof widget.WidgetView) { | |||
|
28 | widgetmanager.WidgetManager.register_widget_view(target_name, target); | |||
|
29 | } | |||
29 | } |
|
30 | } | |
30 | } |
|
31 | } | |
31 | } |
|
32 | } | |
32 |
|
||||
33 | return {'WidgetManager': widgetmanager.WidgetManager}; |
|
33 | return {'WidgetManager': widgetmanager.WidgetManager}; | |
34 | }); |
|
34 | }); |
@@ -62,13 +62,13 b' define(["widgets/js/manager",' | |||||
62 | return Backbone.Model.apply(this); |
|
62 | return Backbone.Model.apply(this); | |
63 | }, |
|
63 | }, | |
64 |
|
64 | |||
65 | send: function (content, callbacks) { |
|
65 | send: function (content, callbacks, buffers) { | |
66 | /** |
|
66 | /** | |
67 | * Send a custom msg over the comm. |
|
67 | * Send a custom msg over the comm. | |
68 | */ |
|
68 | */ | |
69 | if (this.comm !== undefined) { |
|
69 | if (this.comm !== undefined) { | |
70 | var data = {method: 'custom', content: content}; |
|
70 | var data = {method: 'custom', content: content}; | |
71 | this.comm.send(data, callbacks); |
|
71 | this.comm.send(data, callbacks, {}, buffers); | |
72 | this.pending_msgs++; |
|
72 | this.pending_msgs++; | |
73 | } |
|
73 | } | |
74 | }, |
|
74 | }, | |
@@ -136,12 +136,31 b' define(["widgets/js/manager",' | |||||
136 | * Handle incoming comm msg. |
|
136 | * Handle incoming comm msg. | |
137 | */ |
|
137 | */ | |
138 | var method = msg.content.data.method; |
|
138 | var method = msg.content.data.method; | |
|
139 | ||||
139 | var that = this; |
|
140 | var that = this; | |
140 | switch (method) { |
|
141 | switch (method) { | |
141 | case 'update': |
|
142 | case 'update': | |
142 | this.state_change = this.state_change |
|
143 | this.state_change = this.state_change | |
143 | .then(function() { |
|
144 | .then(function() { | |
144 |
r |
|
145 | var state = msg.content.data.state || {}; | |
|
146 | var buffer_keys = msg.content.data.buffers || []; | |||
|
147 | var buffers = msg.buffers || []; | |||
|
148 | for (var i=0; i<buffer_keys.length; i++) { | |||
|
149 | state[buffer_keys[i]] = buffers[i]; | |||
|
150 | } | |||
|
151 | ||||
|
152 | // deserialize fields that have custom deserializers | |||
|
153 | var serializers = that.constructor.serializers; | |||
|
154 | if (serializers) { | |||
|
155 | for (var k in state) { | |||
|
156 | if (serializers[k] && serializers[k].deserialize) { | |||
|
157 | state[k] = (serializers[k].deserialize)(state[k], that); | |||
|
158 | } | |||
|
159 | } | |||
|
160 | } | |||
|
161 | return utils.resolve_promises_dict(state); | |||
|
162 | }).then(function(state) { | |||
|
163 | return that.set_state(state); | |||
145 | }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true)) |
|
164 | }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true)) | |
146 | .then(function() { |
|
165 | .then(function() { | |
147 | var parent_id = msg.parent_header.msg_id; |
|
166 | var parent_id = msg.parent_header.msg_id; | |
@@ -152,7 +171,7 b' define(["widgets/js/manager",' | |||||
152 | }).catch(utils.reject("Couldn't resolve state request promise", true)); |
|
171 | }).catch(utils.reject("Couldn't resolve state request promise", true)); | |
153 | break; |
|
172 | break; | |
154 | case 'custom': |
|
173 | case 'custom': | |
155 | this.trigger('msg:custom', msg.content.data.content); |
|
174 | this.trigger('msg:custom', msg.content.data.content, msg.buffers); | |
156 | break; |
|
175 | break; | |
157 | case 'display': |
|
176 | case 'display': | |
158 | this.state_change = this.state_change.then(function() { |
|
177 | this.state_change = this.state_change.then(function() { | |
@@ -165,25 +184,21 b' define(["widgets/js/manager",' | |||||
165 | set_state: function (state) { |
|
184 | set_state: function (state) { | |
166 | var that = this; |
|
185 | var that = this; | |
167 | // Handle when a widget is updated via the python side. |
|
186 | // Handle when a widget is updated via the python side. | |
168 | return this._unpack_models(state).then(function(state) { |
|
187 | return new Promise(function(resolve, reject) { | |
169 | that.state_lock = state; |
|
188 | that.state_lock = state; | |
170 | try { |
|
189 | try { | |
171 | WidgetModel.__super__.set.call(that, state); |
|
190 | WidgetModel.__super__.set.call(that, state); | |
172 | } finally { |
|
191 | } finally { | |
173 | that.state_lock = null; |
|
192 | that.state_lock = null; | |
174 | } |
|
193 | } | |
|
194 | resolve(); | |||
175 | }).catch(utils.reject("Couldn't set model state", true)); |
|
195 | }).catch(utils.reject("Couldn't set model state", true)); | |
176 | }, |
|
196 | }, | |
177 |
|
197 | |||
178 | get_state: function() { |
|
198 | get_state: function() { | |
179 | // Get the serializable state of the model. |
|
199 | // Get the serializable state of the model. | |
180 | var state = this.toJSON(); |
|
200 | // Equivalent to Backbone.Model.toJSON() | |
181 | for (var key in state) { |
|
201 | return _.clone(this.attributes); | |
182 | if (state.hasOwnProperty(key)) { |
|
|||
183 | state[key] = this._pack_models(state[key]); |
|
|||
184 | } |
|
|||
185 | } |
|
|||
186 | return state; |
|
|||
187 | }, |
|
202 | }, | |
188 |
|
203 | |||
189 | _handle_status: function (msg, callbacks) { |
|
204 | _handle_status: function (msg, callbacks) { | |
@@ -243,6 +258,19 b' define(["widgets/js/manager",' | |||||
243 | * Handle sync to the back-end. Called when a model.save() is called. |
|
258 | * Handle sync to the back-end. Called when a model.save() is called. | |
244 | * |
|
259 | * | |
245 | * Make sure a comm exists. |
|
260 | * Make sure a comm exists. | |
|
261 | ||||
|
262 | * Parameters | |||
|
263 | * ---------- | |||
|
264 | * method : create, update, patch, delete, read | |||
|
265 | * create/update always send the full attribute set | |||
|
266 | * patch - only send attributes listed in options.attrs, and if we are queuing | |||
|
267 | * up messages, combine with previous messages that have not been sent yet | |||
|
268 | * model : the model we are syncing | |||
|
269 | * will normally be the same as `this` | |||
|
270 | * options : dict | |||
|
271 | * the `attrs` key, if it exists, gives an {attr: value} dict that should be synced, | |||
|
272 | * otherwise, sync all attributes | |||
|
273 | * | |||
246 | */ |
|
274 | */ | |
247 | var error = options.error || function() { |
|
275 | var error = options.error || function() { | |
248 | console.error('Backbone sync error:', arguments); |
|
276 | console.error('Backbone sync error:', arguments); | |
@@ -252,8 +280,11 b' define(["widgets/js/manager",' | |||||
252 | return false; |
|
280 | return false; | |
253 | } |
|
281 | } | |
254 |
|
282 | |||
255 | // Delete any key value pairs that the back-end already knows about. |
|
283 | var attrs = (method === 'patch') ? options.attrs : model.get_state(options); | |
256 | var attrs = (method === 'patch') ? options.attrs : model.toJSON(options); |
|
284 | ||
|
285 | // the state_lock lists attributes that are currently be changed right now from a kernel message | |||
|
286 | // we don't want to send these non-changes back to the kernel, so we delete them out of attrs | |||
|
287 | // (but we only delete them if the value hasn't changed from the value stored in the state_lock | |||
257 | if (this.state_lock !== null) { |
|
288 | if (this.state_lock !== null) { | |
258 | var keys = Object.keys(this.state_lock); |
|
289 | var keys = Object.keys(this.state_lock); | |
259 | for (var i=0; i<keys.length; i++) { |
|
290 | for (var i=0; i<keys.length; i++) { | |
@@ -264,8 +295,6 b' define(["widgets/js/manager",' | |||||
264 | } |
|
295 | } | |
265 | } |
|
296 | } | |
266 |
|
297 | |||
267 | // Only sync if there are attributes to send to the back-end. |
|
|||
268 | attrs = this._pack_models(attrs); |
|
|||
269 | if (_.size(attrs) > 0) { |
|
298 | if (_.size(attrs) > 0) { | |
270 |
|
299 | |||
271 | // If this message was sent via backbone itself, it will not |
|
300 | // If this message was sent via backbone itself, it will not | |
@@ -297,8 +326,7 b' define(["widgets/js/manager",' | |||||
297 | } else { |
|
326 | } else { | |
298 | // We haven't exceeded the throttle, send the message like |
|
327 | // We haven't exceeded the throttle, send the message like | |
299 | // normal. |
|
328 | // normal. | |
300 | var data = {method: 'backbone', sync_data: attrs}; |
|
329 | this.send_sync_message(attrs, callbacks); | |
301 | this.comm.send(data, callbacks); |
|
|||
302 | this.pending_msgs++; |
|
330 | this.pending_msgs++; | |
303 | } |
|
331 | } | |
304 | } |
|
332 | } | |
@@ -308,68 +336,49 b' define(["widgets/js/manager",' | |||||
308 | this._buffered_state_diff = {}; |
|
336 | this._buffered_state_diff = {}; | |
309 | }, |
|
337 | }, | |
310 |
|
338 | |||
311 | save_changes: function(callbacks) { |
|
|||
312 | /** |
|
|||
313 | * Push this model's state to the back-end |
|
|||
314 | * |
|
|||
315 | * This invokes a Backbone.Sync. |
|
|||
316 | */ |
|
|||
317 | this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks}); |
|
|||
318 | }, |
|
|||
319 |
|
339 | |||
320 |
|
|
340 | send_sync_message: function(attrs, callbacks) { | |
321 | /** |
|
341 | // prepare and send a comm message syncing attrs | |
322 | * Replace models with model ids recursively. |
|
|||
323 | */ |
|
|||
324 | var that = this; |
|
342 | var that = this; | |
325 | var packed; |
|
343 | // first, build a state dictionary with key=the attribute and the value | |
326 | if (value instanceof Backbone.Model) { |
|
344 | // being the value or the promise of the serialized value | |
327 | return "IPY_MODEL_" + value.id; |
|
345 | var serializers = this.constructor.serializers; | |
328 |
|
346 | if (serializers) { | ||
329 | } else if ($.isArray(value)) { |
|
347 | for (k in attrs) { | |
330 | packed = []; |
|
348 | if (serializers[k] && serializers[k].serialize) { | |
331 | _.each(value, function(sub_value, key) { |
|
349 | attrs[k] = (serializers[k].serialize)(attrs[k], this); | |
332 | packed.push(that._pack_models(sub_value)); |
|
350 | } | |
333 | }); |
|
|||
334 | return packed; |
|
|||
335 | } else if (value instanceof Date || value instanceof String) { |
|
|||
336 | return value; |
|
|||
337 | } else if (value instanceof Object) { |
|
|||
338 | packed = {}; |
|
|||
339 | _.each(value, function(sub_value, key) { |
|
|||
340 | packed[key] = that._pack_models(sub_value); |
|
|||
341 | }); |
|
|||
342 | return packed; |
|
|||
343 |
|
||||
344 | } else { |
|
|||
345 | return value; |
|
|||
346 | } |
|
351 | } | |
|
352 | } | |||
|
353 | utils.resolve_promises_dict(attrs).then(function(state) { | |||
|
354 | // get binary values, then send | |||
|
355 | var keys = Object.keys(state); | |||
|
356 | var buffers = []; | |||
|
357 | var buffer_keys = []; | |||
|
358 | for (var i=0; i<keys.length; i++) { | |||
|
359 | var key = keys[i]; | |||
|
360 | var value = state[key]; | |||
|
361 | if (value.buffer instanceof ArrayBuffer | |||
|
362 | || value instanceof ArrayBuffer) { | |||
|
363 | buffers.push(value); | |||
|
364 | buffer_keys.push(key); | |||
|
365 | delete state[key]; | |||
|
366 | } | |||
|
367 | } | |||
|
368 | that.comm.send({method: 'backbone', sync_data: state, buffer_keys: buffer_keys}, callbacks, {}, buffers); | |||
|
369 | }).catch(function(error) { | |||
|
370 | that.pending_msgs--; | |||
|
371 | return (utils.reject("Couldn't send widget sync message", true))(error); | |||
|
372 | }); | |||
347 | }, |
|
373 | }, | |
348 |
|
374 | |||
349 |
|
|
375 | save_changes: function(callbacks) { | |
350 | /** |
|
376 | /** | |
351 | * Replace model ids with models recursively. |
|
377 | * Push this model's state to the back-end | |
|
378 | * | |||
|
379 | * This invokes a Backbone.Sync. | |||
352 | */ |
|
380 | */ | |
353 | var that = this; |
|
381 | this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks}); | |
354 | var unpacked; |
|
|||
355 | if ($.isArray(value)) { |
|
|||
356 | unpacked = []; |
|
|||
357 | _.each(value, function(sub_value, key) { |
|
|||
358 | unpacked.push(that._unpack_models(sub_value)); |
|
|||
359 | }); |
|
|||
360 | return Promise.all(unpacked); |
|
|||
361 | } else if (value instanceof Object) { |
|
|||
362 | unpacked = {}; |
|
|||
363 | _.each(value, function(sub_value, key) { |
|
|||
364 | unpacked[key] = that._unpack_models(sub_value); |
|
|||
365 | }); |
|
|||
366 | return utils.resolve_promises_dict(unpacked); |
|
|||
367 | } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") { |
|
|||
368 | // get_model returns a promise already |
|
|||
369 | return this.widget_manager.get_model(value.slice(10, value.length)); |
|
|||
370 | } else { |
|
|||
371 | return Promise.resolve(value); |
|
|||
372 | } |
|
|||
373 | }, |
|
382 | }, | |
374 |
|
383 | |||
375 | on_some_change: function(keys, callback, context) { |
|
384 | on_some_change: function(keys, callback, context) { | |
@@ -387,6 +396,14 b' define(["widgets/js/manager",' | |||||
387 | }, this); |
|
396 | }, this); | |
388 |
|
397 | |||
389 | }, |
|
398 | }, | |
|
399 | ||||
|
400 | toJSON: function(options) { | |||
|
401 | /** | |||
|
402 | * Serialize the model. See the types.js deserialization function | |||
|
403 | * and the kernel-side serializer/deserializer | |||
|
404 | */ | |||
|
405 | return "IPY_MODEL_"+this.id; | |||
|
406 | } | |||
390 | }); |
|
407 | }); | |
391 | widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel); |
|
408 | widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel); | |
392 |
|
409 | |||
@@ -426,7 +443,7 b' define(["widgets/js/manager",' | |||||
426 | */ |
|
443 | */ | |
427 | var that = this; |
|
444 | var that = this; | |
428 | options = $.extend({ parent: this }, options || {}); |
|
445 | options = $.extend({ parent: this }, options || {}); | |
429 |
return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view" |
|
446 | return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view", true)); | |
430 | }, |
|
447 | }, | |
431 |
|
448 | |||
432 | callbacks: function(){ |
|
449 | callbacks: function(){ | |
@@ -444,11 +461,11 b' define(["widgets/js/manager",' | |||||
444 | */ |
|
461 | */ | |
445 | }, |
|
462 | }, | |
446 |
|
463 | |||
447 | send: function (content) { |
|
464 | send: function (content, buffers) { | |
448 | /** |
|
465 | /** | |
449 | * Send a custom msg associated with this view. |
|
466 | * Send a custom msg associated with this view. | |
450 | */ |
|
467 | */ | |
451 | this.model.send(content, this.callbacks()); |
|
468 | this.model.send(content, this.callbacks(), buffers); | |
452 | }, |
|
469 | }, | |
453 |
|
470 | |||
454 | touch: function () { |
|
471 | touch: function () { | |
@@ -558,7 +575,7 b' define(["widgets/js/manager",' | |||||
558 | /** |
|
575 | /** | |
559 | * Makes browser interpret a numerical string as a pixel value. |
|
576 | * Makes browser interpret a numerical string as a pixel value. | |
560 | */ |
|
577 | */ | |
561 | if (/^\d+\.?(\d+)?$/.test(value.trim())) { |
|
578 | if (value && /^\d+\.?(\d+)?$/.test(value.trim())) { | |
562 | return value.trim() + 'px'; |
|
579 | return value.trim() + 'px'; | |
563 | } |
|
580 | } | |
564 | return value; |
|
581 | return value; |
@@ -4,10 +4,41 b'' | |||||
4 | define([ |
|
4 | define([ | |
5 | "widgets/js/widget", |
|
5 | "widgets/js/widget", | |
6 | "jqueryui", |
|
6 | "jqueryui", | |
|
7 | "underscore", | |||
7 | "base/js/utils", |
|
8 | "base/js/utils", | |
8 | "bootstrap", |
|
9 | "bootstrap", | |
9 | ], function(widget, $, utils){ |
|
10 | ], function(widget, $, _, utils){ | |
10 | "use strict"; |
|
11 | "use strict"; | |
|
12 | var unpack_models = function unpack_models(value, model) { | |||
|
13 | /** | |||
|
14 | * Replace model ids with models recursively. | |||
|
15 | */ | |||
|
16 | var unpacked; | |||
|
17 | if ($.isArray(value)) { | |||
|
18 | unpacked = []; | |||
|
19 | _.each(value, function(sub_value, key) { | |||
|
20 | unpacked.push(unpack_models(sub_value, model)); | |||
|
21 | }); | |||
|
22 | return Promise.all(unpacked); | |||
|
23 | } else if (value instanceof Object) { | |||
|
24 | unpacked = {}; | |||
|
25 | _.each(value, function(sub_value, key) { | |||
|
26 | unpacked[key] = unpack_models(sub_value, model); | |||
|
27 | }); | |||
|
28 | return utils.resolve_promises_dict(unpacked); | |||
|
29 | } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") { | |||
|
30 | // get_model returns a promise already | |||
|
31 | return model.widget_manager.get_model(value.slice(10, value.length)); | |||
|
32 | } else { | |||
|
33 | return Promise.resolve(value); | |||
|
34 | } | |||
|
35 | }; | |||
|
36 | ||||
|
37 | var BoxModel = widget.WidgetModel.extend({}, { | |||
|
38 | serializers: _.extend({ | |||
|
39 | children: {deserialize: unpack_models} | |||
|
40 | }, widget.WidgetModel.serializers) | |||
|
41 | }); | |||
11 |
|
42 | |||
12 | var BoxView = widget.DOMWidgetView.extend({ |
|
43 | var BoxView = widget.DOMWidgetView.extend({ | |
13 | initialize: function(){ |
|
44 | initialize: function(){ | |
@@ -148,6 +179,8 b' define([' | |||||
148 | }); |
|
179 | }); | |
149 |
|
180 | |||
150 | return { |
|
181 | return { | |
|
182 | 'unpack_models': unpack_models, | |||
|
183 | 'BoxModel': BoxModel, | |||
151 | 'BoxView': BoxView, |
|
184 | 'BoxView': BoxView, | |
152 | 'FlexBoxView': FlexBoxView, |
|
185 | 'FlexBoxView': FlexBoxView, | |
153 | }; |
|
186 | }; |
@@ -337,6 +337,19 b' casper.execute_cell_then = function(index, then_callback, expect_failure) {' | |||||
337 | return return_val; |
|
337 | return return_val; | |
338 | }; |
|
338 | }; | |
339 |
|
339 | |||
|
340 | casper.append_cell_execute_then = function(text, then_callback, expect_failure) { | |||
|
341 | // Append a code cell and execute it, optionally calling a then_callback | |||
|
342 | var c = this.append_cell(text); | |||
|
343 | return this.execute_cell_then(c, then_callback, expect_failure); | |||
|
344 | }; | |||
|
345 | ||||
|
346 | casper.assert_output_equals = function(text, output_text, message) { | |||
|
347 | // Append a code cell with the text, then assert the output is equal to output_text | |||
|
348 | this.append_cell_execute_then(text, function(index) { | |||
|
349 | this.test.assertEquals(this.get_output_cell(index).text.trim(), output_text, message); | |||
|
350 | }); | |||
|
351 | }; | |||
|
352 | ||||
340 | casper.wait_for_element = function(index, selector){ |
|
353 | casper.wait_for_element = function(index, selector){ | |
341 | // Utility function that allows us to easily wait for an element |
|
354 | // Utility function that allows us to easily wait for an element | |
342 | // within a cell. Uses JQuery selector to look for the element. |
|
355 | // within a cell. Uses JQuery selector to look for the element. |
@@ -40,63 +40,12 b' casper.notebook_test(function () {' | |||||
40 | var index; |
|
40 | var index; | |
41 |
|
41 | |||
42 | index = this.append_cell( |
|
42 | index = this.append_cell( | |
43 |
'from IPython.html import widgets |
|
43 | ['from IPython.html import widgets', | |
44 |
'from IPython.display import display, clear_output |
|
44 | 'from IPython.display import display, clear_output', | |
45 | 'print("Success")'); |
|
45 | 'print("Success")'].join('\n')); | |
46 | this.execute_cell_then(index); |
|
46 | this.execute_cell_then(index); | |
47 |
|
47 | |||
48 | this.then(function () { |
|
48 | this.then(function () { | |
49 |
|
||||
50 | // Functions that can be used to test the packing and unpacking APIs |
|
|||
51 | var that = this; |
|
|||
52 | var test_pack = function (input) { |
|
|||
53 | var output = that.evaluate(function(input) { |
|
|||
54 | var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined); |
|
|||
55 | var results = model._pack_models(input); |
|
|||
56 | return results; |
|
|||
57 | }, {input: input}); |
|
|||
58 | that.test.assert(recursive_compare(input, output), |
|
|||
59 | JSON.stringify(input) + ' passed through Model._pack_model unchanged'); |
|
|||
60 | }; |
|
|||
61 | var test_unpack = function (input) { |
|
|||
62 | that.thenEvaluate(function(input) { |
|
|||
63 | window.results = undefined; |
|
|||
64 | var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined); |
|
|||
65 | model._unpack_models(input).then(function(results) { |
|
|||
66 | window.results = results; |
|
|||
67 | }); |
|
|||
68 | }, {input: input}); |
|
|||
69 |
|
||||
70 | that.waitFor(function check() { |
|
|||
71 | return that.evaluate(function() { |
|
|||
72 | return window.results; |
|
|||
73 | }); |
|
|||
74 | }); |
|
|||
75 |
|
||||
76 | that.then(function() { |
|
|||
77 | var results = that.evaluate(function() { |
|
|||
78 | return window.results; |
|
|||
79 | }); |
|
|||
80 | that.test.assert(recursive_compare(input, results), |
|
|||
81 | JSON.stringify(input) + ' passed through Model._unpack_model unchanged'); |
|
|||
82 | }); |
|
|||
83 | }; |
|
|||
84 | var test_packing = function(input) { |
|
|||
85 | test_pack(input); |
|
|||
86 | test_unpack(input); |
|
|||
87 | }; |
|
|||
88 |
|
||||
89 | test_packing({0: 'hi', 1: 'bye'}); |
|
|||
90 | test_packing(['hi', 'bye']); |
|
|||
91 | test_packing(['hi', 5]); |
|
|||
92 | test_packing(['hi', '5']); |
|
|||
93 | test_packing([1.0, 0]); |
|
|||
94 | test_packing([1.0, false]); |
|
|||
95 | test_packing([1, false]); |
|
|||
96 | test_packing([1, false, {a: 'hi'}]); |
|
|||
97 | test_packing([1, false, ['hi']]); |
|
|||
98 | test_packing([String('hi'), Date("Thu Nov 13 2014 13:46:21 GMT-0500")]) |
|
|||
99 |
|
||||
100 | // Test multi-set, single touch code. First create a custom widget. |
|
49 | // Test multi-set, single touch code. First create a custom widget. | |
101 | this.thenEvaluate(function() { |
|
50 | this.thenEvaluate(function() { | |
102 | var MultiSetView = IPython.DOMWidgetView.extend({ |
|
51 | var MultiSetView = IPython.DOMWidgetView.extend({ | |
@@ -113,20 +62,20 b' casper.notebook_test(function () {' | |||||
113 |
|
62 | |||
114 | // Try creating the multiset widget, verify that sets the values correctly. |
|
63 | // Try creating the multiset widget, verify that sets the values correctly. | |
115 | var multiset = {}; |
|
64 | var multiset = {}; | |
116 | multiset.index = this.append_cell( |
|
65 | multiset.index = this.append_cell([ | |
117 |
'from IPython.utils.traitlets import Unicode, CInt |
|
66 | 'from IPython.utils.traitlets import Unicode, CInt', | |
118 |
'class MultiSetWidget(widgets.Widget): |
|
67 | 'class MultiSetWidget(widgets.Widget):', | |
119 |
' _view_name = Unicode("MultiSetView", sync=True) |
|
68 | ' _view_name = Unicode("MultiSetView", sync=True)', | |
120 |
' a = CInt(0, sync=True) |
|
69 | ' a = CInt(0, sync=True)', | |
121 |
' b = CInt(0, sync=True) |
|
70 | ' b = CInt(0, sync=True)', | |
122 |
' c = CInt(0, sync=True) |
|
71 | ' c = CInt(0, sync=True)', | |
123 |
' d = CInt(-1, sync=True) |
|
72 | ' d = CInt(-1, sync=True)', // See if it sends a full state. | |
124 |
' def set_state(self, sync_data): |
|
73 | ' def set_state(self, sync_data):', | |
125 |
' widgets.Widget.set_state(self, sync_data) |
|
74 | ' widgets.Widget.set_state(self, sync_data)', | |
126 |
' self.d = len(sync_data) |
|
75 | ' self.d = len(sync_data)', | |
127 |
'multiset = MultiSetWidget() |
|
76 | 'multiset = MultiSetWidget()', | |
128 |
'display(multiset) |
|
77 | 'display(multiset)', | |
129 | 'print(multiset.model_id)'); |
|
78 | 'print(multiset.model_id)'].join('\n')); | |
130 | this.execute_cell_then(multiset.index, function(index) { |
|
79 | this.execute_cell_then(multiset.index, function(index) { | |
131 | multiset.model_id = this.get_output_cell(index).text.trim(); |
|
80 | multiset.model_id = this.get_output_cell(index).text.trim(); | |
132 | }); |
|
81 | }); | |
@@ -148,16 +97,16 b' casper.notebook_test(function () {' | |||||
148 | }); |
|
97 | }); | |
149 |
|
98 | |||
150 | var textbox = {}; |
|
99 | var textbox = {}; | |
151 | throttle_index = this.append_cell( |
|
100 | throttle_index = this.append_cell([ | |
152 |
'import time |
|
101 | 'import time', | |
153 |
'textbox = widgets.Text() |
|
102 | 'textbox = widgets.Text()', | |
154 |
'display(textbox) |
|
103 | 'display(textbox)', | |
155 |
'textbox._dom_classes = ["my-throttle-textbox"] |
|
104 | 'textbox._dom_classes = ["my-throttle-textbox"]', | |
156 |
'def handle_change(name, old, new): |
|
105 | 'def handle_change(name, old, new):', | |
157 |
' display(len(new)) |
|
106 | ' display(len(new))', | |
158 |
' time.sleep(0.5) |
|
107 | ' time.sleep(0.5)', | |
159 |
'textbox.on_trait_change(handle_change, "value") |
|
108 | 'textbox.on_trait_change(handle_change, "value")', | |
160 | 'print(textbox.model_id)'); |
|
109 | 'print(textbox.model_id)'].join('\n')); | |
161 | this.execute_cell_then(throttle_index, function(index){ |
|
110 | this.execute_cell_then(throttle_index, function(index){ | |
162 | textbox.model_id = this.get_output_cell(index).text.trim(); |
|
111 | textbox.model_id = this.get_output_cell(index).text.trim(); | |
163 |
|
112 | |||
@@ -169,7 +118,7 b' casper.notebook_test(function () {' | |||||
169 | '.my-throttle-textbox'), 'Textbox exists.'); |
|
118 | '.my-throttle-textbox'), 'Textbox exists.'); | |
170 |
|
119 | |||
171 | // Send 20 characters |
|
120 | // Send 20 characters | |
172 |
this.sendKeys('.my-throttle-textbox input', ' |
|
121 | this.sendKeys('.my-throttle-textbox input', '12345678901234567890'); | |
173 | }); |
|
122 | }); | |
174 |
|
123 | |||
175 | this.wait_for_widget(textbox); |
|
124 | this.wait_for_widget(textbox); | |
@@ -188,4 +137,173 b' casper.notebook_test(function () {' | |||||
188 | var last_state = outputs[outputs.length-1].data['text/plain']; |
|
137 | var last_state = outputs[outputs.length-1].data['text/plain']; | |
189 | this.test.assertEquals(last_state, "20", "Last state sent when throttling."); |
|
138 | this.test.assertEquals(last_state, "20", "Last state sent when throttling."); | |
190 | }); |
|
139 | }); | |
|
140 | ||||
|
141 | ||||
|
142 | this.thenEvaluate(function() { | |||
|
143 | define('TestWidget', ['widgets/js/widget', 'base/js/utils', 'underscore'], function(widget, utils, _) { | |||
|
144 | var floatArray = { | |||
|
145 | deserialize: function (value, model) { | |||
|
146 | if (value===null) {return null;} | |||
|
147 | // DataView -> float64 typed array | |||
|
148 | return new Float64Array(value.buffer); | |||
|
149 | }, | |||
|
150 | // serialization automatically handled since the | |||
|
151 | // attribute is an ArrayBuffer view | |||
|
152 | }; | |||
|
153 | ||||
|
154 | var floatList = { | |||
|
155 | deserialize: function (value, model) { | |||
|
156 | // list of floats -> list of strings | |||
|
157 | return value.map(function(x) {return x.toString()}); | |||
|
158 | }, | |||
|
159 | serialize: function(value, model) { | |||
|
160 | // list of strings -> list of floats | |||
|
161 | return value.map(function(x) {return parseFloat(x);}) | |||
|
162 | } | |||
|
163 | }; | |||
|
164 | ||||
|
165 | var TestWidgetModel = widget.WidgetModel.extend({}, { | |||
|
166 | serializers: _.extend({ | |||
|
167 | array_list: floatList, | |||
|
168 | array_binary: floatArray | |||
|
169 | }, widget.WidgetModel.serializers) | |||
|
170 | }); | |||
|
171 | ||||
|
172 | var TestWidgetView = widget.DOMWidgetView.extend({ | |||
|
173 | render: function () { | |||
|
174 | this.listenTo(this.model, 'msg:custom', this.handle_msg); | |||
|
175 | }, | |||
|
176 | handle_msg: function(content, buffers) { | |||
|
177 | this.msg = [content, buffers]; | |||
|
178 | } | |||
|
179 | }); | |||
|
180 | ||||
|
181 | return {TestWidgetModel: TestWidgetModel, TestWidgetView: TestWidgetView}; | |||
|
182 | }); | |||
|
183 | }); | |||
|
184 | ||||
|
185 | var testwidget = {}; | |||
|
186 | this.append_cell_execute_then([ | |||
|
187 | 'from IPython.html import widgets', | |||
|
188 | 'from IPython.utils.traitlets import Unicode, Instance, List', | |||
|
189 | 'from IPython.display import display', | |||
|
190 | 'from array import array', | |||
|
191 | 'def _array_to_memoryview(x):', | |||
|
192 | ' if x is None: return None', | |||
|
193 | ' try:', | |||
|
194 | ' y = memoryview(x)', | |||
|
195 | ' except TypeError:', | |||
|
196 | ' # in python 2, arrays do not support the new buffer protocol', | |||
|
197 | ' y = memoryview(buffer(x))', | |||
|
198 | ' return y', | |||
|
199 | 'def _memoryview_to_array(x):', | |||
|
200 | ' if x is None: return None', | |||
|
201 | ' return array("d", x.tobytes())', | |||
|
202 | 'arrays_binary = {', | |||
|
203 | ' "from_json": _memoryview_to_array,', | |||
|
204 | ' "to_json": _array_to_memoryview', | |||
|
205 | '}', | |||
|
206 | '', | |||
|
207 | 'def _array_to_list(x):', | |||
|
208 | ' return list(x)', | |||
|
209 | 'def _list_to_array(x):', | |||
|
210 | ' return array("d",x)', | |||
|
211 | 'arrays_list = {', | |||
|
212 | ' "from_json": _list_to_array,', | |||
|
213 | ' "to_json": _array_to_list', | |||
|
214 | '}', | |||
|
215 | '', | |||
|
216 | 'class TestWidget(widgets.DOMWidget):', | |||
|
217 | ' _model_module = Unicode("TestWidget", sync=True)', | |||
|
218 | ' _model_name = Unicode("TestWidgetModel", sync=True)', | |||
|
219 | ' _view_module = Unicode("TestWidget", sync=True)', | |||
|
220 | ' _view_name = Unicode("TestWidgetView", sync=True)', | |||
|
221 | ' array_binary = Instance(array, allow_none=True, sync=True, **arrays_binary)', | |||
|
222 | ' array_list = Instance(array, args=("d", [3.0]), allow_none=False, sync=True, **arrays_list)', | |||
|
223 | ' msg = {}', | |||
|
224 | ' def __init__(self, **kwargs):', | |||
|
225 | ' super(widgets.DOMWidget, self).__init__(**kwargs)', | |||
|
226 | ' self.on_msg(self._msg)', | |||
|
227 | ' def _msg(self, _, content, buffers):', | |||
|
228 | ' self.msg = [content, buffers]', | |||
|
229 | 'x=TestWidget()', | |||
|
230 | 'display(x)', | |||
|
231 | 'print(x.model_id)'].join('\n'), function(index){ | |||
|
232 | testwidget.index = index; | |||
|
233 | testwidget.model_id = this.get_output_cell(index).text.trim(); | |||
|
234 | }); | |||
|
235 | this.wait_for_widget(testwidget); | |||
|
236 | ||||
|
237 | ||||
|
238 | this.append_cell_execute_then('x.array_list = array("d", [1.5, 2.0, 3.1])'); | |||
|
239 | this.wait_for_widget(testwidget); | |||
|
240 | this.then(function() { | |||
|
241 | var result = this.evaluate(function(index) { | |||
|
242 | var v = IPython.notebook.get_cell(index).widget_views[0]; | |||
|
243 | var result = v.model.get('array_list'); | |||
|
244 | var z = result.slice(); | |||
|
245 | z[0]+="1234"; | |||
|
246 | z[1]+="5678"; | |||
|
247 | v.model.set('array_list', z); | |||
|
248 | v.touch(); | |||
|
249 | return result; | |||
|
250 | }, testwidget.index); | |||
|
251 | this.test.assertEquals(result, ["1.5", "2", "3.1"], "JSON custom serializer kernel -> js"); | |||
|
252 | }); | |||
|
253 | ||||
|
254 | this.assert_output_equals('print(x.array_list.tolist() == [1.51234, 25678.0, 3.1])', | |||
|
255 | 'True', 'JSON custom serializer js -> kernel'); | |||
|
256 | ||||
|
257 | if (this.slimerjs) { | |||
|
258 | this.append_cell_execute_then("x.array_binary=array('d', [1.5,2.5,5])", function() { | |||
|
259 | this.evaluate(function(index) { | |||
|
260 | var v = IPython.notebook.get_cell(index).widget_views[0]; | |||
|
261 | var z = v.model.get('array_binary'); | |||
|
262 | z[0]*=3; | |||
|
263 | z[1]*=3; | |||
|
264 | z[2]*=3; | |||
|
265 | // we set to null so that we recognize the change | |||
|
266 | // when we set data back to z | |||
|
267 | v.model.set('array_binary', null); | |||
|
268 | v.model.set('array_binary', z); | |||
|
269 | v.touch(); | |||
|
270 | }, textwidget.index); | |||
|
271 | }); | |||
|
272 | this.wait_for_widget(testwidget); | |||
|
273 | this.assert_output_equals('x.array_binary.tolist() == [4.5, 7.5, 15.0]', | |||
|
274 | 'True\n', 'Binary custom serializer js -> kernel') | |||
|
275 | ||||
|
276 | this.append_cell_execute_then('x.send("some content", [memoryview(b"binarycontent"), memoryview("morecontent")])'); | |||
|
277 | this.wait_for_widget(testwidget); | |||
|
278 | ||||
|
279 | this.then(function() { | |||
|
280 | var result = this.evaluate(function(index) { | |||
|
281 | var v = IPython.notebook.get_cell(index).widget_views[0]; | |||
|
282 | var d = new TextDecoder('utf-8'); | |||
|
283 | return {text: v.msg[0], | |||
|
284 | binary0: d.decode(v.msg[1][0]), | |||
|
285 | binary1: d.decode(v.msg[1][1])}; | |||
|
286 | }, testwidget.index); | |||
|
287 | this.test.assertEquals(result, {text: 'some content', | |||
|
288 | binary0: 'binarycontent', | |||
|
289 | binary1: 'morecontent'}, | |||
|
290 | "Binary widget messages kernel -> js"); | |||
|
291 | }); | |||
|
292 | ||||
|
293 | this.then(function() { | |||
|
294 | this.evaluate(function(index) { | |||
|
295 | var v = IPython.notebook.get_cell(index).widget_views[0]; | |||
|
296 | v.send('content back', [new Uint8Array([1,2,3,4]), new Float64Array([2.1828, 3.14159])]) | |||
|
297 | }, testwidget.index); | |||
|
298 | }); | |||
|
299 | this.wait_for_widget(testwidget); | |||
|
300 | this.assert_output_equals([ | |||
|
301 | 'all([x.msg[0] == "content back",', | |||
|
302 | ' x.msg[1][0].tolist() == [1,2,3,4],', | |||
|
303 | ' array("d", x.msg[1][1].tobytes()).tolist() == [2.1828, 3.14159]])'].join('\n'), | |||
|
304 | 'True', 'Binary buffers message js -> kernel'); | |||
|
305 | } else { | |||
|
306 | console.log("skipping binary websocket tests on phantomjs"); | |||
|
307 | } | |||
|
308 | ||||
191 | }); |
|
309 | }); |
@@ -216,10 +216,11 b' class Widget(LoggingConfigurable):' | |||||
216 | key : unicode, or iterable (optional) |
|
216 | key : unicode, or iterable (optional) | |
217 | A single property's name or iterable of property names to sync with the front-end. |
|
217 | A single property's name or iterable of property names to sync with the front-end. | |
218 | """ |
|
218 | """ | |
219 | self._send({ |
|
219 | state, buffer_keys, buffers = self.get_state(key=key) | |
220 |
|
|
220 | msg = {"method": "update", "state": state} | |
221 | "state" : self.get_state(key=key) |
|
221 | if buffer_keys: | |
222 | }) |
|
222 | msg['buffers'] = buffer_keys | |
|
223 | self._send(msg, buffers=buffers) | |||
223 |
|
224 | |||
224 | def get_state(self, key=None): |
|
225 | def get_state(self, key=None): | |
225 | """Gets the widget state, or a piece of it. |
|
226 | """Gets the widget state, or a piece of it. | |
@@ -228,6 +229,16 b' class Widget(LoggingConfigurable):' | |||||
228 | ---------- |
|
229 | ---------- | |
229 | key : unicode or iterable (optional) |
|
230 | key : unicode or iterable (optional) | |
230 | A single property's name or iterable of property names to get. |
|
231 | A single property's name or iterable of property names to get. | |
|
232 | ||||
|
233 | Returns | |||
|
234 | ------- | |||
|
235 | state : dict of states | |||
|
236 | buffer_keys : list of strings | |||
|
237 | the values that are stored in buffers | |||
|
238 | buffers : list of binary memoryviews | |||
|
239 | values to transmit in binary | |||
|
240 | metadata : dict | |||
|
241 | metadata for each field: {key: metadata} | |||
231 | """ |
|
242 | """ | |
232 | if key is None: |
|
243 | if key is None: | |
233 | keys = self.keys |
|
244 | keys = self.keys | |
@@ -238,11 +249,18 b' class Widget(LoggingConfigurable):' | |||||
238 | else: |
|
249 | else: | |
239 | raise ValueError("key must be a string, an iterable of keys, or None") |
|
250 | raise ValueError("key must be a string, an iterable of keys, or None") | |
240 | state = {} |
|
251 | state = {} | |
|
252 | buffers = [] | |||
|
253 | buffer_keys = [] | |||
241 | for k in keys: |
|
254 | for k in keys: | |
242 | f = self.trait_metadata(k, 'to_json', self._trait_to_json) |
|
255 | f = self.trait_metadata(k, 'to_json', self._trait_to_json) | |
243 | value = getattr(self, k) |
|
256 | value = getattr(self, k) | |
244 |
s |
|
257 | serialized = f(value) | |
245 | return state |
|
258 | if isinstance(serialized, memoryview): | |
|
259 | buffers.append(serialized) | |||
|
260 | buffer_keys.append(k) | |||
|
261 | else: | |||
|
262 | state[k] = serialized | |||
|
263 | return state, buffer_keys, buffers | |||
246 |
|
264 | |||
247 | def set_state(self, sync_data): |
|
265 | def set_state(self, sync_data): | |
248 | """Called when a state is received from the front-end.""" |
|
266 | """Called when a state is received from the front-end.""" | |
@@ -253,15 +271,17 b' class Widget(LoggingConfigurable):' | |||||
253 | with self._lock_property(name, json_value): |
|
271 | with self._lock_property(name, json_value): | |
254 | setattr(self, name, from_json(json_value)) |
|
272 | setattr(self, name, from_json(json_value)) | |
255 |
|
273 | |||
256 | def send(self, content): |
|
274 | def send(self, content, buffers=None): | |
257 | """Sends a custom msg to the widget model in the front-end. |
|
275 | """Sends a custom msg to the widget model in the front-end. | |
258 |
|
276 | |||
259 | Parameters |
|
277 | Parameters | |
260 | ---------- |
|
278 | ---------- | |
261 | content : dict |
|
279 | content : dict | |
262 | Content of the message to send. |
|
280 | Content of the message to send. | |
|
281 | buffers : list of binary buffers | |||
|
282 | Binary buffers to send with message | |||
263 | """ |
|
283 | """ | |
264 | self._send({"method": "custom", "content": content}) |
|
284 | self._send({"method": "custom", "content": content}, buffers=buffers) | |
265 |
|
285 | |||
266 | def on_msg(self, callback, remove=False): |
|
286 | def on_msg(self, callback, remove=False): | |
267 | """(Un)Register a custom msg receive callback. |
|
287 | """(Un)Register a custom msg receive callback. | |
@@ -269,9 +289,9 b' class Widget(LoggingConfigurable):' | |||||
269 | Parameters |
|
289 | Parameters | |
270 | ---------- |
|
290 | ---------- | |
271 | callback: callable |
|
291 | callback: callable | |
272 |
callback will be passed t |
|
292 | callback will be passed three arguments when a message arrives:: | |
273 |
|
293 | |||
274 | callback(widget, content) |
|
294 | callback(widget, content, buffers) | |
275 |
|
295 | |||
276 | remove: bool |
|
296 | remove: bool | |
277 | True if the callback should be unregistered.""" |
|
297 | True if the callback should be unregistered.""" | |
@@ -353,7 +373,10 b' class Widget(LoggingConfigurable):' | |||||
353 | # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one. |
|
373 | # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one. | |
354 | if method == 'backbone': |
|
374 | if method == 'backbone': | |
355 | if 'sync_data' in data: |
|
375 | if 'sync_data' in data: | |
|
376 | # get binary buffers too | |||
356 | sync_data = data['sync_data'] |
|
377 | sync_data = data['sync_data'] | |
|
378 | for i,k in enumerate(data.get('buffer_keys', [])): | |||
|
379 | sync_data[k] = msg['buffers'][i] | |||
357 | self.set_state(sync_data) # handles all methods |
|
380 | self.set_state(sync_data) # handles all methods | |
358 |
|
381 | |||
359 | # Handle a state request. |
|
382 | # Handle a state request. | |
@@ -363,15 +386,15 b' class Widget(LoggingConfigurable):' | |||||
363 | # Handle a custom msg from the front-end. |
|
386 | # Handle a custom msg from the front-end. | |
364 | elif method == 'custom': |
|
387 | elif method == 'custom': | |
365 | if 'content' in data: |
|
388 | if 'content' in data: | |
366 | self._handle_custom_msg(data['content']) |
|
389 | self._handle_custom_msg(data['content'], msg['buffers']) | |
367 |
|
390 | |||
368 | # Catch remainder. |
|
391 | # Catch remainder. | |
369 | else: |
|
392 | else: | |
370 | self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) |
|
393 | self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) | |
371 |
|
394 | |||
372 | def _handle_custom_msg(self, content): |
|
395 | def _handle_custom_msg(self, content, buffers): | |
373 | """Called when a custom msg is received.""" |
|
396 | """Called when a custom msg is received.""" | |
374 | self._msg_callbacks(self, content) |
|
397 | self._msg_callbacks(self, content, buffers) | |
375 |
|
398 | |||
376 | def _notify_trait(self, name, old_value, new_value): |
|
399 | def _notify_trait(self, name, old_value, new_value): | |
377 | """Called when a property has been changed.""" |
|
400 | """Called when a property has been changed.""" | |
@@ -393,34 +416,11 b' class Widget(LoggingConfigurable):' | |||||
393 | self._display_callbacks(self, **kwargs) |
|
416 | self._display_callbacks(self, **kwargs) | |
394 |
|
417 | |||
395 | def _trait_to_json(self, x): |
|
418 | def _trait_to_json(self, x): | |
396 | """Convert a trait value to json |
|
419 | """Convert a trait value to json.""" | |
397 |
|
420 | return x | ||
398 | Traverse lists/tuples and dicts and serialize their values as well. |
|
|||
399 | Replace any widgets with their model_id |
|
|||
400 | """ |
|
|||
401 | if isinstance(x, dict): |
|
|||
402 | return {k: self._trait_to_json(v) for k, v in x.items()} |
|
|||
403 | elif isinstance(x, (list, tuple)): |
|
|||
404 | return [self._trait_to_json(v) for v in x] |
|
|||
405 | elif isinstance(x, Widget): |
|
|||
406 | return "IPY_MODEL_" + x.model_id |
|
|||
407 | else: |
|
|||
408 | return x # Value must be JSON-able |
|
|||
409 |
|
421 | |||
410 | def _trait_from_json(self, x): |
|
422 | def _trait_from_json(self, x): | |
411 | """Convert json values to objects |
|
423 | """Convert json values to objects.""" | |
412 |
|
||||
413 | Replace any strings representing valid model id values to Widget references. |
|
|||
414 | """ |
|
|||
415 | if isinstance(x, dict): |
|
|||
416 | return {k: self._trait_from_json(v) for k, v in x.items()} |
|
|||
417 | elif isinstance(x, (list, tuple)): |
|
|||
418 | return [self._trait_from_json(v) for v in x] |
|
|||
419 | elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets: |
|
|||
420 | # we want to support having child widgets at any level in a hierarchy |
|
|||
421 | # trusting that a widget UUID will not appear out in the wild |
|
|||
422 | return Widget.widgets[x[10:]] |
|
|||
423 | else: |
|
|||
424 |
|
|
424 | return x | |
425 |
|
425 | |||
426 | def _ipython_display_(self, **kwargs): |
|
426 | def _ipython_display_(self, **kwargs): | |
@@ -430,9 +430,9 b' class Widget(LoggingConfigurable):' | |||||
430 | self._send({"method": "display"}) |
|
430 | self._send({"method": "display"}) | |
431 | self._handle_displayed(**kwargs) |
|
431 | self._handle_displayed(**kwargs) | |
432 |
|
432 | |||
433 | def _send(self, msg): |
|
433 | def _send(self, msg, buffers=None): | |
434 | """Sends a message to the model in the front-end.""" |
|
434 | """Sends a message to the model in the front-end.""" | |
435 | self.comm.send(msg) |
|
435 | self.comm.send(data=msg, buffers=buffers) | |
436 |
|
436 | |||
437 |
|
437 | |||
438 | class DOMWidget(Widget): |
|
438 | class DOMWidget(Widget): |
@@ -6,19 +6,46 b' Represents a container that can be used to group other widgets.' | |||||
6 | # Copyright (c) IPython Development Team. |
|
6 | # Copyright (c) IPython Development Team. | |
7 | # Distributed under the terms of the Modified BSD License. |
|
7 | # Distributed under the terms of the Modified BSD License. | |
8 |
|
8 | |||
9 | from .widget import DOMWidget, register |
|
9 | from .widget import DOMWidget, Widget, register | |
10 | from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum |
|
10 | from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum | |
11 | from IPython.utils.warn import DeprecatedClass |
|
11 | from IPython.utils.warn import DeprecatedClass | |
12 |
|
12 | |||
|
13 | def _widget_to_json(x): | |||
|
14 | if isinstance(x, dict): | |||
|
15 | return {k: _widget_to_json(v) for k, v in x.items()} | |||
|
16 | elif isinstance(x, (list, tuple)): | |||
|
17 | return [_widget_to_json(v) for v in x] | |||
|
18 | elif isinstance(x, Widget): | |||
|
19 | return "IPY_MODEL_" + x.model_id | |||
|
20 | else: | |||
|
21 | return x | |||
|
22 | ||||
|
23 | def _json_to_widget(x): | |||
|
24 | if isinstance(x, dict): | |||
|
25 | return {k: _json_to_widget(v) for k, v in x.items()} | |||
|
26 | elif isinstance(x, (list, tuple)): | |||
|
27 | return [_json_to_widget(v) for v in x] | |||
|
28 | elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets: | |||
|
29 | return Widget.widgets[x[10:]] | |||
|
30 | else: | |||
|
31 | return x | |||
|
32 | ||||
|
33 | widget_serialization = { | |||
|
34 | 'from_json': _json_to_widget, | |||
|
35 | 'to_json': _widget_to_json | |||
|
36 | } | |||
|
37 | ||||
|
38 | ||||
13 | @register('IPython.Box') |
|
39 | @register('IPython.Box') | |
14 | class Box(DOMWidget): |
|
40 | class Box(DOMWidget): | |
15 | """Displays multiple widgets in a group.""" |
|
41 | """Displays multiple widgets in a group.""" | |
|
42 | _model_name = Unicode('BoxModel', sync=True) | |||
16 | _view_name = Unicode('BoxView', sync=True) |
|
43 | _view_name = Unicode('BoxView', sync=True) | |
17 |
|
44 | |||
18 | # Child widgets in the container. |
|
45 | # Child widgets in the container. | |
19 | # Using a tuple here to force reassignment to update the list. |
|
46 | # Using a tuple here to force reassignment to update the list. | |
20 | # When a proper notifying-list trait exists, that is what should be used here. |
|
47 | # When a proper notifying-list trait exists, that is what should be used here. | |
21 | children = Tuple(sync=True) |
|
48 | children = Tuple(sync=True, **widget_serialization) | |
22 |
|
49 | |||
23 | _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', ''] |
|
50 | _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', ''] | |
24 | overflow_x = CaselessStrEnum( |
|
51 | overflow_x = CaselessStrEnum( |
@@ -67,7 +67,7 b' class Button(DOMWidget):' | |||||
67 | Set to true to remove the callback from the list of callbacks.""" |
|
67 | Set to true to remove the callback from the list of callbacks.""" | |
68 | self._click_handlers.register_callback(callback, remove=remove) |
|
68 | self._click_handlers.register_callback(callback, remove=remove) | |
69 |
|
69 | |||
70 | def _handle_button_msg(self, _, content): |
|
70 | def _handle_button_msg(self, _, content, buffers): | |
71 | """Handle a msg from the front-end. |
|
71 | """Handle a msg from the front-end. | |
72 |
|
72 | |||
73 | Parameters |
|
73 | Parameters |
General Comments 0
You need to be logged in to leave comments.
Login now