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.
This commit is contained in:
Jonathan Frederic 2013-10-24 22:17:21 +00:00
parent 4c7357f4a4
commit 837ef44256
9 changed files with 270 additions and 250 deletions

View File

@ -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;

View File

@ -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 = $('<label />')
.addClass('checkbox')
@ -39,7 +38,7 @@ require(["notebook/js/widget"], function(){
handleChanged: function(e) {
this.user_invoked_update = true;
this.model.set('value', $(e.target).prop('checked'));
this.model.apply(this);
this.model.update_other_views(this);
this.user_invoked_update = false;
},
});
@ -51,8 +50,7 @@ require(["notebook/js/widget"], function(){
// Called when view is rendered.
render : function(){
this.$el
.html('')
.addClass(this.model.comm.comm_id);
.html('');
this.$button = $('<button />')
.addClass('btn')
@ -82,7 +80,7 @@ require(["notebook/js/widget"], function(){
handleClick: function(e) {
this.user_invoked_update = true;
this.model.set('value', ! $(e.target).hasClass('active'));
this.model.apply(this);
this.model.update_other_views(this);
this.user_invoked_update = false;
},
});

View File

@ -1,5 +1,5 @@
require(["notebook/js/widget"], function(){
require(["../static/notebook/js/widget"], function(){
var ButtonWidgetModel = IPython.WidgetModel.extend({});
IPython.notebook.widget_manager.register_widget_model('ButtonWidgetModel', ButtonWidgetModel);
@ -13,7 +13,7 @@ require(["notebook/js/widget"], function(){
.addClass('btn')
.click(function() {
that.model.set('clicks', that.model.get('clicks') + 1);
that.model.apply(that);
that.model.update_other_views(that);
});
this.update(); // Set defaults.

View File

@ -1,4 +1,4 @@
require(["notebook/js/widget"], function(){
require(["../static/notebook/js/widget"], function(){
var ContainerModel = IPython.WidgetModel.extend({});
IPython.notebook.widget_manager.register_widget_model('ContainerWidgetModel', ContainerModel);
@ -6,8 +6,7 @@ require(["notebook/js/widget"], function(){
render : function(){
this.$el = $('<div />')
.addClass('widget_container')
.addClass(this.model.comm.comm_id);
.addClass('widget_container');
},
update : function(){
@ -24,6 +23,10 @@ require(["notebook/js/widget"], function(){
}
}
},
display_child : function($element) {
this.$el.append($element);
},
});
IPython.notebook.widget_manager.register_widget_view('ContainerView', ContainerView);

View File

@ -1,4 +1,4 @@
require(["notebook/js/widget"], function(){
require(["../static/notebook/js/widget"], function(){
var FloatWidgetModel = IPython.WidgetModel.extend({});
IPython.notebook.widget_manager.register_widget_model('FloatWidgetModel', FloatWidgetModel);
});

View File

@ -1,4 +1,4 @@
require(["notebook/js/widget"], function(){
require(["../static/notebook/js/widget"], function(){
var IntWidgetModel = IPython.WidgetModel.extend({});
IPython.notebook.widget_manager.register_widget_model('IntWidgetModel', IntWidgetModel);
});

View File

@ -1,4 +1,4 @@
require(["notebook/js/widget"], function(){
require(["../static/notebook/js/widget"], function(){
var SelectionWidgetModel = IPython.WidgetModel.extend({});
IPython.notebook.widget_manager.register_widget_model('SelectionWidgetModel', SelectionWidgetModel);
@ -8,8 +8,7 @@ require(["notebook/js/widget"], function(){
render : function(){
this.$el
.html('')
.addClass(this.model.comm.comm_id);
.html('');
this.$buttongroup = $('<div />')
.addClass('widget_item')
.addClass('btn-group')
@ -44,7 +43,7 @@ require(["notebook/js/widget"], function(){
.html(items[index])
.on('click', function(e){
that.model.set('value', $(e.target).html(), this );
that.model.apply(that);
that.model.update_other_views(that);
})
this.$droplist.append($('<li />').append(item_button))
@ -72,8 +71,7 @@ require(["notebook/js/widget"], function(){
// Called when view is rendered.
render : function(){
this.$el
.html('')
.addClass(this.model.comm.comm_id);
.html('');
this.update();
},
@ -99,7 +97,7 @@ require(["notebook/js/widget"], function(){
.prependTo($label)
.on('click', function(e){
that.model.set('value', $(e.target).val(), this);
that.model.apply();
that.model.update_other_views();
});
}
@ -137,8 +135,7 @@ require(["notebook/js/widget"], function(){
// Called when view is rendered.
render : function(){
this.$el
.html('')
.addClass(this.model.comm.comm_id);
.html('');
this.$buttongroup = $('<div />')
.addClass('btn-group')
.attr('data-toggle', 'buttons-radio')
@ -164,7 +161,7 @@ require(["notebook/js/widget"], function(){
.appendTo(this.$buttongroup)
.on('click', function(e){
that.model.set('value', $(e.target).html(), this);
that.model.apply();
that.model.update_other_views();
});
}

View File

@ -1,4 +1,4 @@
require(["notebook/js/widget"], function(){
require(["../static/notebook/js/widget"], function(){
var StringWidgetModel = IPython.WidgetModel.extend({});
IPython.notebook.widget_manager.register_widget_model('StringWidgetModel', StringWidgetModel);
@ -6,8 +6,7 @@ require(["notebook/js/widget"], function(){
// Called when view is rendered.
render : function(){
this.$el = $('<div />')
.addClass(this.model.comm.comm_id);
this.$el = $('<div />');
this.update(); // Set defaults.
},
@ -26,8 +25,7 @@ require(["notebook/js/widget"], function(){
// Called when view is rendered.
render : function(){
this.$el
.html('')
.addClass(this.model.comm.comm_id);
.html('');
this.$textbox = $('<textarea />')
.attr('rows', 5)
.appendTo(this.$el);
@ -50,7 +48,7 @@ require(["notebook/js/widget"], function(){
handleChanging: function(e) {
this.user_invoked_update = true;
this.model.set('value', e.target.value);
this.model.apply(this);
this.model.update_other_views(this);
this.user_invoked_update = false;
},
});
@ -62,8 +60,7 @@ require(["notebook/js/widget"], function(){
// Called when view is rendered.
render : function(){
this.$el
.html('')
.addClass(this.model.comm.comm_id);
.html('');
this.$textbox = $('<input type="text" />')
.addClass('input')
.appendTo(this.$el);
@ -86,7 +83,7 @@ require(["notebook/js/widget"], function(){
handleChanging: function(e) {
this.user_invoked_update = true;
this.model.set('value', e.target.value);
this.model.apply(this);
this.model.update_other_views(this);
this.user_invoked_update = false;
},
});

View File

@ -18,7 +18,7 @@ def init_widget_js():
name = filename.rsplit('.', 1)[0]
if not (name == 'widget' or name == '__init__') and name.startswith('widget_'):
# Remove 'widget_' from the start of the name before compiling the path.
js_path = 'static/notebook/js/widgets/%s.js' % name[7:]
js_path = '../static/notebook/js/widgets/%s.js' % name[7:]
display(Javascript(data='$.getScript("%s");' % js_path))
@ -141,13 +141,13 @@ class Widget(LoggingConfigurable):
# Show view.
if self.parent is None:
self.comm.send({"method": "show", "view_name": view_name})
self.comm.send({"method": "display", "view_name": view_name})
else:
self.comm.send({"method": "show",
self.comm.send({"method": "display",
"view_name": view_name,
"parent": self.parent.comm.comm_id})
# Now show children if any.
# Now display children if any.
for child in self.children:
child._repr_widget_()
return None