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