diff --git a/IPython/html/static/notebook/js/widgets/widget.js b/IPython/html/static/notebook/js/widgets/widget.js
index 30a9461aa..ed79975a3 100644
--- a/IPython/html/static/notebook/js/widgets/widget.js
+++ b/IPython/html/static/notebook/js/widgets/widget.js
@@ -95,7 +95,7 @@ function(widget_manager, underscore, backbone){
var value = state[key];
this.key_value_lock = [key, value];
try {
- this.set(key, state[key]);
+ this.set(key, this._unpack_models(value));
} finally {
this.key_value_lock = null;
}
@@ -137,32 +137,26 @@ function(widget_manager, underscore, backbone){
// 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') {
- if (this.msg_buffer === null) {
- this.msg_buffer = $.extend({}, model_json); // Copy
- }
- for (attr in options.attrs) {
- var value = options.attrs[attr];
- if (this.key_value_lock === null || attr != this.key_value_lock[0] || value != this.key_value_lock[1]) {
- this.msg_buffer[attr] = value;
- }
- }
- } else {
+ if (this.msg_buffer === null) {
this.msg_buffer = $.extend({}, model_json); // Copy
}
+ for (attr in options.attrs) {
+ var value = this._pack_models(options.attrs[attr]);
+ if (this.key_value_lock === null || attr != this.key_value_lock[0] || value != this.key_value_lock[1]) {
+ this.msg_buffer[attr] = value;
+ }
+ }
} 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 (attr in options.attrs) {
- var value = options.attrs[attr];
- if (this.key_value_lock === null || attr != this.key_value_lock[0] || value != this.key_value_lock[1]) {
- send_json[attr] = value;
- }
+ send_json = {};
+ for (attr in options.attrs) {
+ var value = this._pack_models(options.attrs[attr]);
+ if (this.key_value_lock === null || attr != this.key_value_lock[0] || value != this.key_value_lock[1]) {
+ send_json[attr] = value;
}
}
@@ -177,6 +171,37 @@ function(widget_manager, underscore, backbone){
return model_json;
},
+ _pack_models: function(value) {
+ if (value instanceof Backbone.Model) {
+ return value.id;
+ } else if (value instanceof Object) {
+ var packed = {};
+ for (var key in value) {
+ packed[key] = this._pack_models(value[key]);
+ }
+ return packed;
+ } else {
+ return value;
+ }
+ },
+
+ _unpack_models: function(value) {
+ if (value instanceof Object) {
+ var unpacked = {};
+ for (var key in value) {
+ unpacked[key] = this._unpack_models(value[key]);
+ }
+ return unpacked;
+ } else {
+ var model = this.widget_manager.get_model(value);
+ if (model !== null) {
+ return model;
+ } else {
+ return value;
+ }
+ }
+ },
+
});
@@ -196,24 +221,23 @@ function(widget_manager, underscore, backbone){
// triggered on model change
},
- child_view: function(model_id, options) {
- // create and return a child view, given a model id for a model and (optionally) a view name
+ child_view: function(child_model, options) {
+ // create and return a child view, given a model and (optionally) a view name
// if the view name is not given, it defaults to the model's default view attribute
- var child_model = this.widget_manager.get_model(model_id);
- var child_view = this.widget_manager.create_view(child_model, options);
- this.child_views[model_id] = child_view;
+ var child_view = this.model.widget_manager.create_view(child_model, options);
+ this.child_views[child_model.id] = child_view;
return child_view;
},
update_child_views: function(old_list, new_list) {
- // this function takes an old list and new list of model ids
+ // this function takes an old list and new list of models
// views in child_views that correspond to deleted ids are deleted
// views corresponding to added ids are added child_views
// delete old views
_.each(_.difference(old_list, new_list), function(element, index, list) {
- var view = this.child_views[element];
- delete this.child_views[element];
+ var view = this.child_views[element.id];
+ delete this.child_views[element.id];
view.remove();
}, this);
@@ -247,10 +271,10 @@ function(widget_manager, underscore, backbone){
_.each(_.difference(new_list, old_list), function(item, index, list) {
added_callback(item);
}, this);
- }
+ },
callbacks: function(){
- return this.widget_manager.callbacks(this);
+ return this.model.widget_manager.callbacks(this);
},
render: function(){
@@ -271,7 +295,7 @@ function(widget_manager, underscore, backbone){
// TODO: make changes more granular (e.g., trigger on visible:change)
this.model.on('change', this.update, this);
this.model.on('msg:custom', this.on_msg, this);
- WidgetView.initialize.apply(this, arguments);
+ DOMWidgetView.__super__.initialize.apply(this, arguments);
},
on_msg: function(msg) {
diff --git a/IPython/html/static/notebook/js/widgets/widget_bool.js b/IPython/html/static/notebook/js/widgets/widget_bool.js
index a29be7091..9331d2b25 100644
--- a/IPython/html/static/notebook/js/widgets/widget_bool.js
+++ b/IPython/html/static/notebook/js/widgets/widget_bool.js
@@ -64,7 +64,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
- return IPython.DOMWidgetView.update.apply(this);
+ return CheckboxView.__super__.update.apply(this);
},
});
@@ -88,13 +88,13 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
//
// Called when the model is changed. The model may have been
// changed by another view or by a state update from the back-end.
- if (options === undefined || options.updated_view != this) {
- if (this.model.get('value')) {
- this.$el.addClass('active');
- } else {
- this.$el.removeClass('active');
- }
+ if (this.model.get('value')) {
+ this.$el.addClass('active');
+ } else {
+ this.$el.removeClass('active');
+ }
+ if (options === undefined || options.updated_view != this) {
var disabled = this.model.get('disabled');
this.$el.prop('disabled', disabled);
@@ -105,17 +105,17 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$el.html(description);
}
}
- return IPython.DOMWidgetView.update.apply(this);
+ return ToggleButtonView.__super__.update.apply(this);
},
- events: {"click button" : "handleClick"},
+ events: {"click" : "handleClick"},
// Handles and validates user input.
handleClick: function(e) {
// Calling model.set will trigger all of the other views of the
// model to update.
- this.model.set('value', ! $(e.target).hasClass('active'), {updated_view: this});
+ this.model.set('value', ! $(this.$el).hasClass('active'), {updated_view: this});
this.touch();
},
});
diff --git a/IPython/html/static/notebook/js/widgets/widget_button.js b/IPython/html/static/notebook/js/widgets/widget_button.js
index f02f2e421..b3eff2cd0 100644
--- a/IPython/html/static/notebook/js/widgets/widget_button.js
+++ b/IPython/html/static/notebook/js/widgets/widget_button.js
@@ -49,7 +49,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$el.removeAttr('disabled');
}
- return IPython.DOMWidgetView.update.apply(this);
+ return ButtonView.__super__.update.apply(this);
},
events: {
diff --git a/IPython/html/static/notebook/js/widgets/widget_container.js b/IPython/html/static/notebook/js/widgets/widget_container.js
index 220a229ca..98b16750e 100644
--- a/IPython/html/static/notebook/js/widgets/widget_container.js
+++ b/IPython/html/static/notebook/js/widgets/widget_container.js
@@ -64,7 +64,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager) {
this.$el.empty();
this.update_child_views(old_list, new_list);
_.each(new_list, function(element, index, list) {
- this.$el.append(this.child_views[element].$el);
+ this.$el.append(this.child_views[element.id].$el);
}, this)
},
@@ -74,7 +74,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager) {
// Called when the model is changed. The model may have been
// changed by another view or by a state update from the back-end.
set_flex_properties(this, this.$el);
- return IPython.DOMWidgetView.update.apply(this);
+ return ContainerView.__super__.update.apply(this);
},
});
@@ -258,7 +258,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager) {
this.show();
}
- return IPython.DOMWidgetView.update.apply(this);
+ return ModalView.__super__.update.apply(this);
},
_get_selector_element: function(selector) {
@@ -277,7 +277,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager) {
return this.$window.find(selector.substring(6));
}
} else {
- return IPython.DOMWidgetView._get_selector_element.apply(this, [selector]);
+ return ModalView.__super__._get_selector_element.apply(this, [selector]);
}
},
diff --git a/IPython/html/static/notebook/js/widgets/widget_image.js b/IPython/html/static/notebook/js/widgets/widget_image.js
index eec084d0c..7df56af21 100644
--- a/IPython/html/static/notebook/js/widgets/widget_image.js
+++ b/IPython/html/static/notebook/js/widgets/widget_image.js
@@ -47,7 +47,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
} else {
this.$el.removeAttr('height');
}
- return IPython.DOMWidgetView.update.apply(this);
+ return ImageView.__super__.update.apply(this);
},
});
diff --git a/IPython/html/static/notebook/js/widgets/widget_selection.js b/IPython/html/static/notebook/js/widgets/widget_selection.js
index 9504982fb..80489b72f 100644
--- a/IPython/html/static/notebook/js/widgets/widget_selection.js
+++ b/IPython/html/static/notebook/js/widgets/widget_selection.js
@@ -101,7 +101,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
- return IPython.DOMWidgetView.update.apply(this);
+ return DropdownView.__super__.update.apply(this);
},
// Handle when a value is clicked.
@@ -193,7 +193,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
- return IPython.DOMWidgetView.update.apply(this);
+ return RadioButtonsView.__super__.update.apply(this);
},
// Handle when a value is clicked.
@@ -280,7 +280,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
- return IPython.DOMWidgetView.update.apply(this);
+ return ToggleButtonsView.__super__.update.apply(this);
},
// Handle when a value is clicked.
@@ -364,7 +364,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
- return IPython.DOMWidgetView.update.apply(this);
+ return ListBoxView.__super__.update.apply(this);
},
// Handle when a value is clicked.
diff --git a/IPython/html/static/notebook/js/widgets/widget_selectioncontainer.js b/IPython/html/static/notebook/js/widgets/widget_selectioncontainer.js
index 8a7894c47..6186f6664 100644
--- a/IPython/html/static/notebook/js/widgets/widget_selectioncontainer.js
+++ b/IPython/html/static/notebook/js/widgets/widget_selectioncontainer.js
@@ -77,7 +77,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
}
}
}
- return IPython.DOMWidgetView.update.apply(this);
+ return AccordionView.__super__.update.apply(this);
},
add_child_view: function(view) {
@@ -130,7 +130,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
initialize: function() {
this.containers = [];
- IPython.DOMWidgetView.initialize.apply(this, arguments);
+ TabView.__super__.initialize.apply(this, arguments);
},
render: function(){
@@ -181,7 +181,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.select_page(selected_index);
}
}
- return IPython.DOMWidgetView.update.apply(this);
+ return TabView.__super__.update.apply(this);
},
add_child_view: function(view) {
diff --git a/IPython/html/static/notebook/js/widgets/widget_string.js b/IPython/html/static/notebook/js/widgets/widget_string.js
index c0eb870f1..3663ed892 100644
--- a/IPython/html/static/notebook/js/widgets/widget_string.js
+++ b/IPython/html/static/notebook/js/widgets/widget_string.js
@@ -31,7 +31,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
// Called when the model is changed. The model may have been
// changed by another view or by a state update from the back-end.
this.$el.html(this.model.get('value'));
- return IPython.DOMWidgetView.update.apply(this);
+ return HTMLView.__super__.update.apply(this);
},
});
@@ -54,7 +54,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$el.html(this.model.get('value'));
MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$el.get(0)]);
- return IPython.DOMWidgetView.update.apply(this);
+ return LatexView.__super__.update.apply(this);
},
});
@@ -114,7 +114,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
- return IPython.DOMWidgetView.update.apply(this);
+ return TextAreaView.__super__.update.apply(this);
},
events: {"keyup textarea": "handleChanging",
@@ -173,7 +173,7 @@ define(["notebook/js/widgets/widget"], function(widget_manager){
this.$label.show();
}
}
- return IPython.DOMWidgetView.update.apply(this);
+ return TextBoxView.__super__.update.apply(this);
},
events: {"keyup input": "handleChanging",
diff --git a/IPython/html/tests/casperjs/test_cases/widgets.js b/IPython/html/tests/casperjs/test_cases/widgets.js
index d355c93af..bcafcf687 100644
--- a/IPython/html/tests/casperjs/test_cases/widgets.js
+++ b/IPython/html/tests/casperjs/test_cases/widgets.js
@@ -27,7 +27,7 @@ casper.notebook_test(function () {
index = this.append_cell(
'names = [name for name in dir(widgets)' +
- ' if name.endswith("Widget") and name!= "Widget"]\n' +
+ ' if name.endswith("Widget") and name!= "Widget" and name!= "DOMWidget"]\n' +
'for name in names:\n' +
' print(name)\n');
this.execute_cell_then(index, function(index){
@@ -37,7 +37,7 @@ casper.notebook_test(function () {
// suffixed).
var javascript_names = this.evaluate(function () {
names = [];
- for (var name in IPython.widget_manager.widget_model_types) {
+ for (var name in IPython.widget_manager._model_types) {
names.push(name.replace('Model',''));
}
return names;
diff --git a/IPython/html/tests/casperjs/test_cases/widgets_bool.js b/IPython/html/tests/casperjs/test_cases/widgets_bool.js
index 7ff7a45da..6a0312952 100644
--- a/IPython/html/tests/casperjs/test_cases/widgets_bool.js
+++ b/IPython/html/tests/casperjs/test_cases/widgets_bool.js
@@ -7,9 +7,15 @@ casper.notebook_test(function () {
this.execute_cell_then(index);
var bool_index = this.append_cell(
- 'bool_widget = widgets.BoolWidget(description="Title", value=True)\n' +
- 'display(bool_widget)\n'+
- 'display(bool_widget, view_name="ToggleButtonView")\n' +
+ 'bool_widgets = [widgets.BoolWidget(description="Title", value=True) for i in range(2)]\n' +
+ 'display(bool_widgets[0])\n' +
+ 'bool_widgets[1].view_name = "ToggleButtonView"\n' +
+ 'display(bool_widgets[1])\n' +
+ 'for widget in bool_widgets:\n' +
+ ' def handle_change(name,old,new):\n' +
+ ' for other_widget in bool_widgets:\n' +
+ ' other_widget.value = new\n' +
+ ' widget.on_trait_change(handle_change, "value")\n' +
'print("Success")');
this.execute_cell_then(bool_index, function(index){
@@ -37,21 +43,21 @@ casper.notebook_test(function () {
'Checkbox labeled correctly.');
this.test.assert(this.cell_element_exists(index,
- '.widget-area .widget-subarea div button'),
+ '.widget-area .widget-subarea button'),
'Toggle button exists.');
this.test.assert(this.cell_element_function(index,
- '.widget-area .widget-subarea div button', 'html')=="Title",
+ '.widget-area .widget-subarea button', 'html')=="Title",
'Toggle button labeled correctly.');
this.test.assert(this.cell_element_function(index,
- '.widget-area .widget-subarea div button', 'hasClass', ['active']),
+ '.widget-area .widget-subarea button', 'hasClass', ['active']),
'Toggle button is toggled.');
});
index = this.append_cell(
- 'bool_widget.value = False\n' +
+ 'bool_widgets[0].value = False\n' +
'print("Success")');
this.execute_cell_then(index, function(index){
@@ -63,18 +69,18 @@ casper.notebook_test(function () {
'Checkbox is not checked. (1)');
this.test.assert(! this.cell_element_function(bool_index,
- '.widget-area .widget-subarea div button', 'hasClass', ['active']),
+ '.widget-area .widget-subarea button', 'hasClass', ['active']),
'Toggle button is not toggled. (1)');
// Try toggling the bool by clicking on the toggle button.
- this.cell_element_function(bool_index, '.widget-area .widget-subarea div button', 'click');
+ this.cell_element_function(bool_index, '.widget-area .widget-subarea button', 'click');
this.test.assert(this.cell_element_function(bool_index,
'.widget-area .widget-subarea .widget-hbox-single input', 'prop', ['checked']),
'Checkbox is checked. (2)');
this.test.assert(this.cell_element_function(bool_index,
- '.widget-area .widget-subarea div button', 'hasClass', ['active']),
+ '.widget-area .widget-subarea button', 'hasClass', ['active']),
'Toggle button is toggled. (2)');
// Try toggling the bool by clicking on the checkbox.
@@ -85,7 +91,7 @@ casper.notebook_test(function () {
'Checkbox is not checked. (3)');
this.test.assert(! this.cell_element_function(bool_index,
- '.widget-area .widget-subarea div button', 'hasClass', ['active']),
+ '.widget-area .widget-subarea button', 'hasClass', ['active']),
'Toggle button is not toggled. (3)');
});
diff --git a/IPython/html/tests/casperjs/test_cases/widgets_selection.js b/IPython/html/tests/casperjs/test_cases/widgets_selection.js
index 14f1d8f95..6c3d7c6c5 100644
--- a/IPython/html/tests/casperjs/test_cases/widgets_selection.js
+++ b/IPython/html/tests/casperjs/test_cases/widgets_selection.js
@@ -42,11 +42,16 @@ casper.notebook_test(function () {
}
selection_index = this.append_cell(
- 'selection = widgets.SelectionWidget(values=["' + selection_values + '"[i] for i in range(4)])\n' +
- 'display(selection)\n' +
- 'display(selection, view_name="ToggleButtonsView")\n' +
- 'display(selection, view_name="RadioButtonsView")\n' +
- 'display(selection, view_name="ListBoxView")\n' +
+ 'selection = [widgets.SelectionWidget(values=["' + selection_values + '"[i] for i in range(4)]) for j in range(4)]\n' +
+ 'selection[1].view_name="ToggleButtonsView"\n' +
+ 'selection[2].view_name="RadioButtonsView"\n' +
+ 'selection[3].view_name="ListBoxView"\n' +
+ '[display(selection[i]) for i in range(4)]\n' +
+ 'for widget in selection:\n' +
+ ' def handle_change(name,old,new):\n' +
+ ' for other_widget in selection:\n' +
+ ' other_widget.value = new\n' +
+ ' widget.on_trait_change(handle_change, "value")\n' +
'print("Success")\n');
this.execute_cell_then(selection_index, function(index){
this.test.assert(this.get_output_cell(index).text == 'Success\n',
@@ -73,7 +78,8 @@ casper.notebook_test(function () {
});
index = this.append_cell(
- 'selection.value = "a"\n' +
+ 'for widget in selection:\n' +
+ ' widget.value = "a"\n' +
'print("Success")\n');
this.execute_cell_then(index, function(index){
this.test.assert(this.get_output_cell(index).text == 'Success\n',
diff --git a/IPython/html/tests/casperjs/test_cases/widgets_string.js b/IPython/html/tests/casperjs/test_cases/widgets_string.js
index 998842898..f271413ad 100644
--- a/IPython/html/tests/casperjs/test_cases/widgets_string.js
+++ b/IPython/html/tests/casperjs/test_cases/widgets_string.js
@@ -7,12 +7,15 @@ casper.notebook_test(function () {
this.execute_cell_then(index);
var string_index = this.append_cell(
- 'string_widget = widgets.StringWidget()\n' +
- 'display(string_widget)\n'+
- 'display(string_widget, view_name="TextAreaView")\n' +
- 'display(string_widget, view_name="HTMLView")\n' +
- 'display(string_widget, view_name="LatexView")\n' +
- 'string_widget.value = "xyz"\n' +
+ 'string_widget = [widgets.StringWidget(), widgets.StringWidget(), widgets.StringWidget(), widgets.StringWidget()]\n' +
+ 'string_widget[0].value = "xyz"\n' +
+ 'string_widget[1].view_name = "TextAreaView"\n' +
+ 'string_widget[1].value = "xyz"\n' +
+ 'string_widget[2].view_name = "HTMLView"\n' +
+ 'string_widget[2].value = "xyz"\n' +
+ 'string_widget[3].view_name = "LatexView"\n' +
+ 'string_widget[3].value = "$\\\\LaTeX{}$"\n' +
+ '[display(widget) for widget in string_widget]\n'+
'print("Success")');
this.execute_cell_then(string_index, function(index){
@@ -39,29 +42,12 @@ casper.notebook_test(function () {
'.widget-area .widget-subarea .widget-hbox-single input[type=text]', 'val')=='xyz',
'Python set textbox value.');
- this.cell_element_function(index,
- '.widget-area .widget-subarea .widget-hbox-single input[type=text]', 'val', [''])
- this.sendKeys('.widget-area .widget-subarea .widget-hbox-single input[type=text]', 'abc');
-
- this.test.assert(this.cell_element_function(index,
- '.widget-area .widget-subarea .widget-hbox textarea', 'val')=='abc',
- 'Textarea updated to textbox contents.');
-
- this.cell_element_function(index,
- '.widget-area .widget-subarea .widget-hbox textarea', 'val', ['']);
- this.sendKeys('.widget-area .widget-subarea .widget-hbox textarea', '$\\LaTeX{}$');
-
- this.test.assert(this.cell_element_function(index,
- '.widget-area .widget-subarea .widget-hbox-single input[type=text]', 'val')=='$\\LaTeX{}$',
- 'Textbox updated to textarea contents.');
});
this.wait(500); // Wait for change to execute in kernel
index = this.append_cell('print(string_widget.value)');
this.execute_cell_then(index, function(index){
- this.test.assert(this.get_output_cell(index).text == '$\\LaTeX{}$\n',
- 'Python updated with correct string widget value.');
this.test.assert(this.cell_element_exists(string_index,
'.widget-area .widget-subarea div span.MathJax_Preview'),
diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py
index ed1b2ab79..238f9544a 100644
--- a/IPython/html/widgets/widget.py
+++ b/IPython/html/widgets/widget.py
@@ -34,11 +34,13 @@ from IPython.utils.py3compat import string_types
@contextmanager
def PropertyLock(instance, key, value):
instance._property_lock = (key, value)
- yield
- del instance._property_lock
+ try:
+ yield
+ finally:
+ del instance._property_lock
def should_send_property(instance, key, value):
- return not hasattr(instance, _property_lock) or \
+ return not hasattr(instance, '_property_lock') or \
key != instance._property_lock[0] or \
value != instance._property_lock[1]
@@ -47,8 +49,9 @@ class Widget(LoggingConfigurable):
# Shared declarations (Class level)
widget_construction_callback = None
+ widgets = []
- keys = ['view_name']
+ keys = ['view_name'] # TODO: Sync = True
def on_widget_constructed(callback):
"""Class method, registers a callback to be called when a widget is
@@ -66,6 +69,7 @@ class Widget(LoggingConfigurable):
# Public declarations (Instance level)
target_name = Unicode('widget', help="""Name of the backbone model
registered in the frontend to create and sync this widget with.""")
+ # model_name
view_name = Unicode(help="""Default view registered in the frontend
to use to represent the widget.""")
@@ -75,23 +79,27 @@ class Widget(LoggingConfigurable):
def __init__(self, **kwargs):
"""Public constructor
"""
+ self.closed = False
self._display_callbacks = []
self._msg_callbacks = []
super(Widget, self).__init__(**kwargs)
self.on_trait_change(self._handle_property_changed, self.keys)
+ Widget.widgets.append(self)
Widget._call_widget_constructed(self)
def __del__(self):
"""Object disposal"""
self.close()
-
def close(self):
"""Close method. Closes the widget which closes the underlying comm.
When the comm is closed, all of the widget views are automatically
removed from the frontend."""
- self._close_communication()
+ if not self.closed:
+ self.closed = True
+ self._close_communication()
+ Widget.widgets.remove(self)
@property
def comm(self):
@@ -109,6 +117,8 @@ class Widget(LoggingConfigurable):
data = msg['content']['data']
method = data['method']
+ # TODO: Log unrecog.
+
# Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
if method == 'backbone' and 'sync_data' in data:
sync_data = data['sync_data']
@@ -124,7 +134,7 @@ class Widget(LoggingConfigurable):
"""Called when a state is recieved from the frontend."""
for name in self.keys:
if name in sync_data:
- value = sync_data[name]
+ value = self._unpack_widgets(sync_data[name])
with PropertyLock(self, name, value):
setattr(self, name, value)
@@ -204,22 +214,57 @@ class Widget(LoggingConfigurable):
else:
keys = [key]
for k in keys:
- value = getattr(self, k)
-
- # a more elegant solution to encoding Widgets would be
- # to tap into the JSON encoder and teach it how to deal
- # with Widget objects, or maybe just teach the JSON
- # encoder to look for a _repr_json property before giving
- # up encoding
- if isinstance(value, Widget):
- value = value.model_id
- elif isinstance(value, list) and len(value)>0 and isinstance(value[0], Widget):
- # assume all elements of the list are widgets
- value = [i.model_id for i in value]
- state[k] = value
+ state[k] = self._pack_widgets(getattr(self, k))
return state
+ def _pack_widgets(self, values):
+ """This function recursively converts all widget instances to model id
+ strings.
+
+ Children widgets will be stored and transmitted to the front-end by
+ their model ids."""
+ if isinstance(values, dict):
+ new_dict = {}
+ for key in values.keys():
+ new_dict[key] = self._pack_widgets(values[key])
+ return new_dict
+ elif isinstance(values, list):
+ new_list = []
+ for value in values:
+ new_list.append(self._pack_widgets(value))
+ return new_list
+ elif isinstance(values, Widget):
+ return values.model_id
+ else:
+ return values
+
+
+ def _unpack_widgets(self, values):
+ """This function recursively converts all model id strings to widget
+ instances.
+
+ Children widgets will be stored and transmitted to the front-end by
+ their model ids."""
+ if isinstance(values, dict):
+ new_dict = {}
+ for key in values.keys():
+ new_dict[key] = self._unpack_widgets(values[key])
+ return new_dict
+ elif isinstance(values, list):
+ new_list = []
+ for value in values:
+ new_list.append(self._unpack_widgets(value))
+ return new_list
+ elif isinstance(values, string_types):
+ for widget in Widget.widgets:
+ if widget.model_id == values:
+ return widget
+ return values
+ else:
+ return values
+
+
def send(self, content):
"""Sends a custom msg to the widget model in the front-end.
@@ -232,8 +277,9 @@ class Widget(LoggingConfigurable):
"custom_content": content})
- def on_msg(self, callback, remove=False):
- """Register a callback for when a custom msg is recieved from the front-end
+ def on_msg(self, callback, remove=False): # TODO: Use lambdas and inspect here
+ """Register or unregister a callback for when a custom msg is recieved
+ from the front-end.
Parameters
----------
@@ -250,7 +296,8 @@ class Widget(LoggingConfigurable):
def on_displayed(self, callback, remove=False):
- """Register a callback to be called when the widget has been displayed
+ """Register or unregister a callback to be called when the widget has
+ been displayed.
Parameters
----------
@@ -282,10 +329,9 @@ class Widget(LoggingConfigurable):
def _open_communication(self):
"""Opens a communication with the front-end."""
# Create a comm.
- if self._comm is None:
- self._comm = Comm(target_name=self.target_name)
- self._comm.on_msg(self._handle_msg)
- self._comm.on_close(self._close_communication)
+ self._comm = Comm(target_name=self.target_name)
+ self._comm.on_msg(self._handle_msg)
+ self._comm.on_close(self._close_communication)
# first update
self.send_state()
@@ -295,7 +341,7 @@ class Widget(LoggingConfigurable):
"""Closes a communication with the front-end."""
if self._comm is not None:
try:
- self._comm.close()
+ self._comm.close() # TODO: Check
finally:
self._comm = None
@@ -361,7 +407,7 @@ class DOMWidget(Widget):
if len(args) == 1:
if isinstance(args[0], dict):
for (key, value) in args[0].items():
- if not (key in self._css[selector] and value in self._css[selector][key]):
+ if not (key in self._css[selector] and value == self._css[selector][key]):
self._css[selector][key] = value
self.send_state('_css')
else:
@@ -379,7 +425,7 @@ class DOMWidget(Widget):
# Only update the property if it has changed.
key = args[0]
value = args[1]
- if not (key in self._css[selector] and value in self._css[selector][key]):
+ if not (key in self._css[selector] and value == self._css[selector][key]):
self._css[selector][key] = value
self.send_state('_css') # Send new state to client.
else:
@@ -398,7 +444,7 @@ class DOMWidget(Widget):
be added to.
"""
class_list = class_names
- if isinstance(list, class_list):
+ if isinstance(class_list, list):
class_list = ' '.join(class_list)
self.send({"msg_type": "add_class",
@@ -418,7 +464,7 @@ class DOMWidget(Widget):
be removed from.
"""
class_list = class_names
- if isinstance(list, class_list):
+ if isinstance(class_list, list):
class_list = ' '.join(class_list)
self.send({"msg_type": "remove_class",