##// END OF EJS Templates
Prepend a sentinel value to model ids to distinguish them from normal UUIDs (from Sylvain Corlay).
Jason Grout -
Show More
@@ -1,473 +1,475 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 return value.id;
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 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
256 var model = this.widget_manager.get_model(value.slice(10, value.length));
257 if (model) {
258 return model;
259 } else {
260 return value;
261 }
255 262 } else {
256 var model = this.widget_manager.get_model(value);
257 if (model) {
258 return model;
259 } else {
260 263 return value;
261 }
262 264 }
263 265 },
264 266
265 267 });
266 268 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
267 269
268 270
269 271 var WidgetView = Backbone.View.extend({
270 272 initialize: function(parameters) {
271 273 // Public constructor.
272 274 this.model.on('change',this.update,this);
273 275 this.options = parameters.options;
274 276 this.child_model_views = {};
275 277 this.child_views = {};
276 278 this.model.views.push(this);
277 279 this.id = this.id || IPython.utils.uuid();
278 280 },
279 281
280 282 update: function(){
281 283 // Triggered on model change.
282 284 //
283 285 // Update view to be consistent with this.model
284 286 },
285 287
286 288 create_child_view: function(child_model, options) {
287 289 // Create and return a child view.
288 290 //
289 291 // -given a model and (optionally) a view name if the view name is
290 292 // not given, it defaults to the model's default view attribute.
291 293
292 294 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
293 295 // it would be great to have the widget manager add the cell metadata
294 296 // to the subview without having to add it here.
295 297 options = $.extend({ parent: this }, options || {});
296 298 var child_view = this.model.widget_manager.create_view(child_model, options, this);
297 299
298 300 // Associate the view id with the model id.
299 301 if (this.child_model_views[child_model.id] === undefined) {
300 302 this.child_model_views[child_model.id] = [];
301 303 }
302 304 this.child_model_views[child_model.id].push(child_view.id);
303 305
304 306 // Remember the view by id.
305 307 this.child_views[child_view.id] = child_view;
306 308 return child_view;
307 309 },
308 310
309 311 pop_child_view: function(child_model) {
310 312 // Delete a child view that was previously created using create_child_view.
311 313 var view_ids = this.child_model_views[child_model.id];
312 314 if (view_ids !== undefined) {
313 315
314 316 // Only delete the first view in the list.
315 317 var view_id = view_ids[0];
316 318 var view = this.child_views[view_id];
317 319 delete this.child_views[view_id];
318 320 view_ids.splice(0,1);
319 321 child_model.views.pop(view);
320 322
321 323 // Remove the view list specific to this model if it is empty.
322 324 if (view_ids.length === 0) {
323 325 delete this.child_model_views[child_model.id];
324 326 }
325 327 return view;
326 328 }
327 329 return null;
328 330 },
329 331
330 332 do_diff: function(old_list, new_list, removed_callback, added_callback) {
331 333 // Difference a changed list and call remove and add callbacks for
332 334 // each removed and added item in the new list.
333 335 //
334 336 // Parameters
335 337 // ----------
336 338 // old_list : array
337 339 // new_list : array
338 340 // removed_callback : Callback(item)
339 341 // Callback that is called for each item removed.
340 342 // added_callback : Callback(item)
341 343 // Callback that is called for each item added.
342 344
343 345 // Walk the lists until an unequal entry is found.
344 346 var i;
345 347 for (i = 0; i < new_list.length; i++) {
346 348 if (i < old_list.length || new_list[i] !== old_list[i]) {
347 349 break;
348 350 }
349 351 }
350 352
351 353 // Remove the non-matching items from the old list.
352 354 for (var j = i; j < old_list.length; j++) {
353 355 removed_callback(old_list[j]);
354 356 }
355 357
356 358 // Add the rest of the new list items.
357 359 for (i; i < new_list.length; i++) {
358 360 added_callback(new_list[i]);
359 361 }
360 362 },
361 363
362 364 callbacks: function(){
363 365 // Create msg callbacks for a comm msg.
364 366 return this.model.callbacks(this);
365 367 },
366 368
367 369 render: function(){
368 370 // Render the view.
369 371 //
370 372 // By default, this is only called the first time the view is created
371 373 },
372 374
373 375 send: function (content) {
374 376 // Send a custom msg associated with this view.
375 377 this.model.send(content, this.callbacks());
376 378 },
377 379
378 380 touch: function () {
379 381 this.model.save_changes(this.callbacks());
380 382 },
381 383 });
382 384
383 385
384 386 var DOMWidgetView = WidgetView.extend({
385 387 initialize: function (options) {
386 388 // Public constructor
387 389
388 390 // In the future we may want to make changes more granular
389 391 // (e.g., trigger on visible:change).
390 392 this.model.on('change', this.update, this);
391 393 this.model.on('msg:custom', this.on_msg, this);
392 394 DOMWidgetView.__super__.initialize.apply(this, arguments);
393 395 },
394 396
395 397 on_msg: function(msg) {
396 398 // Handle DOM specific msgs.
397 399 switch(msg.msg_type) {
398 400 case 'add_class':
399 401 this.add_class(msg.selector, msg.class_list);
400 402 break;
401 403 case 'remove_class':
402 404 this.remove_class(msg.selector, msg.class_list);
403 405 break;
404 406 }
405 407 },
406 408
407 409 add_class: function (selector, class_list) {
408 410 // Add a DOM class to an element.
409 411 this._get_selector_element(selector).addClass(class_list);
410 412 },
411 413
412 414 remove_class: function (selector, class_list) {
413 415 // Remove a DOM class from an element.
414 416 this._get_selector_element(selector).removeClass(class_list);
415 417 },
416 418
417 419 update: function () {
418 420 // Update the contents of this view
419 421 //
420 422 // Called when the model is changed. The model may have been
421 423 // changed by another view or by a state update from the back-end.
422 424 // The very first update seems to happen before the element is
423 425 // finished rendering so we use setTimeout to give the element time
424 426 // to render
425 427 var e = this.$el;
426 428 var visible = this.model.get('visible');
427 429 setTimeout(function() {e.toggle(visible);},0);
428 430
429 431 var css = this.model.get('_css');
430 432 if (css === undefined) {return;}
431 433 for (var i = 0; i < css.length; i++) {
432 434 // Apply the css traits to all elements that match the selector.
433 435 var selector = css[i][0];
434 436 var elements = this._get_selector_element(selector);
435 437 if (elements.length > 0) {
436 438 var trait_key = css[i][1];
437 439 var trait_value = css[i][2];
438 440 elements.css(trait_key ,trait_value);
439 441 }
440 442 }
441 443 },
442 444
443 445 _get_selector_element: function (selector) {
444 446 // Get the elements via the css selector.
445 447
446 448 // If the selector is blank, apply the style to the $el_to_style
447 449 // element. If the $el_to_style element is not defined, use apply
448 450 // the style to the view's element.
449 451 var elements;
450 452 if (!selector) {
451 453 if (this.$el_to_style === undefined) {
452 454 elements = this.$el;
453 455 } else {
454 456 elements = this.$el_to_style;
455 457 }
456 458 } else {
457 459 elements = this.$el.find(selector);
458 460 }
459 461 return elements;
460 462 },
461 463 });
462 464
463 465 var widget = {
464 466 'WidgetModel': WidgetModel,
465 467 'WidgetView': WidgetView,
466 468 'DOMWidgetView': DOMWidgetView,
467 469 };
468 470
469 471 // For backwards compatability.
470 472 $.extend(IPython, widget);
471 473
472 474 return widget;
473 475 });
@@ -1,455 +1,455 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
17 17 from IPython.core.getipython import get_ipython
18 18 from IPython.kernel.comm import Comm
19 19 from IPython.config import LoggingConfigurable
20 20 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int
21 21 from IPython.utils.py3compat import string_types
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Classes
25 25 #-----------------------------------------------------------------------------
26 26 class CallbackDispatcher(LoggingConfigurable):
27 27 """A structure for registering and running callbacks"""
28 28 callbacks = List()
29 29
30 30 def __call__(self, *args, **kwargs):
31 31 """Call all of the registered callbacks."""
32 32 value = None
33 33 for callback in self.callbacks:
34 34 try:
35 35 local_value = callback(*args, **kwargs)
36 36 except Exception as e:
37 37 ip = get_ipython()
38 38 if ip is None:
39 39 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
40 40 else:
41 41 ip.showtraceback()
42 42 else:
43 43 value = local_value if local_value is not None else value
44 44 return value
45 45
46 46 def register_callback(self, callback, remove=False):
47 47 """(Un)Register a callback
48 48
49 49 Parameters
50 50 ----------
51 51 callback: method handle
52 52 Method to be registered or unregistered.
53 53 remove=False: bool
54 54 Whether to unregister the callback."""
55 55
56 56 # (Un)Register the callback.
57 57 if remove and callback in self.callbacks:
58 58 self.callbacks.remove(callback)
59 59 elif not remove and callback not in self.callbacks:
60 60 self.callbacks.append(callback)
61 61
62 62 def _show_traceback(method):
63 63 """decorator for showing tracebacks in IPython"""
64 64 def m(self, *args, **kwargs):
65 65 try:
66 66 return(method(self, *args, **kwargs))
67 67 except Exception as e:
68 68 ip = get_ipython()
69 69 if ip is None:
70 70 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
71 71 else:
72 72 ip.showtraceback()
73 73 return m
74 74
75 75 class Widget(LoggingConfigurable):
76 76 #-------------------------------------------------------------------------
77 77 # Class attributes
78 78 #-------------------------------------------------------------------------
79 79 _widget_construction_callback = None
80 80 widgets = {}
81 81
82 82 @staticmethod
83 83 def on_widget_constructed(callback):
84 84 """Registers a callback to be called when a widget is constructed.
85 85
86 86 The callback must have the following signature:
87 87 callback(widget)"""
88 88 Widget._widget_construction_callback = callback
89 89
90 90 @staticmethod
91 91 def _call_widget_constructed(widget):
92 92 """Static method, called when a widget is constructed."""
93 93 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
94 94 Widget._widget_construction_callback(widget)
95 95
96 96 #-------------------------------------------------------------------------
97 97 # Traits
98 98 #-------------------------------------------------------------------------
99 99 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
100 100 registered in the front-end to create and sync this widget with.""")
101 101 _view_name = Unicode(help="""Default view registered in the front-end
102 102 to use to represent the widget.""", sync=True)
103 103 _comm = Instance('IPython.kernel.comm.Comm')
104 104
105 105 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
106 106 front-end can send before receiving an idle msg from the back-end.""")
107 107
108 108 keys = List()
109 109 def _keys_default(self):
110 110 return [name for name in self.traits(sync=True)]
111 111
112 112 _property_lock = Tuple((None, None))
113 113
114 114 _display_callbacks = Instance(CallbackDispatcher, ())
115 115 _msg_callbacks = Instance(CallbackDispatcher, ())
116 116
117 117 #-------------------------------------------------------------------------
118 118 # (Con/de)structor
119 119 #-------------------------------------------------------------------------
120 120 def __init__(self, **kwargs):
121 121 """Public constructor"""
122 122 super(Widget, self).__init__(**kwargs)
123 123
124 124 self.on_trait_change(self._handle_property_changed, self.keys)
125 125 Widget._call_widget_constructed(self)
126 126
127 127 def __del__(self):
128 128 """Object disposal"""
129 129 self.close()
130 130
131 131 #-------------------------------------------------------------------------
132 132 # Properties
133 133 #-------------------------------------------------------------------------
134 134
135 135 @property
136 136 def comm(self):
137 137 """Gets the Comm associated with this widget.
138 138
139 139 If a Comm doesn't exist yet, a Comm will be created automagically."""
140 140 if self._comm is None:
141 141 # Create a comm.
142 142 self._comm = Comm(target_name=self._model_name)
143 143 self._comm.on_msg(self._handle_msg)
144 144 self._comm.on_close(self._close)
145 145 Widget.widgets[self.model_id] = self
146 146
147 147 # first update
148 148 self.send_state()
149 149 return self._comm
150 150
151 151 @property
152 152 def model_id(self):
153 153 """Gets the model id of this widget.
154 154
155 155 If a Comm doesn't exist yet, a Comm will be created automagically."""
156 156 return self.comm.comm_id
157 157
158 158 #-------------------------------------------------------------------------
159 159 # Methods
160 160 #-------------------------------------------------------------------------
161 161 def _close(self):
162 162 """Private close - cleanup objects, registry entries"""
163 163 del Widget.widgets[self.model_id]
164 164 self._comm = None
165 165
166 166 def close(self):
167 167 """Close method.
168 168
169 169 Closes the widget which closes the underlying comm.
170 170 When the comm is closed, all of the widget views are automatically
171 171 removed from the front-end."""
172 172 if self._comm is not None:
173 173 self._comm.close()
174 174 self._close()
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 (optional)
182 182 A single property's name to sync with the front-end.
183 183 """
184 184 self._send({
185 185 "method" : "update",
186 186 "state" : self.get_state()
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 (optional)
195 195 A single property's name to get.
196 196 """
197 197 keys = self.keys if key is None else [key]
198 198 state = {}
199 199 for k in keys:
200 200 f = self.trait_metadata(k, 'to_json')
201 201 value = getattr(self, k)
202 202 if f is not None:
203 203 state[k] = f(value)
204 204 else:
205 205 state[k] = self._serialize_trait(value)
206 206 return state
207 207
208 208 def send(self, content):
209 209 """Sends a custom msg to the widget model in the front-end.
210 210
211 211 Parameters
212 212 ----------
213 213 content : dict
214 214 Content of the message to send.
215 215 """
216 216 self._send({"method": "custom", "content": content})
217 217
218 218 def on_msg(self, callback, remove=False):
219 219 """(Un)Register a custom msg receive callback.
220 220
221 221 Parameters
222 222 ----------
223 223 callback: callable
224 224 callback will be passed two arguments when a message arrives::
225 225
226 226 callback(widget, content)
227 227
228 228 remove: bool
229 229 True if the callback should be unregistered."""
230 230 self._msg_callbacks.register_callback(callback, remove=remove)
231 231
232 232 def on_displayed(self, callback, remove=False):
233 233 """(Un)Register a widget displayed callback.
234 234
235 235 Parameters
236 236 ----------
237 237 callback: method handler
238 238 Must have a signature of::
239 239
240 240 callback(widget, **kwargs)
241 241
242 242 kwargs from display are passed through without modification.
243 243 remove: bool
244 244 True if the callback should be unregistered."""
245 245 self._display_callbacks.register_callback(callback, remove=remove)
246 246
247 247 #-------------------------------------------------------------------------
248 248 # Support methods
249 249 #-------------------------------------------------------------------------
250 250 @contextmanager
251 251 def _lock_property(self, key, value):
252 252 """Lock a property-value pair.
253 253
254 254 NOTE: This, in addition to the single lock for all state changes, is
255 255 flawed. In the future we may want to look into buffering state changes
256 256 back to the front-end."""
257 257 self._property_lock = (key, value)
258 258 try:
259 259 yield
260 260 finally:
261 261 self._property_lock = (None, None)
262 262
263 263 def _should_send_property(self, key, value):
264 264 """Check the property lock (property_lock)"""
265 265 return key != self._property_lock[0] or \
266 266 value != self._property_lock[1]
267 267
268 268 # Event handlers
269 269 @_show_traceback
270 270 def _handle_msg(self, msg):
271 271 """Called when a msg is received from the front-end"""
272 272 data = msg['content']['data']
273 273 method = data['method']
274 274 if not method in ['backbone', 'custom']:
275 275 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
276 276
277 277 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
278 278 if method == 'backbone' and 'sync_data' in data:
279 279 sync_data = data['sync_data']
280 280 self._handle_receive_state(sync_data) # handles all methods
281 281
282 282 # Handle a custom msg from the front-end
283 283 elif method == 'custom':
284 284 if 'content' in data:
285 285 self._handle_custom_msg(data['content'])
286 286
287 287 def _handle_receive_state(self, sync_data):
288 288 """Called when a state is received from the front-end."""
289 289 for name in self.keys:
290 290 if name in sync_data:
291 291 f = self.trait_metadata(name, 'from_json')
292 292 if f is not None:
293 293 value = f(sync_data[name])
294 294 else:
295 295 value = self._unserialize_trait(sync_data[name])
296 296 with self._lock_property(name, value):
297 297 setattr(self, name, value)
298 298
299 299 def _handle_custom_msg(self, content):
300 300 """Called when a custom msg is received."""
301 301 self._msg_callbacks(self, content)
302 302
303 303 def _handle_property_changed(self, name, old, new):
304 304 """Called when a property has been changed."""
305 305 # Make sure this isn't information that the front-end just sent us.
306 306 if self._should_send_property(name, new):
307 307 # Send new state to front-end
308 308 self.send_state(key=name)
309 309
310 310 def _handle_displayed(self, **kwargs):
311 311 """Called when a view has been displayed for this widget instance"""
312 312 self._display_callbacks(self, **kwargs)
313 313
314 314 def _serialize_trait(self, x):
315 315 """Serialize a trait value to json
316 316
317 317 Traverse lists/tuples and dicts and serialize their values as well.
318 318 Replace any widgets with their model_id
319 319 """
320 320 if isinstance(x, dict):
321 321 return {k: self._serialize_trait(v) for k, v in x.items()}
322 322 elif isinstance(x, (list, tuple)):
323 323 return [self._serialize_trait(v) for v in x]
324 324 elif isinstance(x, Widget):
325 return x.model_id
325 return "IPY_MODEL_" + x.model_id
326 326 else:
327 327 return x # Value must be JSON-able
328 328
329 329 def _unserialize_trait(self, x):
330 330 """Convert json values to objects
331 331
332 332 We explicitly support converting valid string widget UUIDs to Widget references.
333 333 """
334 334 if isinstance(x, dict):
335 335 return {k: self._unserialize_trait(v) for k, v in x.items()}
336 336 elif isinstance(x, (list, tuple)):
337 337 return [self._unserialize_trait(v) for v in x]
338 elif isinstance(x, string_types) and x in Widget.widgets:
338 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
339 339 # we want to support having child widgets at any level in a hierarchy
340 340 # trusting that a widget UUID will not appear out in the wild
341 341 return Widget.widgets[x]
342 342 else:
343 343 return x
344 344
345 345 def _ipython_display_(self, **kwargs):
346 346 """Called when `IPython.display.display` is called on the widget."""
347 347 # Show view. By sending a display message, the comm is opened and the
348 348 # initial state is sent.
349 349 self._send({"method": "display"})
350 350 self._handle_displayed(**kwargs)
351 351
352 352 def _send(self, msg):
353 353 """Sends a message to the model in the front-end."""
354 354 self.comm.send(msg)
355 355
356 356
357 357 class DOMWidget(Widget):
358 358 visible = Bool(True, help="Whether the widget is visible.", sync=True)
359 359 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
360 360
361 361 def get_css(self, key, selector=""):
362 362 """Get a CSS property of the widget.
363 363
364 364 Note: This function does not actually request the CSS from the
365 365 front-end; Only properties that have been set with set_css can be read.
366 366
367 367 Parameters
368 368 ----------
369 369 key: unicode
370 370 CSS key
371 371 selector: unicode (optional)
372 372 JQuery selector used when the CSS key/value was set.
373 373 """
374 374 if selector in self._css and key in self._css[selector]:
375 375 return self._css[selector][key]
376 376 else:
377 377 return None
378 378
379 379 def set_css(self, dict_or_key, value=None, selector=''):
380 380 """Set one or more CSS properties of the widget.
381 381
382 382 This function has two signatures:
383 383 - set_css(css_dict, selector='')
384 384 - set_css(key, value, selector='')
385 385
386 386 Parameters
387 387 ----------
388 388 css_dict : dict
389 389 CSS key/value pairs to apply
390 390 key: unicode
391 391 CSS key
392 392 value:
393 393 CSS value
394 394 selector: unicode (optional, kwarg only)
395 395 JQuery selector to use to apply the CSS key/value. If no selector
396 396 is provided, an empty selector is used. An empty selector makes the
397 397 front-end try to apply the css to a default element. The default
398 398 element is an attribute unique to each view, which is a DOM element
399 399 of the view that should be styled with common CSS (see
400 400 `$el_to_style` in the Javascript code).
401 401 """
402 402 if value is None:
403 403 css_dict = dict_or_key
404 404 else:
405 405 css_dict = {dict_or_key: value}
406 406
407 407 for (key, value) in css_dict.items():
408 408 # First remove the selector/key pair from the css list if it exists.
409 409 # Then add the selector/key pair and new value to the bottom of the
410 410 # list.
411 411 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
412 412 self._css += [(selector, key, value)]
413 413 self.send_state('_css')
414 414
415 415 def add_class(self, class_names, selector=""):
416 416 """Add class[es] to a DOM element.
417 417
418 418 Parameters
419 419 ----------
420 420 class_names: unicode or list
421 421 Class name(s) to add to the DOM element(s).
422 422 selector: unicode (optional)
423 423 JQuery selector to select the DOM element(s) that the class(es) will
424 424 be added to.
425 425 """
426 426 class_list = class_names
427 427 if isinstance(class_list, (list, tuple)):
428 428 class_list = ' '.join(class_list)
429 429
430 430 self.send({
431 431 "msg_type" : "add_class",
432 432 "class_list" : class_list,
433 433 "selector" : selector
434 434 })
435 435
436 436 def remove_class(self, class_names, selector=""):
437 437 """Remove class[es] from a DOM element.
438 438
439 439 Parameters
440 440 ----------
441 441 class_names: unicode or list
442 442 Class name(s) to remove from the DOM element(s).
443 443 selector: unicode (optional)
444 444 JQuery selector to select the DOM element(s) that the class(es) will
445 445 be removed from.
446 446 """
447 447 class_list = class_names
448 448 if isinstance(class_list, (list, tuple)):
449 449 class_list = ' '.join(class_list)
450 450
451 451 self.send({
452 452 "msg_type" : "remove_class",
453 453 "class_list" : class_list,
454 454 "selector" : selector,
455 455 })
General Comments 0
You need to be logged in to leave comments. Login now