diff --git a/IPython/html/static/services/kernels/comm.js b/IPython/html/static/services/kernels/comm.js index 4af2ba6..cffd66f 100644 --- a/IPython/html/static/services/kernels/comm.js +++ b/IPython/html/static/services/kernels/comm.js @@ -106,9 +106,9 @@ define([ console.error('Comm promise not found for comm id ' + content.comm_id); return; } - + var that = this; this.comms[content.comm_id] = this.comms[content.comm_id].then(function(comm) { - this.unregister_comm(comm); + that.unregister_comm(comm); try { comm.handle_close(msg); } catch (e) { diff --git a/IPython/html/static/widgets/js/init.js b/IPython/html/static/widgets/js/init.js index a65ffe4..9dde6b0 100644 --- a/IPython/html/static/widgets/js/init.js +++ b/IPython/html/static/widgets/js/init.js @@ -3,6 +3,7 @@ define([ "widgets/js/manager", + "widgets/js/widget_link", "widgets/js/widget_bool", "widgets/js/widget_button", "widgets/js/widget_box", @@ -13,10 +14,15 @@ define([ "widgets/js/widget_selection", "widgets/js/widget_selectioncontainer", "widgets/js/widget_string", -], function(widgetmanager) { +], function(widgetmanager, linkModels) { + for (var target_name in linkModels) { + if (linkModels.hasOwnProperty(target_name)) { + widgetmanager.WidgetManager.register_widget_model(target_name, linkModels[target_name]); + } + } // Register all of the loaded views with the widget manager. - for (var i = 1; i < arguments.length; i++) { + for (var i = 2; i < arguments.length; i++) { for (var target_name in arguments[i]) { if (arguments[i].hasOwnProperty(target_name)) { widgetmanager.WidgetManager.register_widget_view(target_name, arguments[i][target_name]); diff --git a/IPython/html/static/widgets/js/widget_link.js b/IPython/html/static/widgets/js/widget_link.js new file mode 100644 index 0000000..ba76dbd --- /dev/null +++ b/IPython/html/static/widgets/js/widget_link.js @@ -0,0 +1,86 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +define([ + "widgets/js/widget", + "jquery", +], function(widget, $){ + var LinkModel = widget.WidgetModel.extend({ + initialize: function() { + this.on("change:widgets", function(model, value, options) { + this.update_bindings(model.previous("widgets") || [], value); + this.update_value(this.get("widgets")[0]); + }, this); + this.once("destroy", function(model, collection, options) { + this.update_bindings(this.get("widgets"), []); + }, this); + }, + update_bindings: function(oldlist, newlist) { + var that = this; + _.each(oldlist, function(elt) {elt[0].off("change:" + elt[1], null, that);}); + _.each(newlist, function(elt) {elt[0].on("change:" + elt[1], + function(model, value, options) { + that.update_value(elt); + }, that); + // TODO: register for any destruction handlers + // to take an item out of the list + }); + }, + update_value: function(elt) { + if (this.updating) {return;} + var model = elt[0]; + var attr = elt[1]; + var new_value = model.get(attr); + this.updating = true; + _.each(_.without(this.get("widgets"), elt), + function(element, index, list) { + if (element[0]) { + element[0].set(element[1], new_value); + element[0].save_changes(); + } + }, this); + this.updating = false; + }, + }); + + var DirectionalLinkModel = widget.WidgetModel.extend({ + initialize: function() { + this.on("change", this.update_bindings, this); + this.once("destroy", function() { + if (this.source) { + this.source[0].off("change:" + this.source[1], null, this); + } + }, this); + }, + update_bindings: function() { + if (this.source) { + this.source[0].off("change:" + this.source[1], null, this); + } + this.source = this.get("source"); + if (this.source) { + this.source[0].on("change:" + this.source[1], function() { this.update_value(this.source); }, this); + this.update_value(this.source); + } + }, + update_value: function(elt) { + if (this.updating) {return;} + var model = elt[0]; + var attr = elt[1]; + var new_value = model.get(attr); + this.updating = true; + _.each(this.get("targets"), + function(element, index, list) { + if (element[0]) { + element[0].set(element[1], new_value); + element[0].save_changes(); + } + }, this); + this.updating = false; + }, + }); + + return { + "LinkModel": LinkModel, + "DirectionalLinkModel": DirectionalLinkModel, + } +}); diff --git a/IPython/html/widgets/__init__.py b/IPython/html/widgets/__init__.py index f72c67d..20b6925 100644 --- a/IPython/html/widgets/__init__.py +++ b/IPython/html/widgets/__init__.py @@ -11,6 +11,7 @@ from .widget_selection import RadioButtons, ToggleButtons, Dropdown, Select from .widget_selectioncontainer import Tab, Accordion from .widget_string import HTML, Latex, Text, Textarea from .interaction import interact, interactive, fixed, interact_manual +from .widget_link import Link, link, DirectionalLink, dlink # Deprecated classes from .widget_bool import CheckboxWidget, ToggleButtonWidget diff --git a/IPython/html/widgets/widget_link.py b/IPython/html/widgets/widget_link.py new file mode 100644 index 0000000..d38c116 --- /dev/null +++ b/IPython/html/widgets/widget_link.py @@ -0,0 +1,61 @@ +"""Link and DirectionalLink classes. + +Propagate changes between widgets on the javascript side +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2014, the IPython Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- +from .widget import Widget +from IPython.utils.traitlets import Unicode, Tuple, Any + +#----------------------------------------------------------------------------- +# Classes +#----------------------------------------------------------------------------- + + +class Link(Widget): + """Link Widget""" + _model_name = Unicode('LinkModel', sync=True) + widgets = Tuple(sync=True, allow_none=False) + + def __init__(self, widgets=(), **kwargs): + kwargs['widgets'] = widgets + super(Link, self).__init__(**kwargs) + + # for compatibility with traitlet links + def unlink(self): + self.close() + + +def link(*args): + return Link(widgets=args) + + +class DirectionalLink(Widget): + """Directional Link Widget""" + _model_name = Unicode('DirectionalLinkModel', sync=True) + targets = Any(sync=True) + source = Tuple(sync=True) + + # Does not quite behave like other widgets but reproduces + # the behavior of IPython.utils.traitlets.directional_link + def __init__(self, source, targets=(), **kwargs): + kwargs['source'] = source + kwargs['targets'] = targets + super(DirectionalLink, self).__init__(**kwargs) + + # for compatibility with traitlet links + def unlink(self): + self.close() + + +def dlink(source, *targets): + return DirectionalLink(source, targets) diff --git a/examples/Interactive Widgets/Widget Events.ipynb b/examples/Interactive Widgets/Widget Events.ipynb index da4841e..75cd86d 100644 --- a/examples/Interactive Widgets/Widget Events.ipynb +++ b/examples/Interactive Widgets/Widget Events.ipynb @@ -201,6 +201,154 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "# Linking Widgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Often, you may want to simply link widget attributes together. Synchronization of attributes can be done in a simpler way than by using bare traitlets events. \n", + "\n", + "The first method is to use the `link` and `directional_link` functions from the `traitlets` module. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Linking traitlets attributes from the server side" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from IPython.utils import traitlets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "caption = widgets.Latex(value = 'The values of slider1, slider2 and slider3 are synchronized')\n", + "sliders1, slider2, slider3 = widgets.IntSlider(description='Slider 1'),\\\n", + " widgets.IntSlider(description='Slider 2'),\\\n", + " widgets.IntSlider(description='Slider 3')\n", + "l = traitlets.link((sliders1, 'value'), (slider2, 'value'), (slider3, 'value'))\n", + "display(caption, sliders1, slider2, slider3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "caption = widgets.Latex(value = 'Changes in source values are reflected in target1 and target2')\n", + "source, target1, target2 = widgets.IntSlider(description='Source'),\\\n", + " widgets.IntSlider(description='Target 1'),\\\n", + " widgets.IntSlider(description='Target 2')\n", + "traitlets.dlink((source, 'value'), (target1, 'value'), (target2, 'value'))\n", + "display(caption, source, target1, target2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Function `traitlets.link` returns a `Link` object. The link can be broken by calling the `unlink` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# l.unlink()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Linking widgets attributes from the client side" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When synchronizing traitlets attributes, you may experience a lag because of the latency dues to the rountrip to the server side. You can also directly link widgets attributes, either in a unidirectional or a bidirectional fashion using the link widgets. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "caption = widgets.Latex(value = 'The values of range1, range2 and range3 are synchronized')\n", + "range1, range2, range3 = widgets.IntSlider(description='Range 1'),\\\n", + " widgets.IntSlider(description='Range 2'),\\\n", + " widgets.IntSlider(description='Range 3')\n", + "l = widgets.link((range1, 'value'), (range2, 'value'), (range3, 'value'))\n", + "display(caption, range1, range2, range3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "caption = widgets.Latex(value = 'Changes in source_range values are reflected in target_range1 and target_range2')\n", + "source_range, target_range1, target_range2 = widgets.IntSlider(description='Source range'),\\\n", + " widgets.IntSlider(description='Target range 1'),\\\n", + " widgets.IntSlider(description='Target range 2')\n", + "widgets.dlink((source_range, 'value'), (target_range1, 'value'), (target_range2, 'value'))\n", + "display(caption, source_range, target_range1, target_range2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Function `widgets.link` returns a `Link` widget. The link can be broken by calling the `unlink` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# l.unlink()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "[Index](Index.ipynb) - [Back](Widget List.ipynb) - [Next](Widget Styling.ipynb)" ] } @@ -213,15 +361,22 @@ ] ], "kernelspec": { + "display_name": "Python 2", + "name": "python2" + }, + "language_info": { "codemirror_mode": { - "name": "python", + "name": "ipython", "version": 2 }, - "display_name": "Python 2", - "language": "python", - "name": "python2" + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.8" }, - "signature": "sha256:05a3e92089b37f68e3134587ffef6ef73830e5f8b3c515ba24640d7c803820c3" + "signature": "sha256:b6eadc174d0d9c1907518d9f37760eb3dca3aec0ef1f3746e6f0537a36e99919" }, "nbformat": 4, "nbformat_minor": 0