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