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