From 837ef442568c330c16bd44a1247243cc29833836 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Thu, 24 Oct 2013 22:17:21 +0000 Subject: [PATCH] LOTS OF WIDGET CHANGES Moved model-like code out of manager. Added parent/child API. Throttling now occurs on a model by model level. View/cell association is fixed for the most part, but there is still one assumption being made in handle_com_msg. --- IPython/html/static/notebook/js/widget.js | 451 +++++++++--------- .../html/static/notebook/js/widgets/bool.js | 12 +- .../html/static/notebook/js/widgets/button.js | 4 +- .../static/notebook/js/widgets/container.js | 9 +- .../html/static/notebook/js/widgets/float.js | 2 +- .../html/static/notebook/js/widgets/int.js | 2 +- .../static/notebook/js/widgets/selection.js | 17 +- .../html/static/notebook/js/widgets/string.js | 15 +- IPython/html/widgets/widget.py | 8 +- 9 files changed, 270 insertions(+), 250 deletions(-) diff --git a/IPython/html/static/notebook/js/widget.js b/IPython/html/static/notebook/js/widget.js index 0f24fedb8..a3d7633b3 100644 --- a/IPython/html/static/notebook/js/widget.js +++ b/IPython/html/static/notebook/js/widget.js @@ -20,8 +20,8 @@ // Use require.js 'define' method so that require.js is intelligent enough to // syncronously load everything within this file when it is being 'required' // elsewhere. -define(["static/components/underscore/underscore-min.js", - "static/components/backbone/backbone-min.js", +define(["../../components/underscore/underscore-min.js", + "../../components/backbone/backbone-min.js", ], function(){ // Only run once on a notebook. @@ -31,15 +31,236 @@ define(["static/components/underscore/underscore-min.js", // WidgetModel class //-------------------------------------------------------------------- var WidgetModel = Backbone.Model.extend({ - apply: function(sender) { + constructor: function(comm_manager, comm, widget_view_types) { + this.comm_manager = comm_manager; + this.widget_view_types = widget_view_types; + this.pending_msgs = 0; + this.msg_throttle = 3; + this.msg_buffer = {}; + this.views = {}; + + // Remember comm associated with the model. + this.comm = comm; + comm.model = this; + + // Hook comm messages up to model. + comm.on_close($.proxy(this.handle_comm_closed, this)); + comm.on_msg($.proxy(this.handle_comm_msg, this)); + + return Backbone.Model.apply(this); + }, + + + update_other_views: function(caller) { + this.last_modified_view = caller; this.save(this.changedAttributes(), {patch: true}); - for (var index in this.views) { - var view = this.views[index]; - if (view !== sender) { + for (var cell_index in this.views) { + var view = this.views[cell_index]; + if (view !== caller) { view.refresh(); } } + }, + + + handle_status: function (output_area, msg) { + //execution_state : ('busy', 'idle', 'starting') + if (msg.content.execution_state=='idle') { + + // Send buffer if this message caused another message to be + // throttled. + if (this.msg_throttle == this.pending_msgs && + this.msg_buffer.length > 0) { + + var output_area = this._get_msg_output_area(msg); + var callbacks = this._make_callbacks(output_area); + var data = {sync_method: 'patch', sync_data: this.msg_buffer}; + comm.send(data, callbacks); + this.msg_buffer = {}; + } else { + + // Only decrease the pending message count if the buffer + // doesn't get flushed (sent). + --this.pending_msgs; + } + } + }, + + + // Custom syncronization logic. + handle_sync: function (method, options) { + var model_json = this.toJSON(); + + // Only send updated state if the state hasn't been changed + // during an update. + if (!this.updating) { + if (this.pending_msgs >= this.msg_throttle) { + // The throttle has been exceeded, buffer the current msg so + // it can be sent once the kernel has finished processing + // some of the existing messages. + if (method=='patch') { + for (var attr in options.attrs) { + this.msg_buffer[attr] = options.attrs[attr]; + } + } else { + this.msg_buffer = $.extend({}, model_json); // Copy + } + + } else { + // We haven't exceeded the throttle, send the message like + // normal. If this is a patch operation, just send the + // changes. + var send_json = model_json; + if (method=='patch') { + send_json = {}; + for (var attr in options.attrs) { + send_json[attr] = options.attrs[attr]; + } + } + + var data = {sync_method: method, sync_data: send_json}; + var output_area = this._get_view_output_area(this.last_modified_view); + var callbacks = this._make_callbacks(); + this.comm.send(data, callbacks); + this.pending_msgs++; + } + } + + // Since the comm is a one-way communication, assume the message + // arrived. + return model_json; + }, + + + // Handle incomming comm msg. + handle_comm_msg: function (comm, msg) { + var method = msg.content.data.method; + switch (method){ + case 'display': + +////////////////////////// TODO: Get cell index via currently executing cell. + var cell_index = IPython.notebook.get_selected_index()-1; + + this.display_view(msg.content.data.view_name, + msg.content.data.parent, + cell_index); + break; + case 'update': + this.handle_update(msg.content.data.state); + break; + } + } + + + // Handle when a widget is updated via the python side. + handle_update: function (state) { + this.updating = true; + try { + for (var key in state) { + if (state.hasOwnProperty(key)) { + if (key == "_css"){ + this.css = state[key]; + } else { + this.set(key, state[key]); + } + } + } + this.id = this.comm.comm_id; + this.save(); + } finally { + this.updating = false; + } + } + + + // Handle when a widget is closed. + handle_comm_closed: function (msg) { + for (var cell_index in this.views) { + var view = this.views[cell_index]; + view.remove(); + } + } + + + // Create view that represents the model. + display_view = function (view_name, parent_comm_id, cell_index) { + var view = new this.widget_view_types[view_name]({model: this}); + view.render(); + this.views[cell_index] = view; + view.cell_index = cell_index; + + // Handle when the view element is remove from the page. + var that = this; + view.$el.on("remove", function(){ + var index = that.views.indexOf(view); + if (index > -1) { + that.views.splice(index, 1); + } + view.remove(); // Clean-up view + + // Close the comm if there are no views left. + if (that.views.length()==0) { + that.comm.close(); + } + }); + + var display_child = null; + if (parent_comm_id != undefined) { + var parent_comm = this.comm_manager.comms[parent_comm_id]; + var parent_model = parent_comm.model; + var parent_view = parent_model.views[cell_id]; + if (parent_view.display_child != undefined) { + display_child = parent_view.display_child; + } + } + + if (display_child != null) { + display_child(view.$el); + } else { + // No parent view is defined or exists. Add the view's + // element to cell's widget div. + var cell = IPython.notebook.get_cell(cell_index); + cell.element.find('.widget_area').find('.widget_subarea') + .append(view.$el) + .parent().show(); // Show the widget_area (parent of widget_subarea) + + } + + // Update the view based on the model contents. + view.refresh(); + } + + + // Build a callback dict. + _make_callbacks: function (output_area) { + var callbacks = {}; + if (output_area != null) { + var that = this; + callbacks = { + iopub : { + output : $.proxy(output_area.handle_output, output_area), + clear_output : $.proxy(output_area.handle_clear_output, output_area), + status : function(msg){ + that.handle_status(output_area, msg); + }, + }, + }; + } + return callbacks; + }, + + + // Get the cell output area corresponding to the view. + _get_view_output_area : function (view) { + return this._get_cell_output_area(view.cell_index); + } + + + // Get the cell output area corresponding to the cell id. + _get_cell_output_area : function (cell_id) { + var cell = IPython.notebook.get_cell(cell_id) + return cell.output_area; } }); @@ -53,8 +274,8 @@ define(["static/components/underscore/underscore-min.js", this.model.on('change',this.refresh,this); }, - refresh: function() { - this.update(); + update: function() { + var results = Backbone.Model.prototype.update.call(this); if (this.model.css != undefined) { for (var selector in this.model.css) { @@ -79,6 +300,7 @@ define(["static/components/underscore/underscore-min.js", } } } + return results; }, }); @@ -86,240 +308,43 @@ define(["static/components/underscore/underscore-min.js", //-------------------------------------------------------------------- // WidgetManager class //-------------------------------------------------------------------- - // Public constructor var WidgetManager = function(comm_manager){ this.comm_manager = comm_manager; this.widget_model_types = {}; this.widget_view_types = {}; - this.model_widget_views = {}; - this.pending_msgs = 0; - this.msg_throttle = 3; - this.msg_buffer = {}; var that = this; Backbone.sync = function(method, model, options, error) { - var result = that.handle_sync(method, model, options); + var result = model.handle_sync(method, options); if (options.success) { options.success(result); } }; } - // Register a widget model type. + WidgetManager.prototype.register_widget_model = function (widget_model_name, widget_model_type) { - // Register the widget with the comm manager. Make sure to pass this object's context // in so `this` works in the call back. this.comm_manager.register_target(widget_model_name, $.proxy(this.handle_com_open, this)); - - // Register the types of the model and view correspong to this widget type. Later - // the widget manager will initialize these when the comm is opened. this.widget_model_types[widget_model_name] = widget_model_type; } - // Register a widget view type. + WidgetManager.prototype.register_widget_view = function (widget_view_name, widget_view_type) { this.widget_view_types[widget_view_name] = widget_view_type; } - // Handle when a comm is opened. + WidgetManager.prototype.handle_com_open = function (comm, msg) { var widget_type_name = msg.content.target_name; - - // Create the corresponding widget model. - var widget_model = new this.widget_model_types[widget_type_name]; - - // Remember comm associated with the model. - widget_model.comm = comm; - comm.model = widget_model; - - // Create an array to remember the views associated with the model. - widget_model.views = []; - - // Add a handle to delete the control when the comm is closed. - var that = this; - var handle_close = function(msg) { - that.handle_comm_closed(comm, msg); - } - comm.on_close(handle_close); - - // Handle incomming messages. - var handle_msg = function(msg) { - that.handle_comm_msg(comm, msg); - } - comm.on_msg(handle_msg); + var widget_model = new this.widget_model_types[widget_type_name](this.comm_manager, comm, view_types); } - // Create view that represents the model. - WidgetManager.prototype.show_view = function (widget_area, widget_model, widget_view_name) { - var widget_view = new this.widget_view_types[widget_view_name]({model: widget_model}); - widget_view.render(); - widget_model.views.push(widget_view); - - // Handle when the view element is remove from the page. - widget_view.$el.on("remove", function(){ - var index = widget_model.views.indexOf(widget_view); - if (index > -1) { - widget_model.views.splice(index, 1); - } - widget_view.remove(); // Clean-up view - - // Close the comm if there are no views left. - if (widget_model.views.length()==0) { - widget_model.comm.close(); - } - }); - - // Add the view's element to cell's widget div. - widget_area - .append(widget_view.$el) - .parent().show(); // Show the widget_area (parent of widget_subarea) - - // Update the view based on the model contents. - widget_view.refresh(); - } - - // Handle incomming comm msg. - WidgetManager.prototype.handle_comm_msg = function (comm, msg) { - // Different logic for different methods. - var method = msg.content.data.method; - switch (method){ - case 'show': - - // TODO: Get cell from registered output handler. - var cell = IPython.notebook.get_cell(IPython.notebook.get_selected_index()-1); - var widget_subarea = cell.element.find('.widget_area').find('.widget_subarea'); - - if (msg.content.data.parent != undefined) { - var find_results = widget_subarea.find("." + msg.content.data.parent); - if (find_results.length > 0) { - widget_subarea = find_results; - } - } - - this.show_view(widget_subarea, comm.model, msg.content.data.view_name); - break; - case 'update': - this.handle_update(comm, msg.content.data.state); - break; - } - } - - // Handle when a widget is updated via the python side. - WidgetManager.prototype.handle_update = function (comm, state) { - this.updating = true; - for (var key in state) { - if (state.hasOwnProperty(key)) { - if (key == "_css"){ - comm.model.css = state[key]; - } else { - comm.model.set(key, state[key]); - } - } - } - comm.model.id = comm.comm_id; - comm.model.save(); - this.updating = false; - } - - // Handle when a widget is closed. - WidgetManager.prototype.handle_comm_closed = function (comm, msg) { - for (var view_index in comm.model.views) { - var view = comm.model.views[view_index]; - view.remove(); - } - } - - // Handle when a msg status changes in the kernel. - WidgetManager.prototype.handle_status = function (msg) { - //execution_state : ('busy', 'idle', 'starting') - if (msg.content.execution_state=='idle') { - // Send buffer if this message caused another message to be - // throttled. - if (this.msg_throttle == --this.pending_msgs && - this.msg_buffer.length > 0) { - var outputarea = this._get_msg_outputarea(msg); - var callbacks = this._make_callbacks(outputarea); - var data = {sync_method: 'patch', sync_data: this.msg_buffer}; - comm.send(data, callbacks); - this.pending_msgs++; - this.msg_buffer = {}; - } - } - } - - // Get the cell output area corresponding to the comm. - WidgetManager.prototype._get_comm_outputarea = function (comm) { - // TODO: get element from comm instead of guessing - var cell = IPython.notebook.get_cell(IPython.notebook.get_selected_index()) - return cell.output_area; - } - - // Get the cell output area corresponding to the msg_id. - WidgetManager.prototype._get_msg_outputarea = function (msg) { - // TODO: get element from msg_id instead of guessing - // msg.parent_header.msg_id - var cell = IPython.notebook.get_cell(IPython.notebook.get_selected_index()) - return cell.output_area; - } - - // Build a callback dict. - WidgetManager.prototype._make_callbacks = function (outputarea) { - var callbacks = {}; - if (outputarea != null) { - callbacks = { - iopub : { - status : $.proxy(this.handle_status, this), - output : $.proxy(outputarea.handle_output, outputarea), - clear_output : $.proxy(outputarea.handle_clear_output, outputarea)} - }; - } - return callbacks; - } - - // Send widget state to python backend. - WidgetManager.prototype.handle_sync = function (method, model, options) { - var model_json = model.toJSON(); - - // Only send updated state if the state hasn't been changed during an update. - if (!this.updating) { - // Create a callback for the output if the widget has an output area associate with it. - var callbacks = this._make_callbacks(this._get_comm_outputarea(comm)); - var comm = model.comm; - - if (this.pending_msgs >= this.msg_throttle) { - // The throttle has been exceeded, buffer the current msg so - // it can be sent once the kernel has finished processing - // some of the existing messages. - if (method=='patch') { - for (var attr in options.attrs) { - this.msg_buffer[attr] = options.attrs[attr]; - } - } else { - this.msg_buffer = $.extend({}, model_json); // Copy - } - } else { - // We haven't exceeded the throttle, send the message like - // normal. If this is a patch operation, just send the - // changes. - var send_json = model_json; - if (method=='patch') { - send_json = {}; - for (var attr in options.attrs) { - send_json[attr] = options.attrs[attr]; - } - } - var data = {sync_method: method, sync_data: send_json}; - comm.send(data, callbacks); - this.pending_msgs++; - } - } - - // Since the comm is a one-way communication, assume the message - // arrived. - return model_json; - } + //-------------------------------------------------------------------- + // Init code + //-------------------------------------------------------------------- IPython.WidgetManager = WidgetManager; IPython.WidgetModel = WidgetModel; IPython.WidgetView = WidgetView; diff --git a/IPython/html/static/notebook/js/widgets/bool.js b/IPython/html/static/notebook/js/widgets/bool.js index 104ec7c71..d12163125 100644 --- a/IPython/html/static/notebook/js/widgets/bool.js +++ b/IPython/html/static/notebook/js/widgets/bool.js @@ -1,5 +1,5 @@ -require(["notebook/js/widget"], function(){ +require(["../static/notebook/js/widget"], function(){ var BoolWidgetModel = IPython.WidgetModel.extend({}); IPython.notebook.widget_manager.register_widget_model('BoolWidgetModel', BoolWidgetModel); @@ -9,8 +9,7 @@ require(["notebook/js/widget"], function(){ // Called when view is rendered. render : function(){ this.$el - .html('') - .addClass(this.model.comm.comm_id); + .html(''); var $label = $('