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