##// END OF EJS Templates
s/fore_color/color...
Jonathan Frederic -
Show More
@@ -1,576 +1,576 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) {
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.key_value_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 delete this.comm.model; // Delete ref so GC will collect widget model.
56 56 delete this.comm;
57 57 delete this.model_id; // Delete id from model so widget manager cleans up.
58 58 _.each(this.views, function(view, i) {
59 59 view.remove();
60 60 });
61 61 },
62 62
63 63 _handle_comm_msg: function (msg) {
64 64 // Handle incoming comm msg.
65 65 var method = msg.content.data.method;
66 66 switch (method) {
67 67 case 'update':
68 68 this.apply_update(msg.content.data.state);
69 69 break;
70 70 case 'custom':
71 71 this.trigger('msg:custom', msg.content.data.content);
72 72 break;
73 73 case 'display':
74 74 this.widget_manager.display_view(msg, this);
75 75 break;
76 76 }
77 77 },
78 78
79 79 apply_update: function (state) {
80 80 // Handle when a widget is updated via the python side.
81 81 var that = this;
82 82 _.each(state, function(value, key) {
83 83 that.key_value_lock = [key, value];
84 84 try {
85 85 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
86 86 } finally {
87 87 that.key_value_lock = null;
88 88 }
89 89 });
90 90 },
91 91
92 92 _handle_status: function (msg, callbacks) {
93 93 // Handle status msgs.
94 94
95 95 // execution_state : ('busy', 'idle', 'starting')
96 96 if (this.comm !== undefined) {
97 97 if (msg.content.execution_state ==='idle') {
98 98 // Send buffer if this message caused another message to be
99 99 // throttled.
100 100 if (this.msg_buffer !== null &&
101 101 (this.get('msg_throttle') || 3) === this.pending_msgs) {
102 102 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
103 103 this.comm.send(data, callbacks);
104 104 this.msg_buffer = null;
105 105 } else {
106 106 --this.pending_msgs;
107 107 }
108 108 }
109 109 }
110 110 },
111 111
112 112 callbacks: function(view) {
113 113 // Create msg callbacks for a comm msg.
114 114 var callbacks = this.widget_manager.callbacks(view);
115 115
116 116 if (callbacks.iopub === undefined) {
117 117 callbacks.iopub = {};
118 118 }
119 119
120 120 var that = this;
121 121 callbacks.iopub.status = function (msg) {
122 122 that._handle_status(msg, callbacks);
123 123 };
124 124 return callbacks;
125 125 },
126 126
127 127 set: function(key, val, options) {
128 128 // Set a value.
129 129 var return_value = WidgetModel.__super__.set.apply(this, arguments);
130 130
131 131 // Backbone only remembers the diff of the most recent set()
132 132 // operation. Calling set multiple times in a row results in a
133 133 // loss of diff information. Here we keep our own running diff.
134 134 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
135 135 return return_value;
136 136 },
137 137
138 138 sync: function (method, model, options) {
139 139 // Handle sync to the back-end. Called when a model.save() is called.
140 140
141 141 // Make sure a comm exists.
142 142 var error = options.error || function() {
143 143 console.error('Backbone sync error:', arguments);
144 144 };
145 145 if (this.comm === undefined) {
146 146 error();
147 147 return false;
148 148 }
149 149
150 150 // Delete any key value pairs that the back-end already knows about.
151 151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
152 152 if (this.key_value_lock !== null) {
153 153 var key = this.key_value_lock[0];
154 154 var value = this.key_value_lock[1];
155 155 if (attrs[key] === value) {
156 156 delete attrs[key];
157 157 }
158 158 }
159 159
160 160 // Only sync if there are attributes to send to the back-end.
161 161 attrs = this._pack_models(attrs);
162 162 if (_.size(attrs) > 0) {
163 163
164 164 // If this message was sent via backbone itself, it will not
165 165 // have any callbacks. It's important that we create callbacks
166 166 // so we can listen for status messages, etc...
167 167 var callbacks = options.callbacks || this.callbacks();
168 168
169 169 // Check throttle.
170 170 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
171 171 // The throttle has been exceeded, buffer the current msg so
172 172 // it can be sent once the kernel has finished processing
173 173 // some of the existing messages.
174 174
175 175 // Combine updates if it is a 'patch' sync, otherwise replace updates
176 176 switch (method) {
177 177 case 'patch':
178 178 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
179 179 break;
180 180 case 'update':
181 181 case 'create':
182 182 this.msg_buffer = attrs;
183 183 break;
184 184 default:
185 185 error();
186 186 return false;
187 187 }
188 188 this.msg_buffer_callbacks = callbacks;
189 189
190 190 } else {
191 191 // We haven't exceeded the throttle, send the message like
192 192 // normal.
193 193 var data = {method: 'backbone', sync_data: attrs};
194 194 this.comm.send(data, callbacks);
195 195 this.pending_msgs++;
196 196 }
197 197 }
198 198 // Since the comm is a one-way communication, assume the message
199 199 // arrived. Don't call success since we don't have a model back from the server
200 200 // this means we miss out on the 'sync' event.
201 201 this._buffered_state_diff = {};
202 202 },
203 203
204 204 save_changes: function(callbacks) {
205 205 // Push this model's state to the back-end
206 206 //
207 207 // This invokes a Backbone.Sync.
208 208 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
209 209 },
210 210
211 211 _pack_models: function(value) {
212 212 // Replace models with model ids recursively.
213 213 var that = this;
214 214 var packed;
215 215 if (value instanceof Backbone.Model) {
216 216 return "IPY_MODEL_" + value.id;
217 217
218 218 } else if ($.isArray(value)) {
219 219 packed = [];
220 220 _.each(value, function(sub_value, key) {
221 221 packed.push(that._pack_models(sub_value));
222 222 });
223 223 return packed;
224 224
225 225 } else if (value instanceof Object) {
226 226 packed = {};
227 227 _.each(value, function(sub_value, key) {
228 228 packed[key] = that._pack_models(sub_value);
229 229 });
230 230 return packed;
231 231
232 232 } else {
233 233 return value;
234 234 }
235 235 },
236 236
237 237 _unpack_models: function(value) {
238 238 // Replace model ids with models recursively.
239 239 var that = this;
240 240 var unpacked;
241 241 if ($.isArray(value)) {
242 242 unpacked = [];
243 243 _.each(value, function(sub_value, key) {
244 244 unpacked.push(that._unpack_models(sub_value));
245 245 });
246 246 return unpacked;
247 247
248 248 } else if (value instanceof Object) {
249 249 unpacked = {};
250 250 _.each(value, function(sub_value, key) {
251 251 unpacked[key] = that._unpack_models(sub_value);
252 252 });
253 253 return unpacked;
254 254
255 255 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
256 256 var model = this.widget_manager.get_model(value.slice(10, value.length));
257 257 if (model) {
258 258 return model;
259 259 } else {
260 260 return value;
261 261 }
262 262 } else {
263 263 return value;
264 264 }
265 265 },
266 266
267 267 });
268 268 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
269 269
270 270
271 271 var WidgetView = Backbone.View.extend({
272 272 initialize: function(parameters) {
273 273 // Public constructor.
274 274 this.model.on('change',this.update,this);
275 275 this.options = parameters.options;
276 276 this.child_model_views = {};
277 277 this.child_views = {};
278 278 this.model.views.push(this);
279 279 this.id = this.id || IPython.utils.uuid();
280 280 this.on('displayed', function() {
281 281 this.is_displayed = true;
282 282 }, this);
283 283 },
284 284
285 285 update: function(){
286 286 // Triggered on model change.
287 287 //
288 288 // Update view to be consistent with this.model
289 289 },
290 290
291 291 create_child_view: function(child_model, options) {
292 292 // Create and return a child view.
293 293 //
294 294 // -given a model and (optionally) a view name if the view name is
295 295 // not given, it defaults to the model's default view attribute.
296 296
297 297 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
298 298 // it would be great to have the widget manager add the cell metadata
299 299 // to the subview without having to add it here.
300 300 options = $.extend({ parent: this }, options || {});
301 301 var child_view = this.model.widget_manager.create_view(child_model, options, this);
302 302
303 303 // Associate the view id with the model id.
304 304 if (this.child_model_views[child_model.id] === undefined) {
305 305 this.child_model_views[child_model.id] = [];
306 306 }
307 307 this.child_model_views[child_model.id].push(child_view.id);
308 308
309 309 // Remember the view by id.
310 310 this.child_views[child_view.id] = child_view;
311 311 return child_view;
312 312 },
313 313
314 314 pop_child_view: function(child_model) {
315 315 // Delete a child view that was previously created using create_child_view.
316 316 var view_ids = this.child_model_views[child_model.id];
317 317 if (view_ids !== undefined) {
318 318
319 319 // Only delete the first view in the list.
320 320 var view_id = view_ids[0];
321 321 var view = this.child_views[view_id];
322 322 delete this.child_views[view_id];
323 323 view_ids.splice(0,1);
324 324 child_model.views.pop(view);
325 325
326 326 // Remove the view list specific to this model if it is empty.
327 327 if (view_ids.length === 0) {
328 328 delete this.child_model_views[child_model.id];
329 329 }
330 330 return view;
331 331 }
332 332 return null;
333 333 },
334 334
335 335 do_diff: function(old_list, new_list, removed_callback, added_callback) {
336 336 // Difference a changed list and call remove and add callbacks for
337 337 // each removed and added item in the new list.
338 338 //
339 339 // Parameters
340 340 // ----------
341 341 // old_list : array
342 342 // new_list : array
343 343 // removed_callback : Callback(item)
344 344 // Callback that is called for each item removed.
345 345 // added_callback : Callback(item)
346 346 // Callback that is called for each item added.
347 347
348 348 // Walk the lists until an unequal entry is found.
349 349 var i;
350 350 for (i = 0; i < new_list.length; i++) {
351 351 if (i >= old_list.length || new_list[i] !== old_list[i]) {
352 352 break;
353 353 }
354 354 }
355 355
356 356 // Remove the non-matching items from the old list.
357 357 for (var j = i; j < old_list.length; j++) {
358 358 removed_callback(old_list[j]);
359 359 }
360 360
361 361 // Add the rest of the new list items.
362 362 for (; i < new_list.length; i++) {
363 363 added_callback(new_list[i]);
364 364 }
365 365 },
366 366
367 367 callbacks: function(){
368 368 // Create msg callbacks for a comm msg.
369 369 return this.model.callbacks(this);
370 370 },
371 371
372 372 render: function(){
373 373 // Render the view.
374 374 //
375 375 // By default, this is only called the first time the view is created
376 376 },
377 377
378 378 show: function(){
379 379 // Show the widget-area
380 380 if (this.options && this.options.cell &&
381 381 this.options.cell.widget_area !== undefined) {
382 382 this.options.cell.widget_area.show();
383 383 }
384 384 },
385 385
386 386 send: function (content) {
387 387 // Send a custom msg associated with this view.
388 388 this.model.send(content, this.callbacks());
389 389 },
390 390
391 391 touch: function () {
392 392 this.model.save_changes(this.callbacks());
393 393 },
394 394
395 395 after_displayed: function (callback, context) {
396 396 // Calls the callback right away is the view is already displayed
397 397 // otherwise, register the callback to the 'displayed' event.
398 398 if (this.is_displayed) {
399 399 callback.apply(context);
400 400 } else {
401 401 this.on('displayed', callback, context);
402 402 }
403 403 },
404 404 });
405 405
406 406
407 407 var DOMWidgetView = WidgetView.extend({
408 408 initialize: function (parameters) {
409 409 // Public constructor
410 410 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
411 411 this.on('displayed', this.show, this);
412 412 this.model.on('change:visible', this.update_visible, this);
413 413 this.model.on('change:_css', this.update_css, this);
414 414
415 415 this.model.on('change:_dom_classes', function(model, new_classes) {
416 416 var old_classes = model.previous('_dom_classes');
417 417 this.update_classes(old_classes, new_classes);
418 418 }, this);
419 419
420 this.model.on('change:fore_color', function (model, value) {
420 this.model.on('change:color', function (model, value) {
421 421 this.update_attr('color', value); }, this);
422 422
423 this.model.on('change:back_color', function (model, value) {
423 this.model.on('change:background_color', function (model, value) {
424 424 this.update_attr('background', value); }, this);
425 425
426 426 this.model.on('change:width', function (model, value) {
427 427 this.update_attr('width', value); }, this);
428 428
429 429 this.model.on('change:height', function (model, value) {
430 430 this.update_attr('height', value); }, this);
431 431
432 432 this.model.on('change:border_color', function (model, value) {
433 433 this.update_attr('border-color', value); }, this);
434 434
435 435 this.model.on('change:border_width', function (model, value) {
436 436 this.update_attr('border-width', value); }, this);
437 437
438 438 this.model.on('change:border_style', function (model, value) {
439 439 this.update_attr('border-style', value); }, this);
440 440
441 441 this.model.on('change:font_style', function (model, value) {
442 442 this.update_attr('font-style', value); }, this);
443 443
444 444 this.model.on('change:font_weight', function (model, value) {
445 445 this.update_attr('font-weight', value); }, this);
446 446
447 447 this.model.on('change:font_size', function (model, value) {
448 448 this.update_attr('font-size', value); }, this);
449 449
450 450 this.model.on('change:font_family', function (model, value) {
451 451 this.update_attr('font-family', value); }, this);
452 452
453 453 this.model.on('change:padding', function (model, value) {
454 454 this.update_attr('padding', value); }, this);
455 455
456 456 this.model.on('change:margin', function (model, value) {
457 457 this.update_attr('margin', value); }, this);
458 458
459 459 this.after_displayed(function() {
460 460 this.update_visible(this.model, this.model.get("visible"));
461 461 this.update_css(this.model, this.model.get("_css"));
462 462
463 463 this.update_classes([], this.model.get('_dom_classes'));
464 this.update_attr('color', this.model.get('fore_color'));
465 this.update_attr('background', this.model.get('back_color'));
464 this.update_attr('color', this.model.get('color'));
465 this.update_attr('background', this.model.get('background_color'));
466 466 this.update_attr('width', this.model.get('width'));
467 467 this.update_attr('height', this.model.get('height'));
468 468 this.update_attr('border-color', this.model.get('border_color'));
469 469 this.update_attr('border-width', this.model.get('border_width'));
470 470 this.update_attr('border-style', this.model.get('border_style'));
471 471 this.update_attr('font-style', this.model.get('font_style'));
472 472 this.update_attr('font-weight', this.model.get('font_weight'));
473 473 this.update_attr('font-size', this.model.get('font_size'));
474 474 this.update_attr('font-family', this.model.get('font_family'));
475 475 this.update_attr('padding', this.model.get('padding'));
476 476 this.update_attr('margin', this.model.get('margin'));
477 477 }, this);
478 478 },
479 479
480 480 update_attr: function(name, value) {
481 481 // Set a css attr of the widget view.
482 482 this.$el.css(name, value);
483 483 },
484 484
485 485 update_visible: function(model, value) {
486 486 // Update visibility
487 487 this.$el.toggle(value);
488 488 },
489 489
490 490 update_css: function (model, css) {
491 491 // Update the css styling of this view.
492 492 var e = this.$el;
493 493 if (css === undefined) {return;}
494 494 for (var i = 0; i < css.length; i++) {
495 495 // Apply the css traits to all elements that match the selector.
496 496 var selector = css[i][0];
497 497 var elements = this._get_selector_element(selector);
498 498 if (elements.length > 0) {
499 499 var trait_key = css[i][1];
500 500 var trait_value = css[i][2];
501 501 elements.css(trait_key ,trait_value);
502 502 }
503 503 }
504 504 },
505 505
506 506 update_classes: function (old_classes, new_classes, $el) {
507 507 // Update the DOM classes applied to an element, default to this.$el.
508 508 if ($el===undefined) {
509 509 $el = this.$el;
510 510 }
511 511 this.do_diff(old_classes, new_classes, function(removed) {
512 512 $el.removeClass(removed);
513 513 }, function(added) {
514 514 $el.addClass(added);
515 515 });
516 516 },
517 517
518 518 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
519 519 // Update the DOM classes applied to the widget based on a single
520 520 // trait's value.
521 521 //
522 522 // Given a trait value classes map, this function automatically
523 523 // handles applying the appropriate classes to the widget element
524 524 // and removing classes that are no longer valid.
525 525 //
526 526 // Parameters
527 527 // ----------
528 528 // class_map: dictionary
529 529 // Dictionary of trait values to class lists.
530 530 // Example:
531 531 // {
532 532 // success: ['alert', 'alert-success'],
533 533 // info: ['alert', 'alert-info'],
534 534 // warning: ['alert', 'alert-warning'],
535 535 // danger: ['alert', 'alert-danger']
536 536 // };
537 537 // trait_name: string
538 538 // Name of the trait to check the value of.
539 539 // previous_trait_value: optional string, default ''
540 540 // Last trait value
541 541 // $el: optional jQuery element handle, defaults to this.$el
542 542 // Element that the classes are applied to.
543 543 var key = previous_trait_value;
544 544 if (key === undefined) {
545 545 key = this.model.previous(trait_name);
546 546 }
547 547 var old_classes = class_map[key] ? class_map[key] : [];
548 548 key = this.model.get(trait_name);
549 549 var new_classes = class_map[key] ? class_map[key] : [];
550 550
551 551 this.update_classes(old_classes, new_classes, $el || this.$el);
552 552 },
553 553
554 554 _get_selector_element: function (selector) {
555 555 // Get the elements via the css selector.
556 556 var elements;
557 557 if (!selector) {
558 558 elements = this.$el;
559 559 } else {
560 560 elements = this.$el.find(selector).addBack(selector);
561 561 }
562 562 return elements;
563 563 },
564 564 });
565 565
566 566 var widget = {
567 567 'WidgetModel': WidgetModel,
568 568 'WidgetView': WidgetView,
569 569 'DOMWidgetView': DOMWidgetView,
570 570 };
571 571
572 572 // For backwards compatability.
573 573 $.extend(IPython, widget);
574 574
575 575 return widget;
576 576 });
@@ -1,427 +1,427 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.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 103 _view_name = Unicode('WidgetView', 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]
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 371 # Show view. By sending a display message, the comm is opened and the
372 372 # initial state is sent.
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 fore_color = Unicode(sync=True)
392 back_color = Unicode(sync=True)
391 color = Unicode(sync=True)
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_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
397 397 'none',
398 398 'hidden',
399 399 'dotted',
400 400 'dashed',
401 401 'solid',
402 402 'double',
403 403 'groove',
404 404 'ridge',
405 405 'inset',
406 406 'outset',
407 407 'initial',
408 408 'inherit', ''],
409 409 default_value='', sync=True)
410 410
411 411 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
412 412 'normal',
413 413 'italic',
414 414 'oblique',
415 415 'initial',
416 416 'inherit', ''],
417 417 default_value='', sync=True)
418 418 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
419 419 'normal',
420 420 'bold',
421 421 'bolder',
422 422 'lighter',
423 423 'initial',
424 424 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
425 425 default_value='', sync=True)
426 426 font_size = CUnicode(sync=True)
427 427 font_family = Unicode(sync=True)
General Comments 0
You need to be logged in to leave comments. Login now