##// END OF EJS Templates
Rebase fixes
Jonathan Frederic -
Show More
@@ -1,1 +1,1
1 Subproject commit 87ff70d96567bf055eb94161a41e7b3e6da31b23
1 Subproject commit 563c9e74b153e9509d94fd448353eeda13f0819c
@@ -1,431 +1,431
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/utils",
9 9 "base/js/namespace",
10 10 "services/kernels/comm"
11 11 ], function (_, Backbone, $, utils, IPython, comm) {
12 12 "use strict";
13 13 //--------------------------------------------------------------------
14 14 // WidgetManager class
15 15 //--------------------------------------------------------------------
16 16 var WidgetManager = function (comm_manager, notebook) {
17 17 /**
18 18 * Public constructor
19 19 */
20 20 WidgetManager._managers.push(this);
21 21
22 22 // Attach a comm manager to the
23 23 this.keyboard_manager = notebook.keyboard_manager;
24 24 this.notebook = notebook;
25 25 this.comm_manager = comm_manager;
26 26 this.comm_target_name = 'ipython.widget';
27 27 this._models = {}; /* Dictionary of model ids and model instance promises */
28 28
29 29 // Register with the comm manager.
30 30 this.comm_manager.register_target(this.comm_target_name, $.proxy(this._handle_comm_open, this));
31 31
32 32 // Load the initial state of the widget manager if a load callback was
33 33 // registered.
34 34 if (WidgetManager._load_callback) {
35 35 this.set_state(WidgetManager._load_callback.call(this));
36 36 }
37 37
38 38 // Setup state saving code.
39 39 var that = this;
40 40 this.notebook.events.on('before_save.Notebook', function() {
41 41 var save_callback = WidgetManager._save_callback;
42 42 var options = WidgetManager._get_state_options;
43 43 if (save_callback) {
44 44 that.get_state(options).then(function(state) {
45 45 save_callback.call(that, state);
46 46 }).catch(utils.reject('Could not call widget save state callback.', true));
47 47 }
48 48 });
49 49 };
50 50
51 51 //--------------------------------------------------------------------
52 52 // Class level
53 53 //--------------------------------------------------------------------
54 54 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
55 55 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
56 56 WidgetManager._managers = []; /* List of widget managers */
57 57 WidgetManager._load_callback = null;
58 58 WidgetManager._save_callback = null;
59 59
60 60 WidgetManager.register_widget_model = function (model_name, model_type) {
61 61 // Registers a widget model by name.
62 62 WidgetManager._model_types[model_name] = model_type;
63 63 };
64 64
65 65 WidgetManager.register_widget_view = function (view_name, view_type) {
66 66 // Registers a widget view by name.
67 67 WidgetManager._view_types[view_name] = view_type;
68 68 };
69 69
70 70 WidgetManager.set_state_callbacks = function (load_callback, save_callback, options) {
71 71 // Registers callbacks for widget state persistence.
72 72 WidgetManager._load_callback = load_callback;
73 73 WidgetManager._save_callback = save_callback;
74 74 WidgetManager._get_state_options = options;
75 75
76 76 // Use the load callback to immediately load widget states.
77 77 WidgetManager._managers.forEach(function(manager) {
78 78 if (load_callback) {
79 79 manager.set_state(load_callback.call(manager));
80 80 }
81 81 });
82 82 };
83 83
84 84 //--------------------------------------------------------------------
85 85 // Instance level
86 86 //--------------------------------------------------------------------
87 87 WidgetManager.prototype.display_view = function(msg, model) {
88 88 /**
89 89 * Displays a view for a particular model.
90 90 */
91 91 var cell = this.get_msg_cell(msg.parent_header.msg_id);
92 92 if (cell === null) {
93 93 return Promise.reject(new Error("Could not determine where the display" +
94 94 " message was from. Widget will not be displayed"));
95 95 } else {
96 96 return this.display_view_in_cell(cell, model)
97 .catch(utils.reject('View could not be displayed.', true));
97 .catch(utils.reject('Could not display view', true));
98 98 }
99 99 };
100 100
101 101 WidgetManager.prototype.display_view_in_cell = function(cell, model) {
102 102 // Displays a view in a cell.
103 103 if (cell.display_widget_view) {
104 104 var that = this;
105 105 return cell.display_widget_view(this.create_view(model, {
106 106 cell: cell,
107 107 // Only set cell_index when view is displayed as directly.
108 108 cell_index: that.notebook.find_cell_index(cell),
109 109 })).then(function(view) {
110 110 that._handle_display_view(view);
111 111 view.trigger('displayed');
112 112 resolve(view);
113 113 }).catch(utils.reject('Could not create or display view', true));
114 114 } else {
115 115 return Promise.reject(new Error('Cell does not have a `display_widget_view` method'));
116 116 }
117 117 };
118 118
119 119 WidgetManager.prototype._handle_display_view = function (view) {
120 120 /**
121 121 * Have the IPython keyboard manager disable its event
122 122 * handling so the widget can capture keyboard input.
123 123 * Note, this is only done on the outer most widgets.
124 124 */
125 125 if (this.keyboard_manager) {
126 126 this.keyboard_manager.register_events(view.$el);
127 127
128 128 if (view.additional_elements) {
129 129 for (var i = 0; i < view.additional_elements.length; i++) {
130 130 this.keyboard_manager.register_events(view.additional_elements[i]);
131 131 }
132 132 }
133 133 }
134 134 };
135 135
136 136 WidgetManager.prototype.create_view = function(model, options) {
137 137 /**
138 138 * Creates a promise for a view of a given model
139 139 *
140 140 * Make sure the view creation is not out of order with
141 141 * any state updates.
142 142 */
143 143 model.state_change = model.state_change.then(function() {
144 144
145 145 return utils.load_class(model.get('_view_name'), model.get('_view_module'),
146 146 WidgetManager._view_types).then(function(ViewType) {
147 147
148 148 // If a view is passed into the method, use that view's cell as
149 149 // the cell for the view that is created.
150 150 options = options || {};
151 151 if (options.parent !== undefined) {
152 152 options.cell = options.parent.options.cell;
153 153 }
154 154 // Create and render the view...
155 155 var parameters = {model: model, options: options};
156 156 var view = new ViewType(parameters);
157 157 view.listenTo(model, 'destroy', view.remove);
158 158 return Promise.resolve(view.render()).then(function() {return view;});
159 159 }).catch(utils.reject("Couldn't create a view for model id '" + String(model.id) + "'", true));
160 160 });
161 161 model.views[utils.uuid()] = model.state_change;
162 162 return model.state_change;
163 163 };
164 164
165 165 WidgetManager.prototype.get_msg_cell = function (msg_id) {
166 166 var cell = null;
167 167 // First, check to see if the msg was triggered by cell execution.
168 168 if (this.notebook) {
169 169 cell = this.notebook.get_msg_cell(msg_id);
170 170 }
171 171 if (cell !== null) {
172 172 return cell;
173 173 }
174 174 // Second, check to see if a get_cell callback was defined
175 175 // for the message. get_cell callbacks are registered for
176 176 // widget messages, so this block is actually checking to see if the
177 177 // message was triggered by a widget.
178 178 var kernel = this.comm_manager.kernel;
179 179 if (kernel) {
180 180 var callbacks = kernel.get_callbacks_for_msg(msg_id);
181 181 if (callbacks && callbacks.iopub &&
182 182 callbacks.iopub.get_cell !== undefined) {
183 183 return callbacks.iopub.get_cell();
184 184 }
185 185 }
186 186
187 187 // Not triggered by a cell or widget (no get_cell callback
188 188 // exists).
189 189 return null;
190 190 };
191 191
192 192 WidgetManager.prototype.callbacks = function (view) {
193 193 /**
194 194 * callback handlers specific a view
195 195 */
196 196 var callbacks = {};
197 197 if (view && view.options.cell) {
198 198
199 199 // Try to get output handlers
200 200 var cell = view.options.cell;
201 201 var handle_output = null;
202 202 var handle_clear_output = null;
203 203 if (cell.output_area) {
204 204 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
205 205 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
206 206 }
207 207
208 208 // Create callback dictionary using what is known
209 209 var that = this;
210 210 callbacks = {
211 211 iopub : {
212 212 output : handle_output,
213 213 clear_output : handle_clear_output,
214 214
215 215 // Special function only registered by widget messages.
216 216 // Allows us to get the cell for a message so we know
217 217 // where to add widgets if the code requires it.
218 218 get_cell : function () {
219 219 return cell;
220 220 },
221 221 },
222 222 };
223 223 }
224 224 return callbacks;
225 225 };
226 226
227 227 WidgetManager.prototype.get_model = function (model_id) {
228 228 /**
229 229 * Get a promise for a model by model id.
230 230 */
231 231 return this._models[model_id];
232 232 };
233 233
234 234 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
235 235 /**
236 236 * Handle when a comm is opened.
237 237 */
238 238 return this.create_model({
239 239 model_name: msg.content.data.model_name,
240 240 model_module: msg.content.data.model_module,
241 241 comm: comm}).catch(utils.reject("Couldn't create a model.", true));
242 242 };
243 243
244 244 WidgetManager.prototype.create_model = function (options) {
245 245 /**
246 246 * Create and return a promise for a new widget model
247 247 *
248 248 * Minimally, one must provide the model_name and widget_class
249 249 * parameters to create a model from Javascript.
250 250 *
251 251 * Example
252 252 * --------
253 253 * JS:
254 254 * IPython.notebook.kernel.widget_manager.create_model({
255 255 * model_name: 'WidgetModel',
256 256 * widget_class: 'IPython.html.widgets.widget_int.IntSlider'})
257 257 * .then(function(model) { console.log('Create success!', model); },
258 258 * $.proxy(console.error, console));
259 259 *
260 260 * Parameters
261 261 * ----------
262 262 * options: dictionary
263 263 * Dictionary of options with the following contents:
264 264 * model_name: string
265 265 * Target name of the widget model to create.
266 266 * model_module: (optional) string
267 267 * Module name of the widget model to create.
268 268 * widget_class: (optional) string
269 269 * Target name of the widget in the back-end.
270 270 * comm: (optional) Comm
271 271 *
272 272 * Create a comm if it wasn't provided.
273 273 */
274 274 var comm = options.comm;
275 275 if (!comm) {
276 276 comm = this.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class});
277 277 }
278 278
279 279 var that = this;
280 280 var model_id = comm.comm_id;
281 281 var model_promise = utils.load_class(options.model_name, options.model_module, WidgetManager._model_types)
282 282 .then(function(ModelType) {
283 283 var widget_model = new ModelType(that, model_id, comm);
284 284 widget_model.once('comm:close', function () {
285 285 delete that._models[model_id];
286 286 });
287 287 widget_model.name = options.model_name;
288 288 widget_model.module = options.model_module;
289 289 return widget_model;
290 290
291 291 }, function(error) {
292 292 delete that._models[model_id];
293 293 var wrapped_error = new utils.WrappedError("Couldn't create model", error);
294 294 return Promise.reject(wrapped_error);
295 295 });
296 296 this._models[model_id] = model_promise;
297 297 return model_promise;
298 298 };
299 299
300 300 WidgetManager.prototype.get_state = function(options) {
301 301 // Asynchronously get the state of the widget manager.
302 302 //
303 303 // This includes all of the widget models and the cells that they are
304 304 // displayed in.
305 305 //
306 306 // Parameters
307 307 // ----------
308 308 // options: dictionary
309 309 // Dictionary of options with the following contents:
310 310 // only_displayed: (optional) boolean=false
311 311 // Only return models with one or more displayed views.
312 312 // not_live: (optional) boolean=false
313 313 // Include models that have comms with severed connections.
314 314 //
315 315 // Returns
316 316 // -------
317 317 // Promise for a state dictionary
318 318 var that = this;
319 319 return utils.resolve_promises_dict(this._models).then(function(models) {
320 320 var state = {};
321 321 for (var model_id in models) {
322 322 if (models.hasOwnProperty(model_id)) {
323 323 var model = models[model_id];
324 324
325 325 // If the model has one or more views defined for it,
326 326 // consider it displayed.
327 327 var displayed_flag = !(options && options.only_displayed) || Object.keys(model.views).length > 0;
328 328 var live_flag = (options && options.not_live) || model.comm_live;
329 329 if (displayed_flag && live_flag) {
330 330 state[model_id] = {
331 331 model_name: model.name,
332 332 model_module: model.module,
333 333 state: model.get_state(),
334 334 views: [],
335 335 };
336 336
337 337 // Get the views that are displayed *now*.
338 338 for (var id in model.views) {
339 339 if (model.views.hasOwnProperty(id)) {
340 340 var view = model.views[id];
341 341 if (view.options.cell_index) {
342 342 state[model_id].views.push(view.options.cell_index);
343 343 }
344 344 }
345 345 }
346 346 }
347 347 }
348 348 }
349 349 return state;
350 350 }).catch(utils.reject('Could not get state of widget manager', true));
351 351 };
352 352
353 353 WidgetManager.prototype.set_state = function(state) {
354 354 // Set the notebook's state.
355 355 //
356 356 // Reconstructs all of the widget models and attempts to redisplay the
357 357 // widgets in the appropriate cells by cell index.
358 358
359 359 // Get the kernel when it's available.
360 360 var that = this;
361 361 return this._get_connected_kernel().then(function(kernel) {
362 362
363 363 // Recreate all the widget models for the given state and
364 364 // display the views.
365 365 that.all_views = [];
366 366 var model_ids = Object.keys(state);
367 367 for (var i = 0; i < model_ids.length; i++) {
368 368 var model_id = model_ids[i];
369 369
370 370 // Recreate a comm using the widget's model id (model_id == comm_id).
371 371 var new_comm = new comm.Comm(kernel.widget_manager.comm_target_name, model_id);
372 372 kernel.comm_manager.register_comm(new_comm);
373 373
374 374 // Create the model using the recreated comm. When the model is
375 375 // created we don't know yet if the comm is valid so set_comm_live
376 376 // false. Once we receive the first state push from the back-end
377 377 // we know the comm is alive.
378 378 var views = kernel.widget_manager.create_model({
379 379 comm: new_comm,
380 380 model_name: state[model_id].model_name,
381 381 model_module: state[model_id].model_module})
382 382 .then(function(model) {
383 383
384 384 model.set_comm_live(false);
385 385 var view_promise = Promise.resolve().then(function() {
386 386 return model.set_state(state[model.id].state);
387 387 }).then(function() {
388 388 model.request_state().then(function() {
389 389 model.set_comm_live(true);
390 390 });
391 391
392 392 // Display the views of the model.
393 393 var views = [];
394 394 var model_views = state[model.id].views;
395 395 for (var j=0; j<model_views.length; j++) {
396 396 var cell_index = model_views[j];
397 397 var cell = that.notebook.get_cell(cell_index);
398 398 views.push(that.display_view_in_cell(cell, model));
399 399 }
400 400 return Promise.all(views);
401 401 });
402 402 return view_promise;
403 403 });
404 404 that.all_views.push(views);
405 405 }
406 406 return Promise.all(that.all_views);
407 407 }).catch(utils.reject('Could not set widget manager state.', true));
408 408 };
409 409
410 410 WidgetManager.prototype._get_connected_kernel = function() {
411 411 // Gets a promise for a connected kernel.
412 412 var that = this;
413 413 return new Promise(function(resolve, reject) {
414 414 if (that.comm_manager &&
415 415 that.comm_manager.kernel &&
416 416 that.comm_manager.kernel.is_connected()) {
417 417
418 418 resolve(that.comm_manager.kernel);
419 419 } else {
420 420 that.notebook.events.on('kernel_connected.Kernel', function(event, data) {
421 421 resolve(data.kernel);
422 422 });
423 423 }
424 424 });
425 425 };
426 426
427 427 // Backwards compatibility.
428 428 IPython.WidgetManager = WidgetManager;
429 429
430 430 return {'WidgetManager': WidgetManager};
431 431 });
@@ -1,757 +1,757
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/utils",
9 9 "base/js/namespace",
10 10 ], function(widgetmanager, _, Backbone, $, utils, IPython){
11 11
12 12 var WidgetModel = Backbone.Model.extend({
13 13 constructor: function (widget_manager, model_id, comm) {
14 14 /**
15 15 * Constructor
16 16 *
17 17 * Creates a WidgetModel instance.
18 18 *
19 19 * Parameters
20 20 * ----------
21 21 * widget_manager : WidgetManager instance
22 22 * model_id : string
23 23 * An ID unique to this model.
24 24 * comm : Comm instance (optional)
25 25 */
26 26 this.widget_manager = widget_manager;
27 27 this.state_change = Promise.resolve();
28 28 this._buffered_state_diff = {};
29 29 this.pending_msgs = 0;
30 30 this.msg_buffer = null;
31 31 this.state_lock = null;
32 32 this.id = model_id;
33 33 this.views = {};
34 34 this._resolve_received_state = {};
35 35
36 36 if (comm !== undefined) {
37 37 // Remember comm associated with the model.
38 38 this.comm = comm;
39 39 comm.model = this;
40 40
41 41 // Hook comm messages up to model.
42 42 comm.on_close($.proxy(this._handle_comm_closed, this));
43 43 comm.on_msg($.proxy(this._handle_comm_msg, this));
44 44
45 45 // Assume the comm is alive.
46 46 this.set_comm_live(true);
47 47 } else {
48 48 this.set_comm_live(false);
49 49 }
50 50 return Backbone.Model.apply(this);
51 51 },
52 52
53 53 send: function (content, callbacks) {
54 54 /**
55 55 * Send a custom msg over the comm.
56 56 */
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 request_state: function(callbacks) {
65 65 /**
66 66 * Request a state push from the back-end.
67 67 */
68 68 if (!this.comm) {
69 69 console.error("Could not request_state because comm doesn't exist!");
70 70 return;
71 71 }
72 72
73 73 var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
74 74
75 75 // Promise that is resolved when a state is received
76 76 // from the back-end.
77 77 var that = this;
78 78 var received_state = new Promise(function(resolve) {
79 79 that._resolve_received_state[msg_id] = resolve;
80 80 });
81 81 return received_state;
82 82 },
83 83
84 84 set_comm_live: function(live) {
85 85 /**
86 86 * Change the comm_live state of the model.
87 87 */
88 88 if (this.comm_live === undefined || this.comm_live != live) {
89 89 this.comm_live = live;
90 90 this.trigger(live ? 'comm:live' : 'comm:dead', {model: this});
91 91 }
92 92 },
93 93
94 94 close: function(comm_closed) {
95 95 /**
96 96 * Close model
97 97 */
98 98 if (this.comm && !comm_closed) {
99 99 this.comm.close();
100 100 }
101 101 this.stopListening();
102 102 this.trigger('destroy', this);
103 103 delete this.comm.model; // Delete ref so GC will collect widget model.
104 104 delete this.comm;
105 105 delete this.model_id; // Delete id from model so widget manager cleans up.
106 106 _.each(this.views, function(v, id, views) {
107 107 v.then(function(view) {
108 108 view.remove();
109 109 delete views[id];
110 110 });
111 111 });
112 112 },
113 113
114 114 _handle_comm_closed: function (msg) {
115 115 /**
116 116 * Handle when a widget is closed.
117 117 */
118 118 this.trigger('comm:close');
119 119 this.close(true);
120 120 },
121 121
122 122 _handle_comm_msg: function (msg) {
123 123 /**
124 124 * Handle incoming comm msg.
125 125 */
126 126 var method = msg.content.data.method;
127 127 var that = this;
128 128 switch (method) {
129 129 case 'update':
130 130 this.state_change = this.state_change
131 131 .then(function() {
132 132 return that.set_state(msg.content.data.state);
133 133 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
134 134 .then(function() {
135 135 var parent_id = msg.parent_header.msg_id;
136 136 if (that._resolve_received_state[parent_id] !== undefined) {
137 137 that._resolve_received_state[parent_id].call();
138 138 delete that._resolve_received_state[parent_id];
139 139 }
140 }).catch(utils.reject("Couldn't resolve state request promise.", true));
140 }).catch(utils.reject("Couldn't resolve state request promise", true));
141 141 break;
142 142 case 'custom':
143 143 this.trigger('msg:custom', msg.content.data.content);
144 144 break;
145 145 case 'display':
146 146 this.widget_manager.display_view(msg, this)
147 .catch(utils.reject('Could not display view.', true));
147 .catch(utils.reject('Could not process display view msg', true));
148 148 break;
149 149 }
150 150 },
151 151
152 152 set_state: function (state) {
153 153 var that = this;
154 154 // Handle when a widget is updated via the python side.
155 155 return this._unpack_models(state).then(function(state) {
156 156 that.state_lock = state;
157 157 try {
158 158 WidgetModel.__super__.set.call(that, state);
159 159 } finally {
160 160 that.state_lock = null;
161 161 }
162 }, utils.reject("Couldn't set model state", true));
162 }).catch(utils.reject("Couldn't set model state", true));
163 163 },
164 164
165 165 get_state: function() {
166 166 // Get the serializable state of the model.
167 167 state = this.toJSON();
168 168 for (var key in state) {
169 169 if (state.hasOwnProperty(key)) {
170 170 state[key] = this._pack_models(state[key]);
171 171 }
172 172 }
173 173 return state;
174 174 },
175 175
176 176 _handle_status: function (msg, callbacks) {
177 177 /**
178 178 * Handle status msgs.
179 179 *
180 180 * execution_state : ('busy', 'idle', 'starting')
181 181 */
182 182 if (this.comm !== undefined) {
183 183 if (msg.content.execution_state ==='idle') {
184 184 // Send buffer if this message caused another message to be
185 185 // throttled.
186 186 if (this.msg_buffer !== null &&
187 187 (this.get('msg_throttle') || 3) === this.pending_msgs) {
188 188 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
189 189 this.comm.send(data, callbacks);
190 190 this.msg_buffer = null;
191 191 } else {
192 192 --this.pending_msgs;
193 193 }
194 194 }
195 195 }
196 196 },
197 197
198 198 callbacks: function(view) {
199 199 /**
200 200 * Create msg callbacks for a comm msg.
201 201 */
202 202 var callbacks = this.widget_manager.callbacks(view);
203 203
204 204 if (callbacks.iopub === undefined) {
205 205 callbacks.iopub = {};
206 206 }
207 207
208 208 var that = this;
209 209 callbacks.iopub.status = function (msg) {
210 210 that._handle_status(msg, callbacks);
211 211 };
212 212 return callbacks;
213 213 },
214 214
215 215 set: function(key, val, options) {
216 216 /**
217 217 * Set a value.
218 218 */
219 219 var return_value = WidgetModel.__super__.set.apply(this, arguments);
220 220
221 221 // Backbone only remembers the diff of the most recent set()
222 222 // operation. Calling set multiple times in a row results in a
223 223 // loss of diff information. Here we keep our own running diff.
224 224 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
225 225 return return_value;
226 226 },
227 227
228 228 sync: function (method, model, options) {
229 229 /**
230 230 * Handle sync to the back-end. Called when a model.save() is called.
231 231 *
232 232 * Make sure a comm exists.
233 233 */
234 234 var error = options.error || function() {
235 235 console.error('Backbone sync error:', arguments);
236 236 };
237 237 if (this.comm === undefined) {
238 238 error();
239 239 return false;
240 240 }
241 241
242 242 // Delete any key value pairs that the back-end already knows about.
243 243 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
244 244 if (this.state_lock !== null) {
245 245 var keys = Object.keys(this.state_lock);
246 246 for (var i=0; i<keys.length; i++) {
247 247 var key = keys[i];
248 248 if (attrs[key] === this.state_lock[key]) {
249 249 delete attrs[key];
250 250 }
251 251 }
252 252 }
253 253
254 254 // Only sync if there are attributes to send to the back-end.
255 255 attrs = this._pack_models(attrs);
256 256 if (_.size(attrs) > 0) {
257 257
258 258 // If this message was sent via backbone itself, it will not
259 259 // have any callbacks. It's important that we create callbacks
260 260 // so we can listen for status messages, etc...
261 261 var callbacks = options.callbacks || this.callbacks();
262 262
263 263 // Check throttle.
264 264 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
265 265 // The throttle has been exceeded, buffer the current msg so
266 266 // it can be sent once the kernel has finished processing
267 267 // some of the existing messages.
268 268
269 269 // Combine updates if it is a 'patch' sync, otherwise replace updates
270 270 switch (method) {
271 271 case 'patch':
272 272 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
273 273 break;
274 274 case 'update':
275 275 case 'create':
276 276 this.msg_buffer = attrs;
277 277 break;
278 278 default:
279 279 error();
280 280 return false;
281 281 }
282 282 this.msg_buffer_callbacks = callbacks;
283 283
284 284 } else {
285 285 // We haven't exceeded the throttle, send the message like
286 286 // normal.
287 287 var data = {method: 'backbone', sync_data: attrs};
288 288 this.comm.send(data, callbacks);
289 289 this.pending_msgs++;
290 290 }
291 291 }
292 292 // Since the comm is a one-way communication, assume the message
293 293 // arrived. Don't call success since we don't have a model back from the server
294 294 // this means we miss out on the 'sync' event.
295 295 this._buffered_state_diff = {};
296 296 },
297 297
298 298 save_changes: function(callbacks) {
299 299 /**
300 300 * Push this model's state to the back-end
301 301 *
302 302 * This invokes a Backbone.Sync.
303 303 */
304 304 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
305 305 },
306 306
307 307 _pack_models: function(value) {
308 308 /**
309 309 * Replace models with model ids recursively.
310 310 */
311 311 var that = this;
312 312 var packed;
313 313 if (value instanceof Backbone.Model) {
314 314 return "IPY_MODEL_" + value.id;
315 315
316 316 } else if ($.isArray(value)) {
317 317 packed = [];
318 318 _.each(value, function(sub_value, key) {
319 319 packed.push(that._pack_models(sub_value));
320 320 });
321 321 return packed;
322 322 } else if (value instanceof Date || value instanceof String) {
323 323 return value;
324 324 } else if (value instanceof Object) {
325 325 packed = {};
326 326 _.each(value, function(sub_value, key) {
327 327 packed[key] = that._pack_models(sub_value);
328 328 });
329 329 return packed;
330 330
331 331 } else {
332 332 return value;
333 333 }
334 334 },
335 335
336 336 _unpack_models: function(value) {
337 337 /**
338 338 * Replace model ids with models recursively.
339 339 */
340 340 var that = this;
341 341 var unpacked;
342 342 if ($.isArray(value)) {
343 343 unpacked = [];
344 344 _.each(value, function(sub_value, key) {
345 345 unpacked.push(that._unpack_models(sub_value));
346 346 });
347 347 return Promise.all(unpacked);
348 348 } else if (value instanceof Object) {
349 349 unpacked = {};
350 350 _.each(value, function(sub_value, key) {
351 351 unpacked[key] = that._unpack_models(sub_value);
352 352 });
353 353 return utils.resolve_promises_dict(unpacked);
354 354 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
355 355 // get_model returns a promise already
356 356 return this.widget_manager.get_model(value.slice(10, value.length));
357 357 } else {
358 358 return Promise.resolve(value);
359 359 }
360 360 },
361 361
362 362 on_some_change: function(keys, callback, context) {
363 363 /**
364 364 * on_some_change(["key1", "key2"], foo, context) differs from
365 365 * on("change:key1 change:key2", foo, context).
366 366 * If the widget attributes key1 and key2 are both modified,
367 367 * the second form will result in foo being called twice
368 368 * while the first will call foo only once.
369 369 */
370 370 this.on('change', function() {
371 371 if (keys.some(this.hasChanged, this)) {
372 372 callback.apply(context);
373 373 }
374 374 }, this);
375 375
376 376 },
377 377 });
378 378 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
379 379
380 380
381 381 var WidgetView = Backbone.View.extend({
382 382 initialize: function(parameters) {
383 383 /**
384 384 * Public constructor.
385 385 */
386 386 this.model.on('change',this.update,this);
387 387 this.options = parameters.options;
388 388 this.on('displayed', function() {
389 389 this.is_displayed = true;
390 390 }, this);
391 391 this.on('remove', function() {
392 392 delete this.model.views[this.id];
393 393 }, this);
394 394 },
395 395
396 396 update: function(){
397 397 /**
398 398 * Triggered on model change.
399 399 *
400 400 * Update view to be consistent with this.model
401 401 */
402 402 },
403 403
404 404 create_child_view: function(child_model, options) {
405 405 /**
406 406 * Create and promise that resolves to a child view of a given model
407 407 */
408 408 var that = this;
409 409 options = $.extend({ parent: this }, options || {});
410 410 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
411 411 },
412 412
413 413 callbacks: function(){
414 414 /**
415 415 * Create msg callbacks for a comm msg.
416 416 */
417 417 return this.model.callbacks(this);
418 418 },
419 419
420 420 render: function(){
421 421 /**
422 422 * Render the view.
423 423 *
424 424 * By default, this is only called the first time the view is created
425 425 */
426 426 },
427 427
428 428 show: function(){
429 429 /**
430 430 * Show the widget-area
431 431 */
432 432 if (this.options && this.options.cell &&
433 433 this.options.cell.widget_area !== undefined) {
434 434 this.options.cell.widget_area.show();
435 435 }
436 436 },
437 437
438 438 send: function (content) {
439 439 /**
440 440 * Send a custom msg associated with this view.
441 441 */
442 442 this.model.send(content, this.callbacks());
443 443 },
444 444
445 445 touch: function () {
446 446 this.model.save_changes(this.callbacks());
447 447 },
448 448
449 449 after_displayed: function (callback, context) {
450 450 /**
451 451 * Calls the callback right away is the view is already displayed
452 452 * otherwise, register the callback to the 'displayed' event.
453 453 */
454 454 if (this.is_displayed) {
455 455 callback.apply(context);
456 456 } else {
457 457 this.on('displayed', callback, context);
458 458 }
459 459 },
460 460
461 461 remove: function () {
462 462 // Raise a remove event when the view is removed.
463 463 WidgetView.__super__.remove.apply(this, arguments);
464 464 this.trigger('remove');
465 465 }
466 466 });
467 467
468 468
469 469 var DOMWidgetView = WidgetView.extend({
470 470 initialize: function (parameters) {
471 471 /**
472 472 * Public constructor
473 473 */
474 474 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
475 475 this.on('displayed', this.show, this);
476 476 this.model.on('change:visible', this.update_visible, this);
477 477 this.model.on('change:_css', this.update_css, this);
478 478
479 479 this.model.on('change:_dom_classes', function(model, new_classes) {
480 480 var old_classes = model.previous('_dom_classes');
481 481 this.update_classes(old_classes, new_classes);
482 482 }, this);
483 483
484 484 this.model.on('change:color', function (model, value) {
485 485 this.update_attr('color', value); }, this);
486 486
487 487 this.model.on('change:background_color', function (model, value) {
488 488 this.update_attr('background', value); }, this);
489 489
490 490 this.model.on('change:width', function (model, value) {
491 491 this.update_attr('width', value); }, this);
492 492
493 493 this.model.on('change:height', function (model, value) {
494 494 this.update_attr('height', value); }, this);
495 495
496 496 this.model.on('change:border_color', function (model, value) {
497 497 this.update_attr('border-color', value); }, this);
498 498
499 499 this.model.on('change:border_width', function (model, value) {
500 500 this.update_attr('border-width', value); }, this);
501 501
502 502 this.model.on('change:border_style', function (model, value) {
503 503 this.update_attr('border-style', value); }, this);
504 504
505 505 this.model.on('change:font_style', function (model, value) {
506 506 this.update_attr('font-style', value); }, this);
507 507
508 508 this.model.on('change:font_weight', function (model, value) {
509 509 this.update_attr('font-weight', value); }, this);
510 510
511 511 this.model.on('change:font_size', function (model, value) {
512 512 this.update_attr('font-size', this._default_px(value)); }, this);
513 513
514 514 this.model.on('change:font_family', function (model, value) {
515 515 this.update_attr('font-family', value); }, this);
516 516
517 517 this.model.on('change:padding', function (model, value) {
518 518 this.update_attr('padding', value); }, this);
519 519
520 520 this.model.on('change:margin', function (model, value) {
521 521 this.update_attr('margin', this._default_px(value)); }, this);
522 522
523 523 this.model.on('change:border_radius', function (model, value) {
524 524 this.update_attr('border-radius', this._default_px(value)); }, this);
525 525
526 526 this.after_displayed(function() {
527 527 this.update_visible(this.model, this.model.get("visible"));
528 528 this.update_classes([], this.model.get('_dom_classes'));
529 529
530 530 this.update_attr('color', this.model.get('color'));
531 531 this.update_attr('background', this.model.get('background_color'));
532 532 this.update_attr('width', this.model.get('width'));
533 533 this.update_attr('height', this.model.get('height'));
534 534 this.update_attr('border-color', this.model.get('border_color'));
535 535 this.update_attr('border-width', this.model.get('border_width'));
536 536 this.update_attr('border-style', this.model.get('border_style'));
537 537 this.update_attr('font-style', this.model.get('font_style'));
538 538 this.update_attr('font-weight', this.model.get('font_weight'));
539 539 this.update_attr('font-size', this.model.get('font_size'));
540 540 this.update_attr('font-family', this.model.get('font_family'));
541 541 this.update_attr('padding', this.model.get('padding'));
542 542 this.update_attr('margin', this.model.get('margin'));
543 543 this.update_attr('border-radius', this.model.get('border_radius'));
544 544
545 545 this.update_css(this.model, this.model.get("_css"));
546 546 }, this);
547 547 },
548 548
549 549 _default_px: function(value) {
550 550 /**
551 551 * Makes browser interpret a numerical string as a pixel value.
552 552 */
553 553 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
554 554 return value.trim() + 'px';
555 555 }
556 556 return value;
557 557 },
558 558
559 559 update_attr: function(name, value) {
560 560 /**
561 561 * Set a css attr of the widget view.
562 562 */
563 563 this.$el.css(name, value);
564 564 },
565 565
566 566 update_visible: function(model, value) {
567 567 /**
568 568 * Update visibility
569 569 */
570 570 this.$el.toggle(value);
571 571 },
572 572
573 573 update_css: function (model, css) {
574 574 /**
575 575 * Update the css styling of this view.
576 576 */
577 577 var e = this.$el;
578 578 if (css === undefined) {return;}
579 579 for (var i = 0; i < css.length; i++) {
580 580 // Apply the css traits to all elements that match the selector.
581 581 var selector = css[i][0];
582 582 var elements = this._get_selector_element(selector);
583 583 if (elements.length > 0) {
584 584 var trait_key = css[i][1];
585 585 var trait_value = css[i][2];
586 586 elements.css(trait_key ,trait_value);
587 587 }
588 588 }
589 589 },
590 590
591 591 update_classes: function (old_classes, new_classes, $el) {
592 592 /**
593 593 * Update the DOM classes applied to an element, default to this.$el.
594 594 */
595 595 if ($el===undefined) {
596 596 $el = this.$el;
597 597 }
598 598 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
599 599 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
600 600 },
601 601
602 602 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
603 603 /**
604 604 * Update the DOM classes applied to the widget based on a single
605 605 * trait's value.
606 606 *
607 607 * Given a trait value classes map, this function automatically
608 608 * handles applying the appropriate classes to the widget element
609 609 * and removing classes that are no longer valid.
610 610 *
611 611 * Parameters
612 612 * ----------
613 613 * class_map: dictionary
614 614 * Dictionary of trait values to class lists.
615 615 * Example:
616 616 * {
617 617 * success: ['alert', 'alert-success'],
618 618 * info: ['alert', 'alert-info'],
619 619 * warning: ['alert', 'alert-warning'],
620 620 * danger: ['alert', 'alert-danger']
621 621 * };
622 622 * trait_name: string
623 623 * Name of the trait to check the value of.
624 624 * previous_trait_value: optional string, default ''
625 625 * Last trait value
626 626 * $el: optional jQuery element handle, defaults to this.$el
627 627 * Element that the classes are applied to.
628 628 */
629 629 var key = previous_trait_value;
630 630 if (key === undefined) {
631 631 key = this.model.previous(trait_name);
632 632 }
633 633 var old_classes = class_map[key] ? class_map[key] : [];
634 634 key = this.model.get(trait_name);
635 635 var new_classes = class_map[key] ? class_map[key] : [];
636 636
637 637 this.update_classes(old_classes, new_classes, $el || this.$el);
638 638 },
639 639
640 640 _get_selector_element: function (selector) {
641 641 /**
642 642 * Get the elements via the css selector.
643 643 */
644 644 var elements;
645 645 if (!selector) {
646 646 elements = this.$el;
647 647 } else {
648 648 elements = this.$el.find(selector).addBack(selector);
649 649 }
650 650 return elements;
651 651 },
652 652
653 653 typeset: function(element, text){
654 654 utils.typeset.apply(null, arguments);
655 655 },
656 656 });
657 657
658 658
659 659 var ViewList = function(create_view, remove_view, context) {
660 660 /**
661 661 * - create_view and remove_view are default functions called when adding or removing views
662 662 * - create_view takes a model and returns a view or a promise for a view for that model
663 663 * - remove_view takes a view and destroys it (including calling `view.remove()`)
664 664 * - each time the update() function is called with a new list, the create and remove
665 665 * callbacks will be called in an order so that if you append the views created in the
666 666 * create callback and remove the views in the remove callback, you will duplicate
667 667 * the order of the list.
668 668 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
669 669 * - the context defaults to the created ViewList. If you pass another context, the create and remove
670 670 * will be called in that context.
671 671 */
672 672
673 673 this.initialize.apply(this, arguments);
674 674 };
675 675
676 676 _.extend(ViewList.prototype, {
677 677 initialize: function(create_view, remove_view, context) {
678 678 this.state_change = Promise.resolve();
679 679 this._handler_context = context || this;
680 680 this._models = [];
681 681 this.views = [];
682 682 this._create_view = create_view;
683 683 this._remove_view = remove_view || function(view) {view.remove();};
684 684 },
685 685
686 686 update: function(new_models, create_view, remove_view, context) {
687 687 /**
688 688 * the create_view, remove_view, and context arguments override the defaults
689 689 * specified when the list is created.
690 690 * returns a promise that resolves after this update is done
691 691 */
692 692 var remove = remove_view || this._remove_view;
693 693 var create = create_view || this._create_view;
694 694 if (create === undefined || remove === undefined){
695 695 console.error("Must define a create a remove function");
696 696 }
697 697 var context = context || this._handler_context;
698 698 var added_views = [];
699 699 var that = this;
700 700 this.state_change = this.state_change.then(function() {
701 701 var i;
702 702 // first, skip past the beginning of the lists if they are identical
703 703 for (i = 0; i < new_models.length; i++) {
704 704 if (i >= that._models.length || new_models[i] !== that._models[i]) {
705 705 break;
706 706 }
707 707 }
708 708 var first_removed = i;
709 709 // Remove the non-matching items from the old list.
710 710 for (var j = first_removed; j < that._models.length; j++) {
711 711 remove.call(context, that.views[j]);
712 712 }
713 713
714 714 // Add the rest of the new list items.
715 715 for (; i < new_models.length; i++) {
716 716 added_views.push(create.call(context, new_models[i]));
717 717 }
718 718 // make a copy of the input array
719 719 that._models = new_models.slice();
720 720 return Promise.all(added_views).then(function(added) {
721 721 Array.prototype.splice.apply(that.views, [first_removed, that.views.length].concat(added));
722 722 return that.views;
723 723 });
724 724 });
725 725 return this.state_change;
726 726 },
727 727
728 728 remove: function() {
729 729 /**
730 730 * removes every view in the list; convenience function for `.update([])`
731 731 * that should be faster
732 732 * returns a promise that resolves after this removal is done
733 733 */
734 734 var that = this;
735 735 this.state_change = this.state_change.then(function() {
736 736 for (var i = 0; i < that.views.length; i++) {
737 737 that._remove_view.call(that._handler_context, that.views[i]);
738 738 }
739 739 that._models = [];
740 740 that.views = [];
741 741 });
742 742 return this.state_change;
743 743 },
744 744 });
745 745
746 746 var widget = {
747 747 'WidgetModel': WidgetModel,
748 748 'WidgetView': WidgetView,
749 749 'DOMWidgetView': DOMWidgetView,
750 750 'ViewList': ViewList,
751 751 };
752 752
753 753 // For backwards compatability.
754 754 $.extend(IPython, widget);
755 755
756 756 return widget;
757 757 });
General Comments 0
You need to be logged in to leave comments. Login now