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