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