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