##// END OF EJS Templates
Added on_view_displayed and on_close callbacks to widget manager.
Jonathan Frederic -
Show More
@@ -1,521 +1,551 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
68 on_view_displayed: function (callback) {
69 this._view_displayed_callback = callback;
70 }
71
72
73 on_close: function (callback) {
74 this._close_callback = callback;
75 }
76
77
68 78 // Handle when a widget is closed.
69 79 _handle_comm_closed: function (msg) {
70 80 for (var cell in this.views) {
71 81 var views = this.views[cell];
72 82 for (var view_index in views) {
73 83 var view = views[view_index];
74 84 view.remove();
75 85 }
76 86 }
77 87 },
78 88
79 89
80 90 // Handle incomming comm msg.
81 91 _handle_comm_msg: function (msg) {
82 92 var method = msg.content.data.method;
83 93 switch (method){
84 94 case 'display':
85 95
86 // Try to get the cell index.
96 // Try to get the cell.
87 97 var cell = this._get_msg_cell(msg.parent_header.msg_id);
88 98 if (cell == null) {
89 99 console.log("Could not determine where the display" +
90 100 " message was from. Widget will not be displayed")
91 101 } else {
92 102 this._display_view(msg.content.data.view_name,
93 103 msg.content.data.parent,
94 104 cell);
95 105 }
96 106 break;
97 107 case 'update':
98 108 this._handle_update(msg.content.data.state);
99 109 break;
100 110 }
101 111 },
102 112
103 113
104 114 // Handle when a widget is updated via the python side.
105 115 _handle_update: function (state) {
106 116 this.updating = true;
107 117 try {
108 118 for (var key in state) {
109 119 if (state.hasOwnProperty(key)) {
110 120 if (key == "_css"){
111 121
112 122 // Set the css value of the model as an attribute
113 123 // instead of a backbone trait because we are only
114 124 // interested in backend css -> frontend css. In
115 125 // other words, if the css dict changes in the
116 126 // frontend, we don't need to push the changes to
117 127 // the backend.
118 128 this.css = state[key];
119 129 } else {
120 130 this.set(key, state[key]);
121 131 }
122 132 }
123 133 }
124 134 this.id = this.comm.comm_id;
125 135 this.save();
126 136 } finally {
127 137 this.updating = false;
128 138 }
129 139 },
130 140
131 141
132 142 _handle_status: function (cell, msg) {
133 143 //execution_state : ('busy', 'idle', 'starting')
134 144 if (msg.content.execution_state=='idle') {
135 145
136 146 // Send buffer if this message caused another message to be
137 147 // throttled.
138 148 if (this.msg_buffer != null &&
139 149 this.msg_throttle == this.pending_msgs) {
140 150
141 151 var cell = this._get_msg_cell(msg.parent_header.msg_id);
142 152 var callbacks = this._make_callbacks(cell);
143 153 var data = {sync_method: 'update', sync_data: this.msg_buffer};
144 154 this.comm.send(data, callbacks);
145 155 this.msg_buffer = null;
146 156 } else {
147 157
148 158 // Only decrease the pending message count if the buffer
149 159 // doesn't get flushed (sent).
150 160 --this.pending_msgs;
151 161 }
152 162 }
153 163 },
154 164
155 165
156 166 // Custom syncronization logic.
157 167 _handle_sync: function (method, options) {
158 168 var model_json = this.toJSON();
159 169
160 170 // Only send updated state if the state hasn't been changed
161 171 // during an update.
162 172 if (!this.updating) {
163 173 if (this.pending_msgs >= this.msg_throttle) {
164 174 // The throttle has been exceeded, buffer the current msg so
165 175 // it can be sent once the kernel has finished processing
166 176 // some of the existing messages.
167 177 if (method=='patch') {
168 178 if (this.msg_buffer == null) {
169 179 this.msg_buffer = $.extend({}, model_json); // Copy
170 180 }
171 181 for (var attr in options.attrs) {
172 182 this.msg_buffer[attr] = options.attrs[attr];
173 183 }
174 184 } else {
175 185 this.msg_buffer = $.extend({}, model_json); // Copy
176 186 }
177 187
178 188 } else {
179 189 // We haven't exceeded the throttle, send the message like
180 190 // normal. If this is a patch operation, just send the
181 191 // changes.
182 192 var send_json = model_json;
183 193 if (method=='patch') {
184 194 send_json = {};
185 195 for (var attr in options.attrs) {
186 196 send_json[attr] = options.attrs[attr];
187 197 }
188 198 }
189 199
190 200 var data = {sync_method: method, sync_data: send_json};
191 201
192 202 var cell = null;
193 203 if (this.last_modified_view != undefined && this.last_modified_view != null) {
194 204 cell = this.last_modified_view.cell;
195 205 }
196 206
197 207 var callbacks = this._make_callbacks(cell);
198 208 this.comm.send(data, callbacks);
199 209 this.pending_msgs++;
200 210 }
201 211 }
202 212
203 213 // Since the comm is a one-way communication, assume the message
204 214 // arrived.
205 215 return model_json;
206 216 },
207 217
208 218
219 _handle_view_displayed: function(view) {
220 if (this._view_displayed_callback) {
221 try {
222 this._view_displayed_callback(view)
223 } catch (e) {
224 console.log("Exception in widget model view displayed callback", e, view, this);
225 }
226 }
227 }
228
229
209 230 // Create view that represents the model.
210 231 _display_view: function (view_name, parent_comm_id, cell) {
211 232 var new_views = [];
212 233
213 234 // Try creating and adding the view to it's parent.
214 235 var displayed = false;
215 236 if (parent_comm_id != undefined) {
216 237 var parent_comm = this.comm_manager.comms[parent_comm_id];
217 238 var parent_model = parent_comm.model;
218 239 var parent_views = parent_model.views[cell];
219 240 for (var parent_view_index in parent_views) {
220 241 var parent_view = parent_views[parent_view_index];
221 242 if (parent_view.display_child != undefined) {
222 243 var view = this._create_view(view_name, cell);
223 244 if (view != null) {
224 245 new_views.push(view);
225 246 parent_view.display_child(view);
226 247 displayed = true;
248 this._handle_view_displayed(view);
227 249 }
228 250 }
229 251 }
230 252 }
231 253
232 254 // If no parent view is defined or exists. Add the view's
233 255 // element to cell's widget div.
234 256 if (!displayed) {
235 257 var view = this._create_view(view_name, cell);
236 258 if (view != null) {
237 259 new_views.push(view);
238 260
239 261 if (cell.widget_subarea != undefined && cell.widget_subarea != null) {
240 262 cell.widget_area.show();
241 263 cell.widget_subarea.append(view.$el);
264 this._handle_view_displayed(view);
242 265 }
243 266 }
244 267 }
245 268
246 269 // Force the new view(s) to update their selves
247 270 for (var view_index in new_views) {
248 271 var view = new_views[view_index];
249 272 view.update();
250 273 }
251 274 },
252 275
253 276
254 277 // Create a view
255 278 _create_view: function (view_name, cell) {
256 279 var view_type = this.widget_manager.widget_view_types[view_name];
257 280 if (view_type != undefined && view_type != null) {
258 281 var view = new view_type({model: this});
259 282 view.render();
260 283 if (this.views[cell]==undefined) {
261 284 this.views[cell] = []
262 285 }
263 286 this.views[cell].push(view);
264 287 view.cell = cell;
265 288
266 289 // Handle when the view element is remove from the page.
267 290 var that = this;
268 291 view.$el.on("remove", function(){
269 292 var index = that.views[cell].indexOf(view);
270 293 if (index > -1) {
271 294 that.views[cell].splice(index, 1);
272 295 }
273 296 view.remove(); // Clean-up view
274 297 if (that.views[cell].length()==0) {
275 298 delete that.views[cell];
276 299 }
277 300
278 301 // Close the comm if there are no views left.
279 302 if (that.views.length()==0) {
303 if (that._close_callback) {
304 try {
305 that._close_callback(that)
306 } catch (e) {
307 console.log("Exception in widget model close callback", e, that);
308 }
309 }
280 310 that.comm.close();
281 311 }
282 312 });
283 313 return view;
284 314 }
285 315 return null;
286 316 },
287 317
288 318
289 319 // Build a callback dict.
290 320 _make_callbacks: function (cell) {
291 321 var callbacks = {};
292 322 if (cell != null && cell.output_area != undefined && cell.output_area != null) {
293 323 var that = this;
294 324 callbacks = {
295 325 iopub : {
296 326 output : $.proxy(cell.output_area.handle_output, cell.output_area),
297 327 clear_output : $.proxy(cell.output_area.handle_clear_output, cell.output_area),
298 328 status : function(msg){
299 329 that._handle_status(cell, msg);
300 330 },
301 331
302 332 // Special function only registered by widget messages.
303 333 // Allows us to get the cell for a message so we know
304 334 // where to add widgets if the code requires it.
305 335 get_cell : function() {
306 336 if (that.last_modified_view != undefined &&
307 337 that.last_modified_view.cell != undefined) {
308 338 return that.last_modified_view.cell;
309 339 } else {
310 340 return null
311 341 }
312 342 },
313 343 },
314 344 };
315 345 }
316 346 return callbacks;
317 347 },
318 348
319 349
320 350 // Get the output area corresponding to the msg_id.
321 351 // cell is an instance of IPython.Cell
322 352 _get_msg_cell: function (msg_id) {
323 353
324 354 // First, check to see if the msg was triggered by cell execution.
325 355 var cell = this.widget_manager.get_msg_cell(msg_id);
326 356 if (cell != null) {
327 357 return cell;
328 358 }
329 359
330 360 // Second, check to see if a get_cell callback was defined
331 361 // for the message. get_cell callbacks are registered for
332 362 // widget messages, so this block is actually checking to see if the
333 363 // message was triggered by a widget.
334 364 var kernel = this.comm_manager.kernel;
335 365 var callbacks = kernel.get_callbacks_for_msg(msg_id);
336 366 if (callbacks != undefined &&
337 367 callbacks.iopub != undefined &&
338 368 callbacks.iopub.get_cell != undefined) {
339 369
340 370 return callbacks.iopub.get_cell();
341 371 }
342 372
343 373 // Not triggered by a cell or widget (no get_cell callback
344 374 // exists).
345 375 return null;
346 376 },
347 377
348 378 });
349 379
350 380
351 381 //--------------------------------------------------------------------
352 382 // WidgetView class
353 383 //--------------------------------------------------------------------
354 384 var WidgetView = Backbone.View.extend({
355 385
356 386 initialize: function() {
357 387 this.visible = true;
358 388 this.model.on('change',this.update,this);
359 389 this._add_class_calls = this.model.get('_add_class')[0];
360 390 this._remove_class_calls = this.model.get('_remove_class')[0];
361 391 },
362 392
363 393 update: function() {
364 394 if (this.model.get('visible') != undefined) {
365 395 if (this.visible != this.model.get('visible')) {
366 396 this.visible = this.model.get('visible');
367 397 if (this.visible) {
368 398 this.$el.show();
369 399 } else {
370 400 this.$el.hide();
371 401 }
372 402 }
373 403 }
374 404
375 405 if (this.model.css != undefined) {
376 406 for (var selector in this.model.css) {
377 407 if (this.model.css.hasOwnProperty(selector)) {
378 408
379 409 // Apply the css traits to all elements that match the selector.
380 410 var elements = this._get_selector_element(selector);
381 411 if (elements.length > 0) {
382 412 var css_traits = this.model.css[selector];
383 413 for (var css_key in css_traits) {
384 414 if (css_traits.hasOwnProperty(css_key)) {
385 415 elements.css(css_key, css_traits[css_key]);
386 416 }
387 417 }
388 418 }
389 419 }
390 420 }
391 421 }
392 422
393 423 var add_class = this.model.get('_add_class');
394 424 if (add_class != undefined){
395 425 var add_class_calls = add_class[0];
396 426 if (add_class_calls > this._add_class_calls) {
397 427 this._add_class_calls = add_class_calls;
398 428 var elements = this._get_selector_element(add_class[1]);
399 429 if (elements.length > 0) {
400 430 elements.addClass(add_class[2]);
401 431 }
402 432 }
403 433 }
404 434
405 435 var remove_class = this.model.get('_remove_class');
406 436 if (remove_class != undefined){
407 437 var remove_class_calls = remove_class[0];
408 438 if (remove_class_calls > this._remove_class_calls) {
409 439 this._remove_class_calls = remove_class_calls;
410 440 var elements = this._get_selector_element(remove_class[1]);
411 441 if (elements.length > 0) {
412 442 elements.removeClass(remove_class[2]);
413 443 }
414 444 }
415 445 }
416 446 },
417 447
418 448 _get_selector_element: function(selector) {
419 449 // Get the elements via the css selector. If the selector is
420 450 // blank, apply the style to the $el_to_style element. If
421 451 // the $el_to_style element is not defined, use apply the
422 452 // style to the view's element.
423 453 var elements = this.$el.find(selector);
424 454 if (selector=='') {
425 455 if (this.$el_to_style == undefined) {
426 456 elements = this.$el;
427 457 } else {
428 458 elements = this.$el_to_style;
429 459 }
430 460 }
431 461 return elements;
432 462 },
433 463 });
434 464
435 465
436 466 //--------------------------------------------------------------------
437 467 // WidgetManager class
438 468 //--------------------------------------------------------------------
439 469 var WidgetManager = function(){
440 470 this.comm_manager = null;
441 471 this.widget_model_types = {};
442 472 this.widget_view_types = {};
443 473
444 474 var that = this;
445 475 Backbone.sync = function(method, model, options, error) {
446 476 var result = model._handle_sync(method, options);
447 477 if (options.success) {
448 478 options.success(result);
449 479 }
450 480 };
451 481 }
452 482
453 483
454 484 WidgetManager.prototype.attach_comm_manager = function (comm_manager) {
455 485 this.comm_manager = comm_manager;
456 486
457 487 // Register already register widget model types with the comm manager.
458 488 for (var widget_model_name in this.widget_model_types) {
459 489 this.comm_manager.register_target(widget_model_name, $.proxy(this._handle_com_open, this));
460 490 }
461 491 }
462 492
463 493
464 494 WidgetManager.prototype.register_widget_model = function (widget_model_name, widget_model_type) {
465 495 // Register the widget with the comm manager. Make sure to pass this object's context
466 496 // in so `this` works in the call back.
467 497 if (this.comm_manager!=null) {
468 498 this.comm_manager.register_target(widget_model_name, $.proxy(this._handle_com_open, this));
469 499 }
470 500 this.widget_model_types[widget_model_name] = widget_model_type;
471 501 }
472 502
473 503
474 504 WidgetManager.prototype.register_widget_view = function (widget_view_name, widget_view_type) {
475 505 this.widget_view_types[widget_view_name] = widget_view_type;
476 506 }
477 507
478 508
479 509 WidgetManager.prototype.get_msg_cell = function (msg_id) {
480 510 if (IPython.notebook != undefined && IPython.notebook != null) {
481 511 return IPython.notebook.get_msg_cell(msg_id);
482 512 }
483 513 }
484 514
485 515
486 516 WidgetManager.prototype.on_create_widget = function (callback) {
487 517 this._create_widget_callback = callback;
488 518 }
489 519
490 520
491 521 WidgetManager.prototype._handle_create_widget = function (widget_model) {
492 522 if (this._create_widget_callback) {
493 523 try {
494 524 this._create_widget_callback(widget_model);
495 525 } catch (e) {
496 526 console.log("Exception in WidgetManager callback", e, widget_model);
497 527 }
498 528 }
499 529 }
500 530
501 531
502 532 WidgetManager.prototype._handle_com_open = function (comm, msg) {
503 533 var widget_type_name = msg.content.target_name;
504 534 var widget_model = new this.widget_model_types[widget_type_name](this.comm_manager, comm, this);
505 535 this._handle_create_widget(widget_model);
506 536 }
507 537
508 538
509 539 //--------------------------------------------------------------------
510 540 // Init code
511 541 //--------------------------------------------------------------------
512 542 IPython.WidgetManager = WidgetManager;
513 543 IPython.WidgetModel = WidgetModel;
514 544 IPython.WidgetView = WidgetView;
515 545
516 546 if (IPython.widget_manager==undefined || IPython.widget_manager==null) {
517 547 IPython.widget_manager = new WidgetManager();
518 548 }
519 549
520 550 return IPython.widget_manager;
521 551 });
General Comments 0
You need to be logged in to leave comments. Login now