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