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",