##// END OF EJS Templates
Added support for custom widget msgs
Jonathan Frederic -
Show More
@@ -1,553 +1,587 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 // 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 "use strict";
19 19
20 20 // Use require.js 'define' method so that require.js is intelligent enough to
21 21 // syncronously load everything within this file when it is being 'required'
22 22 // elsewhere.
23 23 define(["components/underscore/underscore-min",
24 24 "components/backbone/backbone-min",
25 25 ], function(underscore, backbone){
26 26
27 27
28 28 //--------------------------------------------------------------------
29 29 // WidgetModel class
30 30 //--------------------------------------------------------------------
31 31 var WidgetModel = Backbone.Model.extend({
32 32 constructor: function(comm_manager, comm, widget_manager) {
33 33 this.comm_manager = comm_manager;
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.views = {};
39 39
40 40 // Remember comm associated with the model.
41 41 this.comm = comm;
42 42 comm.model = this;
43 43
44 44 // Hook comm messages up to model.
45 45 comm.on_close($.proxy(this._handle_comm_closed, this));
46 46 comm.on_msg($.proxy(this._handle_comm_msg, this));
47 47
48 48 return Backbone.Model.apply(this);
49 49 },
50 50
51 51
52 52 update_other_views: function(caller) {
53 53 this.last_modified_view = caller;
54 54 this.save(this.changedAttributes(), {patch: true});
55 55
56 56 for (var cell in this.views) {
57 57 var views = this.views[cell];
58 58 for (var view_index in views) {
59 59 var view = views[view_index];
60 60 if (view !== caller) {
61 61 view.update();
62 62 }
63 63 }
64 64 }
65 65 },
66 66
67 send = function (content) {
68
69 // Used the last modified view as the sender of the message. This
70 // will insure that any python code triggered by the sent message
71 // can create and display widgets and output.
72 var cell = null;
73 if (this.last_modified_view != undefined &&
74 this.last_modified_view.cell != undefined) {
75 cell = this.last_modified_view.cell;
76 }
77 var callbacks = this._make_callbacks(cell);
78 var data = {custom_content: content};
79 this.comm.send(data, callbacks);
80 },
81
67 82
68 83 on_view_displayed: function (callback) {
69 84 this._view_displayed_callback = callback;
70 }
85 },
71 86
72 87
73 88 on_close: function (callback) {
74 89 this._close_callback = callback;
90 },
91
92
93 on_msg: function (callback) {
94 this._msg_callback = callback;
95 },
96
97
98 _handle_custom_msg: function (content) {
99 if (this._msg_callback) {
100 try {
101 this._msg_callback(content);
102 } catch (e) {
103 console.log("Exception in widget model msg callback", e, content);
75 104 }
105 }
106 },
76 107
77 108
78 109 // Handle when a widget is closed.
79 110 _handle_comm_closed: function (msg) {
80 111 for (var cell in this.views) {
81 112 var views = this.views[cell];
82 113 for (var view_index in views) {
83 114 var view = views[view_index];
84 115 view.remove();
85 116 }
86 117 }
87 118 delete this.comm.model; // Delete ref so GC will collect widget model.
88 119 },
89 120
90 121
91 122 // Handle incomming comm msg.
92 123 _handle_comm_msg: function (msg) {
93 124 var method = msg.content.data.method;
94 125 switch (method){
95 126 case 'display':
96 127
97 128 // Try to get the cell.
98 129 var cell = this._get_msg_cell(msg.parent_header.msg_id);
99 130 if (cell == null) {
100 131 console.log("Could not determine where the display" +
101 132 " message was from. Widget will not be displayed")
102 133 } else {
103 134 this._display_view(msg.content.data.view_name,
104 135 msg.content.data.parent,
105 136 cell);
106 137 }
107 138 break;
108 139 case 'update':
109 140 this._handle_update(msg.content.data.state);
110 141 break;
142 case 'custom':
143 this._handle_custom_msg(msg.content.data.custom_content);
144 break;
111 145 }
112 146 },
113 147
114 148
115 149 // Handle when a widget is updated via the python side.
116 150 _handle_update: function (state) {
117 151 this.updating = true;
118 152 try {
119 153 for (var key in state) {
120 154 if (state.hasOwnProperty(key)) {
121 155 if (key == "_css"){
122 156
123 157 // Set the css value of the model as an attribute
124 158 // instead of a backbone trait because we are only
125 159 // interested in backend css -> frontend css. In
126 160 // other words, if the css dict changes in the
127 161 // frontend, we don't need to push the changes to
128 162 // the backend.
129 163 this.css = state[key];
130 164 } else {
131 165 this.set(key, state[key]);
132 166 }
133 167 }
134 168 }
135 169 this.id = this.comm.comm_id;
136 170 this.save();
137 171 } finally {
138 172 this.updating = false;
139 173 }
140 174 },
141 175
142 176
143 177 _handle_status: function (cell, msg) {
144 178 //execution_state : ('busy', 'idle', 'starting')
145 179 if (msg.content.execution_state=='idle') {
146 180
147 181 // Send buffer if this message caused another message to be
148 182 // throttled.
149 183 if (this.msg_buffer != null &&
150 184 this.msg_throttle == this.pending_msgs) {
151 185
152 186 var cell = this._get_msg_cell(msg.parent_header.msg_id);
153 187 var callbacks = this._make_callbacks(cell);
154 188 var data = {sync_method: 'update', sync_data: this.msg_buffer};
155 189 this.comm.send(data, callbacks);
156 190 this.msg_buffer = null;
157 191 } else {
158 192
159 193 // Only decrease the pending message count if the buffer
160 194 // doesn't get flushed (sent).
161 195 --this.pending_msgs;
162 196 }
163 197 }
164 198 },
165 199
166 200
167 201 // Custom syncronization logic.
168 202 _handle_sync: function (method, options) {
169 203 var model_json = this.toJSON();
170 204
171 205 // Only send updated state if the state hasn't been changed
172 206 // during an update.
173 207 if (!this.updating) {
174 208 if (this.pending_msgs >= this.msg_throttle) {
175 209 // The throttle has been exceeded, buffer the current msg so
176 210 // it can be sent once the kernel has finished processing
177 211 // some of the existing messages.
178 212 if (method=='patch') {
179 213 if (this.msg_buffer == null) {
180 214 this.msg_buffer = $.extend({}, model_json); // Copy
181 215 }
182 216 for (var attr in options.attrs) {
183 217 this.msg_buffer[attr] = options.attrs[attr];
184 218 }
185 219 } else {
186 220 this.msg_buffer = $.extend({}, model_json); // Copy
187 221 }
188 222
189 223 } else {
190 224 // We haven't exceeded the throttle, send the message like
191 225 // normal. If this is a patch operation, just send the
192 226 // changes.
193 227 var send_json = model_json;
194 228 if (method=='patch') {
195 229 send_json = {};
196 230 for (var attr in options.attrs) {
197 231 send_json[attr] = options.attrs[attr];
198 232 }
199 233 }
200 234
201 235 var data = {sync_method: method, sync_data: send_json};
202 236
203 237 var cell = null;
204 238 if (this.last_modified_view != undefined && this.last_modified_view != null) {
205 239 cell = this.last_modified_view.cell;
206 240 }
207 241
208 242 var callbacks = this._make_callbacks(cell);
209 243 this.comm.send(data, callbacks);
210 244 this.pending_msgs++;
211 245 }
212 246 }
213 247
214 248 // Since the comm is a one-way communication, assume the message
215 249 // arrived.
216 250 return model_json;
217 251 },
218 252
219 253
220 254 _handle_view_displayed: function(view) {
221 255 if (this._view_displayed_callback) {
222 256 try {
223 257 this._view_displayed_callback(view)
224 258 } catch (e) {
225 259 console.log("Exception in widget model view displayed callback", e, view, this);
226 260 }
227 261 }
228 }
262 },
229 263
230 264
231 265 // Create view that represents the model.
232 266 _display_view: function (view_name, parent_comm_id, cell) {
233 267 var new_views = [];
234 268
235 269 // Try creating and adding the view to it's parent.
236 270 var displayed = false;
237 271 if (parent_comm_id != undefined) {
238 272 var parent_comm = this.comm_manager.comms[parent_comm_id];
239 273 var parent_model = parent_comm.model;
240 274 var parent_views = parent_model.views[cell];
241 275 for (var parent_view_index in parent_views) {
242 276 var parent_view = parent_views[parent_view_index];
243 277 if (parent_view.display_child != undefined) {
244 278 var view = this._create_view(view_name, cell);
245 279 if (view != null) {
246 280 new_views.push(view);
247 281 parent_view.display_child(view);
248 282 displayed = true;
249 283 this._handle_view_displayed(view);
250 284 }
251 285 }
252 286 }
253 287 }
254 288
255 289 // If no parent view is defined or exists. Add the view's
256 290 // element to cell's widget div.
257 291 if (!displayed) {
258 292 var view = this._create_view(view_name, cell);
259 293 if (view != null) {
260 294 new_views.push(view);
261 295
262 296 if (cell.widget_subarea != undefined && cell.widget_subarea != null) {
263 297 cell.widget_area.show();
264 298 cell.widget_subarea.append(view.$el);
265 299 this._handle_view_displayed(view);
266 300 }
267 301 }
268 302 }
269 303
270 304 // Force the new view(s) to update their selves
271 305 for (var view_index in new_views) {
272 306 var view = new_views[view_index];
273 307 view.update();
274 308 }
275 309 },
276 310
277 311
278 312 // Create a view
279 313 _create_view: function (view_name, cell) {
280 314 var view_type = this.widget_manager.widget_view_types[view_name];
281 315 if (view_type != undefined && view_type != null) {
282 316 var view = new view_type({model: this});
283 317 view.render();
284 318 if (this.views[cell]==undefined) {
285 319 this.views[cell] = []
286 320 }
287 321 this.views[cell].push(view);
288 322 view.cell = cell;
289 323
290 324 // Handle when the view element is remove from the page.
291 325 var that = this;
292 326 view.$el.on("remove", function(){
293 327 var index = that.views[cell].indexOf(view);
294 328 if (index > -1) {
295 329 that.views[cell].splice(index, 1);
296 330 }
297 331 view.remove(); // Clean-up view
298 332 if (that.views[cell].length()==0) {
299 333 delete that.views[cell];
300 334 }
301 335
302 336 // Close the comm if there are no views left.
303 337 if (that.views.length()==0) {
304 338 if (that._close_callback) {
305 339 try {
306 340 that._close_callback(that)
307 341 } catch (e) {
308 342 console.log("Exception in widget model close callback", e, that);
309 343 }
310 344 }
311 345 that.comm.close();
312 346 delete that.comm.model; // Delete ref so GC will collect widget model.
313 347 }
314 348 });
315 349 return view;
316 350 }
317 351 return null;
318 352 },
319 353
320 354
321 355 // Build a callback dict.
322 356 _make_callbacks: function (cell) {
323 357 var callbacks = {};
324 358 if (cell != null && cell.output_area != undefined && cell.output_area != null) {
325 359 var that = this;
326 360 callbacks = {
327 361 iopub : {
328 362 output : $.proxy(cell.output_area.handle_output, cell.output_area),
329 363 clear_output : $.proxy(cell.output_area.handle_clear_output, cell.output_area),
330 364 status : function(msg){
331 365 that._handle_status(cell, msg);
332 366 },
333 367
334 368 // Special function only registered by widget messages.
335 369 // Allows us to get the cell for a message so we know
336 370 // where to add widgets if the code requires it.
337 371 get_cell : function() {
338 372 if (that.last_modified_view != undefined &&
339 373 that.last_modified_view.cell != undefined) {
340 374 return that.last_modified_view.cell;
341 375 } else {
342 376 return null
343 377 }
344 378 },
345 379 },
346 380 };
347 381 }
348 382 return callbacks;
349 383 },
350 384
351 385
352 386 // Get the output area corresponding to the msg_id.
353 387 // cell is an instance of IPython.Cell
354 388 _get_msg_cell: function (msg_id) {
355 389
356 390 // First, check to see if the msg was triggered by cell execution.
357 391 var cell = this.widget_manager.get_msg_cell(msg_id);
358 392 if (cell != null) {
359 393 return cell;
360 394 }
361 395
362 396 // Second, check to see if a get_cell callback was defined
363 397 // for the message. get_cell callbacks are registered for
364 398 // widget messages, so this block is actually checking to see if the
365 399 // message was triggered by a widget.
366 400 var kernel = this.comm_manager.kernel;
367 401 var callbacks = kernel.get_callbacks_for_msg(msg_id);
368 402 if (callbacks != undefined &&
369 403 callbacks.iopub != undefined &&
370 404 callbacks.iopub.get_cell != undefined) {
371 405
372 406 return callbacks.iopub.get_cell();
373 407 }
374 408
375 409 // Not triggered by a cell or widget (no get_cell callback
376 410 // exists).
377 411 return null;
378 412 },
379 413
380 414 });
381 415
382 416
383 417 //--------------------------------------------------------------------
384 418 // WidgetView class
385 419 //--------------------------------------------------------------------
386 420 var WidgetView = Backbone.View.extend({
387 421
388 422 initialize: function() {
389 423 this.visible = true;
390 424 this.model.on('change',this.update,this);
391 425 this._add_class_calls = this.model.get('_add_class')[0];
392 426 this._remove_class_calls = this.model.get('_remove_class')[0];
393 427 },
394 428
395 429 update: function() {
396 430 if (this.model.get('visible') != undefined) {
397 431 if (this.visible != this.model.get('visible')) {
398 432 this.visible = this.model.get('visible');
399 433 if (this.visible) {
400 434 this.$el.show();
401 435 } else {
402 436 this.$el.hide();
403 437 }
404 438 }
405 439 }
406 440
407 441 if (this.model.css != undefined) {
408 442 for (var selector in this.model.css) {
409 443 if (this.model.css.hasOwnProperty(selector)) {
410 444
411 445 // Apply the css traits to all elements that match the selector.
412 446 var elements = this._get_selector_element(selector);
413 447 if (elements.length > 0) {
414 448 var css_traits = this.model.css[selector];
415 449 for (var css_key in css_traits) {
416 450 if (css_traits.hasOwnProperty(css_key)) {
417 451 elements.css(css_key, css_traits[css_key]);
418 452 }
419 453 }
420 454 }
421 455 }
422 456 }
423 457 }
424 458
425 459 var add_class = this.model.get('_add_class');
426 460 if (add_class != undefined){
427 461 var add_class_calls = add_class[0];
428 462 if (add_class_calls > this._add_class_calls) {
429 463 this._add_class_calls = add_class_calls;
430 464 var elements = this._get_selector_element(add_class[1]);
431 465 if (elements.length > 0) {
432 466 elements.addClass(add_class[2]);
433 467 }
434 468 }
435 469 }
436 470
437 471 var remove_class = this.model.get('_remove_class');
438 472 if (remove_class != undefined){
439 473 var remove_class_calls = remove_class[0];
440 474 if (remove_class_calls > this._remove_class_calls) {
441 475 this._remove_class_calls = remove_class_calls;
442 476 var elements = this._get_selector_element(remove_class[1]);
443 477 if (elements.length > 0) {
444 478 elements.removeClass(remove_class[2]);
445 479 }
446 480 }
447 481 }
448 482 },
449 483
450 484 _get_selector_element: function(selector) {
451 485 // Get the elements via the css selector. If the selector is
452 486 // blank, apply the style to the $el_to_style element. If
453 487 // the $el_to_style element is not defined, use apply the
454 488 // style to the view's element.
455 489 var elements = this.$el.find(selector);
456 490 if (selector=='') {
457 491 if (this.$el_to_style == undefined) {
458 492 elements = this.$el;
459 493 } else {
460 494 elements = this.$el_to_style;
461 495 }
462 496 }
463 497 return elements;
464 498 },
465 499 });
466 500
467 501
468 502 //--------------------------------------------------------------------
469 503 // WidgetManager class
470 504 //--------------------------------------------------------------------
471 505 var WidgetManager = function(){
472 506 this.comm_manager = null;
473 507 this.widget_model_types = {};
474 508 this.widget_view_types = {};
475 509
476 510 var that = this;
477 511 Backbone.sync = function(method, model, options, error) {
478 512 var result = model._handle_sync(method, options);
479 513 if (options.success) {
480 514 options.success(result);
481 515 }
482 516 };
483 517 }
484 518
485 519
486 520 WidgetManager.prototype.attach_comm_manager = function (comm_manager) {
487 521 this.comm_manager = comm_manager;
488 522
489 523 // Register already register widget model types with the comm manager.
490 524 for (var widget_model_name in this.widget_model_types) {
491 525 this.comm_manager.register_target(widget_model_name, $.proxy(this._handle_com_open, this));
492 526 }
493 527 }
494 528
495 529
496 530 WidgetManager.prototype.register_widget_model = function (widget_model_name, widget_model_type) {
497 531 // Register the widget with the comm manager. Make sure to pass this object's context
498 532 // in so `this` works in the call back.
499 533 if (this.comm_manager!=null) {
500 534 this.comm_manager.register_target(widget_model_name, $.proxy(this._handle_com_open, this));
501 535 }
502 536 this.widget_model_types[widget_model_name] = widget_model_type;
503 537 }
504 538
505 539
506 540 WidgetManager.prototype.register_widget_view = function (widget_view_name, widget_view_type) {
507 541 this.widget_view_types[widget_view_name] = widget_view_type;
508 542 }
509 543
510 544
511 545 WidgetManager.prototype.get_msg_cell = function (msg_id) {
512 546 if (IPython.notebook != undefined && IPython.notebook != null) {
513 547 return IPython.notebook.get_msg_cell(msg_id);
514 548 }
515 549 }
516 550
517 551
518 552 WidgetManager.prototype.on_create_widget = function (callback) {
519 553 this._create_widget_callback = callback;
520 554 }
521 555
522 556
523 557 WidgetManager.prototype._handle_create_widget = function (widget_model) {
524 558 if (this._create_widget_callback) {
525 559 try {
526 560 this._create_widget_callback(widget_model);
527 561 } catch (e) {
528 562 console.log("Exception in WidgetManager callback", e, widget_model);
529 563 }
530 564 }
531 565 }
532 566
533 567
534 568 WidgetManager.prototype._handle_com_open = function (comm, msg) {
535 569 var widget_type_name = msg.content.target_name;
536 570 var widget_model = new this.widget_model_types[widget_type_name](this.comm_manager, comm, this);
537 571 this._handle_create_widget(widget_model);
538 572 }
539 573
540 574
541 575 //--------------------------------------------------------------------
542 576 // Init code
543 577 //--------------------------------------------------------------------
544 578 IPython.WidgetManager = WidgetManager;
545 579 IPython.WidgetModel = WidgetModel;
546 580 IPython.WidgetView = WidgetView;
547 581
548 582 if (IPython.widget_manager==undefined || IPython.widget_manager==null) {
549 583 IPython.widget_manager = new WidgetManager();
550 584 }
551 585
552 586 return IPython.widget_manager;
553 587 });
@@ -1,356 +1,415 b''
1 1 """Base Widget class. Allows user to create widgets in the backend that render
2 2 in the IPython notebook frontend.
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 copy import copy
16 16 from glob import glob
17 17 import uuid
18 18 import sys
19 19 import os
20 20 import inspect
21 21 import types
22 22
23 23 import IPython
24 24 from IPython.kernel.comm import Comm
25 25 from IPython.config import LoggingConfigurable
26 26 from IPython.utils.traitlets import Unicode, Dict, List, Instance, Bool
27 27 from IPython.display import Javascript, display
28 28 from IPython.utils.py3compat import string_types
29 29
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Classes
33 33 #-----------------------------------------------------------------------------
34 34 class Widget(LoggingConfigurable):
35 35
36 36 # Shared declarations
37 37 _keys = []
38 38
39 39 # Public declarations
40 40 target_name = Unicode('widget', help="""Name of the backbone model
41 41 registered in the frontend to create and sync this widget with.""")
42 42 default_view_name = Unicode(help="""Default view registered in the frontend
43 43 to use to represent the widget.""")
44 44 parent = Instance('IPython.html.widgets.widget.Widget')
45 45 visible = Bool(True, help="Whether or not the widget is visible.")
46 46
47 47 def _parent_changed(self, name, old, new):
48 48 if self._displayed:
49 49 raise Exception('Parent cannot be set because widget has been displayed.')
50 50 elif new == self:
51 51 raise Exception('Parent cannot be set to self.')
52 52 else:
53 53
54 54 # Parent/child association
55 55 if new is not None and not self in new._children:
56 56 new._children.append(self)
57 57 if old is not None and self in old._children:
58 58 old._children.remove(self)
59 59
60 60 # Private/protected declarations
61 61 _property_lock = False
62 62 _css = Dict() # Internal CSS property dict
63 63 _add_class = List() # Used to add a js class to a DOM element (call#, selector, class_name)
64 64 _remove_class = List() # Used to remove a js class from a DOM element (call#, selector, class_name)
65 65 _displayed = False
66 66 _comm = None
67 67
68 68
69 69 def __init__(self, **kwargs):
70 70 """Public constructor
71 71
72 72 Parameters
73 73 ----------
74 74 parent : Widget instance (optional)
75 75 Widget that this widget instance is child of. When the widget is
76 76 displayed in the frontend, it's corresponding view will be made
77 77 child of the parent's view if the parent's view exists already. If
78 78 the parent's view is displayed, it will automatically display this
79 79 widget's default view as it's child. The default view can be set
80 80 via the default_view_name property.
81 81 """
82 82 self._children = []
83 83 self._add_class = [0]
84 84 self._remove_class = [0]
85 85 self._display_callbacks = []
86 self._msg_callbacks = []
86 87 super(Widget, self).__init__(**kwargs)
87 88
88 89 # Register after init to allow default values to be specified
89 90 self.on_trait_change(self._handle_property_changed, self.keys)
90 91
91 92
92 93 def __del__(self):
93 94 """Object disposal"""
94 95 self.close()
95 96
96 97
97 98 def close(self):
98 99 """Close method. Closes the widget which closes the underlying comm.
99 100 When the comm is closed, all of the widget views are automatically
100 101 removed from the frontend."""
101 102 try:
102 103 self._comm.close()
103 104 del self._comm
104 105 except:
105 106 pass # Comm doesn't exist and/or is already closed.
106 107
107 108
108 109 # Properties
109 110 def _get_keys(self):
110 111 keys = ['visible', '_css', '_add_class', '_remove_class']
111 112 keys.extend(self._keys)
112 113 return keys
113 114 keys = property(_get_keys)
114 115
115 116
116 117 # Event handlers
117 118 def _handle_msg(self, msg):
118 119 """Called when a msg is recieved from the frontend"""
120 data = msg['content']['data']
121
119 122 # Handle backbone sync methods CREATE, PATCH, and UPDATE
120 sync_method = msg['content']['data']['sync_method']
121 sync_data = msg['content']['data']['sync_data']
123 if 'sync_method' in data and 'sync_data' in data:
124 sync_method = data['sync_method']
125 sync_data = data['sync_data']
122 126 self._handle_recieve_state(sync_data) # handles all methods
123 127
128 # Handle a custom msg from the front-end
129 if 'custom_content' in data:
130 self._handle_custom_msg(data['custom_content'])
131
132
133 def _handle_custom_msg(self, content):
134 """Called when a custom msg is recieved."""
135 for handler in self._msg_callbacks:
136 if callable(handler):
137 argspec = inspect.getargspec(handler)
138 nargs = len(argspec[0])
139
140 # Bound methods have an additional 'self' argument
141 if isinstance(handler, types.MethodType):
142 nargs -= 1
143
144 # Call the callback
145 if nargs == 1:
146 handler(content)
147 elif nargs == 2:
148 handler(self, content)
149 else:
150 raise TypeError('Widget msg callback must ' \
151 'accept 1 or 2 arguments, not %d.' % nargs)
152
124 153
125 154 def _handle_recieve_state(self, sync_data):
126 155 """Called when a state is recieved from the frontend."""
127 156 self._property_lock = True
128 157 try:
129 158
130 159 # Use _keys instead of keys - Don't get retrieve the css from the client side.
131 160 for name in self._keys:
132 161 if name in sync_data:
133 162 setattr(self, name, sync_data[name])
134 163 finally:
135 164 self._property_lock = False
136 165
137 166
138 167 def _handle_property_changed(self, name, old, new):
139 168 """Called when a proeprty has been changed."""
140 169 if not self._property_lock and self._comm is not None:
141 170 # TODO: Validate properties.
142 171 # Send new state to frontend
143 172 self.send_state(key=name)
144 173
145 174
146 175 def _handle_close(self):
147 176 """Called when the comm is closed by the frontend."""
148 177 self._comm = None
149 178
150 179
180 def _handle_displayed(self, view_name):
181 """Called when a view has been displayed for this widget instance
182
183 Parameters
184 ----------
185 view_name: unicode
186 Name of the view that was displayed."""
187 for handler in self._display_callbacks:
188 if callable(handler):
189 argspec = inspect.getargspec(handler)
190 nargs = len(argspec[0])
191
192 # Bound methods have an additional 'self' argument
193 if isinstance(handler, types.MethodType):
194 nargs -= 1
195
196 # Call the callback
197 if nargs == 0:
198 handler()
199 elif nargs == 1:
200 handler(self)
201 elif nargs == 2:
202 handler(self, view_name)
203 else:
204 raise TypeError('Widget display callback must ' \
205 'accept 0-2 arguments, not %d.' % nargs)
206
207
151 208 # Public methods
152 209 def send_state(self, key=None):
153 210 """Sends the widget state, or a piece of it, to the frontend.
154 211
155 212 Parameters
156 213 ----------
157 214 key : unicode (optional)
158 215 A single property's name to sync with the frontend.
159 216 """
160 217 if self._comm is not None:
161 218 state = {}
162 219
163 220 # If a key is provided, just send the state of that key.
164 221 keys = []
165 222 if key is None:
166 223 keys.extend(self.keys)
167 224 else:
168 225 keys.append(key)
169 226 for key in self.keys:
170 227 try:
171 228 state[key] = getattr(self, key)
172 229 except Exception as e:
173 230 pass # Eat errors, nom nom nom
174 231 self._comm.send({"method": "update",
175 232 "state": state})
176 233
177 234
178 235 def get_css(self, key, selector=""):
179 236 """Get a CSS property of the widget. Note, this function does not
180 237 actually request the CSS from the front-end; Only properties that have
181 238 been set with set_css can be read.
182 239
183 240 Parameters
184 241 ----------
185 242 key: unicode
186 243 CSS key
187 244 selector: unicode (optional)
188 245 JQuery selector used when the CSS key/value was set.
189 246 """
190 247 if selector in self._css and key in self._css[selector]:
191 248 return self._css[selector][key]
192 249 else:
193 250 return None
194 251
195 252
196 253 def set_css(self, *args, **kwargs):
197 254 """Set one or more CSS properties of the widget (shared among all of the
198 255 views). This function has two signatures:
199 256 - set_css(css_dict, [selector=''])
200 257 - set_css(key, value, [selector=''])
201 258
202 259 Parameters
203 260 ----------
204 261 css_dict : dict
205 262 CSS key/value pairs to apply
206 263 key: unicode
207 264 CSS key
208 265 value
209 266 CSS value
210 267 selector: unicode (optional)
211 268 JQuery selector to use to apply the CSS key/value.
212 269 """
213 270 selector = kwargs.get('selector', '')
214 271
215 272 # Signature 1: set_css(css_dict, [selector=''])
216 273 if len(args) == 1:
217 274 if isinstance(args[0], dict):
218 275 for (key, value) in args[0].items():
219 276 self.set_css(key, value, selector=selector)
220 277 else:
221 278 raise Exception('css_dict must be a dict.')
222 279
223 280 # Signature 2: set_css(key, value, [selector=''])
224 281 elif len(args) == 2 or len(args) == 3:
225 282
226 283 # Selector can be a positional arg if it's the 3rd value
227 284 if len(args) == 3:
228 285 selector = args[2]
229 286 if selector not in self._css:
230 287 self._css[selector] = {}
231 288
232 289 # Only update the property if it has changed.
233 290 key = args[0]
234 291 value = args[1]
235 292 if not (key in self._css[selector] and value in self._css[selector][key]):
236 293 self._css[selector][key] = value
237 294 self.send_state('_css') # Send new state to client.
238 295 else:
239 296 raise Exception('set_css only accepts 1-3 arguments')
240 297
241 298
242 299 def add_class(self, class_name, selector=""):
243 300 """Add class[es] to a DOM element
244 301
245 302 Parameters
246 303 ----------
247 304 class_name: unicode
248 305 Class name(s) to add to the DOM element(s). Multiple class names
249 306 must be space separated.
250 307 selector: unicode (optional)
251 308 JQuery selector to select the DOM element(s) that the class(es) will
252 309 be added to.
253 310 """
254 311 self._add_class = [self._add_class[0] + 1, selector, class_name]
255 312 self.send_state(key='_add_class')
256 313
257 314
258 315 def remove_class(self, class_name, selector=""):
259 316 """Remove class[es] from a DOM element
260 317
261 318 Parameters
262 319 ----------
263 320 class_name: unicode
264 321 Class name(s) to remove from the DOM element(s). Multiple class
265 322 names must be space separated.
266 323 selector: unicode (optional)
267 324 JQuery selector to select the DOM element(s) that the class(es) will
268 325 be removed from.
269 326 """
270 327 self._remove_class = [self._remove_class[0] + 1, selector, class_name]
271 328 self.send_state(key='_remove_class')
272 329
273 330
331 def send(self, content):
332 """Sends a custom msg to the widget model in the front-end.
333
334 Parameters
335 ----------
336 content : dict
337 Content of the message to send.
338 """
339 if self._comm is not None:
340 self._comm.send({"method": "custom",
341 "custom_content": content})
342
343
344 def on_msg(self, callback, remove=False):
345 """Register a callback for when a custom msg is recieved from the front-end
346
347 Parameters
348 ----------
349 callback: method handler
350 Can have a signature of:
351 - callback(content)
352 - callback(sender, content)
353 remove: bool
354 True if the callback should be unregistered."""
355 if remove and callback in self._msg_callbacks:
356 self._msg_callbacks.remove(callback)
357 elif not remove and not callback in self._msg_callbacks:
358 self._msg_callbacks.append(callback)
359
360
274 361 def on_displayed(self, callback, remove=False):
275 362 """Register a callback to be called when the widget has been displayed
276 363
277 364 Parameters
278 365 ----------
279 366 callback: method handler
280 367 Can have a signature of:
281 368 - callback()
282 369 - callback(sender)
283 370 - callback(sender, view_name)
284 371 remove: bool
285 372 True if the callback should be unregistered."""
286 if remove:
373 if remove and callback in self._display_callbacks:
287 374 self._display_callbacks.remove(callback)
288 elif not callback in self._display_callbacks:
375 elif not remove and not callback in self._display_callbacks:
289 376 self._display_callbacks.append(callback)
290 377
291 378
292 def handle_displayed(self, view_name):
293 """Called when a view has been displayed for this widget instance
294
295 Parameters
296 ----------
297 view_name: unicode
298 Name of the view that was displayed."""
299 for handler in self._display_callbacks:
300 if callable(handler):
301 argspec = inspect.getargspec(handler)
302 nargs = len(argspec[0])
303
304 # Bound methods have an additional 'self' argument
305 if isinstance(handler, types.MethodType):
306 nargs -= 1
307
308 # Call the callback
309 if nargs == 0:
310 handler()
311 elif nargs == 1:
312 handler(self)
313 elif nargs == 2:
314 handler(self, view_name)
315 else:
316 raise TypeError('Widget display callback must ' \
317 'accept 0-2 arguments, not %d.' % nargs)
318
319
320 379 # Support methods
321 380 def _repr_widget_(self, view_name=None):
322 381 """Function that is called when `IPython.display.display` is called on
323 382 the widget.
324 383
325 384 Parameters
326 385 ----------
327 386 view_name: unicode (optional)
328 387 View to display in the frontend. Overrides default_view_name."""
329 388
330 389 if not view_name:
331 390 view_name = self.default_view_name
332 391
333 392 # Create a comm.
334 393 if self._comm is None:
335 394 self._comm = Comm(target_name=self.target_name)
336 395 self._comm.on_msg(self._handle_msg)
337 396 self._comm.on_close(self._handle_close)
338 397
339 398 # Make sure model is syncronized
340 399 self.send_state()
341 400
342 401 # Show view.
343 402 if self.parent is None or self.parent._comm is None:
344 403 self._comm.send({"method": "display", "view_name": view_name})
345 404 else:
346 405 self._comm.send({"method": "display",
347 406 "view_name": view_name,
348 407 "parent": self.parent._comm.comm_id})
349 408 self._displayed = True
350 self.handle_displayed(view_name)
409 self._handle_displayed(view_name)
351 410
352 411 # Now display children if any.
353 412 for child in self._children:
354 413 if child != self:
355 414 child._repr_widget_()
356 415 return None
General Comments 0
You need to be logged in to leave comments. Login now