##// END OF EJS Templates
Delete unnecessary save
Jason Grout -
Show More
@@ -1,393 +1,392
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Base Widget Model and View classes
9 // Base Widget Model and View classes
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["notebook/js/widgetmanager",
17 define(["notebook/js/widgetmanager",
18 "underscore",
18 "underscore",
19 "backbone"],
19 "backbone"],
20 function(widget_manager, underscore, backbone){
20 function(widget_manager, underscore, backbone){
21
21
22 var WidgetModel = Backbone.Model.extend({
22 var WidgetModel = Backbone.Model.extend({
23 constructor: function (widget_manager, model_id, comm) {
23 constructor: function (widget_manager, model_id, comm) {
24 // Construcctor
24 // Construcctor
25 //
25 //
26 // Creates a WidgetModel instance.
26 // Creates a WidgetModel instance.
27 //
27 //
28 // Parameters
28 // Parameters
29 // ----------
29 // ----------
30 // widget_manager : WidgetManager instance
30 // widget_manager : WidgetManager instance
31 // model_id : string
31 // model_id : string
32 // An ID unique to this model.
32 // An ID unique to this model.
33 // comm : Comm instance (optional)
33 // comm : Comm instance (optional)
34 this.widget_manager = widget_manager;
34 this.widget_manager = widget_manager;
35 this.pending_msgs = 0;
35 this.pending_msgs = 0;
36 this.msg_throttle = 2;
36 this.msg_throttle = 2;
37 this.msg_buffer = null;
37 this.msg_buffer = null;
38 this.key_value_lock = null;
38 this.key_value_lock = null;
39 this.id = model_id;
39 this.id = model_id;
40 this.views = [];
40 this.views = [];
41
41
42 if (comm !== undefined) {
42 if (comm !== undefined) {
43 // Remember comm associated with the model.
43 // Remember comm associated with the model.
44 this.comm = comm;
44 this.comm = comm;
45 comm.model = this;
45 comm.model = this;
46
46
47 // Hook comm messages up to model.
47 // Hook comm messages up to model.
48 comm.on_close($.proxy(this._handle_comm_closed, this));
48 comm.on_close($.proxy(this._handle_comm_closed, this));
49 comm.on_msg($.proxy(this._handle_comm_msg, this));
49 comm.on_msg($.proxy(this._handle_comm_msg, this));
50 }
50 }
51 return Backbone.Model.apply(this);
51 return Backbone.Model.apply(this);
52 },
52 },
53
53
54 send: function (content, callbacks) {
54 send: function (content, callbacks) {
55 // Send a custom msg over the comm.
55 // Send a custom msg over the comm.
56 if (this.comm !== undefined) {
56 if (this.comm !== undefined) {
57 var data = {method: 'custom', custom_content: content};
57 var data = {method: 'custom', custom_content: content};
58 this.comm.send(data, callbacks);
58 this.comm.send(data, callbacks);
59 }
59 }
60 },
60 },
61
61
62 _handle_comm_closed: function (msg) {
62 _handle_comm_closed: function (msg) {
63 // Handle when a widget is closed.
63 // Handle when a widget is closed.
64 this.trigger('comm:close');
64 this.trigger('comm:close');
65 delete this.comm.model; // Delete ref so GC will collect widget model.
65 delete this.comm.model; // Delete ref so GC will collect widget model.
66 delete this.comm;
66 delete this.comm;
67 delete this.model_id; // Delete id from model so widget manager cleans up.
67 delete this.model_id; // Delete id from model so widget manager cleans up.
68 // TODO: Handle deletion, like this.destroy(), and delete views, etc.
68 // TODO: Handle deletion, like this.destroy(), and delete views, etc.
69 },
69 },
70
70
71 _handle_comm_msg: function (msg) {
71 _handle_comm_msg: function (msg) {
72 // Handle incoming comm msg.
72 // Handle incoming comm msg.
73 var method = msg.content.data.method;
73 var method = msg.content.data.method;
74 switch (method) {
74 switch (method) {
75 case 'update':
75 case 'update':
76 this.apply_update(msg.content.data.state);
76 this.apply_update(msg.content.data.state);
77 break;
77 break;
78 case 'custom':
78 case 'custom':
79 this.trigger('msg:custom', msg.content.data.custom_content);
79 this.trigger('msg:custom', msg.content.data.custom_content);
80 break;
80 break;
81 case 'display':
81 case 'display':
82 this.widget_manager.display_view(msg, this);
82 this.widget_manager.display_view(msg, this);
83 break;
83 break;
84 }
84 }
85 },
85 },
86
86
87 apply_update: function (state) {
87 apply_update: function (state) {
88 // Handle when a widget is updated via the python side.
88 // Handle when a widget is updated via the python side.
89 for (var key in state) {
89 for (var key in state) {
90 if (state.hasOwnProperty(key)) {
90 if (state.hasOwnProperty(key)) {
91 var value = state[key];
91 var value = state[key];
92 this.key_value_lock = [key, value];
92 this.key_value_lock = [key, value];
93 try {
93 try {
94 this.set(key, this._unpack_models(value));
94 this.set(key, this._unpack_models(value));
95 } finally {
95 } finally {
96 this.key_value_lock = null;
96 this.key_value_lock = null;
97 }
97 }
98 }
98 }
99 }
99 }
100 //TODO: are there callbacks that make sense in this case? If so, attach them here as an option
100 //TODO: are there callbacks that make sense in this case? If so, attach them here as an option
101 this.save();
102 },
101 },
103
102
104 _handle_status: function (msg, callbacks) {
103 _handle_status: function (msg, callbacks) {
105 // Handle status msgs.
104 // Handle status msgs.
106
105
107 // execution_state : ('busy', 'idle', 'starting')
106 // execution_state : ('busy', 'idle', 'starting')
108 if (this.comm !== undefined) {
107 if (this.comm !== undefined) {
109 if (msg.content.execution_state ==='idle') {
108 if (msg.content.execution_state ==='idle') {
110 // Send buffer if this message caused another message to be
109 // Send buffer if this message caused another message to be
111 // throttled.
110 // throttled.
112 if (this.msg_buffer !== null &&
111 if (this.msg_buffer !== null &&
113 this.msg_throttle === this.pending_msgs) {
112 this.msg_throttle === this.pending_msgs) {
114 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
113 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
115 this.comm.send(data, callbacks);
114 this.comm.send(data, callbacks);
116 this.msg_buffer = null;
115 this.msg_buffer = null;
117 } else {
116 } else {
118 --this.pending_msgs;
117 --this.pending_msgs;
119 }
118 }
120 }
119 }
121 }
120 }
122 },
121 },
123
122
124 _handle_sync: function (method, options) {
123 _handle_sync: function (method, options) {
125 // Custom syncronization logic.
124 // Custom syncronization logic.
126 var model_json = this.toJSON();
125 var model_json = this.toJSON();
127 var attr;
126 var attr;
128
127
129 // Only send updated state if the state hasn't been changed
128 // Only send updated state if the state hasn't been changed
130 // during an update.
129 // during an update.
131 if (this.comm !== undefined) {
130 if (this.comm !== undefined) {
132 if (this.pending_msgs >= this.msg_throttle) {
131 if (this.pending_msgs >= this.msg_throttle) {
133 // The throttle has been exceeded, buffer the current msg so
132 // The throttle has been exceeded, buffer the current msg so
134 // it can be sent once the kernel has finished processing
133 // it can be sent once the kernel has finished processing
135 // some of the existing messages.
134 // some of the existing messages.
136 if (this.msg_buffer === null) {
135 if (this.msg_buffer === null) {
137 this.msg_buffer = $.extend({}, model_json); // Copy
136 this.msg_buffer = $.extend({}, model_json); // Copy
138 }
137 }
139 for (attr in options.attrs) {
138 for (attr in options.attrs) {
140 var value = this._pack_models(options.attrs[attr]);
139 var value = this._pack_models(options.attrs[attr]);
141 if (this.key_value_lock === null || attr !== this.key_value_lock[0] || value !== this.key_value_lock[1]) {
140 if (this.key_value_lock === null || attr !== this.key_value_lock[0] || value !== this.key_value_lock[1]) {
142 this.msg_buffer[attr] = value;
141 this.msg_buffer[attr] = value;
143 }
142 }
144 }
143 }
145
144
146 } else {
145 } else {
147 // We haven't exceeded the throttle, send the message like
146 // We haven't exceeded the throttle, send the message like
148 // normal. If this is a patch operation, just send the
147 // normal. If this is a patch operation, just send the
149 // changes.
148 // changes.
150 var send_json = model_json;
149 var send_json = model_json;
151 send_json = {};
150 send_json = {};
152 for (attr in options.attrs) {
151 for (attr in options.attrs) {
153 var value = this._pack_models(options.attrs[attr]);
152 var value = this._pack_models(options.attrs[attr]);
154 if (this.key_value_lock === null || attr !== this.key_value_lock[0] || value !== this.key_value_lock[1]) {
153 if (this.key_value_lock === null || attr !== this.key_value_lock[0] || value !== this.key_value_lock[1]) {
155 send_json[attr] = value;
154 send_json[attr] = value;
156 }
155 }
157 }
156 }
158
157
159 var is_empty = true;
158 var is_empty = true;
160 for (var prop in send_json) if (send_json.hasOwnProperty(prop)) is_empty = false;
159 for (var prop in send_json) if (send_json.hasOwnProperty(prop)) is_empty = false;
161 if (!is_empty) {
160 if (!is_empty) {
162 ++this.pending_msgs;
161 ++this.pending_msgs;
163 var data = {method: 'backbone', sync_data: send_json};
162 var data = {method: 'backbone', sync_data: send_json};
164 this.comm.send(data, options.callbacks);
163 this.comm.send(data, options.callbacks);
165 }
164 }
166 }
165 }
167 }
166 }
168
167
169 // Since the comm is a one-way communication, assume the message
168 // Since the comm is a one-way communication, assume the message
170 // arrived.
169 // arrived.
171 return model_json;
170 return model_json;
172 },
171 },
173
172
174 push: function(callbacks) {
173 push: function(callbacks) {
175 // Push this model's state to the back-end
174 // Push this model's state to the back-end
176 //
175 //
177 // This invokes a Backbone.Sync.
176 // This invokes a Backbone.Sync.
178 this.save(this.changedAttributes(), {patch: true, callbacks: callbacks});
177 this.save(this.changedAttributes(), {patch: true, callbacks: callbacks});
179 },
178 },
180
179
181 _pack_models: function(value) {
180 _pack_models: function(value) {
182 // Replace models with model ids recursively.
181 // Replace models with model ids recursively.
183 if (value instanceof Backbone.Model) {
182 if (value instanceof Backbone.Model) {
184 return value.id;
183 return value.id;
185 } else if (value instanceof Object) {
184 } else if (value instanceof Object) {
186 var packed = {};
185 var packed = {};
187 for (var key in value) {
186 for (var key in value) {
188 packed[key] = this._pack_models(value[key]);
187 packed[key] = this._pack_models(value[key]);
189 }
188 }
190 return packed;
189 return packed;
191 } else {
190 } else {
192 return value;
191 return value;
193 }
192 }
194 },
193 },
195
194
196 _unpack_models: function(value) {
195 _unpack_models: function(value) {
197 // Replace model ids with models recursively.
196 // Replace model ids with models recursively.
198 if (value instanceof Object) {
197 if (value instanceof Object) {
199 var unpacked = {};
198 var unpacked = {};
200 for (var key in value) {
199 for (var key in value) {
201 unpacked[key] = this._unpack_models(value[key]);
200 unpacked[key] = this._unpack_models(value[key]);
202 }
201 }
203 return unpacked;
202 return unpacked;
204 } else {
203 } else {
205 var model = this.widget_manager.get_model(value);
204 var model = this.widget_manager.get_model(value);
206 if (model !== null) {
205 if (model !== null) {
207 return model;
206 return model;
208 } else {
207 } else {
209 return value;
208 return value;
210 }
209 }
211 }
210 }
212 },
211 },
213
212
214 });
213 });
215 widget_manager.register_widget_model('WidgetModel', WidgetModel);
214 widget_manager.register_widget_model('WidgetModel', WidgetModel);
216
215
217
216
218 var WidgetView = Backbone.View.extend({
217 var WidgetView = Backbone.View.extend({
219 initialize: function(parameters) {
218 initialize: function(parameters) {
220 // Public constructor.
219 // Public constructor.
221 this.model.on('change',this.update,this);
220 this.model.on('change',this.update,this);
222 this.options = parameters.options;
221 this.options = parameters.options;
223 this.child_views = [];
222 this.child_views = [];
224 this.model.views.push(this);
223 this.model.views.push(this);
225 },
224 },
226
225
227 update: function(){
226 update: function(){
228 // Triggered on model change.
227 // Triggered on model change.
229 //
228 //
230 // Update view to be consistent with this.model
229 // Update view to be consistent with this.model
231 },
230 },
232
231
233 create_child_view: function(child_model, options) {
232 create_child_view: function(child_model, options) {
234 // Create and return a child view.
233 // Create and return a child view.
235 //
234 //
236 // -given a model and (optionally) a view name if the view name is
235 // -given a model and (optionally) a view name if the view name is
237 // not given, it defaults to the model's default view attribute.
236 // not given, it defaults to the model's default view attribute.
238
237
239 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
238 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
240 // it would be great to have the widget manager add the cell metadata
239 // it would be great to have the widget manager add the cell metadata
241 // to the subview without having to add it here.
240 // to the subview without having to add it here.
242 options = options || {};
241 options = options || {};
243 options.cell = this.options.cell;
242 options.cell = this.options.cell;
244 var child_view = this.model.widget_manager.create_view(child_model, options);
243 var child_view = this.model.widget_manager.create_view(child_model, options);
245 this.child_views[child_model.id] = child_view;
244 this.child_views[child_model.id] = child_view;
246 return child_view;
245 return child_view;
247 },
246 },
248
247
249 delete_child_view: function(child_model, options) {
248 delete_child_view: function(child_model, options) {
250 // Delete a child view that was previously created using create_child_view.
249 // Delete a child view that was previously created using create_child_view.
251 var view = this.child_views[child_model.id];
250 var view = this.child_views[child_model.id];
252 delete this.child_views[child_model.id];
251 delete this.child_views[child_model.id];
253 view.remove();
252 view.remove();
254 },
253 },
255
254
256 do_diff: function(old_list, new_list, removed_callback, added_callback) {
255 do_diff: function(old_list, new_list, removed_callback, added_callback) {
257 // Difference a changed list and call remove and add callbacks for
256 // Difference a changed list and call remove and add callbacks for
258 // each removed and added item in the new list.
257 // each removed and added item in the new list.
259 //
258 //
260 // Parameters
259 // Parameters
261 // ----------
260 // ----------
262 // old_list : array
261 // old_list : array
263 // new_list : array
262 // new_list : array
264 // removed_callback : Callback(item)
263 // removed_callback : Callback(item)
265 // Callback that is called for each item removed.
264 // Callback that is called for each item removed.
266 // added_callback : Callback(item)
265 // added_callback : Callback(item)
267 // Callback that is called for each item added.
266 // Callback that is called for each item added.
268
267
269
268
270 // removed items
269 // removed items
271 _.each(_.difference(old_list, new_list), function(item, index, list) {
270 _.each(_.difference(old_list, new_list), function(item, index, list) {
272 removed_callback(item);
271 removed_callback(item);
273 }, this);
272 }, this);
274
273
275 // added items
274 // added items
276 _.each(_.difference(new_list, old_list), function(item, index, list) {
275 _.each(_.difference(new_list, old_list), function(item, index, list) {
277 added_callback(item);
276 added_callback(item);
278 }, this);
277 }, this);
279 },
278 },
280
279
281 callbacks: function(){
280 callbacks: function(){
282 // Create msg callbacks for a comm msg.
281 // Create msg callbacks for a comm msg.
283 return this.model.widget_manager.callbacks(this);
282 return this.model.widget_manager.callbacks(this);
284 },
283 },
285
284
286 render: function(){
285 render: function(){
287 // Render the view.
286 // Render the view.
288 //
287 //
289 // By default, this is only called the first time the view is created
288 // By default, this is only called the first time the view is created
290 },
289 },
291
290
292 send: function (content) {
291 send: function (content) {
293 // Send a custom msg associated with this view.
292 // Send a custom msg associated with this view.
294 this.model.send(content, this.callbacks());
293 this.model.send(content, this.callbacks());
295 },
294 },
296
295
297 touch: function () {
296 touch: function () {
298 this.model.push(this.callbacks());
297 this.model.push(this.callbacks());
299 },
298 },
300
299
301 });
300 });
302
301
303
302
304 var DOMWidgetView = WidgetView.extend({
303 var DOMWidgetView = WidgetView.extend({
305 initialize: function (options) {
304 initialize: function (options) {
306 // Public constructor
305 // Public constructor
307
306
308 // In the future we may want to make changes more granular
307 // In the future we may want to make changes more granular
309 // (e.g., trigger on visible:change).
308 // (e.g., trigger on visible:change).
310 this.model.on('change', this.update, this);
309 this.model.on('change', this.update, this);
311 this.model.on('msg:custom', this.on_msg, this);
310 this.model.on('msg:custom', this.on_msg, this);
312 DOMWidgetView.__super__.initialize.apply(this, arguments);
311 DOMWidgetView.__super__.initialize.apply(this, arguments);
313 },
312 },
314
313
315 on_msg: function(msg) {
314 on_msg: function(msg) {
316 // Handle DOM specific msgs.
315 // Handle DOM specific msgs.
317 switch(msg.msg_type) {
316 switch(msg.msg_type) {
318 case 'add_class':
317 case 'add_class':
319 this.add_class(msg.selector, msg.class_list);
318 this.add_class(msg.selector, msg.class_list);
320 break;
319 break;
321 case 'remove_class':
320 case 'remove_class':
322 this.remove_class(msg.selector, msg.class_list);
321 this.remove_class(msg.selector, msg.class_list);
323 break;
322 break;
324 }
323 }
325 },
324 },
326
325
327 add_class: function (selector, class_list) {
326 add_class: function (selector, class_list) {
328 // Add a DOM class to an element.
327 // Add a DOM class to an element.
329 this._get_selector_element(selector).addClass(class_list);
328 this._get_selector_element(selector).addClass(class_list);
330 },
329 },
331
330
332 remove_class: function (selector, class_list) {
331 remove_class: function (selector, class_list) {
333 // Remove a DOM class from an element.
332 // Remove a DOM class from an element.
334 this._get_selector_element(selector).removeClass(class_list);
333 this._get_selector_element(selector).removeClass(class_list);
335 },
334 },
336
335
337 update: function () {
336 update: function () {
338 // Update the contents of this view
337 // Update the contents of this view
339 //
338 //
340 // Called when the model is changed. The model may have been
339 // Called when the model is changed. The model may have been
341 // changed by another view or by a state update from the back-end.
340 // changed by another view or by a state update from the back-end.
342 // The very first update seems to happen before the element is
341 // The very first update seems to happen before the element is
343 // finished rendering so we use setTimeout to give the element time
342 // finished rendering so we use setTimeout to give the element time
344 // to render
343 // to render
345 var e = this.$el;
344 var e = this.$el;
346 var visible = this.model.get('visible');
345 var visible = this.model.get('visible');
347 setTimeout(function() {e.toggle(visible)},0);
346 setTimeout(function() {e.toggle(visible)},0);
348
347
349 var css = this.model.get('_css');
348 var css = this.model.get('_css');
350 if (css === undefined) {return;}
349 if (css === undefined) {return;}
351 for (var selector in css) {
350 for (var selector in css) {
352 if (css.hasOwnProperty(selector)) {
351 if (css.hasOwnProperty(selector)) {
353 // Apply the css traits to all elements that match the selector.
352 // Apply the css traits to all elements that match the selector.
354 var elements = this._get_selector_element(selector);
353 var elements = this._get_selector_element(selector);
355 if (elements.length > 0) {
354 if (elements.length > 0) {
356 var css_traits = css[selector];
355 var css_traits = css[selector];
357 for (var css_key in css_traits) {
356 for (var css_key in css_traits) {
358 if (css_traits.hasOwnProperty(css_key)) {
357 if (css_traits.hasOwnProperty(css_key)) {
359 elements.css(css_key, css_traits[css_key]);
358 elements.css(css_key, css_traits[css_key]);
360 }
359 }
361 }
360 }
362 }
361 }
363 }
362 }
364 }
363 }
365 },
364 },
366
365
367 _get_selector_element: function (selector) {
366 _get_selector_element: function (selector) {
368 // Get the elements via the css selector.
367 // Get the elements via the css selector.
369
368
370 // If the selector is blank, apply the style to the $el_to_style
369 // If the selector is blank, apply the style to the $el_to_style
371 // element. If the $el_to_style element is not defined, use apply
370 // element. If the $el_to_style element is not defined, use apply
372 // the style to the view's element.
371 // the style to the view's element.
373 var elements;
372 var elements;
374 if (selector === undefined || selector === null || selector === '') {
373 if (selector === undefined || selector === null || selector === '') {
375 if (this.$el_to_style === undefined) {
374 if (this.$el_to_style === undefined) {
376 elements = this.$el;
375 elements = this.$el;
377 } else {
376 } else {
378 elements = this.$el_to_style;
377 elements = this.$el_to_style;
379 }
378 }
380 } else {
379 } else {
381 elements = this.$el.find(selector);
380 elements = this.$el.find(selector);
382 }
381 }
383 return elements;
382 return elements;
384 },
383 },
385 });
384 });
386
385
387 IPython.WidgetModel = WidgetModel;
386 IPython.WidgetModel = WidgetModel;
388 IPython.WidgetView = WidgetView;
387 IPython.WidgetView = WidgetView;
389 IPython.DOMWidgetView = DOMWidgetView;
388 IPython.DOMWidgetView = DOMWidgetView;
390
389
391 // Pass through widget_manager instance (probably not a good practice).
390 // Pass through widget_manager instance (probably not a good practice).
392 return widget_manager;
391 return widget_manager;
393 });
392 });
General Comments 0
You need to be logged in to leave comments. Login now