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