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