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