Show More
@@ -101,7 +101,7 b' define([' | |||
|
101 | 101 | |
|
102 | 102 | this.last_msg_id = null; |
|
103 | 103 | this.completer = null; |
|
104 | ||
|
104 | this.widget_views = []; | |
|
105 | 105 | |
|
106 | 106 | var config = utils.mergeopt(CodeCell, this.config); |
|
107 | 107 | Cell.apply(this,[{ |
@@ -191,12 +191,19 b' define([' | |||
|
191 | 191 | .addClass('widget-subarea') |
|
192 | 192 | .appendTo(widget_area); |
|
193 | 193 | this.widget_subarea = widget_subarea; |
|
194 | var that = this; | |
|
194 | 195 | var widget_clear_buton = $('<button />') |
|
195 | 196 | .addClass('close') |
|
196 | 197 | .html('×') |
|
197 | 198 | .click(function() { |
|
198 |
widget_area.slideUp('', function(){ |
|
|
199 | }) | |
|
199 | widget_area.slideUp('', function(){ | |
|
200 | for (var i = 0; i < that.widget_views.length; i++) { | |
|
201 | that.widget_views[i].remove(); | |
|
202 | } | |
|
203 | that.widget_views = []; | |
|
204 | widget_subarea.html(''); | |
|
205 | }); | |
|
206 | }) | |
|
200 | 207 | .appendTo(widget_prompt); |
|
201 | 208 | |
|
202 | 209 | var output = $('<div></div>'); |
@@ -210,6 +217,24 b' define([' | |||
|
210 | 217 | this.completer = new completer.Completer(this, this.events); |
|
211 | 218 | }; |
|
212 | 219 | |
|
220 | /** | |
|
221 | * Display a widget view in the cell. | |
|
222 | */ | |
|
223 | CodeCell.prototype.display_widget_view = function(view_promise) { | |
|
224 | ||
|
225 | // Display a dummy element | |
|
226 | var dummy = $('<div/>'); | |
|
227 | this.widget_subarea.append(dummy); | |
|
228 | ||
|
229 | // Display the view. | |
|
230 | var that = this; | |
|
231 | return view_promise.then(function(view) { | |
|
232 | dummy.replaceWith(view.$el); | |
|
233 | this.widget_views.push(view); | |
|
234 | return view; | |
|
235 | }); | |
|
236 | }; | |
|
237 | ||
|
213 | 238 | /** @method bind_events */ |
|
214 | 239 | CodeCell.prototype.bind_events = function () { |
|
215 | 240 | Cell.prototype.bind_events.apply(this); |
@@ -322,6 +347,10 b' define([' | |||
|
322 | 347 | this.active_output_area.clear_output(); |
|
323 | 348 | |
|
324 | 349 | // Clear widget area |
|
350 | for (var i = 0; i < this.widget_views.length; i++) { | |
|
351 | this.widget_views[i].remove(); | |
|
352 | } | |
|
353 | this.widget_views = []; | |
|
325 | 354 | this.widget_subarea.html(''); |
|
326 | 355 | this.widget_subarea.height(''); |
|
327 | 356 | this.widget_area.height(''); |
@@ -291,6 +291,13 b' define([' | |||
|
291 | 291 | // Firefox 22 broke $(window).on("beforeunload") |
|
292 | 292 | // I'm not sure why or how. |
|
293 | 293 | window.onbeforeunload = function (e) { |
|
294 | // Raise an event that allows the user to execute custom code on unload | |
|
295 | try { | |
|
296 | that.events.trigger('beforeunload.Notebook', {notebook: that}); | |
|
297 | } catch(e) { | |
|
298 | console.err('Error in "beforeunload.Notebook" event handler.', e); | |
|
299 | } | |
|
300 | ||
|
294 | 301 | // TODO: Make killing the kernel configurable. |
|
295 | 302 | var kill_kernel = false; |
|
296 | 303 | if (kill_kernel) { |
@@ -7,7 +7,8 b' define([' | |||
|
7 | 7 | "jquery", |
|
8 | 8 | "base/js/utils", |
|
9 | 9 | "base/js/namespace", |
|
10 | ], function (_, Backbone, $, utils, IPython) { | |
|
10 | "services/kernels/comm" | |
|
11 | ], function (_, Backbone, $, utils, IPython, comm) { | |
|
11 | 12 | "use strict"; |
|
12 | 13 | //-------------------------------------------------------------------- |
|
13 | 14 | // WidgetManager class |
@@ -22,10 +23,11 b' define([' | |||
|
22 | 23 | this.keyboard_manager = notebook.keyboard_manager; |
|
23 | 24 | this.notebook = notebook; |
|
24 | 25 | this.comm_manager = comm_manager; |
|
26 | this.comm_target_name = 'ipython.widget'; | |
|
25 | 27 | this._models = {}; /* Dictionary of model ids and model instances */ |
|
26 | 28 | |
|
27 | 29 | // Register with the comm manager. |
|
28 |
this.comm_manager.register_target( |
|
|
30 | this.comm_manager.register_target(this.comm_target_name, $.proxy(this._handle_comm_open, this)); | |
|
29 | 31 | }; |
|
30 | 32 | |
|
31 | 33 | //-------------------------------------------------------------------- |
@@ -53,21 +55,34 b' define([' | |||
|
53 | 55 | * Displays a view for a particular model. |
|
54 | 56 | */ |
|
55 | 57 | var that = this; |
|
56 | var cell = this.get_msg_cell(msg.parent_header.msg_id); | |
|
57 | if (cell === null) { | |
|
58 | return Promise.reject(new Error("Could not determine where the display" + | |
|
59 | " message was from. Widget will not be displayed")); | |
|
60 | } else if (cell.widget_subarea) { | |
|
61 | var dummy = $('<div />'); | |
|
62 | cell.widget_subarea.append(dummy); | |
|
63 | return this.create_view(model, {cell: cell}).then( | |
|
64 | function(view) { | |
|
58 | return new Promise(function(resolve, reject) { | |
|
59 | var cell = that.get_msg_cell(msg.parent_header.msg_id); | |
|
60 | if (cell === null) { | |
|
61 | reject(new Error("Could not determine where the display" + | |
|
62 | " message was from. Widget will not be displayed")); | |
|
63 | } else { | |
|
64 | return that.display_view_in_cell(cell, model); | |
|
65 | } | |
|
66 | }); | |
|
67 | }; | |
|
68 | ||
|
69 | WidgetManager.prototype.display_view_in_cell = function(cell, model) { | |
|
70 | // Displays a view in a cell. | |
|
71 | return new Promise(function(resolve, reject) { | |
|
72 | if (cell.display_widget_view) { | |
|
73 | cell.display_widget_view(that.create_view(model, {cell: cell})) | |
|
74 | .then(function(view) { | |
|
75 | ||
|
65 | 76 | that._handle_display_view(view); |
|
66 | dummy.replaceWith(view.$el); | |
|
67 | 77 | view.trigger('displayed'); |
|
68 |
re |
|
|
69 | }).catch(utils.reject('Could not display view', true)); | |
|
70 | } | |
|
78 | resolve(view); | |
|
79 | }, function(error) { | |
|
80 | reject(new utils.WrappedError('Could not display view', error)); | |
|
81 | }); | |
|
82 | } else { | |
|
83 | reject(new Error('Cell does not have a `display_widget_view` method.')); | |
|
84 | } | |
|
85 | }); | |
|
71 | 86 | }; |
|
72 | 87 | |
|
73 | 88 | WidgetManager.prototype._handle_display_view = function (view) { |
@@ -238,6 +253,8 b' define([' | |||
|
238 | 253 | widget_model.once('comm:close', function () { |
|
239 | 254 | delete that._models[model_id]; |
|
240 | 255 | }); |
|
256 | widget_model.name = options.model_name; | |
|
257 | widget_model.module = options.model_module; | |
|
241 | 258 | return widget_model; |
|
242 | 259 | |
|
243 | 260 | }, function(error) { |
@@ -249,6 +266,100 b' define([' | |||
|
249 | 266 | return model_promise; |
|
250 | 267 | }; |
|
251 | 268 | |
|
269 | WidgetManager.prototype.get_state = function(options) { | |
|
270 | // Get the state of the widget manager. | |
|
271 | // | |
|
272 | // This includes all of the widget models and the cells that they are | |
|
273 | // displayed in. | |
|
274 | // | |
|
275 | // Parameters | |
|
276 | // ---------- | |
|
277 | // options: dictionary | |
|
278 | // Dictionary of options with the following contents: | |
|
279 | // only_displayed: (optional) boolean=false | |
|
280 | // Only return models with one or more displayed views. | |
|
281 | // not_alive: (optional) boolean=false | |
|
282 | // Include models that have comms with severed connections. | |
|
283 | return utils.resolve_promise_dict(function(models) { | |
|
284 | var state = {}; | |
|
285 | for (var model_id in models) { | |
|
286 | if (models.hasOwnProperty(model_id)) { | |
|
287 | var model = models[model_id]; | |
|
288 | ||
|
289 | // If the model has one or more views defined for it, | |
|
290 | // consider it displayed. | |
|
291 | var displayed_flag = !(options && options.only_displayed) || Object.keys(model.views).length > 0; | |
|
292 | var alive_flag = (options && options.not_alive) || model.comm_alive; | |
|
293 | if (displayed_flag && alive_flag) { | |
|
294 | state[model.model_id] = { | |
|
295 | model_name: model.name, | |
|
296 | model_module: model.module, | |
|
297 | views: [], | |
|
298 | }; | |
|
299 | ||
|
300 | // Get the views that are displayed *now*. | |
|
301 | for (var id in model.views) { | |
|
302 | if (model.views.hasOwnProperty(id)) { | |
|
303 | var view = model.views[id]; | |
|
304 | var cell_index = this.notebook.find_cell_index(view.options.cell); | |
|
305 | state[model.model_id].views.push(cell_index); | |
|
306 | } | |
|
307 | } | |
|
308 | } | |
|
309 | } | |
|
310 | } | |
|
311 | return state; | |
|
312 | }); | |
|
313 | }; | |
|
314 | ||
|
315 | WidgetManager.prototype.set_state = function(state) { | |
|
316 | // Set the notebook's state. | |
|
317 | // | |
|
318 | // Reconstructs all of the widget models and attempts to redisplay the | |
|
319 | // widgets in the appropriate cells by cell index. | |
|
320 | ||
|
321 | // Get the kernel when it's available. | |
|
322 | var that = this; | |
|
323 | return (new Promise(function(resolve, reject) { | |
|
324 | if (that.kernel) { | |
|
325 | resolve(that.kernel); | |
|
326 | } else { | |
|
327 | that.events.on('kernel_created.Session', function(event, data) { | |
|
328 | resolve(data.kernel); | |
|
329 | }); | |
|
330 | } | |
|
331 | })).then(function(kernel) { | |
|
332 | ||
|
333 | // Recreate all the widget models for the given state. | |
|
334 | that.widget_models = []; | |
|
335 | for (var i = 0; i < state.length; i++) { | |
|
336 | // Recreate a comm using the widget's model id (model_id == comm_id). | |
|
337 | var new_comm = new comm.Comm(kernel.widget_manager.comm_target_name, state[i].model_id); | |
|
338 | kernel.comm_manager.register_comm(new_comm); | |
|
339 | ||
|
340 | // Create the model using the recreated comm. When the model is | |
|
341 | // created we don't know yet if the comm is valid so set_comm_alive | |
|
342 | // false. Once we receive the first state push from the back-end | |
|
343 | // we know the comm is alive. | |
|
344 | var model = kernel.widget_manager.create_model({ | |
|
345 | comm: new_comm, | |
|
346 | model_name: state[i].model_name, | |
|
347 | model_module: state[i].model_module}).then(function(model) { | |
|
348 | model.set_comm_alive(false); | |
|
349 | model.request_state(); | |
|
350 | model.received_state.then(function() { | |
|
351 | model.set_comm_alive(true); | |
|
352 | }); | |
|
353 | return model; | |
|
354 | }); | |
|
355 | that.widget_models.push(model); | |
|
356 | } | |
|
357 | return Promise.all(that.widget_models); | |
|
358 | ||
|
359 | }); | |
|
360 | ||
|
361 | }; | |
|
362 | ||
|
252 | 363 | // Backwards compatibility. |
|
253 | 364 | IPython.WidgetManager = WidgetManager; |
|
254 | 365 |
@@ -32,6 +32,13 b' define(["widgets/js/manager",' | |||
|
32 | 32 | this.id = model_id; |
|
33 | 33 | this.views = {}; |
|
34 | 34 | |
|
35 | // Promise that is resolved when a state is received | |
|
36 | // from the back-end. | |
|
37 | var that = this; | |
|
38 | this.received_state = new Promise(function(resolve) { | |
|
39 | that._resolve_received_state = resolve; | |
|
40 | }); | |
|
41 | ||
|
35 | 42 | if (comm !== undefined) { |
|
36 | 43 | // Remember comm associated with the model. |
|
37 | 44 | this.comm = comm; |
@@ -40,6 +47,11 b' define(["widgets/js/manager",' | |||
|
40 | 47 | // Hook comm messages up to model. |
|
41 | 48 | comm.on_close($.proxy(this._handle_comm_closed, this)); |
|
42 | 49 | comm.on_msg($.proxy(this._handle_comm_msg, this)); |
|
50 | ||
|
51 | // Assume the comm is alive. | |
|
52 | this.set_comm_alive(true); | |
|
53 | } else { | |
|
54 | this.set_comm_alive(false); | |
|
43 | 55 | } |
|
44 | 56 | return Backbone.Model.apply(this); |
|
45 | 57 | }, |
@@ -55,11 +67,34 b' define(["widgets/js/manager",' | |||
|
55 | 67 | } |
|
56 | 68 | }, |
|
57 | 69 | |
|
58 | _handle_comm_closed: function (msg) { | |
|
70 | request_state: function(callbacks) { | |
|
71 | /** | |
|
72 | * Request a state push from the back-end. | |
|
73 | */ | |
|
74 | if (!this.comm) { | |
|
75 | console.error("Could not request_state because comm doesn't exist!"); | |
|
76 | return; | |
|
77 | } | |
|
78 | this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks()); | |
|
79 | }, | |
|
80 | ||
|
81 | set_comm_alive: function(alive) { | |
|
82 | /** | |
|
83 | * Change the comm_alive state of the model. | |
|
84 | */ | |
|
85 | if (this.comm_alive === undefined || this.comm_alive != alive) { | |
|
86 | this.comm_alive = alive; | |
|
87 | this.trigger(alive ? 'comm_is_live' : 'comm_is_dead', {model: this}); | |
|
88 | } | |
|
89 | }, | |
|
90 | ||
|
91 | close: function(comm_closed) { | |
|
59 | 92 | /** |
|
60 | * Handle when a widget is closed. | |
|
93 | * Close model | |
|
61 | 94 | */ |
|
62 |
this. |
|
|
95 | if (this.comm && !comm_closed) { | |
|
96 | this.comm.close(); | |
|
97 | } | |
|
63 | 98 | this.stopListening(); |
|
64 | 99 | this.trigger('destroy', this); |
|
65 | 100 | delete this.comm.model; // Delete ref so GC will collect widget model. |
@@ -73,6 +108,14 b' define(["widgets/js/manager",' | |||
|
73 | 108 | }); |
|
74 | 109 | }, |
|
75 | 110 | |
|
111 | _handle_comm_closed: function (msg) { | |
|
112 | /** | |
|
113 | * Handle when a widget is closed. | |
|
114 | */ | |
|
115 | this.trigger('comm:close'); | |
|
116 | this.close(true); | |
|
117 | }, | |
|
118 | ||
|
76 | 119 | _handle_comm_msg: function (msg) { |
|
77 | 120 | /** |
|
78 | 121 | * Handle incoming comm msg. |
@@ -104,7 +147,20 b' define(["widgets/js/manager",' | |||
|
104 | 147 | } finally { |
|
105 | 148 | that.state_lock = null; |
|
106 | 149 | } |
|
107 | }).catch(utils.reject("Couldn't set model state", true)); | |
|
150 | that._resolve_received_state(); | |
|
151 | return Promise.resolve(); | |
|
152 | }, utils.reject("Couldn't set model state", true)); | |
|
153 | }, | |
|
154 | ||
|
155 | get_state: function() { | |
|
156 | // Get the serializable state of the model. | |
|
157 | state = this.toJSON(); | |
|
158 | for (var key in state) { | |
|
159 | if (state.hasOwnProperty(key)) { | |
|
160 | state[key] = this._pack_models(state[key]); | |
|
161 | } | |
|
162 | } | |
|
163 | return state; | |
|
108 | 164 | }, |
|
109 | 165 | |
|
110 | 166 | _handle_status: function (msg, callbacks) { |
@@ -322,6 +378,9 b' define(["widgets/js/manager",' | |||
|
322 | 378 | this.on('displayed', function() { |
|
323 | 379 | this.is_displayed = true; |
|
324 | 380 | }, this); |
|
381 | this.on('remove', function() { | |
|
382 | delete this.model.views[this.id]; | |
|
383 | }, this); | |
|
325 | 384 | }, |
|
326 | 385 | |
|
327 | 386 | update: function(){ |
@@ -387,6 +446,12 b' define(["widgets/js/manager",' | |||
|
387 | 446 | } else { |
|
388 | 447 | this.on('displayed', callback, context); |
|
389 | 448 | } |
|
449 | }, | |
|
450 | ||
|
451 | remove: function () { | |
|
452 | // Raise a remove event when the view is removed. | |
|
453 | WidgetView.__super__.remove.apply(this, arguments); | |
|
454 | this.trigger('remove'); | |
|
390 | 455 | } |
|
391 | 456 | }); |
|
392 | 457 |
@@ -341,19 +341,26 b' class Widget(LoggingConfigurable):' | |||
|
341 | 341 | """Called when a msg is received from the front-end""" |
|
342 | 342 | data = msg['content']['data'] |
|
343 | 343 | method = data['method'] |
|
344 | if not method in ['backbone', 'custom']: | |
|
345 | self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) | |
|
346 | 344 | |
|
347 | 345 | # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one. |
|
348 |
if method == 'backbone' |
|
|
349 |
|
|
|
350 | self.set_state(sync_data) # handles all methods | |
|
346 | if method == 'backbone': | |
|
347 | if 'sync_data' in data: | |
|
348 | sync_data = data['sync_data'] | |
|
349 | self.set_state(sync_data) # handles all methods | |
|
350 | ||
|
351 | # Handle a state request. | |
|
352 | elif method == 'request_state': | |
|
353 | self.send_state() | |
|
351 | 354 | |
|
352 | # Handle a custom msg from the front-end | |
|
355 | # Handle a custom msg from the front-end. | |
|
353 | 356 | elif method == 'custom': |
|
354 | 357 | if 'content' in data: |
|
355 | 358 | self._handle_custom_msg(data['content']) |
|
356 | 359 | |
|
360 | # Catch remainder. | |
|
361 | else: | |
|
362 | self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) | |
|
363 | ||
|
357 | 364 | def _handle_custom_msg(self, content): |
|
358 | 365 | """Called when a custom msg is received.""" |
|
359 | 366 | self._msg_callbacks(self, content) |
@@ -368,7 +375,7 b' class Widget(LoggingConfigurable):' | |||
|
368 | 375 | # Send the state after the user registered callbacks for trait changes |
|
369 | 376 | # have all fired (allows for user to validate values). |
|
370 | 377 | if self.comm is not None and name in self.keys: |
|
371 | # Make sure this isn't information that the front-end just sent us. | |
|
378 | # Make sure this isn't information that the front-end just sent us. | |
|
372 | 379 | if self._should_send_property(name, new_value): |
|
373 | 380 | # Send new state to front-end |
|
374 | 381 | self.send_state(key=name) |
General Comments 0
You need to be logged in to leave comments.
Login now