##// END OF EJS Templates
Add None as a widget visible value to allow for visiblity: hidden...
Jason Grout -
Show More
@@ -1,679 +1,686 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
35 35 if (comm !== undefined) {
36 36 // Remember comm associated with the model.
37 37 this.comm = comm;
38 38 comm.model = this;
39 39
40 40 // Hook comm messages up to model.
41 41 comm.on_close($.proxy(this._handle_comm_closed, this));
42 42 comm.on_msg($.proxy(this._handle_comm_msg, this));
43 43 }
44 44 return Backbone.Model.apply(this);
45 45 },
46 46
47 47 send: function (content, callbacks) {
48 48 /**
49 49 * Send a custom msg over the comm.
50 50 */
51 51 if (this.comm !== undefined) {
52 52 var data = {method: 'custom', content: content};
53 53 this.comm.send(data, callbacks);
54 54 this.pending_msgs++;
55 55 }
56 56 },
57 57
58 58 _handle_comm_closed: function (msg) {
59 59 /**
60 60 * Handle when a widget is closed.
61 61 */
62 62 this.trigger('comm:close');
63 63 this.stopListening();
64 64 this.trigger('destroy', this);
65 65 delete this.comm.model; // Delete ref so GC will collect widget model.
66 66 delete this.comm;
67 67 delete this.model_id; // Delete id from model so widget manager cleans up.
68 68 for (var id in this.views) {
69 69 if (this.views.hasOwnProperty(id)) {
70 70 this.views[id].remove();
71 71 }
72 72 }
73 73 },
74 74
75 75 _handle_comm_msg: function (msg) {
76 76 /**
77 77 * Handle incoming comm msg.
78 78 */
79 79 var method = msg.content.data.method;
80 80 var that = this;
81 81 switch (method) {
82 82 case 'update':
83 83 this.state_change = this.state_change.then(function() {
84 84 return that.set_state(msg.content.data.state);
85 85 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true));
86 86 break;
87 87 case 'custom':
88 88 this.trigger('msg:custom', msg.content.data.content);
89 89 break;
90 90 case 'display':
91 91 this.widget_manager.display_view(msg, this);
92 92 break;
93 93 }
94 94 },
95 95
96 96 set_state: function (state) {
97 97 var that = this;
98 98 // Handle when a widget is updated via the python side.
99 99 return this._unpack_models(state).then(function(state) {
100 100 that.state_lock = state;
101 101 try {
102 102 WidgetModel.__super__.set.call(that, state);
103 103 } finally {
104 104 that.state_lock = null;
105 105 }
106 106 }).catch(utils.reject("Couldn't set model state", true));
107 107 },
108 108
109 109 _handle_status: function (msg, callbacks) {
110 110 /**
111 111 * Handle status msgs.
112 112 *
113 113 * execution_state : ('busy', 'idle', 'starting')
114 114 */
115 115 if (this.comm !== undefined) {
116 116 if (msg.content.execution_state ==='idle') {
117 117 // Send buffer if this message caused another message to be
118 118 // throttled.
119 119 if (this.msg_buffer !== null &&
120 120 (this.get('msg_throttle') || 3) === this.pending_msgs) {
121 121 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
122 122 this.comm.send(data, callbacks);
123 123 this.msg_buffer = null;
124 124 } else {
125 125 --this.pending_msgs;
126 126 }
127 127 }
128 128 }
129 129 },
130 130
131 131 callbacks: function(view) {
132 132 /**
133 133 * Create msg callbacks for a comm msg.
134 134 */
135 135 var callbacks = this.widget_manager.callbacks(view);
136 136
137 137 if (callbacks.iopub === undefined) {
138 138 callbacks.iopub = {};
139 139 }
140 140
141 141 var that = this;
142 142 callbacks.iopub.status = function (msg) {
143 143 that._handle_status(msg, callbacks);
144 144 };
145 145 return callbacks;
146 146 },
147 147
148 148 set: function(key, val, options) {
149 149 /**
150 150 * Set a value.
151 151 */
152 152 var return_value = WidgetModel.__super__.set.apply(this, arguments);
153 153
154 154 // Backbone only remembers the diff of the most recent set()
155 155 // operation. Calling set multiple times in a row results in a
156 156 // loss of diff information. Here we keep our own running diff.
157 157 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
158 158 return return_value;
159 159 },
160 160
161 161 sync: function (method, model, options) {
162 162 /**
163 163 * Handle sync to the back-end. Called when a model.save() is called.
164 164 *
165 165 * Make sure a comm exists.
166 166 */
167 167 var error = options.error || function() {
168 168 console.error('Backbone sync error:', arguments);
169 169 };
170 170 if (this.comm === undefined) {
171 171 error();
172 172 return false;
173 173 }
174 174
175 175 // Delete any key value pairs that the back-end already knows about.
176 176 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
177 177 if (this.state_lock !== null) {
178 178 var keys = Object.keys(this.state_lock);
179 179 for (var i=0; i<keys.length; i++) {
180 180 var key = keys[i];
181 181 if (attrs[key] === this.state_lock[key]) {
182 182 delete attrs[key];
183 183 }
184 184 }
185 185 }
186 186
187 187 // Only sync if there are attributes to send to the back-end.
188 188 attrs = this._pack_models(attrs);
189 189 if (_.size(attrs) > 0) {
190 190
191 191 // If this message was sent via backbone itself, it will not
192 192 // have any callbacks. It's important that we create callbacks
193 193 // so we can listen for status messages, etc...
194 194 var callbacks = options.callbacks || this.callbacks();
195 195
196 196 // Check throttle.
197 197 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
198 198 // The throttle has been exceeded, buffer the current msg so
199 199 // it can be sent once the kernel has finished processing
200 200 // some of the existing messages.
201 201
202 202 // Combine updates if it is a 'patch' sync, otherwise replace updates
203 203 switch (method) {
204 204 case 'patch':
205 205 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
206 206 break;
207 207 case 'update':
208 208 case 'create':
209 209 this.msg_buffer = attrs;
210 210 break;
211 211 default:
212 212 error();
213 213 return false;
214 214 }
215 215 this.msg_buffer_callbacks = callbacks;
216 216
217 217 } else {
218 218 // We haven't exceeded the throttle, send the message like
219 219 // normal.
220 220 var data = {method: 'backbone', sync_data: attrs};
221 221 this.comm.send(data, callbacks);
222 222 this.pending_msgs++;
223 223 }
224 224 }
225 225 // Since the comm is a one-way communication, assume the message
226 226 // arrived. Don't call success since we don't have a model back from the server
227 227 // this means we miss out on the 'sync' event.
228 228 this._buffered_state_diff = {};
229 229 },
230 230
231 231 save_changes: function(callbacks) {
232 232 /**
233 233 * Push this model's state to the back-end
234 234 *
235 235 * This invokes a Backbone.Sync.
236 236 */
237 237 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
238 238 },
239 239
240 240 _pack_models: function(value) {
241 241 /**
242 242 * Replace models with model ids recursively.
243 243 */
244 244 var that = this;
245 245 var packed;
246 246 if (value instanceof Backbone.Model) {
247 247 return "IPY_MODEL_" + value.id;
248 248
249 249 } else if ($.isArray(value)) {
250 250 packed = [];
251 251 _.each(value, function(sub_value, key) {
252 252 packed.push(that._pack_models(sub_value));
253 253 });
254 254 return packed;
255 255 } else if (value instanceof Date || value instanceof String) {
256 256 return value;
257 257 } else if (value instanceof Object) {
258 258 packed = {};
259 259 _.each(value, function(sub_value, key) {
260 260 packed[key] = that._pack_models(sub_value);
261 261 });
262 262 return packed;
263 263
264 264 } else {
265 265 return value;
266 266 }
267 267 },
268 268
269 269 _unpack_models: function(value) {
270 270 /**
271 271 * Replace model ids with models recursively.
272 272 */
273 273 var that = this;
274 274 var unpacked;
275 275 if ($.isArray(value)) {
276 276 unpacked = [];
277 277 _.each(value, function(sub_value, key) {
278 278 unpacked.push(that._unpack_models(sub_value));
279 279 });
280 280 return Promise.all(unpacked);
281 281 } else if (value instanceof Object) {
282 282 unpacked = {};
283 283 _.each(value, function(sub_value, key) {
284 284 unpacked[key] = that._unpack_models(sub_value);
285 285 });
286 286 return utils.resolve_promises_dict(unpacked);
287 287 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
288 288 // get_model returns a promise already
289 289 return this.widget_manager.get_model(value.slice(10, value.length));
290 290 } else {
291 291 return Promise.resolve(value);
292 292 }
293 293 },
294 294
295 295 on_some_change: function(keys, callback, context) {
296 296 /**
297 297 * on_some_change(["key1", "key2"], foo, context) differs from
298 298 * on("change:key1 change:key2", foo, context).
299 299 * If the widget attributes key1 and key2 are both modified,
300 300 * the second form will result in foo being called twice
301 301 * while the first will call foo only once.
302 302 */
303 303 this.on('change', function() {
304 304 if (keys.some(this.hasChanged, this)) {
305 305 callback.apply(context);
306 306 }
307 307 }, this);
308 308
309 309 },
310 310 });
311 311 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
312 312
313 313
314 314 var WidgetView = Backbone.View.extend({
315 315 initialize: function(parameters) {
316 316 /**
317 317 * Public constructor.
318 318 */
319 319 this.model.on('change',this.update,this);
320 320 this.options = parameters.options;
321 321 this.id = this.id || utils.uuid();
322 322 this.model.views[this.id] = this;
323 323 this.on('displayed', function() {
324 324 this.is_displayed = true;
325 325 }, this);
326 326 },
327 327
328 328 update: function(){
329 329 /**
330 330 * Triggered on model change.
331 331 *
332 332 * Update view to be consistent with this.model
333 333 */
334 334 },
335 335
336 336 create_child_view: function(child_model, options) {
337 337 /**
338 338 * Create and promise that resolves to a child view of a given model
339 339 */
340 340 var that = this;
341 341 options = $.extend({ parent: this }, options || {});
342 342 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
343 343 },
344 344
345 345 callbacks: function(){
346 346 /**
347 347 * Create msg callbacks for a comm msg.
348 348 */
349 349 return this.model.callbacks(this);
350 350 },
351 351
352 352 render: function(){
353 353 /**
354 354 * Render the view.
355 355 *
356 356 * By default, this is only called the first time the view is created
357 357 */
358 358 },
359 359
360 360 show: function(){
361 361 /**
362 362 * Show the widget-area
363 363 */
364 364 if (this.options && this.options.cell &&
365 365 this.options.cell.widget_area !== undefined) {
366 366 this.options.cell.widget_area.show();
367 367 }
368 368 },
369 369
370 370 send: function (content) {
371 371 /**
372 372 * Send a custom msg associated with this view.
373 373 */
374 374 this.model.send(content, this.callbacks());
375 375 },
376 376
377 377 touch: function () {
378 378 this.model.save_changes(this.callbacks());
379 379 },
380 380
381 381 after_displayed: function (callback, context) {
382 382 /**
383 383 * Calls the callback right away is the view is already displayed
384 384 * otherwise, register the callback to the 'displayed' event.
385 385 */
386 386 if (this.is_displayed) {
387 387 callback.apply(context);
388 388 } else {
389 389 this.on('displayed', callback, context);
390 390 }
391 391 },
392 392 });
393 393
394 394
395 395 var DOMWidgetView = WidgetView.extend({
396 396 initialize: function (parameters) {
397 397 /**
398 398 * Public constructor
399 399 */
400 400 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
401 401 this.on('displayed', this.show, this);
402 402 this.model.on('change:visible', this.update_visible, this);
403 403 this.model.on('change:_css', this.update_css, this);
404 404
405 405 this.model.on('change:_dom_classes', function(model, new_classes) {
406 406 var old_classes = model.previous('_dom_classes');
407 407 this.update_classes(old_classes, new_classes);
408 408 }, this);
409 409
410 410 this.model.on('change:color', function (model, value) {
411 411 this.update_attr('color', value); }, this);
412 412
413 413 this.model.on('change:background_color', function (model, value) {
414 414 this.update_attr('background', value); }, this);
415 415
416 416 this.model.on('change:width', function (model, value) {
417 417 this.update_attr('width', value); }, this);
418 418
419 419 this.model.on('change:height', function (model, value) {
420 420 this.update_attr('height', value); }, this);
421 421
422 422 this.model.on('change:border_color', function (model, value) {
423 423 this.update_attr('border-color', value); }, this);
424 424
425 425 this.model.on('change:border_width', function (model, value) {
426 426 this.update_attr('border-width', value); }, this);
427 427
428 428 this.model.on('change:border_style', function (model, value) {
429 429 this.update_attr('border-style', value); }, this);
430 430
431 431 this.model.on('change:font_style', function (model, value) {
432 432 this.update_attr('font-style', value); }, this);
433 433
434 434 this.model.on('change:font_weight', function (model, value) {
435 435 this.update_attr('font-weight', value); }, this);
436 436
437 437 this.model.on('change:font_size', function (model, value) {
438 438 this.update_attr('font-size', this._default_px(value)); }, this);
439 439
440 440 this.model.on('change:font_family', function (model, value) {
441 441 this.update_attr('font-family', value); }, this);
442 442
443 443 this.model.on('change:padding', function (model, value) {
444 444 this.update_attr('padding', value); }, this);
445 445
446 446 this.model.on('change:margin', function (model, value) {
447 447 this.update_attr('margin', this._default_px(value)); }, this);
448 448
449 449 this.model.on('change:border_radius', function (model, value) {
450 450 this.update_attr('border-radius', this._default_px(value)); }, this);
451 451
452 452 this.after_displayed(function() {
453 453 this.update_visible(this.model, this.model.get("visible"));
454 454 this.update_classes([], this.model.get('_dom_classes'));
455 455
456 456 this.update_attr('color', this.model.get('color'));
457 457 this.update_attr('background', this.model.get('background_color'));
458 458 this.update_attr('width', this.model.get('width'));
459 459 this.update_attr('height', this.model.get('height'));
460 460 this.update_attr('border-color', this.model.get('border_color'));
461 461 this.update_attr('border-width', this.model.get('border_width'));
462 462 this.update_attr('border-style', this.model.get('border_style'));
463 463 this.update_attr('font-style', this.model.get('font_style'));
464 464 this.update_attr('font-weight', this.model.get('font_weight'));
465 465 this.update_attr('font-size', this.model.get('font_size'));
466 466 this.update_attr('font-family', this.model.get('font_family'));
467 467 this.update_attr('padding', this.model.get('padding'));
468 468 this.update_attr('margin', this.model.get('margin'));
469 469 this.update_attr('border-radius', this.model.get('border_radius'));
470 470
471 471 this.update_css(this.model, this.model.get("_css"));
472 472 }, this);
473 473 },
474 474
475 475 _default_px: function(value) {
476 476 /**
477 477 * Makes browser interpret a numerical string as a pixel value.
478 478 */
479 479 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
480 480 return value.trim() + 'px';
481 481 }
482 482 return value;
483 483 },
484 484
485 485 update_attr: function(name, value) {
486 486 /**
487 487 * Set a css attr of the widget view.
488 488 */
489 489 this.$el.css(name, value);
490 490 },
491 491
492 492 update_visible: function(model, value) {
493 493 /**
494 494 * Update visibility
495 495 */
496 this.$el.toggle(value);
496 switch(value) {
497 case null: // python None
498 this.$el.show().css('visibility', 'hidden'); break;
499 case false:
500 this.$el.hide(); break;
501 case true:
502 this.$el.show().css('visibility', ''); break;
503 }
497 504 },
498 505
499 506 update_css: function (model, css) {
500 507 /**
501 508 * Update the css styling of this view.
502 509 */
503 510 var e = this.$el;
504 511 if (css === undefined) {return;}
505 512 for (var i = 0; i < css.length; i++) {
506 513 // Apply the css traits to all elements that match the selector.
507 514 var selector = css[i][0];
508 515 var elements = this._get_selector_element(selector);
509 516 if (elements.length > 0) {
510 517 var trait_key = css[i][1];
511 518 var trait_value = css[i][2];
512 519 elements.css(trait_key ,trait_value);
513 520 }
514 521 }
515 522 },
516 523
517 524 update_classes: function (old_classes, new_classes, $el) {
518 525 /**
519 526 * Update the DOM classes applied to an element, default to this.$el.
520 527 */
521 528 if ($el===undefined) {
522 529 $el = this.$el;
523 530 }
524 531 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
525 532 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
526 533 },
527 534
528 535 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
529 536 /**
530 537 * Update the DOM classes applied to the widget based on a single
531 538 * trait's value.
532 539 *
533 540 * Given a trait value classes map, this function automatically
534 541 * handles applying the appropriate classes to the widget element
535 542 * and removing classes that are no longer valid.
536 543 *
537 544 * Parameters
538 545 * ----------
539 546 * class_map: dictionary
540 547 * Dictionary of trait values to class lists.
541 548 * Example:
542 549 * {
543 550 * success: ['alert', 'alert-success'],
544 551 * info: ['alert', 'alert-info'],
545 552 * warning: ['alert', 'alert-warning'],
546 553 * danger: ['alert', 'alert-danger']
547 554 * };
548 555 * trait_name: string
549 556 * Name of the trait to check the value of.
550 557 * previous_trait_value: optional string, default ''
551 558 * Last trait value
552 559 * $el: optional jQuery element handle, defaults to this.$el
553 560 * Element that the classes are applied to.
554 561 */
555 562 var key = previous_trait_value;
556 563 if (key === undefined) {
557 564 key = this.model.previous(trait_name);
558 565 }
559 566 var old_classes = class_map[key] ? class_map[key] : [];
560 567 key = this.model.get(trait_name);
561 568 var new_classes = class_map[key] ? class_map[key] : [];
562 569
563 570 this.update_classes(old_classes, new_classes, $el || this.$el);
564 571 },
565 572
566 573 _get_selector_element: function (selector) {
567 574 /**
568 575 * Get the elements via the css selector.
569 576 */
570 577 var elements;
571 578 if (!selector) {
572 579 elements = this.$el;
573 580 } else {
574 581 elements = this.$el.find(selector).addBack(selector);
575 582 }
576 583 return elements;
577 584 },
578 585 });
579 586
580 587
581 588 var ViewList = function(create_view, remove_view, context) {
582 589 /**
583 590 * - create_view and remove_view are default functions called when adding or removing views
584 591 * - create_view takes a model and returns a view or a promise for a view for that model
585 592 * - remove_view takes a view and destroys it (including calling `view.remove()`)
586 593 * - each time the update() function is called with a new list, the create and remove
587 594 * callbacks will be called in an order so that if you append the views created in the
588 595 * create callback and remove the views in the remove callback, you will duplicate
589 596 * the order of the list.
590 597 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
591 598 * - the context defaults to the created ViewList. If you pass another context, the create and remove
592 599 * will be called in that context.
593 600 */
594 601
595 602 this.initialize.apply(this, arguments);
596 603 };
597 604
598 605 _.extend(ViewList.prototype, {
599 606 initialize: function(create_view, remove_view, context) {
600 607 this.state_change = Promise.resolve();
601 608 this._handler_context = context || this;
602 609 this._models = [];
603 610 this.views = [];
604 611 this._create_view = create_view;
605 612 this._remove_view = remove_view || function(view) {view.remove();};
606 613 },
607 614
608 615 update: function(new_models, create_view, remove_view, context) {
609 616 /**
610 617 * the create_view, remove_view, and context arguments override the defaults
611 618 * specified when the list is created.
612 619 * returns a promise that resolves after this update is done
613 620 */
614 621 var remove = remove_view || this._remove_view;
615 622 var create = create_view || this._create_view;
616 623 if (create === undefined || remove === undefined){
617 624 console.error("Must define a create a remove function");
618 625 }
619 626 var context = context || this._handler_context;
620 627 var added_views = [];
621 628 var that = this;
622 629 this.state_change = this.state_change.then(function() {
623 630 var i;
624 631 // first, skip past the beginning of the lists if they are identical
625 632 for (i = 0; i < new_models.length; i++) {
626 633 if (i >= that._models.length || new_models[i] !== that._models[i]) {
627 634 break;
628 635 }
629 636 }
630 637 var first_removed = i;
631 638 // Remove the non-matching items from the old list.
632 639 for (var j = first_removed; j < that._models.length; j++) {
633 640 remove.call(context, that.views[j]);
634 641 }
635 642
636 643 // Add the rest of the new list items.
637 644 for (; i < new_models.length; i++) {
638 645 added_views.push(create.call(context, new_models[i]));
639 646 }
640 647 // make a copy of the input array
641 648 that._models = new_models.slice();
642 649 return Promise.all(added_views).then(function(added) {
643 650 Array.prototype.splice.apply(that.views, [first_removed, that.views.length].concat(added));
644 651 return that.views;
645 652 });
646 653 });
647 654 return this.state_change;
648 655 },
649 656
650 657 remove: function() {
651 658 /**
652 659 * removes every view in the list; convenience function for `.update([])`
653 660 * that should be faster
654 661 * returns a promise that resolves after this removal is done
655 662 */
656 663 var that = this;
657 664 this.state_change = this.state_change.then(function() {
658 665 for (var i = 0; i < that.views.length; i++) {
659 666 that._remove_view.call(that._handler_context, that.views[i]);
660 667 }
661 668 that._models = [];
662 669 that.views = [];
663 670 });
664 671 return this.state_change;
665 672 },
666 673 });
667 674
668 675 var widget = {
669 676 'WidgetModel': WidgetModel,
670 677 'WidgetView': WidgetView,
671 678 'DOMWidgetView': DOMWidgetView,
672 679 'ViewList': ViewList,
673 680 };
674 681
675 682 // For backwards compatability.
676 683 $.extend(IPython, widget);
677 684
678 685 return widget;
679 686 });
@@ -1,481 +1,481 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 if not method in ['backbone', 'custom']:
345 345 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
346 346
347 347 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
348 348 if method == 'backbone' and 'sync_data' in data:
349 349 sync_data = data['sync_data']
350 350 self.set_state(sync_data) # handles all methods
351 351
352 352 # Handle a custom msg from the front-end
353 353 elif method == 'custom':
354 354 if 'content' in data:
355 355 self._handle_custom_msg(data['content'])
356 356
357 357 def _handle_custom_msg(self, content):
358 358 """Called when a custom msg is received."""
359 359 self._msg_callbacks(self, content)
360 360
361 361 def _notify_trait(self, name, old_value, new_value):
362 362 """Called when a property has been changed."""
363 363 # Trigger default traitlet callback machinery. This allows any user
364 364 # registered validation to be processed prior to allowing the widget
365 365 # machinery to handle the state.
366 366 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
367 367
368 368 # Send the state after the user registered callbacks for trait changes
369 369 # have all fired (allows for user to validate values).
370 370 if self.comm is not None and name in self.keys:
371 371 # Make sure this isn't information that the front-end just sent us.
372 372 if self._should_send_property(name, new_value):
373 373 # Send new state to front-end
374 374 self.send_state(key=name)
375 375
376 376 def _handle_displayed(self, **kwargs):
377 377 """Called when a view has been displayed for this widget instance"""
378 378 self._display_callbacks(self, **kwargs)
379 379
380 380 def _trait_to_json(self, x):
381 381 """Convert a trait value to json
382 382
383 383 Traverse lists/tuples and dicts and serialize their values as well.
384 384 Replace any widgets with their model_id
385 385 """
386 386 if isinstance(x, dict):
387 387 return {k: self._trait_to_json(v) for k, v in x.items()}
388 388 elif isinstance(x, (list, tuple)):
389 389 return [self._trait_to_json(v) for v in x]
390 390 elif isinstance(x, Widget):
391 391 return "IPY_MODEL_" + x.model_id
392 392 else:
393 393 return x # Value must be JSON-able
394 394
395 395 def _trait_from_json(self, x):
396 396 """Convert json values to objects
397 397
398 398 Replace any strings representing valid model id values to Widget references.
399 399 """
400 400 if isinstance(x, dict):
401 401 return {k: self._trait_from_json(v) for k, v in x.items()}
402 402 elif isinstance(x, (list, tuple)):
403 403 return [self._trait_from_json(v) for v in x]
404 404 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
405 405 # we want to support having child widgets at any level in a hierarchy
406 406 # trusting that a widget UUID will not appear out in the wild
407 407 return Widget.widgets[x[10:]]
408 408 else:
409 409 return x
410 410
411 411 def _ipython_display_(self, **kwargs):
412 412 """Called when `IPython.display.display` is called on the widget."""
413 413 # Show view.
414 414 if self._view_name is not None:
415 415 self._send({"method": "display"})
416 416 self._handle_displayed(**kwargs)
417 417
418 418 def _send(self, msg):
419 419 """Sends a message to the model in the front-end."""
420 420 self.comm.send(msg)
421 421
422 422
423 423 class DOMWidget(Widget):
424 visible = Bool(True, help="Whether the widget is visible.", sync=True)
424 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)
425 425 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
426 426 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
427 427
428 428 width = CUnicode(sync=True)
429 429 height = CUnicode(sync=True)
430 430 padding = CUnicode(sync=True)
431 431 margin = CUnicode(sync=True)
432 432
433 433 color = Unicode(sync=True)
434 434 background_color = Unicode(sync=True)
435 435 border_color = Unicode(sync=True)
436 436
437 437 border_width = CUnicode(sync=True)
438 438 border_radius = CUnicode(sync=True)
439 439 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
440 440 'none',
441 441 'hidden',
442 442 'dotted',
443 443 'dashed',
444 444 'solid',
445 445 'double',
446 446 'groove',
447 447 'ridge',
448 448 'inset',
449 449 'outset',
450 450 'initial',
451 451 'inherit', ''],
452 452 default_value='', sync=True)
453 453
454 454 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
455 455 'normal',
456 456 'italic',
457 457 'oblique',
458 458 'initial',
459 459 'inherit', ''],
460 460 default_value='', sync=True)
461 461 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
462 462 'normal',
463 463 'bold',
464 464 'bolder',
465 465 'lighter',
466 466 'initial',
467 467 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
468 468 default_value='', sync=True)
469 469 font_size = CUnicode(sync=True)
470 470 font_family = Unicode(sync=True)
471 471
472 472 def __init__(self, *pargs, **kwargs):
473 473 super(DOMWidget, self).__init__(*pargs, **kwargs)
474 474
475 475 def _validate_border(name, old, new):
476 476 if new is not None and new != '':
477 477 if name != 'border_width' and not self.border_width:
478 478 self.border_width = 1
479 479 if name != 'border_style' and self.border_style == '':
480 480 self.border_style = 'solid'
481 481 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