##// END OF EJS Templates
Merge pull request #5170 from jdfreder/widget-throttle-trait...
Min RK -
r15424:f6ccfaf8 merge
parent child Browse files
Show More
@@ -1,451 +1,450
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Base Widget Model and View classes
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["notebook/js/widgetmanager",
18 18 "underscore",
19 19 "backbone"],
20 20 function(WidgetManager, _, Backbone){
21 21
22 22 var WidgetModel = Backbone.Model.extend({
23 23 constructor: function (widget_manager, model_id, comm) {
24 24 // Constructor
25 25 //
26 26 // Creates a WidgetModel instance.
27 27 //
28 28 // Parameters
29 29 // ----------
30 30 // widget_manager : WidgetManager instance
31 31 // model_id : string
32 32 // An ID unique to this model.
33 33 // comm : Comm instance (optional)
34 34 this.widget_manager = widget_manager;
35 35 this._buffered_state_diff = {};
36 36 this.pending_msgs = 0;
37 this.msg_throttle = 3;
38 37 this.msg_buffer = null;
39 38 this.key_value_lock = null;
40 39 this.id = model_id;
41 40 this.views = [];
42 41
43 42 if (comm !== undefined) {
44 43 // Remember comm associated with the model.
45 44 this.comm = comm;
46 45 comm.model = this;
47 46
48 47 // Hook comm messages up to model.
49 48 comm.on_close($.proxy(this._handle_comm_closed, this));
50 49 comm.on_msg($.proxy(this._handle_comm_msg, this));
51 50 }
52 51 return Backbone.Model.apply(this);
53 52 },
54 53
55 54 send: function (content, callbacks) {
56 55 // Send a custom msg over the comm.
57 56 if (this.comm !== undefined) {
58 57 var data = {method: 'custom', content: content};
59 58 this.comm.send(data, callbacks);
60 59 this.pending_msgs++;
61 60 }
62 61 },
63 62
64 63 _handle_comm_closed: function (msg) {
65 64 // Handle when a widget is closed.
66 65 this.trigger('comm:close');
67 66 delete this.comm.model; // Delete ref so GC will collect widget model.
68 67 delete this.comm;
69 68 delete this.model_id; // Delete id from model so widget manager cleans up.
70 69 _.each(this.views, function(view, i) {
71 70 view.remove();
72 71 });
73 72 },
74 73
75 74 _handle_comm_msg: function (msg) {
76 75 // Handle incoming comm msg.
77 76 var method = msg.content.data.method;
78 77 switch (method) {
79 78 case 'update':
80 79 this.apply_update(msg.content.data.state);
81 80 break;
82 81 case 'custom':
83 82 this.trigger('msg:custom', msg.content.data.content);
84 83 break;
85 84 case 'display':
86 85 this.widget_manager.display_view(msg, this);
87 86 break;
88 87 }
89 88 },
90 89
91 90 apply_update: function (state) {
92 91 // Handle when a widget is updated via the python side.
93 92 var that = this;
94 93 _.each(state, function(value, key) {
95 94 that.key_value_lock = [key, value];
96 95 try {
97 96 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
98 97 } finally {
99 98 that.key_value_lock = null;
100 99 }
101 100 });
102 101 },
103 102
104 103 _handle_status: function (msg, callbacks) {
105 104 // Handle status msgs.
106 105
107 106 // execution_state : ('busy', 'idle', 'starting')
108 107 if (this.comm !== undefined) {
109 108 if (msg.content.execution_state ==='idle') {
110 109 // Send buffer if this message caused another message to be
111 110 // throttled.
112 111 if (this.msg_buffer !== null &&
113 this.msg_throttle === this.pending_msgs) {
112 (this.get('msg_throttle') || 3) === this.pending_msgs) {
114 113 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
115 114 this.comm.send(data, callbacks);
116 115 this.msg_buffer = null;
117 116 } else {
118 117 --this.pending_msgs;
119 118 }
120 119 }
121 120 }
122 121 },
123 122
124 123 callbacks: function(view) {
125 124 // Create msg callbacks for a comm msg.
126 125 var callbacks = this.widget_manager.callbacks(view);
127 126
128 127 if (callbacks.iopub === undefined) {
129 128 callbacks.iopub = {};
130 129 }
131 130
132 131 var that = this;
133 132 callbacks.iopub.status = function (msg) {
134 133 that._handle_status(msg, callbacks);
135 134 };
136 135 return callbacks;
137 136 },
138 137
139 138 set: function(key, val, options) {
140 139 // Set a value.
141 140 var return_value = WidgetModel.__super__.set.apply(this, arguments);
142 141
143 142 // Backbone only remembers the diff of the most recent set()
144 143 // operation. Calling set multiple times in a row results in a
145 144 // loss of diff information. Here we keep our own running diff.
146 145 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
147 146 return return_value;
148 147 },
149 148
150 149 sync: function (method, model, options) {
151 150 // Handle sync to the back-end. Called when a model.save() is called.
152 151
153 152 // Make sure a comm exists.
154 153 var error = options.error || function() {
155 154 console.error('Backbone sync error:', arguments);
156 155 };
157 156 if (this.comm === undefined) {
158 157 error();
159 158 return false;
160 159 }
161 160
162 161 // Delete any key value pairs that the back-end already knows about.
163 162 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
164 163 if (this.key_value_lock !== null) {
165 164 var key = this.key_value_lock[0];
166 165 var value = this.key_value_lock[1];
167 166 if (attrs[key] === value) {
168 167 delete attrs[key];
169 168 }
170 169 }
171 170
172 171 // Only sync if there are attributes to send to the back-end.
173 172 attrs = this._pack_models(attrs);
174 173 if (_.size(attrs) > 0) {
175 174
176 175 // If this message was sent via backbone itself, it will not
177 176 // have any callbacks. It's important that we create callbacks
178 177 // so we can listen for status messages, etc...
179 178 var callbacks = options.callbacks || this.callbacks();
180 179
181 180 // Check throttle.
182 if (this.pending_msgs >= this.msg_throttle) {
181 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
183 182 // The throttle has been exceeded, buffer the current msg so
184 183 // it can be sent once the kernel has finished processing
185 184 // some of the existing messages.
186 185
187 186 // Combine updates if it is a 'patch' sync, otherwise replace updates
188 187 switch (method) {
189 188 case 'patch':
190 189 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
191 190 break;
192 191 case 'update':
193 192 case 'create':
194 193 this.msg_buffer = attrs;
195 194 break;
196 195 default:
197 196 error();
198 197 return false;
199 198 }
200 199 this.msg_buffer_callbacks = callbacks;
201 200
202 201 } else {
203 202 // We haven't exceeded the throttle, send the message like
204 203 // normal.
205 204 var data = {method: 'backbone', sync_data: attrs};
206 205 this.comm.send(data, callbacks);
207 206 this.pending_msgs++;
208 207 }
209 208 }
210 209 // Since the comm is a one-way communication, assume the message
211 210 // arrived. Don't call success since we don't have a model back from the server
212 211 // this means we miss out on the 'sync' event.
213 212 this._buffered_state_diff = {};
214 213 },
215 214
216 215 save_changes: function(callbacks) {
217 216 // Push this model's state to the back-end
218 217 //
219 218 // This invokes a Backbone.Sync.
220 219 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
221 220 },
222 221
223 222 _pack_models: function(value) {
224 223 // Replace models with model ids recursively.
225 224 if (value instanceof Backbone.Model) {
226 225 return value.id;
227 226
228 227 } else if ($.isArray(value)) {
229 228 var packed = [];
230 229 var that = this;
231 230 _.each(value, function(sub_value, key) {
232 231 packed.push(that._pack_models(sub_value));
233 232 });
234 233 return packed;
235 234
236 235 } else if (value instanceof Object) {
237 236 var packed = {};
238 237 var that = this;
239 238 _.each(value, function(sub_value, key) {
240 239 packed[key] = that._pack_models(sub_value);
241 240 });
242 241 return packed;
243 242
244 243 } else {
245 244 return value;
246 245 }
247 246 },
248 247
249 248 _unpack_models: function(value) {
250 249 // Replace model ids with models recursively.
251 250 if ($.isArray(value)) {
252 251 var unpacked = [];
253 252 var that = this;
254 253 _.each(value, function(sub_value, key) {
255 254 unpacked.push(that._unpack_models(sub_value));
256 255 });
257 256 return unpacked;
258 257
259 258 } else if (value instanceof Object) {
260 259 var unpacked = {};
261 260 var that = this;
262 261 _.each(value, function(sub_value, key) {
263 262 unpacked[key] = that._unpack_models(sub_value);
264 263 });
265 264 return unpacked;
266 265
267 266 } else {
268 267 var model = this.widget_manager.get_model(value);
269 268 if (model) {
270 269 return model;
271 270 } else {
272 271 return value;
273 272 }
274 273 }
275 274 },
276 275
277 276 });
278 277 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
279 278
280 279
281 280 var WidgetView = Backbone.View.extend({
282 281 initialize: function(parameters) {
283 282 // Public constructor.
284 283 this.model.on('change',this.update,this);
285 284 this.options = parameters.options;
286 285 this.child_views = [];
287 286 this.model.views.push(this);
288 287 },
289 288
290 289 update: function(){
291 290 // Triggered on model change.
292 291 //
293 292 // Update view to be consistent with this.model
294 293 },
295 294
296 295 create_child_view: function(child_model, options) {
297 296 // Create and return a child view.
298 297 //
299 298 // -given a model and (optionally) a view name if the view name is
300 299 // not given, it defaults to the model's default view attribute.
301 300
302 301 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
303 302 // it would be great to have the widget manager add the cell metadata
304 303 // to the subview without having to add it here.
305 304 var child_view = this.model.widget_manager.create_view(child_model, options || {}, this);
306 305 this.child_views[child_model.id] = child_view;
307 306 return child_view;
308 307 },
309 308
310 309 delete_child_view: function(child_model, options) {
311 310 // Delete a child view that was previously created using create_child_view.
312 311 var view = this.child_views[child_model.id];
313 312 if (view !== undefined) {
314 313 delete this.child_views[child_model.id];
315 314 view.remove();
316 315 }
317 316 },
318 317
319 318 do_diff: function(old_list, new_list, removed_callback, added_callback) {
320 319 // Difference a changed list and call remove and add callbacks for
321 320 // each removed and added item in the new list.
322 321 //
323 322 // Parameters
324 323 // ----------
325 324 // old_list : array
326 325 // new_list : array
327 326 // removed_callback : Callback(item)
328 327 // Callback that is called for each item removed.
329 328 // added_callback : Callback(item)
330 329 // Callback that is called for each item added.
331 330
332 331
333 332 // removed items
334 333 _.each(_.difference(old_list, new_list), function(item, index, list) {
335 334 removed_callback(item);
336 335 }, this);
337 336
338 337 // added items
339 338 _.each(_.difference(new_list, old_list), function(item, index, list) {
340 339 added_callback(item);
341 340 }, this);
342 341 },
343 342
344 343 callbacks: function(){
345 344 // Create msg callbacks for a comm msg.
346 345 return this.model.callbacks(this);
347 346 },
348 347
349 348 render: function(){
350 349 // Render the view.
351 350 //
352 351 // By default, this is only called the first time the view is created
353 352 },
354 353
355 354 send: function (content) {
356 355 // Send a custom msg associated with this view.
357 356 this.model.send(content, this.callbacks());
358 357 },
359 358
360 359 touch: function () {
361 360 this.model.save_changes(this.callbacks());
362 361 },
363 362 });
364 363
365 364
366 365 var DOMWidgetView = WidgetView.extend({
367 366 initialize: function (options) {
368 367 // Public constructor
369 368
370 369 // In the future we may want to make changes more granular
371 370 // (e.g., trigger on visible:change).
372 371 this.model.on('change', this.update, this);
373 372 this.model.on('msg:custom', this.on_msg, this);
374 373 DOMWidgetView.__super__.initialize.apply(this, arguments);
375 374 },
376 375
377 376 on_msg: function(msg) {
378 377 // Handle DOM specific msgs.
379 378 switch(msg.msg_type) {
380 379 case 'add_class':
381 380 this.add_class(msg.selector, msg.class_list);
382 381 break;
383 382 case 'remove_class':
384 383 this.remove_class(msg.selector, msg.class_list);
385 384 break;
386 385 }
387 386 },
388 387
389 388 add_class: function (selector, class_list) {
390 389 // Add a DOM class to an element.
391 390 this._get_selector_element(selector).addClass(class_list);
392 391 },
393 392
394 393 remove_class: function (selector, class_list) {
395 394 // Remove a DOM class from an element.
396 395 this._get_selector_element(selector).removeClass(class_list);
397 396 },
398 397
399 398 update: function () {
400 399 // Update the contents of this view
401 400 //
402 401 // Called when the model is changed. The model may have been
403 402 // changed by another view or by a state update from the back-end.
404 403 // The very first update seems to happen before the element is
405 404 // finished rendering so we use setTimeout to give the element time
406 405 // to render
407 406 var e = this.$el;
408 407 var visible = this.model.get('visible');
409 408 setTimeout(function() {e.toggle(visible);},0);
410 409
411 410 var css = this.model.get('_css');
412 411 if (css === undefined) {return;}
413 412 var that = this;
414 413 _.each(css, function(css_traits, selector){
415 414 // Apply the css traits to all elements that match the selector.
416 415 var elements = that._get_selector_element(selector);
417 416 if (elements.length > 0) {
418 417 _.each(css_traits, function(css_value, css_key){
419 418 elements.css(css_key, css_value);
420 419 });
421 420 }
422 421 });
423 422 },
424 423
425 424 _get_selector_element: function (selector) {
426 425 // Get the elements via the css selector.
427 426
428 427 // If the selector is blank, apply the style to the $el_to_style
429 428 // element. If the $el_to_style element is not defined, use apply
430 429 // the style to the view's element.
431 430 var elements;
432 431 if (!selector) {
433 432 if (this.$el_to_style === undefined) {
434 433 elements = this.$el;
435 434 } else {
436 435 elements = this.$el_to_style;
437 436 }
438 437 } else {
439 438 elements = this.$el.find(selector);
440 439 }
441 440 return elements;
442 441 },
443 442 });
444 443
445 444 IPython.WidgetModel = WidgetModel;
446 445 IPython.WidgetView = WidgetView;
447 446 IPython.DOMWidgetView = DOMWidgetView;
448 447
449 448 // Pass through WidgetManager namespace.
450 449 return WidgetManager;
451 450 });
@@ -1,441 +1,443
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 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple
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 closed = Bool(False)
106 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
107 front-end can send before receiving an idle msg from the back-end.""")
106 108
107 109 keys = List()
108 110 def _keys_default(self):
109 111 return [name for name in self.traits(sync=True)]
110 112
111 113 _property_lock = Tuple((None, None))
112 114
113 115 _display_callbacks = Instance(CallbackDispatcher, ())
114 116 _msg_callbacks = Instance(CallbackDispatcher, ())
115 117
116 118 #-------------------------------------------------------------------------
117 119 # (Con/de)structor
118 120 #-------------------------------------------------------------------------
119 121 def __init__(self, **kwargs):
120 122 """Public constructor"""
121 123 super(Widget, self).__init__(**kwargs)
122 124
123 125 self.on_trait_change(self._handle_property_changed, self.keys)
124 126 Widget._call_widget_constructed(self)
125 127
126 128 def __del__(self):
127 129 """Object disposal"""
128 130 self.close()
129 131
130 132 #-------------------------------------------------------------------------
131 133 # Properties
132 134 #-------------------------------------------------------------------------
133 135
134 136 @property
135 137 def comm(self):
136 138 """Gets the Comm associated with this widget.
137 139
138 140 If a Comm doesn't exist yet, a Comm will be created automagically."""
139 141 if self._comm is None:
140 142 # Create a comm.
141 143 self._comm = Comm(target_name=self._model_name)
142 144 self._comm.on_msg(self._handle_msg)
143 145 self._comm.on_close(self._close)
144 146 Widget.widgets[self.model_id] = self
145 147
146 148 # first update
147 149 self.send_state()
148 150 return self._comm
149 151
150 152 @property
151 153 def model_id(self):
152 154 """Gets the model id of this widget.
153 155
154 156 If a Comm doesn't exist yet, a Comm will be created automagically."""
155 157 return self.comm.comm_id
156 158
157 159 #-------------------------------------------------------------------------
158 160 # Methods
159 161 #-------------------------------------------------------------------------
160 162 def _close(self):
161 163 """Private close - cleanup objects, registry entries"""
162 164 del Widget.widgets[self.model_id]
163 165 self._comm = None
164 166 self.closed = True
165 167
166 168 def close(self):
167 169 """Close method.
168 170
169 171 Closes the widget which closes the underlying comm.
170 172 When the comm is closed, all of the widget views are automatically
171 173 removed from the front-end."""
172 174 if not self.closed:
173 175 self._comm.close()
174 176 self._close()
175 177
176 178 def send_state(self, key=None):
177 179 """Sends the widget state, or a piece of it, to the front-end.
178 180
179 181 Parameters
180 182 ----------
181 183 key : unicode (optional)
182 184 A single property's name to sync with the front-end.
183 185 """
184 186 self._send({
185 187 "method" : "update",
186 188 "state" : self.get_state()
187 189 })
188 190
189 191 def get_state(self, key=None):
190 192 """Gets the widget state, or a piece of it.
191 193
192 194 Parameters
193 195 ----------
194 196 key : unicode (optional)
195 197 A single property's name to get.
196 198 """
197 199 keys = self.keys if key is None else [key]
198 200 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
199 201
200 202 def send(self, content):
201 203 """Sends a custom msg to the widget model in the front-end.
202 204
203 205 Parameters
204 206 ----------
205 207 content : dict
206 208 Content of the message to send.
207 209 """
208 210 self._send({"method": "custom", "content": content})
209 211
210 212 def on_msg(self, callback, remove=False):
211 213 """(Un)Register a custom msg receive callback.
212 214
213 215 Parameters
214 216 ----------
215 217 callback: callable
216 218 callback will be passed two arguments when a message arrives::
217 219
218 220 callback(widget, content)
219 221
220 222 remove: bool
221 223 True if the callback should be unregistered."""
222 224 self._msg_callbacks.register_callback(callback, remove=remove)
223 225
224 226 def on_displayed(self, callback, remove=False):
225 227 """(Un)Register a widget displayed callback.
226 228
227 229 Parameters
228 230 ----------
229 231 callback: method handler
230 232 Must have a signature of::
231 233
232 234 callback(widget, **kwargs)
233 235
234 236 kwargs from display are passed through without modification.
235 237 remove: bool
236 238 True if the callback should be unregistered."""
237 239 self._display_callbacks.register_callback(callback, remove=remove)
238 240
239 241 #-------------------------------------------------------------------------
240 242 # Support methods
241 243 #-------------------------------------------------------------------------
242 244 @contextmanager
243 245 def _lock_property(self, key, value):
244 246 """Lock a property-value pair.
245 247
246 248 NOTE: This, in addition to the single lock for all state changes, is
247 249 flawed. In the future we may want to look into buffering state changes
248 250 back to the front-end."""
249 251 self._property_lock = (key, value)
250 252 try:
251 253 yield
252 254 finally:
253 255 self._property_lock = (None, None)
254 256
255 257 def _should_send_property(self, key, value):
256 258 """Check the property lock (property_lock)"""
257 259 return key != self._property_lock[0] or \
258 260 value != self._property_lock[1]
259 261
260 262 # Event handlers
261 263 @_show_traceback
262 264 def _handle_msg(self, msg):
263 265 """Called when a msg is received from the front-end"""
264 266 data = msg['content']['data']
265 267 method = data['method']
266 268 if not method in ['backbone', 'custom']:
267 269 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
268 270
269 271 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
270 272 if method == 'backbone' and 'sync_data' in data:
271 273 sync_data = data['sync_data']
272 274 self._handle_receive_state(sync_data) # handles all methods
273 275
274 276 # Handle a custom msg from the front-end
275 277 elif method == 'custom':
276 278 if 'content' in data:
277 279 self._handle_custom_msg(data['content'])
278 280
279 281 def _handle_receive_state(self, sync_data):
280 282 """Called when a state is received from the front-end."""
281 283 for name in self.keys:
282 284 if name in sync_data:
283 285 value = self._unpack_widgets(sync_data[name])
284 286 with self._lock_property(name, value):
285 287 setattr(self, name, value)
286 288
287 289 def _handle_custom_msg(self, content):
288 290 """Called when a custom msg is received."""
289 291 self._msg_callbacks(self, content)
290 292
291 293 def _handle_property_changed(self, name, old, new):
292 294 """Called when a property has been changed."""
293 295 # Make sure this isn't information that the front-end just sent us.
294 296 if self._should_send_property(name, new):
295 297 # Send new state to front-end
296 298 self.send_state(key=name)
297 299
298 300 def _handle_displayed(self, **kwargs):
299 301 """Called when a view has been displayed for this widget instance"""
300 302 self._display_callbacks(self, **kwargs)
301 303
302 304 def _pack_widgets(self, x):
303 305 """Recursively converts all widget instances to model id strings.
304 306
305 307 Children widgets will be stored and transmitted to the front-end by
306 308 their model ids. Return value must be JSON-able."""
307 309 if isinstance(x, dict):
308 310 return {k: self._pack_widgets(v) for k, v in x.items()}
309 311 elif isinstance(x, list):
310 312 return [self._pack_widgets(v) for v in x]
311 313 elif isinstance(x, Widget):
312 314 return x.model_id
313 315 else:
314 316 return x # Value must be JSON-able
315 317
316 318 def _unpack_widgets(self, x):
317 319 """Recursively converts all model id strings to widget instances.
318 320
319 321 Children widgets will be stored and transmitted to the front-end by
320 322 their model ids."""
321 323 if isinstance(x, dict):
322 324 return {k: self._unpack_widgets(v) for k, v in x.items()}
323 325 elif isinstance(x, list):
324 326 return [self._unpack_widgets(v) for v in x]
325 327 elif isinstance(x, string_types):
326 328 return x if x not in Widget.widgets else Widget.widgets[x]
327 329 else:
328 330 return x
329 331
330 332 def _ipython_display_(self, **kwargs):
331 333 """Called when `IPython.display.display` is called on the widget."""
332 334 # Show view. By sending a display message, the comm is opened and the
333 335 # initial state is sent.
334 336 self._send({"method": "display"})
335 337 self._handle_displayed(**kwargs)
336 338
337 339 def _send(self, msg):
338 340 """Sends a message to the model in the front-end."""
339 341 self.comm.send(msg)
340 342
341 343
342 344 class DOMWidget(Widget):
343 345 visible = Bool(True, help="Whether the widget is visible.", sync=True)
344 346 _css = Dict(sync=True) # Internal CSS property dict
345 347
346 348 def get_css(self, key, selector=""):
347 349 """Get a CSS property of the widget.
348 350
349 351 Note: This function does not actually request the CSS from the
350 352 front-end; Only properties that have been set with set_css can be read.
351 353
352 354 Parameters
353 355 ----------
354 356 key: unicode
355 357 CSS key
356 358 selector: unicode (optional)
357 359 JQuery selector used when the CSS key/value was set.
358 360 """
359 361 if selector in self._css and key in self._css[selector]:
360 362 return self._css[selector][key]
361 363 else:
362 364 return None
363 365
364 366 def set_css(self, dict_or_key, value=None, selector=''):
365 367 """Set one or more CSS properties of the widget.
366 368
367 369 This function has two signatures:
368 370 - set_css(css_dict, selector='')
369 371 - set_css(key, value, selector='')
370 372
371 373 Parameters
372 374 ----------
373 375 css_dict : dict
374 376 CSS key/value pairs to apply
375 377 key: unicode
376 378 CSS key
377 379 value:
378 380 CSS value
379 381 selector: unicode (optional, kwarg only)
380 382 JQuery selector to use to apply the CSS key/value. If no selector
381 383 is provided, an empty selector is used. An empty selector makes the
382 384 front-end try to apply the css to a default element. The default
383 385 element is an attribute unique to each view, which is a DOM element
384 386 of the view that should be styled with common CSS (see
385 387 `$el_to_style` in the Javascript code).
386 388 """
387 389 if not selector in self._css:
388 390 self._css[selector] = {}
389 391 my_css = self._css[selector]
390 392
391 393 if value is None:
392 394 css_dict = dict_or_key
393 395 else:
394 396 css_dict = {dict_or_key: value}
395 397
396 398 for (key, value) in css_dict.items():
397 399 if not (key in my_css and value == my_css[key]):
398 400 my_css[key] = value
399 401 self.send_state('_css')
400 402
401 403 def add_class(self, class_names, selector=""):
402 404 """Add class[es] to a DOM element.
403 405
404 406 Parameters
405 407 ----------
406 408 class_names: unicode or list
407 409 Class name(s) to add to the DOM element(s).
408 410 selector: unicode (optional)
409 411 JQuery selector to select the DOM element(s) that the class(es) will
410 412 be added to.
411 413 """
412 414 class_list = class_names
413 415 if isinstance(class_list, list):
414 416 class_list = ' '.join(class_list)
415 417
416 418 self.send({
417 419 "msg_type" : "add_class",
418 420 "class_list" : class_list,
419 421 "selector" : selector
420 422 })
421 423
422 424 def remove_class(self, class_names, selector=""):
423 425 """Remove class[es] from a DOM element.
424 426
425 427 Parameters
426 428 ----------
427 429 class_names: unicode or list
428 430 Class name(s) to remove from the DOM element(s).
429 431 selector: unicode (optional)
430 432 JQuery selector to select the DOM element(s) that the class(es) will
431 433 be removed from.
432 434 """
433 435 class_list = class_names
434 436 if isinstance(class_list, list):
435 437 class_list = ' '.join(class_list)
436 438
437 439 self.send({
438 440 "msg_type" : "remove_class",
439 441 "class_list" : class_list,
440 442 "selector" : selector,
441 443 })
General Comments 0
You need to be logged in to leave comments. Login now