##// END OF EJS Templates
Document set_state_callbacks and,...
Jonathan Frederic -
Show More
@@ -1,431 +1,446 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/utils",
9 9 "base/js/namespace",
10 10 "services/kernels/comm"
11 11 ], function (_, Backbone, $, utils, IPython, comm) {
12 12 "use strict";
13 13 //--------------------------------------------------------------------
14 14 // WidgetManager class
15 15 //--------------------------------------------------------------------
16 16 var WidgetManager = function (comm_manager, notebook) {
17 17 /**
18 18 * Public constructor
19 19 */
20 20 WidgetManager._managers.push(this);
21 21
22 22 // Attach a comm manager to the
23 23 this.keyboard_manager = notebook.keyboard_manager;
24 24 this.notebook = notebook;
25 25 this.comm_manager = comm_manager;
26 26 this.comm_target_name = 'ipython.widget';
27 27 this._models = {}; /* Dictionary of model ids and model instance promises */
28 28
29 29 // Register with the comm manager.
30 30 this.comm_manager.register_target(this.comm_target_name, $.proxy(this._handle_comm_open, this));
31 31
32 32 // Load the initial state of the widget manager if a load callback was
33 33 // registered.
34 var that = this;
34 35 if (WidgetManager._load_callback) {
35 this.set_state(WidgetManager._load_callback.call(this));
36 Promise.resolve(WidgetManager._load_callback.call(this)).then(function(state) {
37 that.set_state(state);
38 }).catch(utils.reject('Error loading widget manager state', true));
36 39 }
37 40
38 41 // Setup state saving code.
39 var that = this;
40 42 this.notebook.events.on('before_save.Notebook', function() {
41 43 var save_callback = WidgetManager._save_callback;
42 44 var options = WidgetManager._get_state_options;
43 45 if (save_callback) {
44 46 that.get_state(options).then(function(state) {
45 47 save_callback.call(that, state);
46 48 }).catch(utils.reject('Could not call widget save state callback.', true));
47 49 }
48 50 });
49 51 };
50 52
51 53 //--------------------------------------------------------------------
52 54 // Class level
53 55 //--------------------------------------------------------------------
54 56 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
55 57 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
56 58 WidgetManager._managers = []; /* List of widget managers */
57 59 WidgetManager._load_callback = null;
58 60 WidgetManager._save_callback = null;
59 61
60 62 WidgetManager.register_widget_model = function (model_name, model_type) {
61 63 // Registers a widget model by name.
62 64 WidgetManager._model_types[model_name] = model_type;
63 65 };
64 66
65 67 WidgetManager.register_widget_view = function (view_name, view_type) {
66 68 // Registers a widget view by name.
67 69 WidgetManager._view_types[view_name] = view_type;
68 70 };
69 71
70 72 WidgetManager.set_state_callbacks = function (load_callback, save_callback, options) {
71 73 // Registers callbacks for widget state persistence.
74 //
75 // Parameters
76 // ----------
77 // load_callback: function()
78 // function that is called when the widget manager state should be
79 // loaded. This function should return a promise for the widget
80 // manager state. An empty state is an empty dictionary `{}`.
81 // save_callback: function(state as dictionary)
82 // function that is called when the notebook is saved or autosaved.
83 // The current state of the widget manager is passed in as the first
84 // argument.
72 85 WidgetManager._load_callback = load_callback;
73 86 WidgetManager._save_callback = save_callback;
74 87 WidgetManager._get_state_options = options;
75 88
76 89 // Use the load callback to immediately load widget states.
77 90 WidgetManager._managers.forEach(function(manager) {
78 91 if (load_callback) {
79 manager.set_state(load_callback.call(manager));
92 Promise.resolve(load_callback.call(manager)).then(function(state) {
93 manager.set_state(state);
94 }).catch(utils.reject('Error loading widget manager state', true));
80 95 }
81 96 });
82 97 };
83 98
84 99 //--------------------------------------------------------------------
85 100 // Instance level
86 101 //--------------------------------------------------------------------
87 102 WidgetManager.prototype.display_view = function(msg, model) {
88 103 /**
89 104 * Displays a view for a particular model.
90 105 */
91 106 var cell = this.get_msg_cell(msg.parent_header.msg_id);
92 107 if (cell === null) {
93 108 return Promise.reject(new Error("Could not determine where the display" +
94 109 " message was from. Widget will not be displayed"));
95 110 } else {
96 111 return this.display_view_in_cell(cell, model)
97 112 .catch(utils.reject('Could not display view', true));
98 113 }
99 114 };
100 115
101 116 WidgetManager.prototype.display_view_in_cell = function(cell, model) {
102 117 // Displays a view in a cell.
103 118 if (cell.display_widget_view) {
104 119 var that = this;
105 120 return cell.display_widget_view(this.create_view(model, {
106 121 cell: cell,
107 122 // Only set cell_index when view is displayed as directly.
108 123 cell_index: that.notebook.find_cell_index(cell),
109 124 })).then(function(view) {
110 125 that._handle_display_view(view);
111 126 view.trigger('displayed');
112 127 resolve(view);
113 128 }).catch(utils.reject('Could not create or display view', true));
114 129 } else {
115 130 return Promise.reject(new Error('Cell does not have a `display_widget_view` method'));
116 131 }
117 132 };
118 133
119 134 WidgetManager.prototype._handle_display_view = function (view) {
120 135 /**
121 136 * Have the IPython keyboard manager disable its event
122 137 * handling so the widget can capture keyboard input.
123 138 * Note, this is only done on the outer most widgets.
124 139 */
125 140 if (this.keyboard_manager) {
126 141 this.keyboard_manager.register_events(view.$el);
127 142
128 143 if (view.additional_elements) {
129 144 for (var i = 0; i < view.additional_elements.length; i++) {
130 145 this.keyboard_manager.register_events(view.additional_elements[i]);
131 146 }
132 147 }
133 148 }
134 149 };
135 150
136 151 WidgetManager.prototype.create_view = function(model, options) {
137 152 /**
138 153 * Creates a promise for a view of a given model
139 154 *
140 155 * Make sure the view creation is not out of order with
141 156 * any state updates.
142 157 */
143 158 model.state_change = model.state_change.then(function() {
144 159
145 160 return utils.load_class(model.get('_view_name'), model.get('_view_module'),
146 161 WidgetManager._view_types).then(function(ViewType) {
147 162
148 163 // If a view is passed into the method, use that view's cell as
149 164 // the cell for the view that is created.
150 165 options = options || {};
151 166 if (options.parent !== undefined) {
152 167 options.cell = options.parent.options.cell;
153 168 }
154 169 // Create and render the view...
155 170 var parameters = {model: model, options: options};
156 171 var view = new ViewType(parameters);
157 172 view.listenTo(model, 'destroy', view.remove);
158 173 return Promise.resolve(view.render()).then(function() {return view;});
159 174 }).catch(utils.reject("Couldn't create a view for model id '" + String(model.id) + "'", true));
160 175 });
161 176 model.views[utils.uuid()] = model.state_change;
162 177 return model.state_change;
163 178 };
164 179
165 180 WidgetManager.prototype.get_msg_cell = function (msg_id) {
166 181 var cell = null;
167 182 // First, check to see if the msg was triggered by cell execution.
168 183 if (this.notebook) {
169 184 cell = this.notebook.get_msg_cell(msg_id);
170 185 }
171 186 if (cell !== null) {
172 187 return cell;
173 188 }
174 189 // Second, check to see if a get_cell callback was defined
175 190 // for the message. get_cell callbacks are registered for
176 191 // widget messages, so this block is actually checking to see if the
177 192 // message was triggered by a widget.
178 193 var kernel = this.comm_manager.kernel;
179 194 if (kernel) {
180 195 var callbacks = kernel.get_callbacks_for_msg(msg_id);
181 196 if (callbacks && callbacks.iopub &&
182 197 callbacks.iopub.get_cell !== undefined) {
183 198 return callbacks.iopub.get_cell();
184 199 }
185 200 }
186 201
187 202 // Not triggered by a cell or widget (no get_cell callback
188 203 // exists).
189 204 return null;
190 205 };
191 206
192 207 WidgetManager.prototype.callbacks = function (view) {
193 208 /**
194 209 * callback handlers specific a view
195 210 */
196 211 var callbacks = {};
197 212 if (view && view.options.cell) {
198 213
199 214 // Try to get output handlers
200 215 var cell = view.options.cell;
201 216 var handle_output = null;
202 217 var handle_clear_output = null;
203 218 if (cell.output_area) {
204 219 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
205 220 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
206 221 }
207 222
208 223 // Create callback dictionary using what is known
209 224 var that = this;
210 225 callbacks = {
211 226 iopub : {
212 227 output : handle_output,
213 228 clear_output : handle_clear_output,
214 229
215 230 // Special function only registered by widget messages.
216 231 // Allows us to get the cell for a message so we know
217 232 // where to add widgets if the code requires it.
218 233 get_cell : function () {
219 234 return cell;
220 235 },
221 236 },
222 237 };
223 238 }
224 239 return callbacks;
225 240 };
226 241
227 242 WidgetManager.prototype.get_model = function (model_id) {
228 243 /**
229 244 * Get a promise for a model by model id.
230 245 */
231 246 return this._models[model_id];
232 247 };
233 248
234 249 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
235 250 /**
236 251 * Handle when a comm is opened.
237 252 */
238 253 return this.create_model({
239 254 model_name: msg.content.data.model_name,
240 255 model_module: msg.content.data.model_module,
241 256 comm: comm}).catch(utils.reject("Couldn't create a model.", true));
242 257 };
243 258
244 259 WidgetManager.prototype.create_model = function (options) {
245 260 /**
246 261 * Create and return a promise for a new widget model
247 262 *
248 263 * Minimally, one must provide the model_name and widget_class
249 264 * parameters to create a model from Javascript.
250 265 *
251 266 * Example
252 267 * --------
253 268 * JS:
254 269 * IPython.notebook.kernel.widget_manager.create_model({
255 270 * model_name: 'WidgetModel',
256 271 * widget_class: 'IPython.html.widgets.widget_int.IntSlider'})
257 272 * .then(function(model) { console.log('Create success!', model); },
258 273 * $.proxy(console.error, console));
259 274 *
260 275 * Parameters
261 276 * ----------
262 277 * options: dictionary
263 278 * Dictionary of options with the following contents:
264 279 * model_name: string
265 280 * Target name of the widget model to create.
266 281 * model_module: (optional) string
267 282 * Module name of the widget model to create.
268 283 * widget_class: (optional) string
269 284 * Target name of the widget in the back-end.
270 285 * comm: (optional) Comm
271 286 *
272 287 * Create a comm if it wasn't provided.
273 288 */
274 289 var comm = options.comm;
275 290 if (!comm) {
276 291 comm = this.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class});
277 292 }
278 293
279 294 var that = this;
280 295 var model_id = comm.comm_id;
281 296 var model_promise = utils.load_class(options.model_name, options.model_module, WidgetManager._model_types)
282 297 .then(function(ModelType) {
283 298 var widget_model = new ModelType(that, model_id, comm);
284 299 widget_model.once('comm:close', function () {
285 300 delete that._models[model_id];
286 301 });
287 302 widget_model.name = options.model_name;
288 303 widget_model.module = options.model_module;
289 304 return widget_model;
290 305
291 306 }, function(error) {
292 307 delete that._models[model_id];
293 308 var wrapped_error = new utils.WrappedError("Couldn't create model", error);
294 309 return Promise.reject(wrapped_error);
295 310 });
296 311 this._models[model_id] = model_promise;
297 312 return model_promise;
298 313 };
299 314
300 315 WidgetManager.prototype.get_state = function(options) {
301 316 // Asynchronously get the state of the widget manager.
302 317 //
303 318 // This includes all of the widget models and the cells that they are
304 319 // displayed in.
305 320 //
306 321 // Parameters
307 322 // ----------
308 323 // options: dictionary
309 324 // Dictionary of options with the following contents:
310 325 // only_displayed: (optional) boolean=false
311 326 // Only return models with one or more displayed views.
312 327 // not_live: (optional) boolean=false
313 328 // Include models that have comms with severed connections.
314 329 //
315 330 // Returns
316 331 // -------
317 332 // Promise for a state dictionary
318 333 var that = this;
319 334 return utils.resolve_promises_dict(this._models).then(function(models) {
320 335 var state = {};
321 336 for (var model_id in models) {
322 337 if (models.hasOwnProperty(model_id)) {
323 338 var model = models[model_id];
324 339
325 340 // If the model has one or more views defined for it,
326 341 // consider it displayed.
327 342 var displayed_flag = !(options && options.only_displayed) || Object.keys(model.views).length > 0;
328 343 var live_flag = (options && options.not_live) || model.comm_live;
329 344 if (displayed_flag && live_flag) {
330 345 state[model_id] = {
331 346 model_name: model.name,
332 347 model_module: model.module,
333 348 state: model.get_state(),
334 349 views: [],
335 350 };
336 351
337 352 // Get the views that are displayed *now*.
338 353 for (var id in model.views) {
339 354 if (model.views.hasOwnProperty(id)) {
340 355 var view = model.views[id];
341 356 if (view.options.cell_index) {
342 357 state[model_id].views.push(view.options.cell_index);
343 358 }
344 359 }
345 360 }
346 361 }
347 362 }
348 363 }
349 364 return state;
350 365 }).catch(utils.reject('Could not get state of widget manager', true));
351 366 };
352 367
353 368 WidgetManager.prototype.set_state = function(state) {
354 369 // Set the notebook's state.
355 370 //
356 371 // Reconstructs all of the widget models and attempts to redisplay the
357 372 // widgets in the appropriate cells by cell index.
358 373
359 374 // Get the kernel when it's available.
360 375 var that = this;
361 376 return this._get_connected_kernel().then(function(kernel) {
362 377
363 378 // Recreate all the widget models for the given state and
364 379 // display the views.
365 380 that.all_views = [];
366 381 var model_ids = Object.keys(state);
367 382 for (var i = 0; i < model_ids.length; i++) {
368 383 var model_id = model_ids[i];
369 384
370 385 // Recreate a comm using the widget's model id (model_id == comm_id).
371 386 var new_comm = new comm.Comm(kernel.widget_manager.comm_target_name, model_id);
372 387 kernel.comm_manager.register_comm(new_comm);
373 388
374 389 // Create the model using the recreated comm. When the model is
375 390 // created we don't know yet if the comm is valid so set_comm_live
376 391 // false. Once we receive the first state push from the back-end
377 392 // we know the comm is alive.
378 393 var views = kernel.widget_manager.create_model({
379 394 comm: new_comm,
380 395 model_name: state[model_id].model_name,
381 396 model_module: state[model_id].model_module})
382 397 .then(function(model) {
383 398
384 399 model.set_comm_live(false);
385 400 var view_promise = Promise.resolve().then(function() {
386 401 return model.set_state(state[model.id].state);
387 402 }).then(function() {
388 403 model.request_state().then(function() {
389 404 model.set_comm_live(true);
390 405 });
391 406
392 407 // Display the views of the model.
393 408 var views = [];
394 409 var model_views = state[model.id].views;
395 410 for (var j=0; j<model_views.length; j++) {
396 411 var cell_index = model_views[j];
397 412 var cell = that.notebook.get_cell(cell_index);
398 413 views.push(that.display_view_in_cell(cell, model));
399 414 }
400 415 return Promise.all(views);
401 416 });
402 417 return view_promise;
403 418 });
404 419 that.all_views.push(views);
405 420 }
406 421 return Promise.all(that.all_views);
407 422 }).catch(utils.reject('Could not set widget manager state.', true));
408 423 };
409 424
410 425 WidgetManager.prototype._get_connected_kernel = function() {
411 426 // Gets a promise for a connected kernel.
412 427 var that = this;
413 428 return new Promise(function(resolve, reject) {
414 429 if (that.comm_manager &&
415 430 that.comm_manager.kernel &&
416 431 that.comm_manager.kernel.is_connected()) {
417 432
418 433 resolve(that.comm_manager.kernel);
419 434 } else {
420 435 that.notebook.events.on('kernel_connected.Kernel', function(event, data) {
421 436 resolve(data.kernel);
422 437 });
423 438 }
424 439 });
425 440 };
426 441
427 442 // Backwards compatibility.
428 443 IPython.WidgetManager = WidgetManager;
429 444
430 445 return {'WidgetManager': WidgetManager};
431 446 });
General Comments 0
You need to be logged in to leave comments. Login now