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