##// END OF EJS Templates
Merge pull request #7097 from jasongrout/widget-visibility...
Jonathan Frederic -
r19417:6daf9b39 merge
parent child Browse files
Show More
@@ -1,735 +1,742 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
12 12 var WidgetModel = Backbone.Model.extend({
13 13 constructor: function (widget_manager, model_id, comm) {
14 14 /**
15 15 * Constructor
16 16 *
17 17 * Creates a WidgetModel instance.
18 18 *
19 19 * Parameters
20 20 * ----------
21 21 * widget_manager : WidgetManager instance
22 22 * model_id : string
23 23 * An ID unique to this model.
24 24 * comm : Comm instance (optional)
25 25 */
26 26 this.widget_manager = widget_manager;
27 27 this.state_change = Promise.resolve();
28 28 this._buffered_state_diff = {};
29 29 this.pending_msgs = 0;
30 30 this.msg_buffer = null;
31 31 this.state_lock = null;
32 32 this.id = model_id;
33 33 this.views = {};
34 34 this._resolve_received_state = {};
35 35
36 36 if (comm !== undefined) {
37 37 // Remember comm associated with the model.
38 38 this.comm = comm;
39 39 comm.model = this;
40 40
41 41 // Hook comm messages up to model.
42 42 comm.on_close($.proxy(this._handle_comm_closed, this));
43 43 comm.on_msg($.proxy(this._handle_comm_msg, this));
44 44
45 45 // Assume the comm is alive.
46 46 this.set_comm_live(true);
47 47 } else {
48 48 this.set_comm_live(false);
49 49 }
50 50 return Backbone.Model.apply(this);
51 51 },
52 52
53 53 send: function (content, callbacks) {
54 54 /**
55 55 * Send a custom msg over the comm.
56 56 */
57 57 if (this.comm !== undefined) {
58 58 var data = {method: 'custom', content: content};
59 59 this.comm.send(data, callbacks);
60 60 this.pending_msgs++;
61 61 }
62 62 },
63 63
64 64 request_state: function(callbacks) {
65 65 /**
66 66 * Request a state push from the back-end.
67 67 */
68 68 if (!this.comm) {
69 69 console.error("Could not request_state because comm doesn't exist!");
70 70 return;
71 71 }
72 72
73 73 var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
74 74
75 75 // Promise that is resolved when a state is received
76 76 // from the back-end.
77 77 var that = this;
78 78 var received_state = new Promise(function(resolve) {
79 79 that._resolve_received_state[msg_id] = resolve;
80 80 });
81 81 return received_state;
82 82 },
83 83
84 84 set_comm_live: function(live) {
85 85 /**
86 86 * Change the comm_live state of the model.
87 87 */
88 88 if (this.comm_live === undefined || this.comm_live != live) {
89 89 this.comm_live = live;
90 90 this.trigger(live ? 'comm:live' : 'comm:dead', {model: this});
91 91 }
92 92 },
93 93
94 94 close: function(comm_closed) {
95 95 /**
96 96 * Close model
97 97 */
98 98 if (this.comm && !comm_closed) {
99 99 this.comm.close();
100 100 }
101 101 this.stopListening();
102 102 this.trigger('destroy', this);
103 103 delete this.comm.model; // Delete ref so GC will collect widget model.
104 104 delete this.comm;
105 105 delete this.model_id; // Delete id from model so widget manager cleans up.
106 106 _.each(this.views, function(v, id, views) {
107 107 v.then(function(view) {
108 108 view.remove();
109 109 delete views[id];
110 110 });
111 111 });
112 112 },
113 113
114 114 _handle_comm_closed: function (msg) {
115 115 /**
116 116 * Handle when a widget is closed.
117 117 */
118 118 this.trigger('comm:close');
119 119 this.close(true);
120 120 },
121 121
122 122 _handle_comm_msg: function (msg) {
123 123 /**
124 124 * Handle incoming comm msg.
125 125 */
126 126 var method = msg.content.data.method;
127 127 var that = this;
128 128 switch (method) {
129 129 case 'update':
130 130 this.state_change = this.state_change
131 131 .then(function() {
132 132 return that.set_state(msg.content.data.state);
133 133 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
134 134 .then(function() {
135 135 var parent_id = msg.parent_header.msg_id;
136 136 if (that._resolve_received_state[parent_id] !== undefined) {
137 137 that._resolve_received_state[parent_id].call();
138 138 delete that._resolve_received_state[parent_id];
139 139 }
140 140 }).catch(utils.reject("Couldn't resolve state request promise", true));
141 141 break;
142 142 case 'custom':
143 143 this.trigger('msg:custom', msg.content.data.content);
144 144 break;
145 145 case 'display':
146 146 this.widget_manager.display_view(msg, this)
147 147 .catch(utils.reject('Could not process display view msg', true));
148 148 break;
149 149 }
150 150 },
151 151
152 152 set_state: function (state) {
153 153 var that = this;
154 154 // Handle when a widget is updated via the python side.
155 155 return this._unpack_models(state).then(function(state) {
156 156 that.state_lock = state;
157 157 try {
158 158 WidgetModel.__super__.set.call(that, state);
159 159 } finally {
160 160 that.state_lock = null;
161 161 }
162 162 }).catch(utils.reject("Couldn't set model state", true));
163 163 },
164 164
165 165 get_state: function() {
166 166 // Get the serializable state of the model.
167 167 state = this.toJSON();
168 168 for (var key in state) {
169 169 if (state.hasOwnProperty(key)) {
170 170 state[key] = this._pack_models(state[key]);
171 171 }
172 172 }
173 173 return state;
174 174 },
175 175
176 176 _handle_status: function (msg, callbacks) {
177 177 /**
178 178 * Handle status msgs.
179 179 *
180 180 * execution_state : ('busy', 'idle', 'starting')
181 181 */
182 182 if (this.comm !== undefined) {
183 183 if (msg.content.execution_state ==='idle') {
184 184 // Send buffer if this message caused another message to be
185 185 // throttled.
186 186 if (this.msg_buffer !== null &&
187 187 (this.get('msg_throttle') || 3) === this.pending_msgs) {
188 188 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
189 189 this.comm.send(data, callbacks);
190 190 this.msg_buffer = null;
191 191 } else {
192 192 --this.pending_msgs;
193 193 }
194 194 }
195 195 }
196 196 },
197 197
198 198 callbacks: function(view) {
199 199 /**
200 200 * Create msg callbacks for a comm msg.
201 201 */
202 202 var callbacks = this.widget_manager.callbacks(view);
203 203
204 204 if (callbacks.iopub === undefined) {
205 205 callbacks.iopub = {};
206 206 }
207 207
208 208 var that = this;
209 209 callbacks.iopub.status = function (msg) {
210 210 that._handle_status(msg, callbacks);
211 211 };
212 212 return callbacks;
213 213 },
214 214
215 215 set: function(key, val, options) {
216 216 /**
217 217 * Set a value.
218 218 */
219 219 var return_value = WidgetModel.__super__.set.apply(this, arguments);
220 220
221 221 // Backbone only remembers the diff of the most recent set()
222 222 // operation. Calling set multiple times in a row results in a
223 223 // loss of diff information. Here we keep our own running diff.
224 224 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
225 225 return return_value;
226 226 },
227 227
228 228 sync: function (method, model, options) {
229 229 /**
230 230 * Handle sync to the back-end. Called when a model.save() is called.
231 231 *
232 232 * Make sure a comm exists.
233 233 */
234 234 var error = options.error || function() {
235 235 console.error('Backbone sync error:', arguments);
236 236 };
237 237 if (this.comm === undefined) {
238 238 error();
239 239 return false;
240 240 }
241 241
242 242 // Delete any key value pairs that the back-end already knows about.
243 243 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
244 244 if (this.state_lock !== null) {
245 245 var keys = Object.keys(this.state_lock);
246 246 for (var i=0; i<keys.length; i++) {
247 247 var key = keys[i];
248 248 if (attrs[key] === this.state_lock[key]) {
249 249 delete attrs[key];
250 250 }
251 251 }
252 252 }
253 253
254 254 // Only sync if there are attributes to send to the back-end.
255 255 attrs = this._pack_models(attrs);
256 256 if (_.size(attrs) > 0) {
257 257
258 258 // If this message was sent via backbone itself, it will not
259 259 // have any callbacks. It's important that we create callbacks
260 260 // so we can listen for status messages, etc...
261 261 var callbacks = options.callbacks || this.callbacks();
262 262
263 263 // Check throttle.
264 264 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
265 265 // The throttle has been exceeded, buffer the current msg so
266 266 // it can be sent once the kernel has finished processing
267 267 // some of the existing messages.
268 268
269 269 // Combine updates if it is a 'patch' sync, otherwise replace updates
270 270 switch (method) {
271 271 case 'patch':
272 272 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
273 273 break;
274 274 case 'update':
275 275 case 'create':
276 276 this.msg_buffer = attrs;
277 277 break;
278 278 default:
279 279 error();
280 280 return false;
281 281 }
282 282 this.msg_buffer_callbacks = callbacks;
283 283
284 284 } else {
285 285 // We haven't exceeded the throttle, send the message like
286 286 // normal.
287 287 var data = {method: 'backbone', sync_data: attrs};
288 288 this.comm.send(data, callbacks);
289 289 this.pending_msgs++;
290 290 }
291 291 }
292 292 // Since the comm is a one-way communication, assume the message
293 293 // arrived. Don't call success since we don't have a model back from the server
294 294 // this means we miss out on the 'sync' event.
295 295 this._buffered_state_diff = {};
296 296 },
297 297
298 298 save_changes: function(callbacks) {
299 299 /**
300 300 * Push this model's state to the back-end
301 301 *
302 302 * This invokes a Backbone.Sync.
303 303 */
304 304 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
305 305 },
306 306
307 307 _pack_models: function(value) {
308 308 /**
309 309 * Replace models with model ids recursively.
310 310 */
311 311 var that = this;
312 312 var packed;
313 313 if (value instanceof Backbone.Model) {
314 314 return "IPY_MODEL_" + value.id;
315 315
316 316 } else if ($.isArray(value)) {
317 317 packed = [];
318 318 _.each(value, function(sub_value, key) {
319 319 packed.push(that._pack_models(sub_value));
320 320 });
321 321 return packed;
322 322 } else if (value instanceof Date || value instanceof String) {
323 323 return value;
324 324 } else if (value instanceof Object) {
325 325 packed = {};
326 326 _.each(value, function(sub_value, key) {
327 327 packed[key] = that._pack_models(sub_value);
328 328 });
329 329 return packed;
330 330
331 331 } else {
332 332 return value;
333 333 }
334 334 },
335 335
336 336 _unpack_models: function(value) {
337 337 /**
338 338 * Replace model ids with models recursively.
339 339 */
340 340 var that = this;
341 341 var unpacked;
342 342 if ($.isArray(value)) {
343 343 unpacked = [];
344 344 _.each(value, function(sub_value, key) {
345 345 unpacked.push(that._unpack_models(sub_value));
346 346 });
347 347 return Promise.all(unpacked);
348 348 } else if (value instanceof Object) {
349 349 unpacked = {};
350 350 _.each(value, function(sub_value, key) {
351 351 unpacked[key] = that._unpack_models(sub_value);
352 352 });
353 353 return utils.resolve_promises_dict(unpacked);
354 354 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
355 355 // get_model returns a promise already
356 356 return this.widget_manager.get_model(value.slice(10, value.length));
357 357 } else {
358 358 return Promise.resolve(value);
359 359 }
360 360 },
361 361
362 362 on_some_change: function(keys, callback, context) {
363 363 /**
364 364 * on_some_change(["key1", "key2"], foo, context) differs from
365 365 * on("change:key1 change:key2", foo, context).
366 366 * If the widget attributes key1 and key2 are both modified,
367 367 * the second form will result in foo being called twice
368 368 * while the first will call foo only once.
369 369 */
370 370 this.on('change', function() {
371 371 if (keys.some(this.hasChanged, this)) {
372 372 callback.apply(context);
373 373 }
374 374 }, this);
375 375
376 376 },
377 377 });
378 378 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
379 379
380 380
381 381 var WidgetView = Backbone.View.extend({
382 382 initialize: function(parameters) {
383 383 /**
384 384 * Public constructor.
385 385 */
386 386 this.model.on('change',this.update,this);
387 387 this.options = parameters.options;
388 388 this.on('displayed', function() {
389 389 this.is_displayed = true;
390 390 }, this);
391 391 },
392 392
393 393 update: function(){
394 394 /**
395 395 * Triggered on model change.
396 396 *
397 397 * Update view to be consistent with this.model
398 398 */
399 399 },
400 400
401 401 create_child_view: function(child_model, options) {
402 402 /**
403 403 * Create and promise that resolves to a child view of a given model
404 404 */
405 405 var that = this;
406 406 options = $.extend({ parent: this }, options || {});
407 407 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
408 408 },
409 409
410 410 callbacks: function(){
411 411 /**
412 412 * Create msg callbacks for a comm msg.
413 413 */
414 414 return this.model.callbacks(this);
415 415 },
416 416
417 417 render: function(){
418 418 /**
419 419 * Render the view.
420 420 *
421 421 * By default, this is only called the first time the view is created
422 422 */
423 423 },
424 424
425 425 send: function (content) {
426 426 /**
427 427 * Send a custom msg associated with this view.
428 428 */
429 429 this.model.send(content, this.callbacks());
430 430 },
431 431
432 432 touch: function () {
433 433 this.model.save_changes(this.callbacks());
434 434 },
435 435
436 436 after_displayed: function (callback, context) {
437 437 /**
438 438 * Calls the callback right away is the view is already displayed
439 439 * otherwise, register the callback to the 'displayed' event.
440 440 */
441 441 if (this.is_displayed) {
442 442 callback.apply(context);
443 443 } else {
444 444 this.on('displayed', callback, context);
445 445 }
446 446 },
447 447
448 448 remove: function () {
449 449 // Raise a remove event when the view is removed.
450 450 WidgetView.__super__.remove.apply(this, arguments);
451 451 this.trigger('remove');
452 452 }
453 453 });
454 454
455 455
456 456 var DOMWidgetView = WidgetView.extend({
457 457 initialize: function (parameters) {
458 458 /**
459 459 * Public constructor
460 460 */
461 461 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
462 462 this.model.on('change:visible', this.update_visible, this);
463 463 this.model.on('change:_css', this.update_css, this);
464 464
465 465 this.model.on('change:_dom_classes', function(model, new_classes) {
466 466 var old_classes = model.previous('_dom_classes');
467 467 this.update_classes(old_classes, new_classes);
468 468 }, this);
469 469
470 470 this.model.on('change:color', function (model, value) {
471 471 this.update_attr('color', value); }, this);
472 472
473 473 this.model.on('change:background_color', function (model, value) {
474 474 this.update_attr('background', value); }, this);
475 475
476 476 this.model.on('change:width', function (model, value) {
477 477 this.update_attr('width', value); }, this);
478 478
479 479 this.model.on('change:height', function (model, value) {
480 480 this.update_attr('height', value); }, this);
481 481
482 482 this.model.on('change:border_color', function (model, value) {
483 483 this.update_attr('border-color', value); }, this);
484 484
485 485 this.model.on('change:border_width', function (model, value) {
486 486 this.update_attr('border-width', value); }, this);
487 487
488 488 this.model.on('change:border_style', function (model, value) {
489 489 this.update_attr('border-style', value); }, this);
490 490
491 491 this.model.on('change:font_style', function (model, value) {
492 492 this.update_attr('font-style', value); }, this);
493 493
494 494 this.model.on('change:font_weight', function (model, value) {
495 495 this.update_attr('font-weight', value); }, this);
496 496
497 497 this.model.on('change:font_size', function (model, value) {
498 498 this.update_attr('font-size', this._default_px(value)); }, this);
499 499
500 500 this.model.on('change:font_family', function (model, value) {
501 501 this.update_attr('font-family', value); }, this);
502 502
503 503 this.model.on('change:padding', function (model, value) {
504 504 this.update_attr('padding', value); }, this);
505 505
506 506 this.model.on('change:margin', function (model, value) {
507 507 this.update_attr('margin', this._default_px(value)); }, this);
508 508
509 509 this.model.on('change:border_radius', function (model, value) {
510 510 this.update_attr('border-radius', this._default_px(value)); }, this);
511 511
512 512 this.after_displayed(function() {
513 513 this.update_visible(this.model, this.model.get("visible"));
514 514 this.update_classes([], this.model.get('_dom_classes'));
515 515
516 516 this.update_attr('color', this.model.get('color'));
517 517 this.update_attr('background', this.model.get('background_color'));
518 518 this.update_attr('width', this.model.get('width'));
519 519 this.update_attr('height', this.model.get('height'));
520 520 this.update_attr('border-color', this.model.get('border_color'));
521 521 this.update_attr('border-width', this.model.get('border_width'));
522 522 this.update_attr('border-style', this.model.get('border_style'));
523 523 this.update_attr('font-style', this.model.get('font_style'));
524 524 this.update_attr('font-weight', this.model.get('font_weight'));
525 525 this.update_attr('font-size', this.model.get('font_size'));
526 526 this.update_attr('font-family', this.model.get('font_family'));
527 527 this.update_attr('padding', this.model.get('padding'));
528 528 this.update_attr('margin', this.model.get('margin'));
529 529 this.update_attr('border-radius', this.model.get('border_radius'));
530 530
531 531 this.update_css(this.model, this.model.get("_css"));
532 532 }, this);
533 533 },
534 534
535 535 _default_px: function(value) {
536 536 /**
537 537 * Makes browser interpret a numerical string as a pixel value.
538 538 */
539 539 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
540 540 return value.trim() + 'px';
541 541 }
542 542 return value;
543 543 },
544 544
545 545 update_attr: function(name, value) {
546 546 /**
547 547 * Set a css attr of the widget view.
548 548 */
549 549 this.$el.css(name, value);
550 550 },
551 551
552 552 update_visible: function(model, value) {
553 553 /**
554 554 * Update visibility
555 555 */
556 this.$el.toggle(value);
556 switch(value) {
557 case null: // python None
558 this.$el.show().css('visibility', 'hidden'); break;
559 case false:
560 this.$el.hide(); break;
561 case true:
562 this.$el.show().css('visibility', ''); break;
563 }
557 564 },
558 565
559 566 update_css: function (model, css) {
560 567 /**
561 568 * Update the css styling of this view.
562 569 */
563 570 var e = this.$el;
564 571 if (css === undefined) {return;}
565 572 for (var i = 0; i < css.length; i++) {
566 573 // Apply the css traits to all elements that match the selector.
567 574 var selector = css[i][0];
568 575 var elements = this._get_selector_element(selector);
569 576 if (elements.length > 0) {
570 577 var trait_key = css[i][1];
571 578 var trait_value = css[i][2];
572 579 elements.css(trait_key ,trait_value);
573 580 }
574 581 }
575 582 },
576 583
577 584 update_classes: function (old_classes, new_classes, $el) {
578 585 /**
579 586 * Update the DOM classes applied to an element, default to this.$el.
580 587 */
581 588 if ($el===undefined) {
582 589 $el = this.$el;
583 590 }
584 591 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
585 592 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
586 593 },
587 594
588 595 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
589 596 /**
590 597 * Update the DOM classes applied to the widget based on a single
591 598 * trait's value.
592 599 *
593 600 * Given a trait value classes map, this function automatically
594 601 * handles applying the appropriate classes to the widget element
595 602 * and removing classes that are no longer valid.
596 603 *
597 604 * Parameters
598 605 * ----------
599 606 * class_map: dictionary
600 607 * Dictionary of trait values to class lists.
601 608 * Example:
602 609 * {
603 610 * success: ['alert', 'alert-success'],
604 611 * info: ['alert', 'alert-info'],
605 612 * warning: ['alert', 'alert-warning'],
606 613 * danger: ['alert', 'alert-danger']
607 614 * };
608 615 * trait_name: string
609 616 * Name of the trait to check the value of.
610 617 * previous_trait_value: optional string, default ''
611 618 * Last trait value
612 619 * $el: optional jQuery element handle, defaults to this.$el
613 620 * Element that the classes are applied to.
614 621 */
615 622 var key = previous_trait_value;
616 623 if (key === undefined) {
617 624 key = this.model.previous(trait_name);
618 625 }
619 626 var old_classes = class_map[key] ? class_map[key] : [];
620 627 key = this.model.get(trait_name);
621 628 var new_classes = class_map[key] ? class_map[key] : [];
622 629
623 630 this.update_classes(old_classes, new_classes, $el || this.$el);
624 631 },
625 632
626 633 _get_selector_element: function (selector) {
627 634 /**
628 635 * Get the elements via the css selector.
629 636 */
630 637 var elements;
631 638 if (!selector) {
632 639 elements = this.$el;
633 640 } else {
634 641 elements = this.$el.find(selector).addBack(selector);
635 642 }
636 643 return elements;
637 644 },
638 645
639 646 typeset: function(element, text){
640 647 utils.typeset.apply(null, arguments);
641 648 },
642 649 });
643 650
644 651
645 652 var ViewList = function(create_view, remove_view, context) {
646 653 /**
647 654 * - create_view and remove_view are default functions called when adding or removing views
648 655 * - create_view takes a model and returns a view or a promise for a view for that model
649 656 * - remove_view takes a view and destroys it (including calling `view.remove()`)
650 657 * - each time the update() function is called with a new list, the create and remove
651 658 * callbacks will be called in an order so that if you append the views created in the
652 659 * create callback and remove the views in the remove callback, you will duplicate
653 660 * the order of the list.
654 661 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
655 662 * - the context defaults to the created ViewList. If you pass another context, the create and remove
656 663 * will be called in that context.
657 664 */
658 665
659 666 this.initialize.apply(this, arguments);
660 667 };
661 668
662 669 _.extend(ViewList.prototype, {
663 670 initialize: function(create_view, remove_view, context) {
664 671 this._handler_context = context || this;
665 672 this._models = [];
666 673 this.views = []; // list of promises for views
667 674 this._create_view = create_view;
668 675 this._remove_view = remove_view || function(view) {view.remove();};
669 676 },
670 677
671 678 update: function(new_models, create_view, remove_view, context) {
672 679 /**
673 680 * the create_view, remove_view, and context arguments override the defaults
674 681 * specified when the list is created.
675 682 * after this function, the .views attribute is a list of promises for views
676 683 * if you want to perform some action on the list of views, do something like
677 684 * `Promise.all(myviewlist.views).then(function(views) {...});`
678 685 */
679 686 var remove = remove_view || this._remove_view;
680 687 var create = create_view || this._create_view;
681 688 var context = context || this._handler_context;
682 689 var i = 0;
683 690 // first, skip past the beginning of the lists if they are identical
684 691 for (; i < new_models.length; i++) {
685 692 if (i >= this._models.length || new_models[i] !== this._models[i]) {
686 693 break;
687 694 }
688 695 }
689 696
690 697 var first_removed = i;
691 698 // Remove the non-matching items from the old list.
692 699 var removed = this.views.splice(first_removed, this.views.length-first_removed);
693 700 for (var j = 0; j < removed.length; j++) {
694 701 removed[j].then(function(view) {
695 702 remove.call(context, view)
696 703 });
697 704 }
698 705
699 706 // Add the rest of the new list items.
700 707 for (; i < new_models.length; i++) {
701 708 this.views.push(Promise.resolve(create.call(context, new_models[i])));
702 709 }
703 710 // make a copy of the input array
704 711 this._models = new_models.slice();
705 712 },
706 713
707 714 remove: function() {
708 715 /**
709 716 * removes every view in the list; convenience function for `.update([])`
710 717 * that should be faster
711 718 * returns a promise that resolves after this removal is done
712 719 */
713 720 var that = this;
714 721 return Promise.all(this.views).then(function(views) {
715 722 for (var i = 0; i < that.views.length; i++) {
716 723 that._remove_view.call(that._handler_context, views[i]);
717 724 }
718 725 that.views = [];
719 726 that._models = [];
720 727 });
721 728 },
722 729 });
723 730
724 731 var widget = {
725 732 'WidgetModel': WidgetModel,
726 733 'WidgetView': WidgetView,
727 734 'DOMWidgetView': DOMWidgetView,
728 735 'ViewList': ViewList,
729 736 };
730 737
731 738 // For backwards compatability.
732 739 $.extend(IPython, widget);
733 740
734 741 return widget;
735 742 });
@@ -1,489 +1,489 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
26 26 #-----------------------------------------------------------------------------
27 27 # Classes
28 28 #-----------------------------------------------------------------------------
29 29 class CallbackDispatcher(LoggingConfigurable):
30 30 """A structure for registering and running callbacks"""
31 31 callbacks = List()
32 32
33 33 def __call__(self, *args, **kwargs):
34 34 """Call all of the registered callbacks."""
35 35 value = None
36 36 for callback in self.callbacks:
37 37 try:
38 38 local_value = callback(*args, **kwargs)
39 39 except Exception as e:
40 40 ip = get_ipython()
41 41 if ip is None:
42 42 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
43 43 else:
44 44 ip.showtraceback()
45 45 else:
46 46 value = local_value if local_value is not None else value
47 47 return value
48 48
49 49 def register_callback(self, callback, remove=False):
50 50 """(Un)Register a callback
51 51
52 52 Parameters
53 53 ----------
54 54 callback: method handle
55 55 Method to be registered or unregistered.
56 56 remove=False: bool
57 57 Whether to unregister the callback."""
58 58
59 59 # (Un)Register the callback.
60 60 if remove and callback in self.callbacks:
61 61 self.callbacks.remove(callback)
62 62 elif not remove and callback not in self.callbacks:
63 63 self.callbacks.append(callback)
64 64
65 65 def _show_traceback(method):
66 66 """decorator for showing tracebacks in IPython"""
67 67 def m(self, *args, **kwargs):
68 68 try:
69 69 return(method(self, *args, **kwargs))
70 70 except Exception as e:
71 71 ip = get_ipython()
72 72 if ip is None:
73 73 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
74 74 else:
75 75 ip.showtraceback()
76 76 return m
77 77
78 78
79 79 def register(key=None):
80 80 """Returns a decorator registering a widget class in the widget registry.
81 81 If no key is provided, the class name is used as a key. A key is
82 82 provided for each core IPython widget so that the frontend can use
83 83 this key regardless of the language of the kernel"""
84 84 def wrap(widget):
85 85 l = key if key is not None else widget.__module__ + widget.__name__
86 86 Widget.widget_types[l] = widget
87 87 return widget
88 88 return wrap
89 89
90 90
91 91 class Widget(LoggingConfigurable):
92 92 #-------------------------------------------------------------------------
93 93 # Class attributes
94 94 #-------------------------------------------------------------------------
95 95 _widget_construction_callback = None
96 96 widgets = {}
97 97 widget_types = {}
98 98
99 99 @staticmethod
100 100 def on_widget_constructed(callback):
101 101 """Registers a callback to be called when a widget is constructed.
102 102
103 103 The callback must have the following signature:
104 104 callback(widget)"""
105 105 Widget._widget_construction_callback = callback
106 106
107 107 @staticmethod
108 108 def _call_widget_constructed(widget):
109 109 """Static method, called when a widget is constructed."""
110 110 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
111 111 Widget._widget_construction_callback(widget)
112 112
113 113 @staticmethod
114 114 def handle_comm_opened(comm, msg):
115 115 """Static method, called when a widget is constructed."""
116 116 widget_class = import_item(msg['content']['data']['widget_class'])
117 117 widget = widget_class(comm=comm)
118 118
119 119
120 120 #-------------------------------------------------------------------------
121 121 # Traits
122 122 #-------------------------------------------------------------------------
123 123 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
124 124 in which to find _model_name. If empty, look in the global registry.""")
125 125 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
126 126 registered in the front-end to create and sync this widget with.""")
127 127 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
128 128 If empty, look in the global registry.""", sync=True)
129 129 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
130 130 to use to represent the widget.""", sync=True)
131 131 comm = Instance('IPython.kernel.comm.Comm')
132 132
133 133 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
134 134 front-end can send before receiving an idle msg from the back-end.""")
135 135
136 136 version = Int(0, sync=True, help="""Widget's version""")
137 137 keys = List()
138 138 def _keys_default(self):
139 139 return [name for name in self.traits(sync=True)]
140 140
141 141 _property_lock = Tuple((None, None))
142 142 _send_state_lock = Int(0)
143 143 _states_to_send = Set(allow_none=False)
144 144 _display_callbacks = Instance(CallbackDispatcher, ())
145 145 _msg_callbacks = Instance(CallbackDispatcher, ())
146 146
147 147 #-------------------------------------------------------------------------
148 148 # (Con/de)structor
149 149 #-------------------------------------------------------------------------
150 150 def __init__(self, **kwargs):
151 151 """Public constructor"""
152 152 self._model_id = kwargs.pop('model_id', None)
153 153 super(Widget, self).__init__(**kwargs)
154 154
155 155 Widget._call_widget_constructed(self)
156 156 self.open()
157 157
158 158 def __del__(self):
159 159 """Object disposal"""
160 160 self.close()
161 161
162 162 #-------------------------------------------------------------------------
163 163 # Properties
164 164 #-------------------------------------------------------------------------
165 165
166 166 def open(self):
167 167 """Open a comm to the frontend if one isn't already open."""
168 168 if self.comm is None:
169 169 args = dict(target_name='ipython.widget',
170 170 data={'model_name': self._model_name,
171 171 'model_module': self._model_module})
172 172 if self._model_id is not None:
173 173 args['comm_id'] = self._model_id
174 174 self.comm = Comm(**args)
175 175
176 176 def _comm_changed(self, name, new):
177 177 """Called when the comm is changed."""
178 178 if new is None:
179 179 return
180 180 self._model_id = self.model_id
181 181
182 182 self.comm.on_msg(self._handle_msg)
183 183 Widget.widgets[self.model_id] = self
184 184
185 185 # first update
186 186 self.send_state()
187 187
188 188 @property
189 189 def model_id(self):
190 190 """Gets the model id of this widget.
191 191
192 192 If a Comm doesn't exist yet, a Comm will be created automagically."""
193 193 return self.comm.comm_id
194 194
195 195 #-------------------------------------------------------------------------
196 196 # Methods
197 197 #-------------------------------------------------------------------------
198 198
199 199 def close(self):
200 200 """Close method.
201 201
202 202 Closes the underlying comm.
203 203 When the comm is closed, all of the widget views are automatically
204 204 removed from the front-end."""
205 205 if self.comm is not None:
206 206 Widget.widgets.pop(self.model_id, None)
207 207 self.comm.close()
208 208 self.comm = None
209 209
210 210 def send_state(self, key=None):
211 211 """Sends the widget state, or a piece of it, to the front-end.
212 212
213 213 Parameters
214 214 ----------
215 215 key : unicode, or iterable (optional)
216 216 A single property's name or iterable of property names to sync with the front-end.
217 217 """
218 218 self._send({
219 219 "method" : "update",
220 220 "state" : self.get_state(key=key)
221 221 })
222 222
223 223 def get_state(self, key=None):
224 224 """Gets the widget state, or a piece of it.
225 225
226 226 Parameters
227 227 ----------
228 228 key : unicode or iterable (optional)
229 229 A single property's name or iterable of property names to get.
230 230 """
231 231 if key is None:
232 232 keys = self.keys
233 233 elif isinstance(key, string_types):
234 234 keys = [key]
235 235 elif isinstance(key, collections.Iterable):
236 236 keys = key
237 237 else:
238 238 raise ValueError("key must be a string, an iterable of keys, or None")
239 239 state = {}
240 240 for k in keys:
241 241 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
242 242 value = getattr(self, k)
243 243 state[k] = f(value)
244 244 return state
245 245
246 246 def set_state(self, sync_data):
247 247 """Called when a state is received from the front-end."""
248 248 for name in self.keys:
249 249 if name in sync_data:
250 250 json_value = sync_data[name]
251 251 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
252 252 with self._lock_property(name, json_value):
253 253 setattr(self, name, from_json(json_value))
254 254
255 255 def send(self, content):
256 256 """Sends a custom msg to the widget model in the front-end.
257 257
258 258 Parameters
259 259 ----------
260 260 content : dict
261 261 Content of the message to send.
262 262 """
263 263 self._send({"method": "custom", "content": content})
264 264
265 265 def on_msg(self, callback, remove=False):
266 266 """(Un)Register a custom msg receive callback.
267 267
268 268 Parameters
269 269 ----------
270 270 callback: callable
271 271 callback will be passed two arguments when a message arrives::
272 272
273 273 callback(widget, content)
274 274
275 275 remove: bool
276 276 True if the callback should be unregistered."""
277 277 self._msg_callbacks.register_callback(callback, remove=remove)
278 278
279 279 def on_displayed(self, callback, remove=False):
280 280 """(Un)Register a widget displayed callback.
281 281
282 282 Parameters
283 283 ----------
284 284 callback: method handler
285 285 Must have a signature of::
286 286
287 287 callback(widget, **kwargs)
288 288
289 289 kwargs from display are passed through without modification.
290 290 remove: bool
291 291 True if the callback should be unregistered."""
292 292 self._display_callbacks.register_callback(callback, remove=remove)
293 293
294 294 #-------------------------------------------------------------------------
295 295 # Support methods
296 296 #-------------------------------------------------------------------------
297 297 @contextmanager
298 298 def _lock_property(self, key, value):
299 299 """Lock a property-value pair.
300 300
301 301 The value should be the JSON state of the property.
302 302
303 303 NOTE: This, in addition to the single lock for all state changes, is
304 304 flawed. In the future we may want to look into buffering state changes
305 305 back to the front-end."""
306 306 self._property_lock = (key, value)
307 307 try:
308 308 yield
309 309 finally:
310 310 self._property_lock = (None, None)
311 311
312 312 @contextmanager
313 313 def hold_sync(self):
314 314 """Hold syncing any state until the context manager is released"""
315 315 # We increment a value so that this can be nested. Syncing will happen when
316 316 # all levels have been released.
317 317 self._send_state_lock += 1
318 318 try:
319 319 yield
320 320 finally:
321 321 self._send_state_lock -=1
322 322 if self._send_state_lock == 0:
323 323 self.send_state(self._states_to_send)
324 324 self._states_to_send.clear()
325 325
326 326 def _should_send_property(self, key, value):
327 327 """Check the property lock (property_lock)"""
328 328 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
329 329 if (key == self._property_lock[0]
330 330 and to_json(value) == self._property_lock[1]):
331 331 return False
332 332 elif self._send_state_lock > 0:
333 333 self._states_to_send.add(key)
334 334 return False
335 335 else:
336 336 return True
337 337
338 338 # Event handlers
339 339 @_show_traceback
340 340 def _handle_msg(self, msg):
341 341 """Called when a msg is received from the front-end"""
342 342 data = msg['content']['data']
343 343 method = data['method']
344 344
345 345 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
346 346 if method == 'backbone':
347 347 if 'sync_data' in data:
348 348 sync_data = data['sync_data']
349 349 self.set_state(sync_data) # handles all methods
350 350
351 351 # Handle a state request.
352 352 elif method == 'request_state':
353 353 self.send_state()
354 354
355 355 # Handle a custom msg from the front-end.
356 356 elif method == 'custom':
357 357 if 'content' in data:
358 358 self._handle_custom_msg(data['content'])
359 359
360 360 # Catch remainder.
361 361 else:
362 362 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
363 363
364 364 def _handle_custom_msg(self, content):
365 365 """Called when a custom msg is received."""
366 366 self._msg_callbacks(self, content)
367 367
368 368 def _notify_trait(self, name, old_value, new_value):
369 369 """Called when a property has been changed."""
370 370 # Trigger default traitlet callback machinery. This allows any user
371 371 # registered validation to be processed prior to allowing the widget
372 372 # machinery to handle the state.
373 373 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
374 374
375 375 # Send the state after the user registered callbacks for trait changes
376 376 # have all fired (allows for user to validate values).
377 377 if self.comm is not None and name in self.keys:
378 378 # Make sure this isn't information that the front-end just sent us.
379 379 if self._should_send_property(name, new_value):
380 380 # Send new state to front-end
381 381 self.send_state(key=name)
382 382
383 383 def _handle_displayed(self, **kwargs):
384 384 """Called when a view has been displayed for this widget instance"""
385 385 self._display_callbacks(self, **kwargs)
386 386
387 387 def _trait_to_json(self, x):
388 388 """Convert a trait value to json
389 389
390 390 Traverse lists/tuples and dicts and serialize their values as well.
391 391 Replace any widgets with their model_id
392 392 """
393 393 if isinstance(x, dict):
394 394 return {k: self._trait_to_json(v) for k, v in x.items()}
395 395 elif isinstance(x, (list, tuple)):
396 396 return [self._trait_to_json(v) for v in x]
397 397 elif isinstance(x, Widget):
398 398 return "IPY_MODEL_" + x.model_id
399 399 else:
400 400 return x # Value must be JSON-able
401 401
402 402 def _trait_from_json(self, x):
403 403 """Convert json values to objects
404 404
405 405 Replace any strings representing valid model id values to Widget references.
406 406 """
407 407 if isinstance(x, dict):
408 408 return {k: self._trait_from_json(v) for k, v in x.items()}
409 409 elif isinstance(x, (list, tuple)):
410 410 return [self._trait_from_json(v) for v in x]
411 411 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
412 412 # we want to support having child widgets at any level in a hierarchy
413 413 # trusting that a widget UUID will not appear out in the wild
414 414 return Widget.widgets[x[10:]]
415 415 else:
416 416 return x
417 417
418 418 def _ipython_display_(self, **kwargs):
419 419 """Called when `IPython.display.display` is called on the widget."""
420 420 # Show view.
421 421 if self._view_name is not None:
422 422 self._send({"method": "display"})
423 423 self._handle_displayed(**kwargs)
424 424
425 425 def _send(self, msg):
426 426 """Sends a message to the model in the front-end."""
427 427 self.comm.send(msg)
428 428
429 429
430 430 class DOMWidget(Widget):
431 visible = Bool(True, help="Whether the widget is visible.", sync=True)
431 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)
432 432 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
433 433 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
434 434
435 435 width = CUnicode(sync=True)
436 436 height = CUnicode(sync=True)
437 437 # A default padding of 2.5 px makes the widgets look nice when displayed inline.
438 438 padding = CUnicode("2.5px", sync=True)
439 439 margin = CUnicode(sync=True)
440 440
441 441 color = Unicode(sync=True)
442 442 background_color = Unicode(sync=True)
443 443 border_color = Unicode(sync=True)
444 444
445 445 border_width = CUnicode(sync=True)
446 446 border_radius = CUnicode(sync=True)
447 447 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
448 448 'none',
449 449 'hidden',
450 450 'dotted',
451 451 'dashed',
452 452 'solid',
453 453 'double',
454 454 'groove',
455 455 'ridge',
456 456 'inset',
457 457 'outset',
458 458 'initial',
459 459 'inherit', ''],
460 460 default_value='', sync=True)
461 461
462 462 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
463 463 'normal',
464 464 'italic',
465 465 'oblique',
466 466 'initial',
467 467 'inherit', ''],
468 468 default_value='', sync=True)
469 469 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
470 470 'normal',
471 471 'bold',
472 472 'bolder',
473 473 'lighter',
474 474 'initial',
475 475 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
476 476 default_value='', sync=True)
477 477 font_size = CUnicode(sync=True)
478 478 font_family = Unicode(sync=True)
479 479
480 480 def __init__(self, *pargs, **kwargs):
481 481 super(DOMWidget, self).__init__(*pargs, **kwargs)
482 482
483 483 def _validate_border(name, old, new):
484 484 if new is not None and new != '':
485 485 if name != 'border_width' and not self.border_width:
486 486 self.border_width = 1
487 487 if name != 'border_style' and self.border_style == '':
488 488 self.border_style = 'solid'
489 489 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
@@ -1,752 +1,775 b''
1 1 {
2 2 "cells": [
3 3 {
4 4 "cell_type": "markdown",
5 5 "metadata": {},
6 6 "source": [
7 7 "[Index](Index.ipynb) - [Back](Widget Events.ipynb) - [Next](Custom Widget - Hello World.ipynb)"
8 8 ]
9 9 },
10 10 {
11 11 "cell_type": "code",
12 12 "execution_count": null,
13 13 "metadata": {
14 14 "collapsed": false
15 15 },
16 16 "outputs": [],
17 17 "source": [
18 18 "%%html\n",
19 19 "<style>\n",
20 20 ".example-container { background: #999999; padding: 2px; min-height: 100px; }\n",
21 21 ".example-container.sm { min-height: 50px; }\n",
22 22 ".example-box { background: #9999FF; width: 50px; height: 50px; text-align: center; vertical-align: middle; color: white; font-weight: bold; margin: 2px;}\n",
23 23 ".example-box.med { width: 65px; height: 65px; } \n",
24 24 ".example-box.lrg { width: 80px; height: 80px; } \n",
25 25 "</style>"
26 26 ]
27 27 },
28 28 {
29 29 "cell_type": "code",
30 30 "execution_count": null,
31 31 "metadata": {
32 32 "collapsed": false
33 33 },
34 34 "outputs": [],
35 35 "source": [
36 36 "from IPython.html import widgets\n",
37 37 "from IPython.display import display"
38 38 ]
39 39 },
40 40 {
41 41 "cell_type": "markdown",
42 42 "metadata": {
43 43 "slideshow": {
44 44 "slide_type": "slide"
45 45 }
46 46 },
47 47 "source": [
48 48 "# Widget Styling"
49 49 ]
50 50 },
51 51 {
52 52 "cell_type": "markdown",
53 53 "metadata": {},
54 54 "source": [
55 55 "## Basic styling"
56 56 ]
57 57 },
58 58 {
59 59 "cell_type": "markdown",
60 60 "metadata": {},
61 61 "source": [
62 62 "The widgets distributed with IPython can be styled by setting the following traits:\n",
63 63 "\n",
64 64 "- width \n",
65 65 "- height \n",
66 66 "- fore_color \n",
67 67 "- back_color \n",
68 68 "- border_color \n",
69 69 "- border_width \n",
70 70 "- border_style \n",
71 71 "- font_style \n",
72 72 "- font_weight \n",
73 73 "- font_size \n",
74 74 "- font_family \n",
75 75 "\n",
76 76 "The example below shows how a `Button` widget can be styled:"
77 77 ]
78 78 },
79 79 {
80 80 "cell_type": "code",
81 81 "execution_count": null,
82 82 "metadata": {
83 83 "collapsed": false
84 84 },
85 85 "outputs": [],
86 86 "source": [
87 87 "button = widgets.Button(\n",
88 88 " description='Hello World!',\n",
89 89 " width=100, # Integers are interpreted as pixel measurements.\n",
90 90 " height='2em', # em is valid HTML unit of measurement.\n",
91 91 " color='lime', # Colors can be set by name,\n",
92 92 " background_color='#0022FF', # and also by color code.\n",
93 93 " border_color='red')\n",
94 94 "display(button)"
95 95 ]
96 96 },
97 97 {
98 98 "cell_type": "markdown",
99 99 "metadata": {
100 100 "slideshow": {
101 101 "slide_type": "slide"
102 102 }
103 103 },
104 104 "source": [
105 105 "## Parent/child relationships"
106 106 ]
107 107 },
108 108 {
109 109 "cell_type": "markdown",
110 110 "metadata": {},
111 111 "source": [
112 112 "To display widget A inside widget B, widget A must be a child of widget B. Widgets that can contain other widgets have a **`children` attribute**. This attribute can be **set via a keyword argument** in the widget's constructor **or after construction**. Calling display on an **object with children automatically displays those children**, too."
113 113 ]
114 114 },
115 115 {
116 116 "cell_type": "code",
117 117 "execution_count": null,
118 118 "metadata": {
119 119 "collapsed": false
120 120 },
121 121 "outputs": [],
122 122 "source": [
123 123 "from IPython.display import display\n",
124 124 "\n",
125 125 "float_range = widgets.FloatSlider()\n",
126 126 "string = widgets.Text(value='hi')\n",
127 127 "container = widgets.Box(children=[float_range, string])\n",
128 128 "\n",
129 129 "container.border_color = 'red'\n",
130 130 "container.border_style = 'dotted'\n",
131 131 "container.border_width = 3\n",
132 132 "display(container) # Displays the `container` and all of it's children."
133 133 ]
134 134 },
135 135 {
136 136 "cell_type": "markdown",
137 137 "metadata": {},
138 138 "source": [
139 139 "### After the parent is displayed"
140 140 ]
141 141 },
142 142 {
143 143 "cell_type": "markdown",
144 144 "metadata": {
145 145 "slideshow": {
146 146 "slide_type": "slide"
147 147 }
148 148 },
149 149 "source": [
150 150 "Children **can be added to parents** after the parent has been displayed. The **parent is responsible for rendering its children**."
151 151 ]
152 152 },
153 153 {
154 154 "cell_type": "code",
155 155 "execution_count": null,
156 156 "metadata": {
157 157 "collapsed": false
158 158 },
159 159 "outputs": [],
160 160 "source": [
161 161 "container = widgets.Box()\n",
162 162 "container.border_color = 'red'\n",
163 163 "container.border_style = 'dotted'\n",
164 164 "container.border_width = 3\n",
165 165 "display(container)\n",
166 166 "\n",
167 167 "int_range = widgets.IntSlider()\n",
168 168 "container.children=[int_range]"
169 169 ]
170 170 },
171 171 {
172 172 "cell_type": "markdown",
173 173 "metadata": {
174 174 "slideshow": {
175 175 "slide_type": "slide"
176 176 }
177 177 },
178 178 "source": [
179 179 "## Fancy boxes"
180 180 ]
181 181 },
182 182 {
183 183 "cell_type": "markdown",
184 184 "metadata": {},
185 185 "source": [
186 186 "If you need to display a more complicated set of widgets, there are **specialized containers** that you can use. To display **multiple sets of widgets**, you can use an **`Accordion` or a `Tab` in combination with one `Box` per set of widgets** (as seen below). The \"pages\" of these widgets are their children. To set the titles of the pages, one must **call `set_title` after the widget has been displayed**."
187 187 ]
188 188 },
189 189 {
190 190 "cell_type": "markdown",
191 191 "metadata": {},
192 192 "source": [
193 193 "### Accordion"
194 194 ]
195 195 },
196 196 {
197 197 "cell_type": "code",
198 198 "execution_count": null,
199 199 "metadata": {
200 200 "collapsed": false
201 201 },
202 202 "outputs": [],
203 203 "source": [
204 204 "name1 = widgets.Text(description='Location:')\n",
205 205 "zip1 = widgets.BoundedIntText(description='Zip:', min=0, max=99999)\n",
206 206 "page1 = widgets.Box(children=[name1, zip1])\n",
207 207 "\n",
208 208 "name2 = widgets.Text(description='Location:')\n",
209 209 "zip2 = widgets.BoundedIntText(description='Zip:', min=0, max=99999)\n",
210 210 "page2 = widgets.Box(children=[name2, zip2])\n",
211 211 "\n",
212 212 "accord = widgets.Accordion(children=[page1, page2])\n",
213 213 "display(accord)\n",
214 214 "\n",
215 215 "accord.set_title(0, 'From')\n",
216 216 "accord.set_title(1, 'To')"
217 217 ]
218 218 },
219 219 {
220 220 "cell_type": "markdown",
221 221 "metadata": {
222 222 "slideshow": {
223 223 "slide_type": "slide"
224 224 }
225 225 },
226 226 "source": [
227 227 "### TabWidget"
228 228 ]
229 229 },
230 230 {
231 231 "cell_type": "code",
232 232 "execution_count": null,
233 233 "metadata": {
234 234 "collapsed": false
235 235 },
236 236 "outputs": [],
237 237 "source": [
238 238 "name = widgets.Text(description='Name:')\n",
239 239 "color = widgets.Dropdown(description='Color:', values=['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'])\n",
240 240 "page1 = widgets.Box(children=[name, color])\n",
241 241 "\n",
242 242 "age = widgets.IntSlider(description='Age:', min=0, max=120, value=50)\n",
243 243 "gender = widgets.RadioButtons(description='Gender:', values=['male', 'female'])\n",
244 244 "page2 = widgets.Box(children=[age, gender])\n",
245 245 "\n",
246 246 "tabs = widgets.Tab(children=[page1, page2])\n",
247 247 "display(tabs)\n",
248 248 "\n",
249 249 "tabs.set_title(0, 'Name')\n",
250 250 "tabs.set_title(1, 'Details')"
251 251 ]
252 252 },
253 253 {
254 254 "cell_type": "markdown",
255 255 "metadata": {
256 256 "slideshow": {
257 257 "slide_type": "slide"
258 258 }
259 259 },
260 260 "source": [
261 261 "### Popup"
262 262 ]
263 263 },
264 264 {
265 265 "cell_type": "markdown",
266 266 "metadata": {},
267 267 "source": [
268 268 "Unlike the other two special containers, the `Popup` is only **designed to display one set of widgets**. The `Popup` can be used to **display widgets outside of the widget area**. "
269 269 ]
270 270 },
271 271 {
272 272 "cell_type": "code",
273 273 "execution_count": null,
274 274 "metadata": {
275 275 "collapsed": false
276 276 },
277 277 "outputs": [],
278 278 "source": [
279 279 "counter = widgets.IntText(description='Counter:')\n",
280 280 "popup = widgets.Popup(children=[counter], description='Popup Demo', button_text='Popup Button')\n",
281 281 "display(popup)"
282 282 ]
283 283 },
284 284 {
285 285 "cell_type": "code",
286 286 "execution_count": null,
287 287 "metadata": {
288 288 "collapsed": false
289 289 },
290 290 "outputs": [],
291 291 "source": [
292 292 "counter.value += 1"
293 293 ]
294 294 },
295 295 {
296 296 "cell_type": "code",
297 297 "execution_count": null,
298 298 "metadata": {
299 299 "collapsed": false
300 300 },
301 301 "outputs": [],
302 302 "source": []
303 303 },
304 304 {
305 305 "cell_type": "code",
306 306 "execution_count": null,
307 307 "metadata": {
308 308 "collapsed": false
309 309 },
310 310 "outputs": [],
311 311 "source": []
312 312 },
313 313 {
314 314 "cell_type": "code",
315 315 "execution_count": null,
316 316 "metadata": {
317 317 "collapsed": false
318 318 },
319 319 "outputs": [],
320 320 "source": []
321 321 },
322 322 {
323 323 "cell_type": "code",
324 324 "execution_count": null,
325 325 "metadata": {
326 326 "collapsed": false
327 327 },
328 328 "outputs": [],
329 329 "source": []
330 330 },
331 331 {
332 332 "cell_type": "code",
333 333 "execution_count": null,
334 334 "metadata": {
335 335 "collapsed": false
336 336 },
337 337 "outputs": [],
338 338 "source": []
339 339 },
340 340 {
341 341 "cell_type": "code",
342 342 "execution_count": null,
343 343 "metadata": {
344 344 "collapsed": false
345 345 },
346 346 "outputs": [],
347 347 "source": []
348 348 },
349 349 {
350 350 "cell_type": "code",
351 351 "execution_count": null,
352 352 "metadata": {
353 353 "collapsed": false
354 354 },
355 355 "outputs": [],
356 356 "source": []
357 357 },
358 358 {
359 359 "cell_type": "code",
360 360 "execution_count": null,
361 361 "metadata": {
362 362 "collapsed": false
363 363 },
364 364 "outputs": [],
365 365 "source": []
366 366 },
367 367 {
368 368 "cell_type": "code",
369 369 "execution_count": null,
370 370 "metadata": {
371 371 "collapsed": false
372 372 },
373 373 "outputs": [],
374 374 "source": []
375 375 },
376 376 {
377 377 "cell_type": "code",
378 378 "execution_count": null,
379 379 "metadata": {
380 380 "collapsed": false
381 381 },
382 382 "outputs": [],
383 383 "source": []
384 384 },
385 385 {
386 386 "cell_type": "code",
387 387 "execution_count": null,
388 388 "metadata": {
389 389 "collapsed": false
390 390 },
391 391 "outputs": [],
392 392 "source": []
393 393 },
394 394 {
395 395 "cell_type": "code",
396 396 "execution_count": null,
397 397 "metadata": {
398 398 "collapsed": false
399 399 },
400 400 "outputs": [],
401 401 "source": []
402 402 },
403 403 {
404 404 "cell_type": "code",
405 405 "execution_count": null,
406 406 "metadata": {
407 407 "collapsed": false
408 408 },
409 409 "outputs": [],
410 410 "source": []
411 411 },
412 412 {
413 413 "cell_type": "code",
414 414 "execution_count": null,
415 415 "metadata": {
416 416 "collapsed": false
417 417 },
418 418 "outputs": [],
419 419 "source": []
420 420 },
421 421 {
422 422 "cell_type": "code",
423 423 "execution_count": null,
424 424 "metadata": {
425 425 "collapsed": false
426 426 },
427 427 "outputs": [],
428 428 "source": [
429 429 "counter.value += 1"
430 430 ]
431 431 },
432 432 {
433 433 "cell_type": "code",
434 434 "execution_count": null,
435 435 "metadata": {
436 436 "collapsed": false
437 437 },
438 438 "outputs": [],
439 439 "source": [
440 440 "popup.close()"
441 441 ]
442 442 },
443 443 {
444 444 "cell_type": "markdown",
445 445 "metadata": {
446 446 "slideshow": {
447 447 "slide_type": "slide"
448 448 }
449 449 },
450 450 "source": [
451 451 "# Alignment"
452 452 ]
453 453 },
454 454 {
455 455 "cell_type": "markdown",
456 456 "metadata": {},
457 457 "source": [
458 458 "Most widgets have a **`description` attribute**, which allows a label for the widget to be defined.\n",
459 459 "The label of the widget **has a fixed minimum width**.\n",
460 460 "The text of the label is **always right aligned and the widget is left aligned**:"
461 461 ]
462 462 },
463 463 {
464 464 "cell_type": "code",
465 465 "execution_count": null,
466 466 "metadata": {
467 467 "collapsed": false
468 468 },
469 469 "outputs": [],
470 470 "source": [
471 471 "display(widgets.Text(description=\"a:\"))\n",
472 472 "display(widgets.Text(description=\"aa:\"))\n",
473 473 "display(widgets.Text(description=\"aaa:\"))"
474 474 ]
475 475 },
476 476 {
477 477 "cell_type": "markdown",
478 478 "metadata": {
479 479 "slideshow": {
480 480 "slide_type": "slide"
481 481 }
482 482 },
483 483 "source": [
484 484 "If a **label is longer** than the minimum width, the **widget is shifted to the right**:"
485 485 ]
486 486 },
487 487 {
488 488 "cell_type": "code",
489 489 "execution_count": null,
490 490 "metadata": {
491 491 "collapsed": false
492 492 },
493 493 "outputs": [],
494 494 "source": [
495 495 "display(widgets.Text(description=\"a:\"))\n",
496 496 "display(widgets.Text(description=\"aa:\"))\n",
497 497 "display(widgets.Text(description=\"aaa:\"))\n",
498 498 "display(widgets.Text(description=\"aaaaaaaaaaaaaaaaaa:\"))"
499 499 ]
500 500 },
501 501 {
502 502 "cell_type": "markdown",
503 503 "metadata": {
504 504 "slideshow": {
505 505 "slide_type": "slide"
506 506 }
507 507 },
508 508 "source": [
509 509 "If a `description` is **not set** for the widget, the **label is not displayed**:"
510 510 ]
511 511 },
512 512 {
513 513 "cell_type": "code",
514 514 "execution_count": null,
515 515 "metadata": {
516 516 "collapsed": false
517 517 },
518 518 "outputs": [],
519 519 "source": [
520 520 "display(widgets.Text(description=\"a:\"))\n",
521 521 "display(widgets.Text(description=\"aa:\"))\n",
522 522 "display(widgets.Text(description=\"aaa:\"))\n",
523 523 "display(widgets.Text())"
524 524 ]
525 525 },
526 526 {
527 527 "cell_type": "markdown",
528 528 "metadata": {
529 529 "slideshow": {
530 530 "slide_type": "slide"
531 531 }
532 532 },
533 533 "source": [
534 534 "## Flex boxes"
535 535 ]
536 536 },
537 537 {
538 538 "cell_type": "markdown",
539 539 "metadata": {},
540 540 "source": [
541 541 "Widgets can be aligned using the `FlexBox`, `HBox`, and `VBox` widgets."
542 542 ]
543 543 },
544 544 {
545 545 "cell_type": "markdown",
546 546 "metadata": {
547 547 "slideshow": {
548 548 "slide_type": "slide"
549 549 }
550 550 },
551 551 "source": [
552 552 "### Application to widgets"
553 553 ]
554 554 },
555 555 {
556 556 "cell_type": "markdown",
557 557 "metadata": {},
558 558 "source": [
559 559 "Widgets display vertically by default:"
560 560 ]
561 561 },
562 562 {
563 563 "cell_type": "code",
564 564 "execution_count": null,
565 565 "metadata": {
566 566 "collapsed": false
567 567 },
568 568 "outputs": [],
569 569 "source": [
570 570 "buttons = [widgets.Button(description=str(i)) for i in range(3)]\n",
571 571 "display(*buttons)"
572 572 ]
573 573 },
574 574 {
575 575 "cell_type": "markdown",
576 576 "metadata": {
577 577 "slideshow": {
578 578 "slide_type": "slide"
579 579 }
580 580 },
581 581 "source": [
582 582 "### Using hbox"
583 583 ]
584 584 },
585 585 {
586 586 "cell_type": "markdown",
587 587 "metadata": {},
588 588 "source": [
589 589 "To make widgets display horizontally, you need to **child them to a `HBox` widget**."
590 590 ]
591 591 },
592 592 {
593 593 "cell_type": "code",
594 594 "execution_count": null,
595 595 "metadata": {
596 596 "collapsed": false
597 597 },
598 598 "outputs": [],
599 599 "source": [
600 600 "container = widgets.HBox(children=buttons)\n",
601 601 "display(container)"
602 602 ]
603 603 },
604 604 {
605 605 "cell_type": "markdown",
606 606 "metadata": {},
607 607 "source": [
608 608 "By setting the width of the container to 100% and its `pack` to `center`, you can center the buttons."
609 609 ]
610 610 },
611 611 {
612 612 "cell_type": "code",
613 613 "execution_count": null,
614 614 "metadata": {
615 615 "collapsed": false
616 616 },
617 617 "outputs": [],
618 618 "source": [
619 619 "container.width = '100%'\n",
620 620 "container.pack = 'center'"
621 621 ]
622 622 },
623 623 {
624 624 "cell_type": "markdown",
625 625 "metadata": {
626 626 "slideshow": {
627 627 "slide_type": "slide"
628 628 }
629 629 },
630 630 "source": [
631 631 "## Visibility"
632 632 ]
633 633 },
634 634 {
635 635 "cell_type": "markdown",
636 636 "metadata": {},
637 637 "source": [
638 638 "Sometimes it is necessary to **hide or show widgets** in place, **without having to re-display** the widget.\n",
639 "The `visibility` property of widgets can be used to hide or show **widgets that have already been displayed** (as seen below)."
639 "The `visible` property of widgets can be used to hide or show **widgets that have already been displayed** (as seen below). The `visible` property can be:\n",
640 "* `True` - the widget is displayed\n",
641 "* `False` - the widget is hidden, and the empty space where the widget would be is collapsed\n",
642 "* `None` - the widget is hidden, and the empty space where the widget would be is shown"
640 643 ]
641 644 },
642 645 {
643 646 "cell_type": "code",
644 647 "execution_count": null,
645 648 "metadata": {
646 649 "collapsed": false
647 650 },
648 651 "outputs": [],
649 652 "source": [
650 "string = widgets.Latex(value=\"Hello World!\")\n",
651 "display(string) "
653 "w1 = widgets.Latex(value=\"First line\")\n",
654 "w2 = widgets.Latex(value=\"Second line\")\n",
655 "w3 = widgets.Latex(value=\"Third line\")\n",
656 "display(w1, w2, w3)"
657 ]
658 },
659 {
660 "cell_type": "code",
661 "execution_count": null,
662 "metadata": {
663 "collapsed": true
664 },
665 "outputs": [],
666 "source": [
667 "w2.visible=None"
652 668 ]
653 669 },
654 670 {
655 671 "cell_type": "code",
656 672 "execution_count": null,
657 673 "metadata": {
658 674 "collapsed": false
659 675 },
660 676 "outputs": [],
661 677 "source": [
662 "string.visible=False"
678 "w2.visible=False"
663 679 ]
664 680 },
665 681 {
666 682 "cell_type": "code",
667 683 "execution_count": null,
668 684 "metadata": {
669 685 "collapsed": false
670 686 },
671 687 "outputs": [],
672 688 "source": [
673 "string.visible=True"
689 "w2.visible=True"
674 690 ]
675 691 },
676 692 {
677 693 "cell_type": "markdown",
678 694 "metadata": {
679 695 "slideshow": {
680 696 "slide_type": "slide"
681 697 }
682 698 },
683 699 "source": [
684 700 "### Another example"
685 701 ]
686 702 },
687 703 {
688 704 "cell_type": "markdown",
689 705 "metadata": {},
690 706 "source": [
691 707 "In the example below, a form is rendered, which conditionally displays widgets depending on the state of other widgets. Try toggling the student checkbox."
692 708 ]
693 709 },
694 710 {
695 711 "cell_type": "code",
696 712 "execution_count": null,
697 713 "metadata": {
698 714 "collapsed": false
699 715 },
700 716 "outputs": [],
701 717 "source": [
702 718 "form = widgets.VBox()\n",
703 719 "first = widgets.Text(description=\"First Name:\")\n",
704 720 "last = widgets.Text(description=\"Last Name:\")\n",
705 721 "\n",
706 722 "student = widgets.Checkbox(description=\"Student:\", value=False)\n",
707 723 "school_info = widgets.VBox(visible=False, children=[\n",
708 724 " widgets.Text(description=\"School:\"),\n",
709 725 " widgets.IntText(description=\"Grade:\", min=0, max=12)\n",
710 726 " ])\n",
711 727 "\n",
712 728 "pet = widgets.Text(description=\"Pet's Name:\")\n",
713 729 "form.children = [first, last, student, school_info, pet]\n",
714 730 "display(form)\n",
715 731 "\n",
716 732 "def on_student_toggle(name, value):\n",
717 733 " if value:\n",
718 734 " school_info.visible = True\n",
719 735 " else:\n",
720 736 " school_info.visible = False\n",
721 737 "student.on_trait_change(on_student_toggle, 'value')\n"
722 738 ]
723 739 },
724 740 {
725 741 "cell_type": "markdown",
726 742 "metadata": {},
727 743 "source": [
728 744 "[Index](Index.ipynb) - [Back](Widget Events.ipynb) - [Next](Custom Widget - Hello World.ipynb)"
729 745 ]
730 746 }
731 747 ],
732 748 "metadata": {
733 749 "cell_tags": [
734 750 [
735 751 "<None>",
736 752 null
737 753 ]
738 754 ],
739 755 "kernelspec": {
756 "display_name": "Python 2",
757 "name": "python2"
758 },
759 "language_info": {
740 760 "codemirror_mode": {
741 "name": "python",
761 "name": "ipython",
742 762 "version": 2
743 763 },
744 "display_name": "Python 2",
745 "language": "python",
746 "name": "python2"
764 "file_extension": ".py",
765 "mimetype": "text/x-python",
766 "name": "python",
767 "nbconvert_exporter": "python",
768 "pygments_lexer": "ipython2",
769 "version": "2.7.8"
747 770 },
748 "signature": "sha256:8bb091be85fd5e7f76082b1b4b98cddec92a856334935ac2c702fe5ec03f6eff"
771 "signature": "sha256:198630bf2c2eb00401b60a395ebc75049099864b62f0faaf416da02f9808c40b"
749 772 },
750 773 "nbformat": 4,
751 774 "nbformat_minor": 0
752 775 } No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now