##// END OF EJS Templates
Set default view to None and never display widget with no view
Sylvain Corlay -
Show More
@@ -1,610 +1,607
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/namespace",
9 9 ], function(widgetmanager, _, Backbone, $, IPython){
10 10
11 11 var WidgetModel = Backbone.Model.extend({
12 12 constructor: function (widget_manager, model_id, comm) {
13 13 // Constructor
14 14 //
15 15 // Creates a WidgetModel instance.
16 16 //
17 17 // Parameters
18 18 // ----------
19 19 // widget_manager : WidgetManager instance
20 20 // model_id : string
21 21 // An ID unique to this model.
22 22 // comm : Comm instance (optional)
23 23 this.widget_manager = widget_manager;
24 24 this._buffered_state_diff = {};
25 25 this.pending_msgs = 0;
26 26 this.msg_buffer = null;
27 27 this.state_lock = null;
28 28 this.id = model_id;
29 29 this.views = [];
30 30
31 31 if (comm !== undefined) {
32 32 // Remember comm associated with the model.
33 33 this.comm = comm;
34 34 comm.model = this;
35 35
36 36 // Hook comm messages up to model.
37 37 comm.on_close($.proxy(this._handle_comm_closed, this));
38 38 comm.on_msg($.proxy(this._handle_comm_msg, this));
39 39 }
40 40 return Backbone.Model.apply(this);
41 41 },
42 42
43 43 send: function (content, callbacks) {
44 44 // Send a custom msg over the comm.
45 45 if (this.comm !== undefined) {
46 46 var data = {method: 'custom', content: content};
47 47 this.comm.send(data, callbacks);
48 48 this.pending_msgs++;
49 49 }
50 50 },
51 51
52 52 _handle_comm_closed: function (msg) {
53 53 // Handle when a widget is closed.
54 54 this.trigger('comm:close');
55 55 this.stopListening();
56 56 this.trigger('destroy', this);
57 57 delete this.comm.model; // Delete ref so GC will collect widget model.
58 58 delete this.comm;
59 59 delete this.model_id; // Delete id from model so widget manager cleans up.
60 60 _.each(this.views, function(view, i) {
61 61 view.remove();
62 62 });
63 63 },
64 64
65 65 _handle_comm_msg: function (msg) {
66 66 // Handle incoming comm msg.
67 67 var method = msg.content.data.method;
68 68 switch (method) {
69 69 case 'update':
70 70 this.apply_update(msg.content.data.state);
71 71 break;
72 72 case 'custom':
73 73 this.trigger('msg:custom', msg.content.data.content);
74 74 break;
75 75 case 'display':
76 76 this.widget_manager.display_view(msg, this);
77 77 break;
78 78 }
79 79 },
80 80
81 81 apply_update: function (state) {
82 82 // Handle when a widget is updated via the python side.
83 83 this.state_lock = state;
84 84 try {
85 85 var that = this;
86 86 WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
87 87 obj[key] = that._unpack_models(state[key]);
88 88 return obj;
89 89 }, {})]);
90 90 } finally {
91 91 this.state_lock = null;
92 92 }
93 93 },
94 94
95 95 _handle_status: function (msg, callbacks) {
96 96 // Handle status msgs.
97 97
98 98 // execution_state : ('busy', 'idle', 'starting')
99 99 if (this.comm !== undefined) {
100 100 if (msg.content.execution_state ==='idle') {
101 101 // Send buffer if this message caused another message to be
102 102 // throttled.
103 103 if (this.msg_buffer !== null &&
104 104 (this.get('msg_throttle') || 3) === this.pending_msgs) {
105 105 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
106 106 this.comm.send(data, callbacks);
107 107 this.msg_buffer = null;
108 108 } else {
109 109 --this.pending_msgs;
110 110 }
111 111 }
112 112 }
113 113 },
114 114
115 115 callbacks: function(view) {
116 116 // Create msg callbacks for a comm msg.
117 117 var callbacks = this.widget_manager.callbacks(view);
118 118
119 119 if (callbacks.iopub === undefined) {
120 120 callbacks.iopub = {};
121 121 }
122 122
123 123 var that = this;
124 124 callbacks.iopub.status = function (msg) {
125 125 that._handle_status(msg, callbacks);
126 126 };
127 127 return callbacks;
128 128 },
129 129
130 130 set: function(key, val, options) {
131 131 // Set a value.
132 132 var return_value = WidgetModel.__super__.set.apply(this, arguments);
133 133
134 134 // Backbone only remembers the diff of the most recent set()
135 135 // operation. Calling set multiple times in a row results in a
136 136 // loss of diff information. Here we keep our own running diff.
137 137 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
138 138 return return_value;
139 139 },
140 140
141 141 sync: function (method, model, options) {
142 142 // Handle sync to the back-end. Called when a model.save() is called.
143 143
144 144 // Make sure a comm exists.
145 145 var error = options.error || function() {
146 146 console.error('Backbone sync error:', arguments);
147 147 };
148 148 if (this.comm === undefined) {
149 149 error();
150 150 return false;
151 151 }
152 152
153 153 // Delete any key value pairs that the back-end already knows about.
154 154 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
155 155 if (this.state_lock !== null) {
156 156 var keys = Object.keys(this.state_lock);
157 157 for (var i=0; i<keys.length; i++) {
158 158 var key = keys[i];
159 159 if (attrs[key] === this.state_lock[key]) {
160 160 delete attrs[key];
161 161 }
162 162 }
163 163 }
164 164
165 165 // Only sync if there are attributes to send to the back-end.
166 166 attrs = this._pack_models(attrs);
167 167 if (_.size(attrs) > 0) {
168 168
169 169 // If this message was sent via backbone itself, it will not
170 170 // have any callbacks. It's important that we create callbacks
171 171 // so we can listen for status messages, etc...
172 172 var callbacks = options.callbacks || this.callbacks();
173 173
174 174 // Check throttle.
175 175 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
176 176 // The throttle has been exceeded, buffer the current msg so
177 177 // it can be sent once the kernel has finished processing
178 178 // some of the existing messages.
179 179
180 180 // Combine updates if it is a 'patch' sync, otherwise replace updates
181 181 switch (method) {
182 182 case 'patch':
183 183 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
184 184 break;
185 185 case 'update':
186 186 case 'create':
187 187 this.msg_buffer = attrs;
188 188 break;
189 189 default:
190 190 error();
191 191 return false;
192 192 }
193 193 this.msg_buffer_callbacks = callbacks;
194 194
195 195 } else {
196 196 // We haven't exceeded the throttle, send the message like
197 197 // normal.
198 198 var data = {method: 'backbone', sync_data: attrs};
199 199 this.comm.send(data, callbacks);
200 200 this.pending_msgs++;
201 201 }
202 202 }
203 203 // Since the comm is a one-way communication, assume the message
204 204 // arrived. Don't call success since we don't have a model back from the server
205 205 // this means we miss out on the 'sync' event.
206 206 this._buffered_state_diff = {};
207 207 },
208 208
209 209 save_changes: function(callbacks) {
210 210 // Push this model's state to the back-end
211 211 //
212 212 // This invokes a Backbone.Sync.
213 213 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
214 214 },
215 215
216 216 _pack_models: function(value) {
217 217 // Replace models with model ids recursively.
218 218 var that = this;
219 219 var packed;
220 220 if (value instanceof Backbone.Model) {
221 221 return "IPY_MODEL_" + value.id;
222 222
223 223 } else if ($.isArray(value)) {
224 224 packed = [];
225 225 _.each(value, function(sub_value, key) {
226 226 packed.push(that._pack_models(sub_value));
227 227 });
228 228 return packed;
229 229
230 230 } else if (value instanceof Object) {
231 231 packed = {};
232 232 _.each(value, function(sub_value, key) {
233 233 packed[key] = that._pack_models(sub_value);
234 234 });
235 235 return packed;
236 236
237 237 } else {
238 238 return value;
239 239 }
240 240 },
241 241
242 242 _unpack_models: function(value) {
243 243 // Replace model ids with models recursively.
244 244 var that = this;
245 245 var unpacked;
246 246 if ($.isArray(value)) {
247 247 unpacked = [];
248 248 _.each(value, function(sub_value, key) {
249 249 unpacked.push(that._unpack_models(sub_value));
250 250 });
251 251 return unpacked;
252 252
253 253 } else if (value instanceof Object) {
254 254 unpacked = {};
255 255 _.each(value, function(sub_value, key) {
256 256 unpacked[key] = that._unpack_models(sub_value);
257 257 });
258 258 return unpacked;
259 259
260 260 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
261 261 var model = this.widget_manager.get_model(value.slice(10, value.length));
262 262 if (model) {
263 263 return model;
264 264 } else {
265 265 return value;
266 266 }
267 267 } else {
268 268 return value;
269 269 }
270 270 },
271 271
272 272 on_atomic_change: function(keys, callback, context) {
273 273 // on__atomic_change(["key1", "key2"], foo, context) differs from
274 274 // on("change:key1 change:key2", foo, context).
275 275 // If the widget attributes key1 and key2 are both modified,
276 276 // the second form will result in foo being called twice
277 277 // while the first will call foo only once.
278 278 this.on('change', function() {
279 279 if (keys.some(this.hasChanged, this)) {
280 280 callback.apply(context);
281 281 }
282 282 }, this);
283 283
284 284 },
285 285 });
286 286 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
287 287
288 288
289 289 var WidgetView = Backbone.View.extend({
290 290 initialize: function(parameters) {
291 291 // Public constructor.
292 292 this.model.on('change',this.update,this);
293 293 this.options = parameters.options;
294 294 this.child_model_views = {};
295 295 this.child_views = {};
296 296 this.model.views.push(this);
297 297 this.id = this.id || IPython.utils.uuid();
298 298 this.on('displayed', function() {
299 299 this.is_displayed = true;
300 300 }, this);
301 301 },
302 302
303 303 update: function(){
304 304 // Triggered on model change.
305 305 //
306 306 // Update view to be consistent with this.model
307 307 },
308 308
309 309 create_child_view: function(child_model, options) {
310 310 // Create and return a child view.
311 311 //
312 312 // -given a model and (optionally) a view name if the view name is
313 313 // not given, it defaults to the model's default view attribute.
314 314
315 315 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
316 316 // it would be great to have the widget manager add the cell metadata
317 317 // to the subview without having to add it here.
318 318 options = $.extend({ parent: this }, options || {});
319 319 var child_view = this.model.widget_manager.create_view(child_model, options, this);
320 320
321 321 // Associate the view id with the model id.
322 322 if (this.child_model_views[child_model.id] === undefined) {
323 323 this.child_model_views[child_model.id] = [];
324 324 }
325 325 this.child_model_views[child_model.id].push(child_view.id);
326 326
327 327 // Remember the view by id.
328 328 this.child_views[child_view.id] = child_view;
329 329 return child_view;
330 330 },
331 331
332 332 pop_child_view: function(child_model) {
333 333 // Delete a child view that was previously created using create_child_view.
334 334 var view_ids = this.child_model_views[child_model.id];
335 335 if (view_ids !== undefined) {
336 336
337 337 // Only delete the first view in the list.
338 338 var view_id = view_ids[0];
339 339 var view = this.child_views[view_id];
340 340 delete this.child_views[view_id];
341 341 view_ids.splice(0,1);
342 342 child_model.views.pop(view);
343 343
344 344 // Remove the view list specific to this model if it is empty.
345 345 if (view_ids.length === 0) {
346 346 delete this.child_model_views[child_model.id];
347 347 }
348 348 return view;
349 349 }
350 350 return null;
351 351 },
352 352
353 353 do_diff: function(old_list, new_list, removed_callback, added_callback) {
354 354 // Difference a changed list and call remove and add callbacks for
355 355 // each removed and added item in the new list.
356 356 //
357 357 // Parameters
358 358 // ----------
359 359 // old_list : array
360 360 // new_list : array
361 361 // removed_callback : Callback(item)
362 362 // Callback that is called for each item removed.
363 363 // added_callback : Callback(item)
364 364 // Callback that is called for each item added.
365 365
366 366 // Walk the lists until an unequal entry is found.
367 367 var i;
368 368 for (i = 0; i < new_list.length; i++) {
369 369 if (i >= old_list.length || new_list[i] !== old_list[i]) {
370 370 break;
371 371 }
372 372 }
373 373
374 374 // Remove the non-matching items from the old list.
375 375 for (var j = i; j < old_list.length; j++) {
376 376 removed_callback(old_list[j]);
377 377 }
378 378
379 379 // Add the rest of the new list items.
380 380 for (; i < new_list.length; i++) {
381 381 added_callback(new_list[i]);
382 382 }
383 383 },
384 384
385 385 callbacks: function(){
386 386 // Create msg callbacks for a comm msg.
387 387 return this.model.callbacks(this);
388 388 },
389 389
390 390 render: function(){
391 391 // Render the view.
392 392 //
393 393 // By default, this is only called the first time the view is created
394 394 },
395 395
396 396 show: function(){
397 397 // Show the widget-area
398 398 if (this.options && this.options.cell &&
399 399 this.options.cell.widget_area !== undefined) {
400 400 this.options.cell.widget_area.show();
401 401 }
402 402 },
403 403
404 404 send: function (content) {
405 405 // Send a custom msg associated with this view.
406 406 this.model.send(content, this.callbacks());
407 407 },
408 408
409 409 touch: function () {
410 410 this.model.save_changes(this.callbacks());
411 411 },
412 412
413 413 after_displayed: function (callback, context) {
414 414 // Calls the callback right away is the view is already displayed
415 415 // otherwise, register the callback to the 'displayed' event.
416 416 if (this.is_displayed) {
417 417 callback.apply(context);
418 418 } else {
419 419 this.on('displayed', callback, context);
420 420 }
421 421 },
422 422 });
423 423
424 424
425 widgetmanager.WidgetManager.register_widget_view('WidgetView', WidgetView);
426
427 425 var DOMWidgetView = WidgetView.extend({
428 426 initialize: function (parameters) {
429 427 // Public constructor
430 428 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
431 429 this.on('displayed', this.show, this);
432 430 this.model.on('change:visible', this.update_visible, this);
433 431 this.model.on('change:_css', this.update_css, this);
434 432
435 433 this.model.on('change:_dom_classes', function(model, new_classes) {
436 434 var old_classes = model.previous('_dom_classes');
437 435 this.update_classes(old_classes, new_classes);
438 436 }, this);
439 437
440 438 this.model.on('change:color', function (model, value) {
441 439 this.update_attr('color', value); }, this);
442 440
443 441 this.model.on('change:background_color', function (model, value) {
444 442 this.update_attr('background', value); }, this);
445 443
446 444 this.model.on('change:width', function (model, value) {
447 445 this.update_attr('width', value); }, this);
448 446
449 447 this.model.on('change:height', function (model, value) {
450 448 this.update_attr('height', value); }, this);
451 449
452 450 this.model.on('change:border_color', function (model, value) {
453 451 this.update_attr('border-color', value); }, this);
454 452
455 453 this.model.on('change:border_width', function (model, value) {
456 454 this.update_attr('border-width', value); }, this);
457 455
458 456 this.model.on('change:border_style', function (model, value) {
459 457 this.update_attr('border-style', value); }, this);
460 458
461 459 this.model.on('change:font_style', function (model, value) {
462 460 this.update_attr('font-style', value); }, this);
463 461
464 462 this.model.on('change:font_weight', function (model, value) {
465 463 this.update_attr('font-weight', value); }, this);
466 464
467 465 this.model.on('change:font_size', function (model, value) {
468 466 this.update_attr('font-size', this._default_px(value)); }, this);
469 467
470 468 this.model.on('change:font_family', function (model, value) {
471 469 this.update_attr('font-family', value); }, this);
472 470
473 471 this.model.on('change:padding', function (model, value) {
474 472 this.update_attr('padding', value); }, this);
475 473
476 474 this.model.on('change:margin', function (model, value) {
477 475 this.update_attr('margin', this._default_px(value)); }, this);
478 476
479 477 this.model.on('change:border_radius', function (model, value) {
480 478 this.update_attr('border-radius', this._default_px(value)); }, this);
481 479
482 480 this.after_displayed(function() {
483 481 this.update_visible(this.model, this.model.get("visible"));
484 482 this.update_css(this.model, this.model.get("_css"));
485 483
486 484 this.update_classes([], this.model.get('_dom_classes'));
487 485 this.update_attr('color', this.model.get('color'));
488 486 this.update_attr('background', this.model.get('background_color'));
489 487 this.update_attr('width', this.model.get('width'));
490 488 this.update_attr('height', this.model.get('height'));
491 489 this.update_attr('border-color', this.model.get('border_color'));
492 490 this.update_attr('border-width', this.model.get('border_width'));
493 491 this.update_attr('border-style', this.model.get('border_style'));
494 492 this.update_attr('font-style', this.model.get('font_style'));
495 493 this.update_attr('font-weight', this.model.get('font_weight'));
496 494 this.update_attr('font-size', this.model.get('font_size'));
497 495 this.update_attr('font-family', this.model.get('font_family'));
498 496 this.update_attr('padding', this.model.get('padding'));
499 497 this.update_attr('margin', this.model.get('margin'));
500 498 this.update_attr('border-radius', this.model.get('border_radius'));
501 499 }, this);
502 500 },
503 501
504 502 _default_px: function(value) {
505 503 // Makes browser interpret a numerical string as a pixel value.
506 504 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
507 505 return value.trim() + 'px';
508 506 }
509 507 return value;
510 508 },
511 509
512 510 update_attr: function(name, value) {
513 511 // Set a css attr of the widget view.
514 512 this.$el.css(name, value);
515 513 },
516 514
517 515 update_visible: function(model, value) {
518 516 // Update visibility
519 517 this.$el.toggle(value);
520 518 },
521 519
522 520 update_css: function (model, css) {
523 521 // Update the css styling of this view.
524 522 var e = this.$el;
525 523 if (css === undefined) {return;}
526 524 for (var i = 0; i < css.length; i++) {
527 525 // Apply the css traits to all elements that match the selector.
528 526 var selector = css[i][0];
529 527 var elements = this._get_selector_element(selector);
530 528 if (elements.length > 0) {
531 529 var trait_key = css[i][1];
532 530 var trait_value = css[i][2];
533 531 elements.css(trait_key ,trait_value);
534 532 }
535 533 }
536 534 },
537 535
538 536 update_classes: function (old_classes, new_classes, $el) {
539 537 // Update the DOM classes applied to an element, default to this.$el.
540 538 if ($el===undefined) {
541 539 $el = this.$el;
542 540 }
543 541 this.do_diff(old_classes, new_classes, function(removed) {
544 542 $el.removeClass(removed);
545 543 }, function(added) {
546 544 $el.addClass(added);
547 545 });
548 546 },
549 547
550 548 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
551 549 // Update the DOM classes applied to the widget based on a single
552 550 // trait's value.
553 551 //
554 552 // Given a trait value classes map, this function automatically
555 553 // handles applying the appropriate classes to the widget element
556 554 // and removing classes that are no longer valid.
557 555 //
558 556 // Parameters
559 557 // ----------
560 558 // class_map: dictionary
561 559 // Dictionary of trait values to class lists.
562 560 // Example:
563 561 // {
564 562 // success: ['alert', 'alert-success'],
565 563 // info: ['alert', 'alert-info'],
566 564 // warning: ['alert', 'alert-warning'],
567 565 // danger: ['alert', 'alert-danger']
568 566 // };
569 567 // trait_name: string
570 568 // Name of the trait to check the value of.
571 569 // previous_trait_value: optional string, default ''
572 570 // Last trait value
573 571 // $el: optional jQuery element handle, defaults to this.$el
574 572 // Element that the classes are applied to.
575 573 var key = previous_trait_value;
576 574 if (key === undefined) {
577 575 key = this.model.previous(trait_name);
578 576 }
579 577 var old_classes = class_map[key] ? class_map[key] : [];
580 578 key = this.model.get(trait_name);
581 579 var new_classes = class_map[key] ? class_map[key] : [];
582 580
583 581 this.update_classes(old_classes, new_classes, $el || this.$el);
584 582 },
585 583
586 584 _get_selector_element: function (selector) {
587 585 // Get the elements via the css selector.
588 586 var elements;
589 587 if (!selector) {
590 588 elements = this.$el;
591 589 } else {
592 590 elements = this.$el.find(selector).addBack(selector);
593 591 }
594 592 return elements;
595 593 },
596 594 });
597 595
598 widgetmanager.WidgetManager.register_widget_view('DOMWidgetView', DOMWidgetView);
599 596
600 597 var widget = {
601 598 'WidgetModel': WidgetModel,
602 599 'WidgetView': WidgetView,
603 600 'DOMWidgetView': DOMWidgetView,
604 601 };
605 602
606 603 // For backwards compatability.
607 604 $.extend(IPython, widget);
608 605
609 606 return widget;
610 607 });
@@ -1,439 +1,439
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.traitlets import Unicode, Dict, Instance, Bool, List, \
22 22 CaselessStrEnum, Tuple, CUnicode, Int, Set
23 23 from IPython.utils.py3compat import string_types
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Classes
27 27 #-----------------------------------------------------------------------------
28 28 class CallbackDispatcher(LoggingConfigurable):
29 29 """A structure for registering and running callbacks"""
30 30 callbacks = List()
31 31
32 32 def __call__(self, *args, **kwargs):
33 33 """Call all of the registered callbacks."""
34 34 value = None
35 35 for callback in self.callbacks:
36 36 try:
37 37 local_value = callback(*args, **kwargs)
38 38 except Exception as e:
39 39 ip = get_ipython()
40 40 if ip is None:
41 41 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
42 42 else:
43 43 ip.showtraceback()
44 44 else:
45 45 value = local_value if local_value is not None else value
46 46 return value
47 47
48 48 def register_callback(self, callback, remove=False):
49 49 """(Un)Register a callback
50 50
51 51 Parameters
52 52 ----------
53 53 callback: method handle
54 54 Method to be registered or unregistered.
55 55 remove=False: bool
56 56 Whether to unregister the callback."""
57 57
58 58 # (Un)Register the callback.
59 59 if remove and callback in self.callbacks:
60 60 self.callbacks.remove(callback)
61 61 elif not remove and callback not in self.callbacks:
62 62 self.callbacks.append(callback)
63 63
64 64 def _show_traceback(method):
65 65 """decorator for showing tracebacks in IPython"""
66 66 def m(self, *args, **kwargs):
67 67 try:
68 68 return(method(self, *args, **kwargs))
69 69 except Exception as e:
70 70 ip = get_ipython()
71 71 if ip is None:
72 72 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
73 73 else:
74 74 ip.showtraceback()
75 75 return m
76 76
77 77 class Widget(LoggingConfigurable):
78 78 #-------------------------------------------------------------------------
79 79 # Class attributes
80 80 #-------------------------------------------------------------------------
81 81 _widget_construction_callback = None
82 82 widgets = {}
83 83
84 84 @staticmethod
85 85 def on_widget_constructed(callback):
86 86 """Registers a callback to be called when a widget is constructed.
87 87
88 88 The callback must have the following signature:
89 89 callback(widget)"""
90 90 Widget._widget_construction_callback = callback
91 91
92 92 @staticmethod
93 93 def _call_widget_constructed(widget):
94 94 """Static method, called when a widget is constructed."""
95 95 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
96 96 Widget._widget_construction_callback(widget)
97 97
98 98 #-------------------------------------------------------------------------
99 99 # Traits
100 100 #-------------------------------------------------------------------------
101 101 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
102 102 registered in the front-end to create and sync this widget with.""")
103 _view_name = Unicode('WidgetView', help="""Default view registered in the front-end
103 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
104 104 to use to represent the widget.""", sync=True)
105 105 comm = Instance('IPython.kernel.comm.Comm')
106 106
107 107 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
108 108 front-end can send before receiving an idle msg from the back-end.""")
109 109
110 110 keys = List()
111 111 def _keys_default(self):
112 112 return [name for name in self.traits(sync=True)]
113 113
114 114 _property_lock = Tuple((None, None))
115 115 _send_state_lock = Int(0)
116 116 _states_to_send = Set(allow_none=False)
117 117 _display_callbacks = Instance(CallbackDispatcher, ())
118 118 _msg_callbacks = Instance(CallbackDispatcher, ())
119 119
120 120 #-------------------------------------------------------------------------
121 121 # (Con/de)structor
122 122 #-------------------------------------------------------------------------
123 123 def __init__(self, **kwargs):
124 124 """Public constructor"""
125 125 self._model_id = kwargs.pop('model_id', None)
126 126 super(Widget, self).__init__(**kwargs)
127 127
128 128 self.on_trait_change(self._handle_property_changed, self.keys)
129 129 Widget._call_widget_constructed(self)
130 130 self.open()
131 131
132 132 def __del__(self):
133 133 """Object disposal"""
134 134 self.close()
135 135
136 136 #-------------------------------------------------------------------------
137 137 # Properties
138 138 #-------------------------------------------------------------------------
139 139
140 140 def open(self):
141 141 """Open a comm to the frontend if one isn't already open."""
142 142 if self.comm is None:
143 143 if self._model_id is None:
144 144 self.comm = Comm(target_name=self._model_name)
145 145 self._model_id = self.model_id
146 146 else:
147 147 self.comm = Comm(target_name=self._model_name, comm_id=self._model_id)
148 148 self.comm.on_msg(self._handle_msg)
149 149 Widget.widgets[self.model_id] = self
150 150
151 151 # first update
152 152 self.send_state()
153 153
154 154 @property
155 155 def model_id(self):
156 156 """Gets the model id of this widget.
157 157
158 158 If a Comm doesn't exist yet, a Comm will be created automagically."""
159 159 return self.comm.comm_id
160 160
161 161 #-------------------------------------------------------------------------
162 162 # Methods
163 163 #-------------------------------------------------------------------------
164 164
165 165 def close(self):
166 166 """Close method.
167 167
168 168 Closes the underlying comm.
169 169 When the comm is closed, all of the widget views are automatically
170 170 removed from the front-end."""
171 171 if self.comm is not None:
172 172 Widget.widgets.pop(self.model_id, None)
173 173 self.comm.close()
174 174 self.comm = None
175 175
176 176 def send_state(self, key=None):
177 177 """Sends the widget state, or a piece of it, to the front-end.
178 178
179 179 Parameters
180 180 ----------
181 181 key : unicode, or iterable (optional)
182 182 A single property's name or iterable of property names to sync with the front-end.
183 183 """
184 184 self._send({
185 185 "method" : "update",
186 186 "state" : self.get_state(key=key)
187 187 })
188 188
189 189 def get_state(self, key=None):
190 190 """Gets the widget state, or a piece of it.
191 191
192 192 Parameters
193 193 ----------
194 194 key : unicode or iterable (optional)
195 195 A single property's name or iterable of property names to get.
196 196 """
197 197 if key is None:
198 198 keys = self.keys
199 199 elif isinstance(key, string_types):
200 200 keys = [key]
201 201 elif isinstance(key, collections.Iterable):
202 202 keys = key
203 203 else:
204 204 raise ValueError("key must be a string, an iterable of keys, or None")
205 205 state = {}
206 206 for k in keys:
207 207 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
208 208 value = getattr(self, k)
209 209 state[k] = f(value)
210 210 return state
211 211
212 212 def send(self, content):
213 213 """Sends a custom msg to the widget model in the front-end.
214 214
215 215 Parameters
216 216 ----------
217 217 content : dict
218 218 Content of the message to send.
219 219 """
220 220 self._send({"method": "custom", "content": content})
221 221
222 222 def on_msg(self, callback, remove=False):
223 223 """(Un)Register a custom msg receive callback.
224 224
225 225 Parameters
226 226 ----------
227 227 callback: callable
228 228 callback will be passed two arguments when a message arrives::
229 229
230 230 callback(widget, content)
231 231
232 232 remove: bool
233 233 True if the callback should be unregistered."""
234 234 self._msg_callbacks.register_callback(callback, remove=remove)
235 235
236 236 def on_displayed(self, callback, remove=False):
237 237 """(Un)Register a widget displayed callback.
238 238
239 239 Parameters
240 240 ----------
241 241 callback: method handler
242 242 Must have a signature of::
243 243
244 244 callback(widget, **kwargs)
245 245
246 246 kwargs from display are passed through without modification.
247 247 remove: bool
248 248 True if the callback should be unregistered."""
249 249 self._display_callbacks.register_callback(callback, remove=remove)
250 250
251 251 #-------------------------------------------------------------------------
252 252 # Support methods
253 253 #-------------------------------------------------------------------------
254 254 @contextmanager
255 255 def _lock_property(self, key, value):
256 256 """Lock a property-value pair.
257 257
258 258 The value should be the JSON state of the property.
259 259
260 260 NOTE: This, in addition to the single lock for all state changes, is
261 261 flawed. In the future we may want to look into buffering state changes
262 262 back to the front-end."""
263 263 self._property_lock = (key, value)
264 264 try:
265 265 yield
266 266 finally:
267 267 self._property_lock = (None, None)
268 268
269 269 @contextmanager
270 270 def hold_sync(self):
271 271 """Hold syncing any state until the context manager is released"""
272 272 # We increment a value so that this can be nested. Syncing will happen when
273 273 # all levels have been released.
274 274 self._send_state_lock += 1
275 275 try:
276 276 yield
277 277 finally:
278 278 self._send_state_lock -=1
279 279 if self._send_state_lock == 0:
280 280 self.send_state(self._states_to_send)
281 281 self._states_to_send.clear()
282 282
283 283 def _should_send_property(self, key, value):
284 284 """Check the property lock (property_lock)"""
285 285 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
286 286 if (key == self._property_lock[0]
287 287 and to_json(value) == self._property_lock[1]):
288 288 return False
289 289 elif self._send_state_lock > 0:
290 290 self._states_to_send.add(key)
291 291 return False
292 292 else:
293 293 return True
294 294
295 295 # Event handlers
296 296 @_show_traceback
297 297 def _handle_msg(self, msg):
298 298 """Called when a msg is received from the front-end"""
299 299 data = msg['content']['data']
300 300 method = data['method']
301 301 if not method in ['backbone', 'custom']:
302 302 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
303 303
304 304 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
305 305 if method == 'backbone' and 'sync_data' in data:
306 306 sync_data = data['sync_data']
307 307 self._handle_receive_state(sync_data) # handles all methods
308 308
309 309 # Handle a custom msg from the front-end
310 310 elif method == 'custom':
311 311 if 'content' in data:
312 312 self._handle_custom_msg(data['content'])
313 313
314 314 def _handle_receive_state(self, sync_data):
315 315 """Called when a state is received from the front-end."""
316 316 for name in self.keys:
317 317 if name in sync_data:
318 318 json_value = sync_data[name]
319 319 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
320 320 with self._lock_property(name, json_value):
321 321 setattr(self, name, from_json(json_value))
322 322
323 323 def _handle_custom_msg(self, content):
324 324 """Called when a custom msg is received."""
325 325 self._msg_callbacks(self, content)
326 326
327 327 def _handle_property_changed(self, name, old, new):
328 328 """Called when a property has been changed."""
329 329 # Make sure this isn't information that the front-end just sent us.
330 330 if self._should_send_property(name, new):
331 331 # Send new state to front-end
332 332 self.send_state(key=name)
333 333
334 334 def _handle_displayed(self, **kwargs):
335 335 """Called when a view has been displayed for this widget instance"""
336 336 self._display_callbacks(self, **kwargs)
337 337
338 338 def _trait_to_json(self, x):
339 339 """Convert a trait value to json
340 340
341 341 Traverse lists/tuples and dicts and serialize their values as well.
342 342 Replace any widgets with their model_id
343 343 """
344 344 if isinstance(x, dict):
345 345 return {k: self._trait_to_json(v) for k, v in x.items()}
346 346 elif isinstance(x, (list, tuple)):
347 347 return [self._trait_to_json(v) for v in x]
348 348 elif isinstance(x, Widget):
349 349 return "IPY_MODEL_" + x.model_id
350 350 else:
351 351 return x # Value must be JSON-able
352 352
353 353 def _trait_from_json(self, x):
354 354 """Convert json values to objects
355 355
356 356 Replace any strings representing valid model id values to Widget references.
357 357 """
358 358 if isinstance(x, dict):
359 359 return {k: self._trait_from_json(v) for k, v in x.items()}
360 360 elif isinstance(x, (list, tuple)):
361 361 return [self._trait_from_json(v) for v in x]
362 362 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
363 363 # we want to support having child widgets at any level in a hierarchy
364 364 # trusting that a widget UUID will not appear out in the wild
365 365 return Widget.widgets[x[10:]]
366 366 else:
367 367 return x
368 368
369 369 def _ipython_display_(self, **kwargs):
370 370 """Called when `IPython.display.display` is called on the widget."""
371 # Show view. By sending a display message, the comm is opened and the
372 # initial state is sent.
371 # Show view.
372 if self._view_name is not None:
373 373 self._send({"method": "display"})
374 374 self._handle_displayed(**kwargs)
375 375
376 376 def _send(self, msg):
377 377 """Sends a message to the model in the front-end."""
378 378 self.comm.send(msg)
379 379
380 380
381 381 class DOMWidget(Widget):
382 382 visible = Bool(True, help="Whether the widget is visible.", sync=True)
383 383 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
384 384 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
385 385
386 386 width = CUnicode(sync=True)
387 387 height = CUnicode(sync=True)
388 388 padding = CUnicode(sync=True)
389 389 margin = CUnicode(sync=True)
390 390
391 391 color = Unicode(sync=True)
392 392 background_color = Unicode(sync=True)
393 393 border_color = Unicode(sync=True)
394 394
395 395 border_width = CUnicode(sync=True)
396 396 border_radius = CUnicode(sync=True)
397 397 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
398 398 'none',
399 399 'hidden',
400 400 'dotted',
401 401 'dashed',
402 402 'solid',
403 403 'double',
404 404 'groove',
405 405 'ridge',
406 406 'inset',
407 407 'outset',
408 408 'initial',
409 409 'inherit', ''],
410 410 default_value='', sync=True)
411 411
412 412 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
413 413 'normal',
414 414 'italic',
415 415 'oblique',
416 416 'initial',
417 417 'inherit', ''],
418 418 default_value='', sync=True)
419 419 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
420 420 'normal',
421 421 'bold',
422 422 'bolder',
423 423 'lighter',
424 424 'initial',
425 425 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
426 426 default_value='', sync=True)
427 427 font_size = CUnicode(sync=True)
428 428 font_family = Unicode(sync=True)
429 429
430 430 def __init__(self, *pargs, **kwargs):
431 431 super(DOMWidget, self).__init__(*pargs, **kwargs)
432 432
433 433 def _validate_border(name, old, new):
434 434 if new is not None and new != '':
435 435 if name != 'border_width' and not self.border_width:
436 436 self.border_width = 1
437 437 if name != 'border_style' and self.border_style == '':
438 438 self.border_style = 'solid'
439 439 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
General Comments 0
You need to be logged in to leave comments. Login now