##// END OF EJS Templates
Keep a running diff instead of forcing a full state update
Jonathan Frederic -
Show More
@@ -1,458 +1,451 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 // 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 "underscore",
19 19 "backbone"],
20 20 function(WidgetManager, _, Backbone){
21 21
22 22 var WidgetModel = Backbone.Model.extend({
23 23 constructor: function (widget_manager, model_id, comm) {
24 24 // Constructor
25 25 //
26 26 // Creates a WidgetModel instance.
27 27 //
28 28 // Parameters
29 29 // ----------
30 30 // widget_manager : WidgetManager instance
31 31 // model_id : string
32 32 // An ID unique to this model.
33 33 // comm : Comm instance (optional)
34 34 this.widget_manager = widget_manager;
35 this._set_calls = 0;
35 this._buffered_state_diff = {};
36 36 this.pending_msgs = 0;
37 37 this.msg_throttle = 3;
38 38 this.msg_buffer = null;
39 39 this.key_value_lock = null;
40 40 this.id = model_id;
41 41 this.views = [];
42 42
43 43 if (comm !== undefined) {
44 44 // Remember comm associated with the model.
45 45 this.comm = comm;
46 46 comm.model = this;
47 47
48 48 // Hook comm messages up to model.
49 49 comm.on_close($.proxy(this._handle_comm_closed, this));
50 50 comm.on_msg($.proxy(this._handle_comm_msg, this));
51 51 }
52 52 return Backbone.Model.apply(this);
53 53 },
54 54
55 55 send: function (content, callbacks) {
56 56 // Send a custom msg over the comm.
57 57 if (this.comm !== undefined) {
58 58 var data = {method: 'custom', content: content};
59 59 this.comm.send(data, callbacks);
60 60 this.pending_msgs++;
61 61 }
62 62 },
63 63
64 64 _handle_comm_closed: function (msg) {
65 65 // Handle when a widget is closed.
66 66 this.trigger('comm:close');
67 67 delete this.comm.model; // Delete ref so GC will collect widget model.
68 68 delete this.comm;
69 69 delete this.model_id; // Delete id from model so widget manager cleans up.
70 70 _.each(this.views, function(view, i) {
71 71 view.remove();
72 72 });
73 73 },
74 74
75 75 _handle_comm_msg: function (msg) {
76 76 // Handle incoming comm msg.
77 77 var method = msg.content.data.method;
78 78 switch (method) {
79 79 case 'update':
80 80 this.apply_update(msg.content.data.state);
81 81 break;
82 82 case 'custom':
83 83 this.trigger('msg:custom', msg.content.data.content);
84 84 break;
85 85 case 'display':
86 86 this.widget_manager.display_view(msg, this);
87 87 break;
88 88 }
89 89 },
90 90
91 91 apply_update: function (state) {
92 92 // Handle when a widget is updated via the python side.
93 93 var that = this;
94 94 _.each(state, function(value, key) {
95 95 that.key_value_lock = [key, value];
96 96 try {
97 97 that.set(key, that._unpack_models(value));
98 98 } finally {
99 99 that.key_value_lock = null;
100 100 }
101 101 });
102 102 },
103 103
104 104 _handle_status: function (msg, callbacks) {
105 105 // Handle status msgs.
106 106
107 107 // execution_state : ('busy', 'idle', 'starting')
108 108 if (this.comm !== undefined) {
109 109 if (msg.content.execution_state ==='idle') {
110 110 // Send buffer if this message caused another message to be
111 111 // throttled.
112 112 if (this.msg_buffer !== null &&
113 113 this.msg_throttle === this.pending_msgs) {
114 114 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
115 115 this.comm.send(data, callbacks);
116 116 this.msg_buffer = null;
117 117 } else {
118 118 --this.pending_msgs;
119 119 }
120 120 }
121 121 }
122 122 },
123 123
124 124 callbacks: function(view) {
125 125 // Create msg callbacks for a comm msg.
126 126 var callbacks = this.widget_manager.callbacks(view);
127 127
128 128 if (callbacks.iopub === undefined) {
129 129 callbacks.iopub = {};
130 130 }
131 131
132 132 var that = this;
133 133 callbacks.iopub.status = function (msg) {
134 134 that._handle_status(msg, callbacks);
135 135 };
136 136 return callbacks;
137 137 },
138 138
139 139 set: function(key, val, options) {
140 140 // Set a value.
141 this._set_calls++;
142 return WidgetModel.__super__.set.apply(this, arguments);
141 var return_value = WidgetModel.__super__.set.apply(this, arguments);
142
143 // Backbone only remembers the diff of the most recent set()
144 // opertation. Calling set multiple times in a row results in a
145 // loss of diff information. Here we keep our own running diff.
146 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
147 return return_value;
143 148 },
144 149
145 150 sync: function (method, model, options) {
146 151 // Handle sync to the back-end. Called when a model.save() is called.
147 152
148 153 // Make sure a comm exists.
149 154 var error = options.error || function() {
150 155 console.error('Backbone sync error:', arguments);
151 156 };
152 157 if (this.comm === undefined) {
153 158 error();
154 159 return false;
155 160 }
156 161
157 162 // Delete any key value pairs that the back-end already knows about.
158 163 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
159 164 if (this.key_value_lock !== null) {
160 165 var key = this.key_value_lock[0];
161 166 var value = this.key_value_lock[1];
162 167 if (attrs[key] === value) {
163 168 delete attrs[key];
164 169 }
165 170 }
166 171
167 172 // Only sync if there are attributes to send to the back-end.
168 173 attrs = this._pack_models(attrs);
169 174 if (_.size(attrs) > 0) {
170 175
171 176 // If this message was sent via backbone itself, it will not
172 177 // have any callbacks. It's important that we create callbacks
173 178 // so we can listen for status messages, etc...
174 179 var callbacks = options.callbacks || this.callbacks();
175 180
176 181 // Check throttle.
177 182 if (this.pending_msgs >= this.msg_throttle) {
178 183 // The throttle has been exceeded, buffer the current msg so
179 184 // it can be sent once the kernel has finished processing
180 185 // some of the existing messages.
181 186
182 187 // Combine updates if it is a 'patch' sync, otherwise replace updates
183 188 switch (method) {
184 189 case 'patch':
185 190 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
186 191 break;
187 192 case 'update':
188 193 case 'create':
189 194 this.msg_buffer = attrs;
190 195 break;
191 196 default:
192 197 error();
193 198 return false;
194 199 }
195 200 this.msg_buffer_callbacks = callbacks;
196 201
197 202 } else {
198 203 // We haven't exceeded the throttle, send the message like
199 204 // normal.
200 205 var data = {method: 'backbone', sync_data: attrs};
201 206 this.comm.send(data, callbacks);
202 207 this.pending_msgs++;
203 208 }
204 209 }
205 210 // Since the comm is a one-way communication, assume the message
206 211 // arrived. Don't call success since we don't have a model back from the server
207 212 // this means we miss out on the 'sync' event.
208 this._set_calls = 0;
213 this._buffered_state_diff = {};
209 214 },
210 215
211 216 save_changes: function(callbacks) {
212 217 // Push this model's state to the back-end
213 218 //
214 219 // This invokes a Backbone.Sync.
215
216 // Backbone only remembers the diff of the most recent set()
217 // opertation. Calling set multiple times in a row results in a
218 // loss of diff information which means we need to send a full
219 // state. If diffing is important to the user, model.set(...) should
220 // only be called once prior to a view.touch(). If multiple
221 // parameters need to be set, use the model.set({key1: val1, key2: val2, ...})
222 // signature.
223 if (self._set_calls <= 1) {
224 this.save(this.changedAttributes(), {patch: true, callbacks: callbacks});
225 } else {
226 this.save(null, {patch: false, callbacks: callbacks});
227 }
220 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
228 221 },
229 222
230 223 _pack_models: function(value) {
231 224 // Replace models with model ids recursively.
232 225 if (value instanceof Backbone.Model) {
233 226 return value.id;
234 227
235 228 } else if ($.isArray(value)) {
236 229 var packed = [];
237 230 var that = this;
238 231 _.each(value, function(sub_value, key) {
239 232 packed.push(that._pack_models(sub_value));
240 233 });
241 234 return packed;
242 235
243 236 } else if (value instanceof Object) {
244 237 var packed = {};
245 238 var that = this;
246 239 _.each(value, function(sub_value, key) {
247 240 packed[key] = that._pack_models(sub_value);
248 241 });
249 242 return packed;
250 243
251 244 } else {
252 245 return value;
253 246 }
254 247 },
255 248
256 249 _unpack_models: function(value) {
257 250 // Replace model ids with models recursively.
258 251 if ($.isArray(value)) {
259 252 var unpacked = [];
260 253 var that = this;
261 254 _.each(value, function(sub_value, key) {
262 255 unpacked.push(that._unpack_models(sub_value));
263 256 });
264 257 return unpacked;
265 258
266 259 } else if (value instanceof Object) {
267 260 var unpacked = {};
268 261 var that = this;
269 262 _.each(value, function(sub_value, key) {
270 263 unpacked[key] = that._unpack_models(sub_value);
271 264 });
272 265 return unpacked;
273 266
274 267 } else {
275 268 var model = this.widget_manager.get_model(value);
276 269 if (model) {
277 270 return model;
278 271 } else {
279 272 return value;
280 273 }
281 274 }
282 275 },
283 276
284 277 });
285 278 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
286 279
287 280
288 281 var WidgetView = Backbone.View.extend({
289 282 initialize: function(parameters) {
290 283 // Public constructor.
291 284 this.model.on('change',this.update,this);
292 285 this.options = parameters.options;
293 286 this.child_views = [];
294 287 this.model.views.push(this);
295 288 },
296 289
297 290 update: function(){
298 291 // Triggered on model change.
299 292 //
300 293 // Update view to be consistent with this.model
301 294 },
302 295
303 296 create_child_view: function(child_model, options) {
304 297 // Create and return a child view.
305 298 //
306 299 // -given a model and (optionally) a view name if the view name is
307 300 // not given, it defaults to the model's default view attribute.
308 301
309 302 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
310 303 // it would be great to have the widget manager add the cell metadata
311 304 // to the subview without having to add it here.
312 305 var child_view = this.model.widget_manager.create_view(child_model, options || {}, this);
313 306 this.child_views[child_model.id] = child_view;
314 307 return child_view;
315 308 },
316 309
317 310 delete_child_view: function(child_model, options) {
318 311 // Delete a child view that was previously created using create_child_view.
319 312 var view = this.child_views[child_model.id];
320 313 if (view !== undefined) {
321 314 delete this.child_views[child_model.id];
322 315 view.remove();
323 316 }
324 317 },
325 318
326 319 do_diff: function(old_list, new_list, removed_callback, added_callback) {
327 320 // Difference a changed list and call remove and add callbacks for
328 321 // each removed and added item in the new list.
329 322 //
330 323 // Parameters
331 324 // ----------
332 325 // old_list : array
333 326 // new_list : array
334 327 // removed_callback : Callback(item)
335 328 // Callback that is called for each item removed.
336 329 // added_callback : Callback(item)
337 330 // Callback that is called for each item added.
338 331
339 332
340 333 // removed items
341 334 _.each(_.difference(old_list, new_list), function(item, index, list) {
342 335 removed_callback(item);
343 336 }, this);
344 337
345 338 // added items
346 339 _.each(_.difference(new_list, old_list), function(item, index, list) {
347 340 added_callback(item);
348 341 }, this);
349 342 },
350 343
351 344 callbacks: function(){
352 345 // Create msg callbacks for a comm msg.
353 346 return this.model.callbacks(this);
354 347 },
355 348
356 349 render: function(){
357 350 // Render the view.
358 351 //
359 352 // By default, this is only called the first time the view is created
360 353 },
361 354
362 355 send: function (content) {
363 356 // Send a custom msg associated with this view.
364 357 this.model.send(content, this.callbacks());
365 358 },
366 359
367 360 touch: function () {
368 361 this.model.save_changes(this.callbacks());
369 362 },
370 363 });
371 364
372 365
373 366 var DOMWidgetView = WidgetView.extend({
374 367 initialize: function (options) {
375 368 // Public constructor
376 369
377 370 // In the future we may want to make changes more granular
378 371 // (e.g., trigger on visible:change).
379 372 this.model.on('change', this.update, this);
380 373 this.model.on('msg:custom', this.on_msg, this);
381 374 DOMWidgetView.__super__.initialize.apply(this, arguments);
382 375 },
383 376
384 377 on_msg: function(msg) {
385 378 // Handle DOM specific msgs.
386 379 switch(msg.msg_type) {
387 380 case 'add_class':
388 381 this.add_class(msg.selector, msg.class_list);
389 382 break;
390 383 case 'remove_class':
391 384 this.remove_class(msg.selector, msg.class_list);
392 385 break;
393 386 }
394 387 },
395 388
396 389 add_class: function (selector, class_list) {
397 390 // Add a DOM class to an element.
398 391 this._get_selector_element(selector).addClass(class_list);
399 392 },
400 393
401 394 remove_class: function (selector, class_list) {
402 395 // Remove a DOM class from an element.
403 396 this._get_selector_element(selector).removeClass(class_list);
404 397 },
405 398
406 399 update: function () {
407 400 // Update the contents of this view
408 401 //
409 402 // Called when the model is changed. The model may have been
410 403 // changed by another view or by a state update from the back-end.
411 404 // The very first update seems to happen before the element is
412 405 // finished rendering so we use setTimeout to give the element time
413 406 // to render
414 407 var e = this.$el;
415 408 var visible = this.model.get('visible');
416 409 setTimeout(function() {e.toggle(visible);},0);
417 410
418 411 var css = this.model.get('_css');
419 412 if (css === undefined) {return;}
420 413 var that = this;
421 414 _.each(css, function(css_traits, selector){
422 415 // Apply the css traits to all elements that match the selector.
423 416 var elements = that._get_selector_element(selector);
424 417 if (elements.length > 0) {
425 418 _.each(css_traits, function(css_value, css_key){
426 419 elements.css(css_key, css_value);
427 420 });
428 421 }
429 422 });
430 423 },
431 424
432 425 _get_selector_element: function (selector) {
433 426 // Get the elements via the css selector.
434 427
435 428 // If the selector is blank, apply the style to the $el_to_style
436 429 // element. If the $el_to_style element is not defined, use apply
437 430 // the style to the view's element.
438 431 var elements;
439 432 if (!selector) {
440 433 if (this.$el_to_style === undefined) {
441 434 elements = this.$el;
442 435 } else {
443 436 elements = this.$el_to_style;
444 437 }
445 438 } else {
446 439 elements = this.$el.find(selector);
447 440 }
448 441 return elements;
449 442 },
450 443 });
451 444
452 445 IPython.WidgetModel = WidgetModel;
453 446 IPython.WidgetView = WidgetView;
454 447 IPython.DOMWidgetView = DOMWidgetView;
455 448
456 449 // Pass through WidgetManager namespace.
457 450 return WidgetManager;
458 451 });
General Comments 0
You need to be logged in to leave comments. Login now