diff --git a/IPython/html/static/notebook/js/codecell.js b/IPython/html/static/notebook/js/codecell.js index d4a9e22..0e0d9f8 100644 --- a/IPython/html/static/notebook/js/codecell.js +++ b/IPython/html/static/notebook/js/codecell.js @@ -101,7 +101,7 @@ define([ this.last_msg_id = null; this.completer = null; - + this.widget_views = []; var config = utils.mergeopt(CodeCell, this.config); Cell.apply(this,[{ @@ -191,12 +191,19 @@ define([ .addClass('widget-subarea') .appendTo(widget_area); this.widget_subarea = widget_subarea; + var that = this; var widget_clear_buton = $('') .addClass('close') .html('×') .click(function() { - widget_area.slideUp('', function(){ widget_subarea.html(''); }); - }) + widget_area.slideUp('', function(){ + for (var i = 0; i < that.widget_views.length; i++) { + that.widget_views[i].remove(); + } + that.widget_views = []; + widget_subarea.html(''); + }); + }) .appendTo(widget_prompt); var output = $('
'); @@ -210,6 +217,24 @@ define([ this.completer = new completer.Completer(this, this.events); }; + /** + * Display a widget view in the cell. + */ + CodeCell.prototype.display_widget_view = function(view_promise) { + + // Display a dummy element + var dummy = $(''); + this.widget_subarea.append(dummy); + + // Display the view. + var that = this; + return view_promise.then(function(view) { + dummy.replaceWith(view.$el); + this.widget_views.push(view); + return view; + }); + }; + /** @method bind_events */ CodeCell.prototype.bind_events = function () { Cell.prototype.bind_events.apply(this); @@ -322,6 +347,10 @@ define([ this.active_output_area.clear_output(); // Clear widget area + for (var i = 0; i < this.widget_views.length; i++) { + this.widget_views[i].remove(); + } + this.widget_views = []; this.widget_subarea.html(''); this.widget_subarea.height(''); this.widget_area.height(''); diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 3fedb9a..add06ba 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -291,6 +291,13 @@ define([ // Firefox 22 broke $(window).on("beforeunload") // I'm not sure why or how. window.onbeforeunload = function (e) { + // Raise an event that allows the user to execute custom code on unload + try { + that.events.trigger('beforeunload.Notebook', {notebook: that}); + } catch(e) { + console.err('Error in "beforeunload.Notebook" event handler.', e); + } + // TODO: Make killing the kernel configurable. var kill_kernel = false; if (kill_kernel) { diff --git a/IPython/html/static/widgets/js/manager.js b/IPython/html/static/widgets/js/manager.js index f3630ab..8d9f24d 100644 --- a/IPython/html/static/widgets/js/manager.js +++ b/IPython/html/static/widgets/js/manager.js @@ -7,7 +7,8 @@ define([ "jquery", "base/js/utils", "base/js/namespace", -], function (_, Backbone, $, utils, IPython) { + "services/kernels/comm" +], function (_, Backbone, $, utils, IPython, comm) { "use strict"; //-------------------------------------------------------------------- // WidgetManager class @@ -22,10 +23,11 @@ define([ this.keyboard_manager = notebook.keyboard_manager; this.notebook = notebook; this.comm_manager = comm_manager; + this.comm_target_name = 'ipython.widget'; this._models = {}; /* Dictionary of model ids and model instances */ // Register with the comm manager. - this.comm_manager.register_target('ipython.widget', $.proxy(this._handle_comm_open, this)); + this.comm_manager.register_target(this.comm_target_name, $.proxy(this._handle_comm_open, this)); }; //-------------------------------------------------------------------- @@ -53,21 +55,34 @@ define([ * Displays a view for a particular model. */ var that = this; - var cell = this.get_msg_cell(msg.parent_header.msg_id); - if (cell === null) { - return Promise.reject(new Error("Could not determine where the display" + - " message was from. Widget will not be displayed")); - } else if (cell.widget_subarea) { - var dummy = $(''); - cell.widget_subarea.append(dummy); - return this.create_view(model, {cell: cell}).then( - function(view) { + return new Promise(function(resolve, reject) { + var cell = that.get_msg_cell(msg.parent_header.msg_id); + if (cell === null) { + reject(new Error("Could not determine where the display" + + " message was from. Widget will not be displayed")); + } else { + return that.display_view_in_cell(cell, model); + } + }); + }; + + WidgetManager.prototype.display_view_in_cell = function(cell, model) { + // Displays a view in a cell. + return new Promise(function(resolve, reject) { + if (cell.display_widget_view) { + cell.display_widget_view(that.create_view(model, {cell: cell})) + .then(function(view) { + that._handle_display_view(view); - dummy.replaceWith(view.$el); view.trigger('displayed'); - return view; - }).catch(utils.reject('Could not display view', true)); - } + resolve(view); + }, function(error) { + reject(new utils.WrappedError('Could not display view', error)); + }); + } else { + reject(new Error('Cell does not have a `display_widget_view` method.')); + } + }); }; WidgetManager.prototype._handle_display_view = function (view) { @@ -238,6 +253,8 @@ define([ widget_model.once('comm:close', function () { delete that._models[model_id]; }); + widget_model.name = options.model_name; + widget_model.module = options.model_module; return widget_model; }, function(error) { @@ -249,6 +266,100 @@ define([ return model_promise; }; + WidgetManager.prototype.get_state = function(options) { + // Get the state of the widget manager. + // + // This includes all of the widget models and the cells that they are + // displayed in. + // + // Parameters + // ---------- + // options: dictionary + // Dictionary of options with the following contents: + // only_displayed: (optional) boolean=false + // Only return models with one or more displayed views. + // not_alive: (optional) boolean=false + // Include models that have comms with severed connections. + return utils.resolve_promise_dict(function(models) { + var state = {}; + for (var model_id in models) { + if (models.hasOwnProperty(model_id)) { + var model = models[model_id]; + + // If the model has one or more views defined for it, + // consider it displayed. + var displayed_flag = !(options && options.only_displayed) || Object.keys(model.views).length > 0; + var alive_flag = (options && options.not_alive) || model.comm_alive; + if (displayed_flag && alive_flag) { + state[model.model_id] = { + model_name: model.name, + model_module: model.module, + views: [], + }; + + // Get the views that are displayed *now*. + for (var id in model.views) { + if (model.views.hasOwnProperty(id)) { + var view = model.views[id]; + var cell_index = this.notebook.find_cell_index(view.options.cell); + state[model.model_id].views.push(cell_index); + } + } + } + } + } + return state; + }); + }; + + WidgetManager.prototype.set_state = function(state) { + // Set the notebook's state. + // + // Reconstructs all of the widget models and attempts to redisplay the + // widgets in the appropriate cells by cell index. + + // Get the kernel when it's available. + var that = this; + return (new Promise(function(resolve, reject) { + if (that.kernel) { + resolve(that.kernel); + } else { + that.events.on('kernel_created.Session', function(event, data) { + resolve(data.kernel); + }); + } + })).then(function(kernel) { + + // Recreate all the widget models for the given state. + that.widget_models = []; + for (var i = 0; i < state.length; i++) { + // Recreate a comm using the widget's model id (model_id == comm_id). + var new_comm = new comm.Comm(kernel.widget_manager.comm_target_name, state[i].model_id); + kernel.comm_manager.register_comm(new_comm); + + // Create the model using the recreated comm. When the model is + // created we don't know yet if the comm is valid so set_comm_alive + // false. Once we receive the first state push from the back-end + // we know the comm is alive. + var model = kernel.widget_manager.create_model({ + comm: new_comm, + model_name: state[i].model_name, + model_module: state[i].model_module}).then(function(model) { + model.set_comm_alive(false); + model.request_state(); + model.received_state.then(function() { + model.set_comm_alive(true); + }); + return model; + }); + that.widget_models.push(model); + } + return Promise.all(that.widget_models); + + }); + + }; + // Backwards compatibility. IPython.WidgetManager = WidgetManager; diff --git a/IPython/html/static/widgets/js/widget.js b/IPython/html/static/widgets/js/widget.js index fb3f150..6b8601a 100644 --- a/IPython/html/static/widgets/js/widget.js +++ b/IPython/html/static/widgets/js/widget.js @@ -32,6 +32,13 @@ define(["widgets/js/manager", this.id = model_id; this.views = {}; + // Promise that is resolved when a state is received + // from the back-end. + var that = this; + this.received_state = new Promise(function(resolve) { + that._resolve_received_state = resolve; + }); + if (comm !== undefined) { // Remember comm associated with the model. this.comm = comm; @@ -40,6 +47,11 @@ define(["widgets/js/manager", // Hook comm messages up to model. comm.on_close($.proxy(this._handle_comm_closed, this)); comm.on_msg($.proxy(this._handle_comm_msg, this)); + + // Assume the comm is alive. + this.set_comm_alive(true); + } else { + this.set_comm_alive(false); } return Backbone.Model.apply(this); }, @@ -55,11 +67,34 @@ define(["widgets/js/manager", } }, - _handle_comm_closed: function (msg) { + request_state: function(callbacks) { + /** + * Request a state push from the back-end. + */ + if (!this.comm) { + console.error("Could not request_state because comm doesn't exist!"); + return; + } + this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks()); + }, + + set_comm_alive: function(alive) { + /** + * Change the comm_alive state of the model. + */ + if (this.comm_alive === undefined || this.comm_alive != alive) { + this.comm_alive = alive; + this.trigger(alive ? 'comm_is_live' : 'comm_is_dead', {model: this}); + } + }, + + close: function(comm_closed) { /** - * Handle when a widget is closed. + * Close model */ - this.trigger('comm:close'); + if (this.comm && !comm_closed) { + this.comm.close(); + } this.stopListening(); this.trigger('destroy', this); delete this.comm.model; // Delete ref so GC will collect widget model. @@ -73,6 +108,14 @@ define(["widgets/js/manager", }); }, + _handle_comm_closed: function (msg) { + /** + * Handle when a widget is closed. + */ + this.trigger('comm:close'); + this.close(true); + }, + _handle_comm_msg: function (msg) { /** * Handle incoming comm msg. @@ -104,7 +147,20 @@ define(["widgets/js/manager", } finally { that.state_lock = null; } - }).catch(utils.reject("Couldn't set model state", true)); + that._resolve_received_state(); + return Promise.resolve(); + }, utils.reject("Couldn't set model state", true)); + }, + + get_state: function() { + // Get the serializable state of the model. + state = this.toJSON(); + for (var key in state) { + if (state.hasOwnProperty(key)) { + state[key] = this._pack_models(state[key]); + } + } + return state; }, _handle_status: function (msg, callbacks) { @@ -322,6 +378,9 @@ define(["widgets/js/manager", this.on('displayed', function() { this.is_displayed = true; }, this); + this.on('remove', function() { + delete this.model.views[this.id]; + }, this); }, update: function(){ @@ -387,6 +446,12 @@ define(["widgets/js/manager", } else { this.on('displayed', callback, context); } + }, + + remove: function () { + // Raise a remove event when the view is removed. + WidgetView.__super__.remove.apply(this, arguments); + this.trigger('remove'); } }); diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py index 5a398fc..b9e8bd8 100644 --- a/IPython/html/widgets/widget.py +++ b/IPython/html/widgets/widget.py @@ -341,19 +341,26 @@ class Widget(LoggingConfigurable): """Called when a msg is received from the front-end""" data = msg['content']['data'] method = data['method'] - if not method in ['backbone', 'custom']: - self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one. - if method == 'backbone' and 'sync_data' in data: - sync_data = data['sync_data'] - self.set_state(sync_data) # handles all methods + if method == 'backbone': + if 'sync_data' in data: + sync_data = data['sync_data'] + self.set_state(sync_data) # handles all methods + + # Handle a state request. + elif method == 'request_state': + self.send_state() - # Handle a custom msg from the front-end + # Handle a custom msg from the front-end. elif method == 'custom': if 'content' in data: self._handle_custom_msg(data['content']) + # Catch remainder. + else: + self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) + def _handle_custom_msg(self, content): """Called when a custom msg is received.""" self._msg_callbacks(self, content) @@ -368,7 +375,7 @@ class Widget(LoggingConfigurable): # Send the state after the user registered callbacks for trait changes # have all fired (allows for user to validate values). if self.comm is not None and name in self.keys: - # Make sure this isn't information that the front-end just sent us. + # Make sure this isn't information that the front-end just sent us. if self._should_send_property(name, new_value): # Send new state to front-end self.send_state(key=name)