##// END OF EJS Templates
work-in-progress for custom js serializers
Jason Grout -
Show More
@@ -0,0 +1,50 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 define([
5 "base/js/utils"
6 ], function(utils){
7
8 return {
9 widget_serialization: {
10 deserialize: function deserialize_models(value, model) {
11 /**
12 * Replace model ids with models recursively.
13 */
14 var unpacked;
15 if ($.isArray(value)) {
16 unpacked = [];
17 _.each(value, function(sub_value, key) {
18 unpacked.push(deserialize_models(sub_value, model));
19 });
20 return Promise.all(unpacked);
21 } else if (value instanceof Object) {
22 unpacked = {};
23 _.each(value, function(sub_value, key) {
24 unpacked[key] = deserialize_models(sub_value, model);
25 });
26 return utils.resolve_promises_dict(unpacked);
27 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
28 // get_model returns a promise already
29 return model.widget_manager.get_model(value.slice(10, value.length));
30 } else {
31 return Promise.resolve(value);
32 }
33 },
34 },
35
36 list_of_numbers: {
37 deserialize: function (value, model) {
38 /* value is a DataView */
39 /* create a float64 typed array */
40 return new Float64Array(value.buffer)
41 },
42 serialize: function (value, model) {
43 return value;
44 },
45 }
46 }
47
48
49
50 });
@@ -1,763 +1,813 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define(["widgets/js/manager",
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/utils",
9 9 "base/js/namespace",
10 10 ], function(widgetmanager, _, Backbone, $, utils, IPython){
11 11 "use strict";
12 12
13 13 var WidgetModel = Backbone.Model.extend({
14 14 constructor: function (widget_manager, model_id, comm) {
15 15 /**
16 16 * Constructor
17 17 *
18 18 * Creates a WidgetModel instance.
19 19 *
20 20 * Parameters
21 21 * ----------
22 22 * widget_manager : WidgetManager instance
23 23 * model_id : string
24 24 * An ID unique to this model.
25 25 * comm : Comm instance (optional)
26 26 */
27 27 this.widget_manager = widget_manager;
28 28 this.state_change = Promise.resolve();
29 29 this._buffered_state_diff = {};
30 30 this.pending_msgs = 0;
31 31 this.msg_buffer = null;
32 32 this.state_lock = null;
33 33 this.id = model_id;
34 34 this.views = {};
35 this.serializers = {};
35 36 this._resolve_received_state = {};
36 37
37 38 if (comm !== undefined) {
38 39 // Remember comm associated with the model.
39 40 this.comm = comm;
40 41 comm.model = this;
41 42
42 43 // Hook comm messages up to model.
43 44 comm.on_close($.proxy(this._handle_comm_closed, this));
44 45 comm.on_msg($.proxy(this._handle_comm_msg, this));
45 46
46 47 // Assume the comm is alive.
47 48 this.set_comm_live(true);
48 49 } else {
49 50 this.set_comm_live(false);
50 51 }
51 52
52 53 // Listen for the events that lead to the websocket being terminated.
53 54 var that = this;
54 55 var died = function() {
55 56 that.set_comm_live(false);
56 57 };
57 58 widget_manager.notebook.events.on('kernel_disconnected.Kernel', died);
58 59 widget_manager.notebook.events.on('kernel_killed.Kernel', died);
59 60 widget_manager.notebook.events.on('kernel_restarting.Kernel', died);
60 61 widget_manager.notebook.events.on('kernel_dead.Kernel', died);
61 62
62 63 return Backbone.Model.apply(this);
63 64 },
64 65
65 send: function (content, callbacks) {
66 send: function (content, callbacks, buffers) {
66 67 /**
67 68 * Send a custom msg over the comm.
68 69 */
69 70 if (this.comm !== undefined) {
70 71 var data = {method: 'custom', content: content};
71 this.comm.send(data, callbacks);
72 this.comm.send(data, callbacks, {}, buffers);
72 73 this.pending_msgs++;
73 74 }
74 75 },
75 76
76 77 request_state: function(callbacks) {
77 78 /**
78 79 * Request a state push from the back-end.
79 80 */
80 81 if (!this.comm) {
81 82 console.error("Could not request_state because comm doesn't exist!");
82 83 return;
83 84 }
84 85
85 86 var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
86 87
87 88 // Promise that is resolved when a state is received
88 89 // from the back-end.
89 90 var that = this;
90 91 var received_state = new Promise(function(resolve) {
91 92 that._resolve_received_state[msg_id] = resolve;
92 93 });
93 94 return received_state;
94 95 },
95 96
96 97 set_comm_live: function(live) {
97 98 /**
98 99 * Change the comm_live state of the model.
99 100 */
100 101 if (this.comm_live === undefined || this.comm_live != live) {
101 102 this.comm_live = live;
102 103 this.trigger(live ? 'comm:live' : 'comm:dead', {model: this});
103 104 }
104 105 },
105 106
106 107 close: function(comm_closed) {
107 108 /**
108 109 * Close model
109 110 */
110 111 if (this.comm && !comm_closed) {
111 112 this.comm.close();
112 113 }
113 114 this.stopListening();
114 115 this.trigger('destroy', this);
115 116 delete this.comm.model; // Delete ref so GC will collect widget model.
116 117 delete this.comm;
117 118 delete this.model_id; // Delete id from model so widget manager cleans up.
118 119 _.each(this.views, function(v, id, views) {
119 120 v.then(function(view) {
120 121 view.remove();
121 122 delete views[id];
122 123 });
123 124 });
124 125 },
125 126
126 127 _handle_comm_closed: function (msg) {
127 128 /**
128 129 * Handle when a widget is closed.
129 130 */
130 131 this.trigger('comm:close');
131 132 this.close(true);
132 133 },
133 134
134 135 _handle_comm_msg: function (msg) {
135 136 /**
136 137 * Handle incoming comm msg.
137 138 */
138 139 var method = msg.content.data.method;
140
139 141 var that = this;
140 142 switch (method) {
141 143 case 'update':
142 144 this.state_change = this.state_change
143 145 .then(function() {
144 return that.set_state(msg.content.data.state);
146 var state = msg.content.data.state || {};
147 var buffer_keys = msg.content.data.buffers || [];
148 var buffers = msg.buffers || [];
149 var metadata = msg.content.data.metadata || {};
150 var i,k;
151 for (var i=0; i<buffer_keys.length; i++) {
152 k = buffer_keys[i];
153 state[k] = buffers[i];
154 }
155
156 // for any metadata specifying a deserializer, set the
157 // state to a promise that resolves to the deserialized version
158 // also, store the serialization function for the attribute
159 var keys = Object.keys(metadata);
160 for (var i=0; i<keys.length; i++) {
161 k = keys[i];
162 if (metadata[k] && metadata[k].serialization) {
163 that.serializers[k] = utils.load_class.apply(that,
164 metadata[k].serialization);
165 state[k] = that.deserialize(that.serializers[k], state[k]);
166 }
167 }
168 return utils.resolve_promises_dict(state);
169 }).then(function(state) {
170 return that.set_state(state);
145 171 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
146 172 .then(function() {
147 173 var parent_id = msg.parent_header.msg_id;
148 174 if (that._resolve_received_state[parent_id] !== undefined) {
149 175 that._resolve_received_state[parent_id].call();
150 176 delete that._resolve_received_state[parent_id];
151 177 }
152 178 }).catch(utils.reject("Couldn't resolve state request promise", true));
153 179 break;
154 180 case 'custom':
155 this.trigger('msg:custom', msg.content.data.content);
181 this.trigger('msg:custom', msg.content.data.content, msg.buffers);
156 182 break;
157 183 case 'display':
158 184 this.state_change = this.state_change.then(function() {
159 185 that.widget_manager.display_view(msg, that);
160 186 }).catch(utils.reject('Could not process display view msg', true));
161 187 break;
162 188 }
163 189 },
164 190
191 deserialize: function(serializer, value) {
192 // given a serializer dict and a value,
193 // return a promise for the deserialized value
194 var that = this;
195 return serializer.then(function(s) {
196 if (s.deserialize) {
197 return s.deserialize(value, that);
198 } else {
199 return value;
200 }
201 });
202 },
203
165 204 set_state: function (state) {
166 205 var that = this;
167 206 // Handle when a widget is updated via the python side.
168 return this._unpack_models(state).then(function(state) {
207 return new Promise(function(resolve, reject) {
169 208 that.state_lock = state;
170 209 try {
171 210 WidgetModel.__super__.set.call(that, state);
172 211 } finally {
173 212 that.state_lock = null;
174 213 }
214 resolve();
175 215 }).catch(utils.reject("Couldn't set model state", true));
176 216 },
177 217
178 218 get_state: function() {
179 219 // Get the serializable state of the model.
180 var state = this.toJSON();
181 for (var key in state) {
182 if (state.hasOwnProperty(key)) {
183 state[key] = this._pack_models(state[key]);
184 }
185 }
186 return state;
220 // Equivalent to Backbone.Model.toJSON()
221 return _.clone(this.attributes);
187 222 },
188
223
189 224 _handle_status: function (msg, callbacks) {
190 225 /**
191 226 * Handle status msgs.
192 227 *
193 228 * execution_state : ('busy', 'idle', 'starting')
194 229 */
195 230 if (this.comm !== undefined) {
196 231 if (msg.content.execution_state ==='idle') {
197 232 // Send buffer if this message caused another message to be
198 233 // throttled.
199 234 if (this.msg_buffer !== null &&
200 235 (this.get('msg_throttle') || 3) === this.pending_msgs) {
201 236 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
202 237 this.comm.send(data, callbacks);
203 238 this.msg_buffer = null;
204 239 } else {
205 240 --this.pending_msgs;
206 241 }
207 242 }
208 243 }
209 244 },
210 245
211 246 callbacks: function(view) {
212 247 /**
213 248 * Create msg callbacks for a comm msg.
214 249 */
215 250 var callbacks = this.widget_manager.callbacks(view);
216 251
217 252 if (callbacks.iopub === undefined) {
218 253 callbacks.iopub = {};
219 254 }
220 255
221 256 var that = this;
222 257 callbacks.iopub.status = function (msg) {
223 258 that._handle_status(msg, callbacks);
224 259 };
225 260 return callbacks;
226 261 },
227 262
228 263 set: function(key, val, options) {
229 264 /**
230 265 * Set a value.
231 266 */
232 267 var return_value = WidgetModel.__super__.set.apply(this, arguments);
233 268
234 269 // Backbone only remembers the diff of the most recent set()
235 270 // operation. Calling set multiple times in a row results in a
236 271 // loss of diff information. Here we keep our own running diff.
237 272 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
238 273 return return_value;
239 274 },
240 275
241 276 sync: function (method, model, options) {
242 277 /**
243 278 * Handle sync to the back-end. Called when a model.save() is called.
244 279 *
245 280 * Make sure a comm exists.
281
282 * Parameters
283 * ----------
284 * method : create, update, patch, delete, read
285 * create/update always send the full attribute set
286 * patch - only send attributes listed in options.attrs, and if we are queuing
287 * up messages, combine with previous messages that have not been sent yet
288 * model : the model we are syncing
289 * will normally be the same as `this`
290 * options : dict
291 * the `attrs` key, if it exists, gives an {attr: value} dict that should be synced,
292 * otherwise, sync all attributes
293 *
246 294 */
247 295 var error = options.error || function() {
248 296 console.error('Backbone sync error:', arguments);
249 297 };
250 298 if (this.comm === undefined) {
251 299 error();
252 300 return false;
253 301 }
254 302
255 // Delete any key value pairs that the back-end already knows about.
256 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
303 var attrs = (method === 'patch') ? options.attrs : model.get_state(options);
304
305 // the state_lock lists attributes that are currently be changed right now from a kernel message
306 // we don't want to send these non-changes back to the kernel, so we delete them out of attrs
307 // (but we only delete them if the value hasn't changed from the value stored in the state_lock
257 308 if (this.state_lock !== null) {
258 309 var keys = Object.keys(this.state_lock);
259 310 for (var i=0; i<keys.length; i++) {
260 311 var key = keys[i];
261 312 if (attrs[key] === this.state_lock[key]) {
262 313 delete attrs[key];
263 314 }
264 315 }
265 316 }
266
267 // Only sync if there are attributes to send to the back-end.
268 attrs = this._pack_models(attrs);
317
269 318 if (_.size(attrs) > 0) {
270 319
271 320 // If this message was sent via backbone itself, it will not
272 321 // have any callbacks. It's important that we create callbacks
273 322 // so we can listen for status messages, etc...
274 323 var callbacks = options.callbacks || this.callbacks();
275 324
276 325 // Check throttle.
277 326 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
278 327 // The throttle has been exceeded, buffer the current msg so
279 328 // it can be sent once the kernel has finished processing
280 329 // some of the existing messages.
281 330
282 331 // Combine updates if it is a 'patch' sync, otherwise replace updates
283 332 switch (method) {
284 333 case 'patch':
285 334 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
286 335 break;
287 336 case 'update':
288 337 case 'create':
289 338 this.msg_buffer = attrs;
290 339 break;
291 340 default:
292 341 error();
293 342 return false;
294 343 }
295 344 this.msg_buffer_callbacks = callbacks;
296 345
297 346 } else {
298 347 // We haven't exceeded the throttle, send the message like
299 348 // normal.
300 var data = {method: 'backbone', sync_data: attrs};
301 this.comm.send(data, callbacks);
302 this.pending_msgs++;
349 this.send_sync_message(attrs, callbacks);
303 350 }
304 351 }
305 352 // Since the comm is a one-way communication, assume the message
306 353 // arrived. Don't call success since we don't have a model back from the server
307 354 // this means we miss out on the 'sync' event.
308 355 this._buffered_state_diff = {};
309 356 },
310 357
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 358
320 _pack_models: function(value) {
321 /**
322 * Replace models with model ids recursively.
323 */
359 send_sync_message: function(attrs, callbacks) {
360 // prepare and send a comm message syncing attrs
324 361 var that = this;
325 var packed;
326 if (value instanceof Backbone.Model) {
327 return "IPY_MODEL_" + value.id;
328
329 } else if ($.isArray(value)) {
330 packed = [];
331 _.each(value, function(sub_value, key) {
332 packed.push(that._pack_models(sub_value));
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;
362 // first, build a state dictionary with key=the attribute and the value
363 // being the value or the promise of the serialized value
364 var state_promise_dict = {};
365 var keys = Object.keys(attrs);
366 for (var i=0; i<keys.length; i++) {
367 // bind k and v locally; needed since we have an inner async function using v
368 (function(k,v) {
369 if (that.serializers[k]) {
370 state_promise_dict[k] = that.serializers[k].then(function(f) {
371 if (f.serialize) {
372 return f.serialize(v, that);
373 } else {
374 return v;
375 }
376 })
377 } else {
378 state_promise_dict[k] = v;
379 }
380 })(keys[i], attrs[keys[i]])
381 }
382 utils.resolve_promises_dict(state_promise_dict).then(function(state) {
383 // get binary values, then send
384 var keys = Object.keys(state);
385 var buffers = [];
386 var buffer_keys = [];
387 for (var i=0; i<keys.length; i++) {
388 var key = keys[i];
389 var value = state[key];
390 if (value.buffer instanceof ArrayBuffer
391 || value instanceof ArrayBuffer) {
392 buffers.push(value);
393 buffer_keys.push(key);
394 delete state[key];
395 }
396 }
397 that.comm.send({method: 'backbone', sync_data: state, buffer_keys: buffer_keys}, callbacks, {}, buffers);
398 that.pending_msgs++;
399 })
400 },
401
402 serialize: function(model, attrs) {
403 // Serialize the attributes into a sync message
404 var keys = Object.keys(attrs);
405 var key, value;
406 var buffers, metadata, buffer_keys, serialize;
407 for (var i=0; i<keys.length; i++) {
408 key = keys[i];
409 serialize = model.serializers[key];
410 if (serialize && serialize.serialize) {
411 attrs[key] = serialize.serialize(attrs[key]);
412 }
346 413 }
347 414 },
348 415
349 _unpack_models: function(value) {
416 save_changes: function(callbacks) {
350 417 /**
351 * Replace model ids with models recursively.
418 * Push this model's state to the back-end
419 *
420 * This invokes a Backbone.Sync.
352 421 */
353 var that = this;
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 }
422 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
373 423 },
374 424
375 425 on_some_change: function(keys, callback, context) {
376 426 /**
377 427 * on_some_change(["key1", "key2"], foo, context) differs from
378 428 * on("change:key1 change:key2", foo, context).
379 429 * If the widget attributes key1 and key2 are both modified,
380 430 * the second form will result in foo being called twice
381 431 * while the first will call foo only once.
382 432 */
383 433 this.on('change', function() {
384 434 if (keys.some(this.hasChanged, this)) {
385 435 callback.apply(context);
386 436 }
387 437 }, this);
388 438
389 },
439 },
390 440 });
391 441 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
392 442
393 443
394 444 var WidgetView = Backbone.View.extend({
395 445 initialize: function(parameters) {
396 446 /**
397 447 * Public constructor.
398 448 */
399 449 this.model.on('change',this.update,this);
400 450
401 451 // Bubble the comm live events.
402 452 this.model.on('comm:live', function() {
403 453 this.trigger('comm:live', this);
404 454 }, this);
405 455 this.model.on('comm:dead', function() {
406 456 this.trigger('comm:dead', this);
407 457 }, this);
408 458
409 459 this.options = parameters.options;
410 460 this.on('displayed', function() {
411 461 this.is_displayed = true;
412 462 }, this);
413 463 },
414 464
415 465 update: function(){
416 466 /**
417 467 * Triggered on model change.
418 468 *
419 469 * Update view to be consistent with this.model
420 470 */
421 471 },
422 472
423 473 create_child_view: function(child_model, options) {
424 474 /**
425 475 * Create and promise that resolves to a child view of a given model
426 476 */
427 477 var that = this;
428 478 options = $.extend({ parent: this }, options || {});
429 479 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
430 480 },
431 481
432 482 callbacks: function(){
433 483 /**
434 484 * Create msg callbacks for a comm msg.
435 485 */
436 486 return this.model.callbacks(this);
437 487 },
438 488
439 489 render: function(){
440 490 /**
441 491 * Render the view.
442 492 *
443 493 * By default, this is only called the first time the view is created
444 494 */
445 495 },
446 496
447 send: function (content) {
497 send: function (content, buffers) {
448 498 /**
449 499 * Send a custom msg associated with this view.
450 500 */
451 this.model.send(content, this.callbacks());
501 this.model.send(content, this.callbacks(), buffers);
452 502 },
453 503
454 504 touch: function () {
455 505 this.model.save_changes(this.callbacks());
456 506 },
457 507
458 508 after_displayed: function (callback, context) {
459 509 /**
460 510 * Calls the callback right away is the view is already displayed
461 511 * otherwise, register the callback to the 'displayed' event.
462 512 */
463 513 if (this.is_displayed) {
464 514 callback.apply(context);
465 515 } else {
466 516 this.on('displayed', callback, context);
467 517 }
468 518 },
469 519
470 520 remove: function () {
471 521 // Raise a remove event when the view is removed.
472 522 WidgetView.__super__.remove.apply(this, arguments);
473 523 this.trigger('remove');
474 524 }
475 525 });
476 526
477 527
478 528 var DOMWidgetView = WidgetView.extend({
479 529 initialize: function (parameters) {
480 530 /**
481 531 * Public constructor
482 532 */
483 533 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
484 534 this.model.on('change:visible', this.update_visible, this);
485 535 this.model.on('change:_css', this.update_css, this);
486 536
487 537 this.model.on('change:_dom_classes', function(model, new_classes) {
488 538 var old_classes = model.previous('_dom_classes');
489 539 this.update_classes(old_classes, new_classes);
490 540 }, this);
491 541
492 542 this.model.on('change:color', function (model, value) {
493 543 this.update_attr('color', value); }, this);
494 544
495 545 this.model.on('change:background_color', function (model, value) {
496 546 this.update_attr('background', value); }, this);
497 547
498 548 this.model.on('change:width', function (model, value) {
499 549 this.update_attr('width', value); }, this);
500 550
501 551 this.model.on('change:height', function (model, value) {
502 552 this.update_attr('height', value); }, this);
503 553
504 554 this.model.on('change:border_color', function (model, value) {
505 555 this.update_attr('border-color', value); }, this);
506 556
507 557 this.model.on('change:border_width', function (model, value) {
508 558 this.update_attr('border-width', value); }, this);
509 559
510 560 this.model.on('change:border_style', function (model, value) {
511 561 this.update_attr('border-style', value); }, this);
512 562
513 563 this.model.on('change:font_style', function (model, value) {
514 564 this.update_attr('font-style', value); }, this);
515 565
516 566 this.model.on('change:font_weight', function (model, value) {
517 567 this.update_attr('font-weight', value); }, this);
518 568
519 569 this.model.on('change:font_size', function (model, value) {
520 570 this.update_attr('font-size', this._default_px(value)); }, this);
521 571
522 572 this.model.on('change:font_family', function (model, value) {
523 573 this.update_attr('font-family', value); }, this);
524 574
525 575 this.model.on('change:padding', function (model, value) {
526 576 this.update_attr('padding', value); }, this);
527 577
528 578 this.model.on('change:margin', function (model, value) {
529 579 this.update_attr('margin', this._default_px(value)); }, this);
530 580
531 581 this.model.on('change:border_radius', function (model, value) {
532 582 this.update_attr('border-radius', this._default_px(value)); }, this);
533 583
534 584 this.after_displayed(function() {
535 585 this.update_visible(this.model, this.model.get("visible"));
536 586 this.update_classes([], this.model.get('_dom_classes'));
537 587
538 588 this.update_attr('color', this.model.get('color'));
539 589 this.update_attr('background', this.model.get('background_color'));
540 590 this.update_attr('width', this.model.get('width'));
541 591 this.update_attr('height', this.model.get('height'));
542 592 this.update_attr('border-color', this.model.get('border_color'));
543 593 this.update_attr('border-width', this.model.get('border_width'));
544 594 this.update_attr('border-style', this.model.get('border_style'));
545 595 this.update_attr('font-style', this.model.get('font_style'));
546 596 this.update_attr('font-weight', this.model.get('font_weight'));
547 597 this.update_attr('font-size', this._default_px(this.model.get('font_size')));
548 598 this.update_attr('font-family', this.model.get('font_family'));
549 599 this.update_attr('padding', this.model.get('padding'));
550 600 this.update_attr('margin', this._default_px(this.model.get('margin')));
551 601 this.update_attr('border-radius', this._default_px(this.model.get('border_radius')));
552 602
553 603 this.update_css(this.model, this.model.get("_css"));
554 604 }, this);
555 605 },
556 606
557 607 _default_px: function(value) {
558 608 /**
559 609 * Makes browser interpret a numerical string as a pixel value.
560 610 */
561 611 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
562 612 return value.trim() + 'px';
563 613 }
564 614 return value;
565 615 },
566 616
567 617 update_attr: function(name, value) {
568 618 /**
569 619 * Set a css attr of the widget view.
570 620 */
571 621 this.$el.css(name, value);
572 622 },
573 623
574 624 update_visible: function(model, value) {
575 625 /**
576 626 * Update visibility
577 627 */
578 628 switch(value) {
579 629 case null: // python None
580 630 this.$el.show().css('visibility', 'hidden'); break;
581 631 case false:
582 632 this.$el.hide(); break;
583 633 case true:
584 634 this.$el.show().css('visibility', ''); break;
585 635 }
586 636 },
587 637
588 638 update_css: function (model, css) {
589 639 /**
590 640 * Update the css styling of this view.
591 641 */
592 642 if (css === undefined) {return;}
593 643 for (var i = 0; i < css.length; i++) {
594 644 // Apply the css traits to all elements that match the selector.
595 645 var selector = css[i][0];
596 646 var elements = this._get_selector_element(selector);
597 647 if (elements.length > 0) {
598 648 var trait_key = css[i][1];
599 649 var trait_value = css[i][2];
600 650 elements.css(trait_key ,trait_value);
601 651 }
602 652 }
603 653 },
604 654
605 655 update_classes: function (old_classes, new_classes, $el) {
606 656 /**
607 657 * Update the DOM classes applied to an element, default to this.$el.
608 658 */
609 659 if ($el===undefined) {
610 660 $el = this.$el;
611 661 }
612 662 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
613 663 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
614 664 },
615 665
616 666 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
617 667 /**
618 668 * Update the DOM classes applied to the widget based on a single
619 669 * trait's value.
620 670 *
621 671 * Given a trait value classes map, this function automatically
622 672 * handles applying the appropriate classes to the widget element
623 673 * and removing classes that are no longer valid.
624 674 *
625 675 * Parameters
626 676 * ----------
627 677 * class_map: dictionary
628 678 * Dictionary of trait values to class lists.
629 679 * Example:
630 680 * {
631 681 * success: ['alert', 'alert-success'],
632 682 * info: ['alert', 'alert-info'],
633 683 * warning: ['alert', 'alert-warning'],
634 684 * danger: ['alert', 'alert-danger']
635 685 * };
636 686 * trait_name: string
637 687 * Name of the trait to check the value of.
638 688 * previous_trait_value: optional string, default ''
639 689 * Last trait value
640 690 * $el: optional jQuery element handle, defaults to this.$el
641 691 * Element that the classes are applied to.
642 692 */
643 693 var key = previous_trait_value;
644 694 if (key === undefined) {
645 695 key = this.model.previous(trait_name);
646 696 }
647 697 var old_classes = class_map[key] ? class_map[key] : [];
648 698 key = this.model.get(trait_name);
649 699 var new_classes = class_map[key] ? class_map[key] : [];
650 700
651 701 this.update_classes(old_classes, new_classes, $el || this.$el);
652 702 },
653 703
654 704 _get_selector_element: function (selector) {
655 705 /**
656 706 * Get the elements via the css selector.
657 707 */
658 708 var elements;
659 709 if (!selector) {
660 710 elements = this.$el;
661 711 } else {
662 712 elements = this.$el.find(selector).addBack(selector);
663 713 }
664 714 return elements;
665 715 },
666 716
667 717 typeset: function(element, text){
668 718 utils.typeset.apply(null, arguments);
669 719 },
670 720 });
671 721
672 722
673 723 var ViewList = function(create_view, remove_view, context) {
674 724 /**
675 725 * - create_view and remove_view are default functions called when adding or removing views
676 726 * - create_view takes a model and returns a view or a promise for a view for that model
677 727 * - remove_view takes a view and destroys it (including calling `view.remove()`)
678 728 * - each time the update() function is called with a new list, the create and remove
679 729 * callbacks will be called in an order so that if you append the views created in the
680 730 * create callback and remove the views in the remove callback, you will duplicate
681 731 * the order of the list.
682 732 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
683 733 * - the context defaults to the created ViewList. If you pass another context, the create and remove
684 734 * will be called in that context.
685 735 */
686 736
687 737 this.initialize.apply(this, arguments);
688 738 };
689 739
690 740 _.extend(ViewList.prototype, {
691 741 initialize: function(create_view, remove_view, context) {
692 742 this._handler_context = context || this;
693 743 this._models = [];
694 744 this.views = []; // list of promises for views
695 745 this._create_view = create_view;
696 746 this._remove_view = remove_view || function(view) {view.remove();};
697 747 },
698 748
699 749 update: function(new_models, create_view, remove_view, context) {
700 750 /**
701 751 * the create_view, remove_view, and context arguments override the defaults
702 752 * specified when the list is created.
703 753 * after this function, the .views attribute is a list of promises for views
704 754 * if you want to perform some action on the list of views, do something like
705 755 * `Promise.all(myviewlist.views).then(function(views) {...});`
706 756 */
707 757 var remove = remove_view || this._remove_view;
708 758 var create = create_view || this._create_view;
709 759 context = context || this._handler_context;
710 760 var i = 0;
711 761 // first, skip past the beginning of the lists if they are identical
712 762 for (; i < new_models.length; i++) {
713 763 if (i >= this._models.length || new_models[i] !== this._models[i]) {
714 764 break;
715 765 }
716 766 }
717 767
718 768 var first_removed = i;
719 769 // Remove the non-matching items from the old list.
720 770 var removed = this.views.splice(first_removed, this.views.length-first_removed);
721 771 for (var j = 0; j < removed.length; j++) {
722 772 removed[j].then(function(view) {
723 773 remove.call(context, view)
724 774 });
725 775 }
726 776
727 777 // Add the rest of the new list items.
728 778 for (; i < new_models.length; i++) {
729 779 this.views.push(Promise.resolve(create.call(context, new_models[i])));
730 780 }
731 781 // make a copy of the input array
732 782 this._models = new_models.slice();
733 783 },
734 784
735 785 remove: function() {
736 786 /**
737 787 * removes every view in the list; convenience function for `.update([])`
738 788 * that should be faster
739 789 * returns a promise that resolves after this removal is done
740 790 */
741 791 var that = this;
742 792 return Promise.all(this.views).then(function(views) {
743 793 for (var i = 0; i < that.views.length; i++) {
744 794 that._remove_view.call(that._handler_context, views[i]);
745 795 }
746 796 that.views = [];
747 797 that._models = [];
748 798 });
749 799 },
750 800 });
751 801
752 802 var widget = {
753 803 'WidgetModel': WidgetModel,
754 804 'WidgetView': WidgetView,
755 805 'DOMWidgetView': DOMWidgetView,
756 806 'ViewList': ViewList,
757 807 };
758 808
759 809 // For backwards compatability.
760 810 $.extend(IPython, widget);
761 811
762 812 return widget;
763 813 });
@@ -1,490 +1,543 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.importstring import import_item
22 22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
23 23 CaselessStrEnum, Tuple, CUnicode, Int, Set
24 24 from IPython.utils.py3compat import string_types
25 25 from .trait_types import Color
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Classes
29 29 #-----------------------------------------------------------------------------
30 30 class CallbackDispatcher(LoggingConfigurable):
31 31 """A structure for registering and running callbacks"""
32 32 callbacks = List()
33 33
34 34 def __call__(self, *args, **kwargs):
35 35 """Call all of the registered callbacks."""
36 36 value = None
37 37 for callback in self.callbacks:
38 38 try:
39 39 local_value = callback(*args, **kwargs)
40 40 except Exception as e:
41 41 ip = get_ipython()
42 42 if ip is None:
43 43 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
44 44 else:
45 45 ip.showtraceback()
46 46 else:
47 47 value = local_value if local_value is not None else value
48 48 return value
49 49
50 50 def register_callback(self, callback, remove=False):
51 51 """(Un)Register a callback
52 52
53 53 Parameters
54 54 ----------
55 55 callback: method handle
56 56 Method to be registered or unregistered.
57 57 remove=False: bool
58 58 Whether to unregister the callback."""
59 59
60 60 # (Un)Register the callback.
61 61 if remove and callback in self.callbacks:
62 62 self.callbacks.remove(callback)
63 63 elif not remove and callback not in self.callbacks:
64 64 self.callbacks.append(callback)
65 65
66 66 def _show_traceback(method):
67 67 """decorator for showing tracebacks in IPython"""
68 68 def m(self, *args, **kwargs):
69 69 try:
70 70 return(method(self, *args, **kwargs))
71 71 except Exception as e:
72 72 ip = get_ipython()
73 73 if ip is None:
74 74 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
75 75 else:
76 76 ip.showtraceback()
77 77 return m
78 78
79 79
80 80 def register(key=None):
81 81 """Returns a decorator registering a widget class in the widget registry.
82 82 If no key is provided, the class name is used as a key. A key is
83 83 provided for each core IPython widget so that the frontend can use
84 84 this key regardless of the language of the kernel"""
85 85 def wrap(widget):
86 86 l = key if key is not None else widget.__module__ + widget.__name__
87 87 Widget.widget_types[l] = widget
88 88 return widget
89 89 return wrap
90 90
91 91
92 def _widget_to_json(x):
93 if isinstance(x, dict):
94 return {k: _widget_to_json(v) for k, v in x.items()}
95 elif isinstance(x, (list, tuple)):
96 return [_widget_to_json(v) for v in x]
97 elif isinstance(x, Widget):
98 return "IPY_MODEL_" + x.model_id
99 else:
100 return x
101
102 def _json_to_widget(x):
103 if isinstance(x, dict):
104 return {k: _json_to_widget(v) for k, v in x.items()}
105 elif isinstance(x, (list, tuple)):
106 return [_json_to_widget(v) for v in x]
107 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
108 return Widget.widgets[x[10:]]
109 else:
110 return x
111
112 widget_serialization = {
113 'from_json': _json_to_widget,
114 'to_json': lambda x: (_widget_to_json(x), {'serialization': ('widget_serialization', 'widgets/js/types')})
115 }
116
117 def _to_binary_list(x):
118 import numpy
119 return memoryview(numpy.array(x, dtype=float)), {'serialization': ('list_of_numbers', 'widgets/js/types')}
120
121 def _from_binary_list(x):
122 import numpy
123 a = numpy.frombuffer(x.tobytes(), dtype=float)
124 return list(a)
125
126 list_of_numbers = {
127 'from_json': _from_binary_list,
128 'to_json': _to_binary_list
129 }
130
131
132
92 133 class Widget(LoggingConfigurable):
93 134 #-------------------------------------------------------------------------
94 135 # Class attributes
95 136 #-------------------------------------------------------------------------
96 137 _widget_construction_callback = None
97 138 widgets = {}
98 139 widget_types = {}
99 140
100 141 @staticmethod
101 142 def on_widget_constructed(callback):
102 143 """Registers a callback to be called when a widget is constructed.
103 144
104 145 The callback must have the following signature:
105 146 callback(widget)"""
106 147 Widget._widget_construction_callback = callback
107 148
108 149 @staticmethod
109 150 def _call_widget_constructed(widget):
110 151 """Static method, called when a widget is constructed."""
111 152 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
112 153 Widget._widget_construction_callback(widget)
113 154
114 155 @staticmethod
115 156 def handle_comm_opened(comm, msg):
116 157 """Static method, called when a widget is constructed."""
117 158 widget_class = import_item(msg['content']['data']['widget_class'])
118 159 widget = widget_class(comm=comm)
119 160
120 161
121 162 #-------------------------------------------------------------------------
122 163 # Traits
123 164 #-------------------------------------------------------------------------
124 165 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
125 166 in which to find _model_name. If empty, look in the global registry.""")
126 167 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
127 168 registered in the front-end to create and sync this widget with.""")
128 169 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
129 170 If empty, look in the global registry.""", sync=True)
130 171 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
131 172 to use to represent the widget.""", sync=True)
132 173 comm = Instance('IPython.kernel.comm.Comm')
133 174
134 175 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
135 176 front-end can send before receiving an idle msg from the back-end.""")
136 177
137 178 version = Int(0, sync=True, help="""Widget's version""")
138 179 keys = List()
139 180 def _keys_default(self):
140 181 return [name for name in self.traits(sync=True)]
141 182
142 183 _property_lock = Tuple((None, None))
143 184 _send_state_lock = Int(0)
144 185 _states_to_send = Set()
145 186 _display_callbacks = Instance(CallbackDispatcher, ())
146 187 _msg_callbacks = Instance(CallbackDispatcher, ())
147 188
148 189 #-------------------------------------------------------------------------
149 190 # (Con/de)structor
150 191 #-------------------------------------------------------------------------
151 192 def __init__(self, **kwargs):
152 193 """Public constructor"""
153 194 self._model_id = kwargs.pop('model_id', None)
154 195 super(Widget, self).__init__(**kwargs)
155 196
156 197 Widget._call_widget_constructed(self)
157 198 self.open()
158 199
159 200 def __del__(self):
160 201 """Object disposal"""
161 202 self.close()
162 203
163 204 #-------------------------------------------------------------------------
164 205 # Properties
165 206 #-------------------------------------------------------------------------
166 207
167 208 def open(self):
168 209 """Open a comm to the frontend if one isn't already open."""
169 210 if self.comm is None:
170 211 args = dict(target_name='ipython.widget',
171 212 data={'model_name': self._model_name,
172 213 'model_module': self._model_module})
173 214 if self._model_id is not None:
174 215 args['comm_id'] = self._model_id
175 216 self.comm = Comm(**args)
176 217
177 218 def _comm_changed(self, name, new):
178 219 """Called when the comm is changed."""
179 220 if new is None:
180 221 return
181 222 self._model_id = self.model_id
182 223
183 224 self.comm.on_msg(self._handle_msg)
184 225 Widget.widgets[self.model_id] = self
185 226
186 227 # first update
187 228 self.send_state()
188 229
189 230 @property
190 231 def model_id(self):
191 232 """Gets the model id of this widget.
192 233
193 234 If a Comm doesn't exist yet, a Comm will be created automagically."""
194 235 return self.comm.comm_id
195 236
196 237 #-------------------------------------------------------------------------
197 238 # Methods
198 239 #-------------------------------------------------------------------------
199 240
200 241 def close(self):
201 242 """Close method.
202 243
203 244 Closes the underlying comm.
204 245 When the comm is closed, all of the widget views are automatically
205 246 removed from the front-end."""
206 247 if self.comm is not None:
207 248 Widget.widgets.pop(self.model_id, None)
208 249 self.comm.close()
209 250 self.comm = None
210 251
211 252 def send_state(self, key=None):
212 253 """Sends the widget state, or a piece of it, to the front-end.
213 254
214 255 Parameters
215 256 ----------
216 257 key : unicode, or iterable (optional)
217 258 A single property's name or iterable of property names to sync with the front-end.
218 259 """
219 self._send({
220 "method" : "update",
221 "state" : self.get_state(key=key)
222 })
260 state, buffer_keys, buffers, metadata = self.get_state(key=key)
261 msg = {"method": "update", "state": state}
262 if buffer_keys:
263 msg['buffers'] = buffer_keys
264 if metadata:
265 msg['metadata'] = metadata
266 self._send(msg, buffers=buffers)
223 267
224 268 def get_state(self, key=None):
225 269 """Gets the widget state, or a piece of it.
226 270
227 271 Parameters
228 272 ----------
229 273 key : unicode or iterable (optional)
230 274 A single property's name or iterable of property names to get.
275
276 Returns
277 -------
278 state : dict of states
279 buffer_keys : list of strings
280 the values that are stored in buffers
281 buffers : list of binary memoryviews
282 values to transmit in binary
283 metadata : dict
284 metadata for each field: {key: metadata}
231 285 """
232 286 if key is None:
233 287 keys = self.keys
234 288 elif isinstance(key, string_types):
235 289 keys = [key]
236 290 elif isinstance(key, collections.Iterable):
237 291 keys = key
238 292 else:
239 293 raise ValueError("key must be a string, an iterable of keys, or None")
240 294 state = {}
295 buffers = []
296 buffer_keys = []
297 metadata = {}
241 298 for k in keys:
242 299 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
243 300 value = getattr(self, k)
244 state[k] = f(value)
245 return state
301 serialized, md = f(value)
302 if isinstance(serialized, memoryview):
303 buffers.append(serialized)
304 buffer_keys.append(k)
305 else:
306 state[k] = serialized
307 if md is not None:
308 metadata[k] = md
309 return state, buffer_keys, buffers, metadata
246 310
247 311 def set_state(self, sync_data):
248 312 """Called when a state is received from the front-end."""
249 313 for name in self.keys:
250 314 if name in sync_data:
251 315 json_value = sync_data[name]
252 316 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
253 317 with self._lock_property(name, json_value):
254 318 setattr(self, name, from_json(json_value))
255 319
256 def send(self, content):
320 def send(self, content, buffers=None):
257 321 """Sends a custom msg to the widget model in the front-end.
258 322
259 323 Parameters
260 324 ----------
261 325 content : dict
262 326 Content of the message to send.
327 buffers : list of binary buffers
328 Binary buffers to send with message
263 329 """
264 self._send({"method": "custom", "content": content})
330 self._send({"method": "custom", "content": content}, buffers=buffers)
265 331
266 332 def on_msg(self, callback, remove=False):
267 333 """(Un)Register a custom msg receive callback.
268 334
269 335 Parameters
270 336 ----------
271 337 callback: callable
272 callback will be passed two arguments when a message arrives::
338 callback will be passed three arguments when a message arrives::
273 339
274 callback(widget, content)
340 callback(widget, content, buffers)
275 341
276 342 remove: bool
277 343 True if the callback should be unregistered."""
278 344 self._msg_callbacks.register_callback(callback, remove=remove)
279 345
280 346 def on_displayed(self, callback, remove=False):
281 347 """(Un)Register a widget displayed callback.
282 348
283 349 Parameters
284 350 ----------
285 351 callback: method handler
286 352 Must have a signature of::
287 353
288 354 callback(widget, **kwargs)
289 355
290 356 kwargs from display are passed through without modification.
291 357 remove: bool
292 358 True if the callback should be unregistered."""
293 359 self._display_callbacks.register_callback(callback, remove=remove)
294 360
295 361 #-------------------------------------------------------------------------
296 362 # Support methods
297 363 #-------------------------------------------------------------------------
298 364 @contextmanager
299 365 def _lock_property(self, key, value):
300 366 """Lock a property-value pair.
301 367
302 368 The value should be the JSON state of the property.
303 369
304 370 NOTE: This, in addition to the single lock for all state changes, is
305 371 flawed. In the future we may want to look into buffering state changes
306 372 back to the front-end."""
307 373 self._property_lock = (key, value)
308 374 try:
309 375 yield
310 376 finally:
311 377 self._property_lock = (None, None)
312 378
313 379 @contextmanager
314 380 def hold_sync(self):
315 381 """Hold syncing any state until the context manager is released"""
316 382 # We increment a value so that this can be nested. Syncing will happen when
317 383 # all levels have been released.
318 384 self._send_state_lock += 1
319 385 try:
320 386 yield
321 387 finally:
322 388 self._send_state_lock -=1
323 389 if self._send_state_lock == 0:
324 390 self.send_state(self._states_to_send)
325 391 self._states_to_send.clear()
326 392
327 393 def _should_send_property(self, key, value):
328 394 """Check the property lock (property_lock)"""
329 395 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
330 396 if (key == self._property_lock[0]
331 397 and to_json(value) == self._property_lock[1]):
332 398 return False
333 399 elif self._send_state_lock > 0:
334 400 self._states_to_send.add(key)
335 401 return False
336 402 else:
337 403 return True
338 404
339 405 # Event handlers
340 406 @_show_traceback
341 407 def _handle_msg(self, msg):
342 408 """Called when a msg is received from the front-end"""
343 409 data = msg['content']['data']
344 410 method = data['method']
345 411
346 412 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
347 413 if method == 'backbone':
348 414 if 'sync_data' in data:
415 # get binary buffers too
349 416 sync_data = data['sync_data']
417 for i,k in enumerate(data.get('buffer_keys', [])):
418 sync_data[k] = msg['buffers'][i]
350 419 self.set_state(sync_data) # handles all methods
351 420
352 421 # Handle a state request.
353 422 elif method == 'request_state':
354 423 self.send_state()
355 424
356 425 # Handle a custom msg from the front-end.
357 426 elif method == 'custom':
358 427 if 'content' in data:
359 self._handle_custom_msg(data['content'])
428 self._handle_custom_msg(data['content'], msg['buffers'])
360 429
361 430 # Catch remainder.
362 431 else:
363 432 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
364 433
365 def _handle_custom_msg(self, content):
434 def _handle_custom_msg(self, content, buffers):
366 435 """Called when a custom msg is received."""
367 self._msg_callbacks(self, content)
436 self._msg_callbacks(self, content, buffers)
368 437
369 438 def _notify_trait(self, name, old_value, new_value):
370 439 """Called when a property has been changed."""
371 440 # Trigger default traitlet callback machinery. This allows any user
372 441 # registered validation to be processed prior to allowing the widget
373 442 # machinery to handle the state.
374 443 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
375 444
376 445 # Send the state after the user registered callbacks for trait changes
377 446 # have all fired (allows for user to validate values).
378 447 if self.comm is not None and name in self.keys:
379 448 # Make sure this isn't information that the front-end just sent us.
380 449 if self._should_send_property(name, new_value):
381 450 # Send new state to front-end
382 451 self.send_state(key=name)
383 452
384 453 def _handle_displayed(self, **kwargs):
385 454 """Called when a view has been displayed for this widget instance"""
386 455 self._display_callbacks(self, **kwargs)
387 456
388 457 def _trait_to_json(self, x):
389 458 """Convert a trait value to json
390 459
391 460 Traverse lists/tuples and dicts and serialize their values as well.
392 461 Replace any widgets with their model_id
393 462 """
394 if isinstance(x, dict):
395 return {k: self._trait_to_json(v) for k, v in x.items()}
396 elif isinstance(x, (list, tuple)):
397 return [self._trait_to_json(v) for v in x]
398 elif isinstance(x, Widget):
399 return "IPY_MODEL_" + x.model_id
400 else:
401 return x # Value must be JSON-able
463 return x, None
402 464
403 465 def _trait_from_json(self, x):
404 466 """Convert json values to objects
405 467
406 468 Replace any strings representing valid model id values to Widget references.
407 469 """
408 if isinstance(x, dict):
409 return {k: self._trait_from_json(v) for k, v in x.items()}
410 elif isinstance(x, (list, tuple)):
411 return [self._trait_from_json(v) for v in x]
412 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
413 # we want to support having child widgets at any level in a hierarchy
414 # trusting that a widget UUID will not appear out in the wild
415 return Widget.widgets[x[10:]]
416 else:
417 return x
470 return x
418 471
419 472 def _ipython_display_(self, **kwargs):
420 473 """Called when `IPython.display.display` is called on the widget."""
421 474 # Show view.
422 475 if self._view_name is not None:
423 476 self._send({"method": "display"})
424 477 self._handle_displayed(**kwargs)
425 478
426 def _send(self, msg):
479 def _send(self, msg, buffers=None):
427 480 """Sends a message to the model in the front-end."""
428 self.comm.send(msg)
481 self.comm.send(data=msg, buffers=buffers)
429 482
430 483
431 484 class DOMWidget(Widget):
432 485 visible = Bool(True, allow_none=True, help="Whether the widget is visible. False collapses the empty space, while None preserves the empty space.", sync=True)
433 486 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
434 487 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
435 488
436 489 width = CUnicode(sync=True)
437 490 height = CUnicode(sync=True)
438 491 # A default padding of 2.5 px makes the widgets look nice when displayed inline.
439 492 padding = CUnicode(sync=True)
440 493 margin = CUnicode(sync=True)
441 494
442 495 color = Color(None, allow_none=True, sync=True)
443 496 background_color = Color(None, allow_none=True, sync=True)
444 497 border_color = Color(None, allow_none=True, sync=True)
445 498
446 499 border_width = CUnicode(sync=True)
447 500 border_radius = CUnicode(sync=True)
448 501 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
449 502 'none',
450 503 'hidden',
451 504 'dotted',
452 505 'dashed',
453 506 'solid',
454 507 'double',
455 508 'groove',
456 509 'ridge',
457 510 'inset',
458 511 'outset',
459 512 'initial',
460 513 'inherit', ''],
461 514 default_value='', sync=True)
462 515
463 516 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
464 517 'normal',
465 518 'italic',
466 519 'oblique',
467 520 'initial',
468 521 'inherit', ''],
469 522 default_value='', sync=True)
470 523 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
471 524 'normal',
472 525 'bold',
473 526 'bolder',
474 527 'lighter',
475 528 'initial',
476 529 'inherit', ''] + list(map(str, range(100,1000,100))),
477 530 default_value='', sync=True)
478 531 font_size = CUnicode(sync=True)
479 532 font_family = Unicode(sync=True)
480 533
481 534 def __init__(self, *pargs, **kwargs):
482 535 super(DOMWidget, self).__init__(*pargs, **kwargs)
483 536
484 537 def _validate_border(name, old, new):
485 538 if new is not None and new != '':
486 539 if name != 'border_width' and not self.border_width:
487 540 self.border_width = 1
488 541 if name != 'border_style' and self.border_style == '':
489 542 self.border_style = 'solid'
490 543 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
@@ -1,80 +1,82 b''
1 1 """Box class.
2 2
3 3 Represents a container that can be used to group other widgets.
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 from .widget import DOMWidget, register
9 from .widget import DOMWidget, register, widget_serialization
10 10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
11 11 from IPython.utils.warn import DeprecatedClass
12 12
13 13 @register('IPython.Box')
14 14 class Box(DOMWidget):
15 15 """Displays multiple widgets in a group."""
16 16 _view_name = Unicode('BoxView', sync=True)
17 17
18 18 # Child widgets in the container.
19 19 # Using a tuple here to force reassignment to update the list.
20 20 # When a proper notifying-list trait exists, that is what should be used here.
21 children = Tuple(sync=True)
21 # TODO: make this tuple serialize models
22 # TODO: enforce that tuples here have a single datatype
23 children = Tuple(sync=True, **widget_serialization)
22 24
23 25 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
24 26 overflow_x = CaselessStrEnum(
25 27 values=_overflow_values,
26 28 default_value='', sync=True, help="""Specifies what
27 29 happens to content that is too large for the rendered region.""")
28 30 overflow_y = CaselessStrEnum(
29 31 values=_overflow_values,
30 32 default_value='', sync=True, help="""Specifies what
31 33 happens to content that is too large for the rendered region.""")
32 34
33 35 box_style = CaselessStrEnum(
34 36 values=['success', 'info', 'warning', 'danger', ''],
35 37 default_value='', allow_none=True, sync=True, help="""Use a
36 38 predefined styling for the box.""")
37 39
38 40 def __init__(self, children = (), **kwargs):
39 41 kwargs['children'] = children
40 42 super(Box, self).__init__(**kwargs)
41 43 self.on_displayed(Box._fire_children_displayed)
42 44
43 45 def _fire_children_displayed(self):
44 46 for child in self.children:
45 47 child._handle_displayed()
46 48
47 49
48 50 @register('IPython.FlexBox')
49 51 class FlexBox(Box):
50 52 """Displays multiple widgets using the flexible box model."""
51 53 _view_name = Unicode('FlexBoxView', sync=True)
52 54 orientation = CaselessStrEnum(values=['vertical', 'horizontal'], default_value='vertical', sync=True)
53 55 flex = Int(0, sync=True, help="""Specify the flexible-ness of the model.""")
54 56 def _flex_changed(self, name, old, new):
55 57 new = min(max(0, new), 2)
56 58 if self.flex != new:
57 59 self.flex = new
58 60
59 61 _locations = ['start', 'center', 'end', 'baseline', 'stretch']
60 62 pack = CaselessStrEnum(
61 63 values=_locations,
62 64 default_value='start', sync=True)
63 65 align = CaselessStrEnum(
64 66 values=_locations,
65 67 default_value='start', sync=True)
66 68
67 69
68 70 def VBox(*pargs, **kwargs):
69 71 """Displays multiple widgets vertically using the flexible box model."""
70 72 kwargs['orientation'] = 'vertical'
71 73 return FlexBox(*pargs, **kwargs)
72 74
73 75 def HBox(*pargs, **kwargs):
74 76 """Displays multiple widgets horizontally using the flexible box model."""
75 77 kwargs['orientation'] = 'horizontal'
76 78 return FlexBox(*pargs, **kwargs)
77 79
78 80
79 81 # Remove in IPython 4.0
80 82 ContainerWidget = DeprecatedClass(Box, 'ContainerWidget')
@@ -1,82 +1,82 b''
1 1 """Button class.
2 2
3 3 Represents a button in the frontend using a widget. Allows user to listen for
4 4 click events on the button and trigger backend code when the clicks are fired.
5 5 """
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (c) 2013, the IPython Development Team.
8 8 #
9 9 # Distributed under the terms of the Modified BSD License.
10 10 #
11 11 # The full license is in the file COPYING.txt, distributed with this software.
12 12 #-----------------------------------------------------------------------------
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Imports
16 16 #-----------------------------------------------------------------------------
17 17 from .widget import DOMWidget, CallbackDispatcher, register
18 18 from IPython.utils.traitlets import Unicode, Bool, CaselessStrEnum
19 19 from IPython.utils.warn import DeprecatedClass
20 20
21 21 #-----------------------------------------------------------------------------
22 22 # Classes
23 23 #-----------------------------------------------------------------------------
24 24 @register('IPython.Button')
25 25 class Button(DOMWidget):
26 26 """Button widget.
27 27 This widget has an `on_click` method that allows you to listen for the
28 28 user clicking on the button. The click event itself is stateless.
29 29
30 30 Parameters
31 31 ----------
32 32 description : str
33 33 description displayed next to the button
34 34 tooltip: str
35 35 tooltip caption of the toggle button
36 36 icon: str
37 37 font-awesome icon name
38 38 """
39 39 _view_name = Unicode('ButtonView', sync=True)
40 40
41 41 # Keys
42 42 description = Unicode('', help="Button label.", sync=True)
43 43 tooltip = Unicode(help="Tooltip caption of the button.", sync=True)
44 44 disabled = Bool(False, help="Enable or disable user changes.", sync=True)
45 45 icon = Unicode('', help= "Font-awesome icon.", sync=True)
46 46
47 47 button_style = CaselessStrEnum(
48 48 values=['primary', 'success', 'info', 'warning', 'danger', ''],
49 49 default_value='', allow_none=True, sync=True, help="""Use a
50 50 predefined styling for the button.""")
51 51
52 52 def __init__(self, **kwargs):
53 53 """Constructor"""
54 54 super(Button, self).__init__(**kwargs)
55 55 self._click_handlers = CallbackDispatcher()
56 56 self.on_msg(self._handle_button_msg)
57 57
58 58 def on_click(self, callback, remove=False):
59 59 """Register a callback to execute when the button is clicked.
60 60
61 61 The callback will be called with one argument,
62 62 the clicked button widget instance.
63 63
64 64 Parameters
65 65 ----------
66 66 remove : bool (optional)
67 67 Set to true to remove the callback from the list of callbacks."""
68 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 71 """Handle a msg from the front-end.
72 72
73 73 Parameters
74 74 ----------
75 75 content: dict
76 76 Content of the msg."""
77 77 if content.get('event', '') == 'click':
78 78 self._click_handlers(self)
79 79
80 80
81 81 # Remove in IPython 4.0
82 82 ButtonWidget = DeprecatedClass(Button, 'ButtonWidget')
General Comments 0
You need to be logged in to leave comments. Login now