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