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