From 34939886a3d78efebcadd28b191f8ef76a9289be Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 21 Aug 2014 21:44:27 +0000 Subject: [PATCH 1/9] Adding Link widget --- IPython/html/static/widgets/js/widget_link.js | 48 +++++++++++++++++++ IPython/html/widgets/__init__.py | 1 + IPython/html/widgets/widget_link.py | 35 ++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 IPython/html/static/widgets/js/widget_link.js create mode 100644 IPython/html/widgets/widget_link.py 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 000000000..6c2842763 --- /dev/null +++ b/IPython/html/static/widgets/js/widget_link.js @@ -0,0 +1,48 @@ +// 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.on("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; + }, + }); + return { + "LinkModel": LinkModel, + } +}); diff --git a/IPython/html/widgets/__init__.py b/IPython/html/widgets/__init__.py index 2e9352254..552f32ee6 100644 --- a/IPython/html/widgets/__init__.py +++ b/IPython/html/widgets/__init__.py @@ -10,6 +10,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 # 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 000000000..17539731c --- /dev/null +++ b/IPython/html/widgets/widget_link.py @@ -0,0 +1,35 @@ +"""Link and DirectionalLink classes. + +Represents a button in the frontend using a widget. Allows user to listen for +click events on the button and trigger backend code when the clicks are fired. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2013, 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 + +#----------------------------------------------------------------------------- +# 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) + +def link(*args): + return Link(widgets=args) From 859de50a68ff0d85109cf1262c731418bd63f6a0 Mon Sep 17 00:00:00 2001 From: Sylvain Corlay Date: Tue, 9 Sep 2014 15:25:24 +0000 Subject: [PATCH 2/9] Adding directional link widget --- IPython/html/static/widgets/js/widget_link.js | 39 +++++++++++++++++++ IPython/html/widgets/__init__.py | 2 +- IPython/html/widgets/widget_link.py | 21 +++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/IPython/html/static/widgets/js/widget_link.js b/IPython/html/static/widgets/js/widget_link.js index 6c2842763..c6571c098 100644 --- a/IPython/html/static/widgets/js/widget_link.js +++ b/IPython/html/static/widgets/js/widget_link.js @@ -42,7 +42,46 @@ define([ this.updating = false; }, }); + + var DirectionalLinkModel = widget.WidgetModel.extend({ + initialize: function() { + this.on("change", this.update_bindings, this); + this.on("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 552f32ee6..922ed6dcd 100644 --- a/IPython/html/widgets/__init__.py +++ b/IPython/html/widgets/__init__.py @@ -10,7 +10,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 +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 index 17539731c..95970a3c1 100644 --- a/IPython/html/widgets/widget_link.py +++ b/IPython/html/widgets/widget_link.py @@ -15,7 +15,7 @@ click events on the button and trigger backend code when the clicks are fired. # Imports #----------------------------------------------------------------------------- from .widget import Widget -from IPython.utils.traitlets import Unicode, Tuple +from IPython.utils.traitlets import Unicode, Tuple, Any #----------------------------------------------------------------------------- # Classes @@ -31,5 +31,24 @@ class Link(Widget): kwargs['widgets'] = widgets super(Link, self).__init__(**kwargs) + 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) + + +def dlink(source, *targets): + return DirectionalLink(source, targets) From bba453aa0bbb8da813bb67d88c6ee0ecdac8b615 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 11 Sep 2014 21:39:05 +0000 Subject: [PATCH 3/9] Fix whitespace --- IPython/html/static/widgets/js/widget_link.js | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/IPython/html/static/widgets/js/widget_link.js b/IPython/html/static/widgets/js/widget_link.js index c6571c098..7ee00d498 100644 --- a/IPython/html/static/widgets/js/widget_link.js +++ b/IPython/html/static/widgets/js/widget_link.js @@ -9,9 +9,9 @@ define([ 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.update_value(this.get("widgets")[0]); }, this); - this.on("destroy", function(model, collection, options) { + this.on("destroy", function(model, collection, options) { this.update_bindings(this.get("widgets"), []); }, this); }, @@ -19,12 +19,12 @@ define([ 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 - }); + 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;} @@ -34,10 +34,10 @@ define([ 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(); - } + if (element[0]) { + element[0].set(element[1], new_value); + element[0].save_changes(); + } }, this); this.updating = false; }, @@ -57,7 +57,7 @@ define([ this.source[0].off("change:" + this.source[1], null, this); } this.source = this.get("source"); - if (this.source) { + if (this.source) { this.source[0].on("change:" + this.source[1], function() { this.update_value(this.source); }, this); this.update_value(this.source); } @@ -70,16 +70,15 @@ define([ 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(); - } + if (element[0]) { + element[0].set(element[1], new_value); + element[0].save_changes(); + } }, this); this.updating = false; }, }); - return { "LinkModel": LinkModel, "DirectionalLinkModel": DirectionalLinkModel, From f5c9f3367146c3295d00c2d6b20da7db888cc05f Mon Sep 17 00:00:00 2001 From: Sylvain Corlay Date: Thu, 11 Sep 2014 22:13:39 +0000 Subject: [PATCH 4/9] Updated example notebook --- .../Interactive Widgets/Widget Events.ipynb | 146 +++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/examples/Interactive Widgets/Widget Events.ipynb b/examples/Interactive Widgets/Widget Events.ipynb index 45ce1fa8f..b979c8dd3 100644 --- a/examples/Interactive Widgets/Widget Events.ipynb +++ b/examples/Interactive Widgets/Widget Events.ipynb @@ -220,6 +220,150 @@ "metadata": {}, "outputs": [] }, + { + "cell_type": "heading", + "level": 1, + "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": "heading", + "level": 2, + "metadata": {}, + "source": [ + "Linking traitlets attributes from the server side" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "from IPython.utils import traitlets" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "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)" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "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)" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "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", + "collapsed": false, + "input": [ + "# l.unlink()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "heading", + "level": 2, + "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", + "collapsed": false, + "input": [ + "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)" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "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)" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Function `widgets.link` returns a `Link` widget. The link can be broken by calling the `close` method." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "# l.close()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, { "cell_type": "markdown", "metadata": {}, @@ -231,4 +375,4 @@ "metadata": {} } ] -} \ No newline at end of file +} From d00be3a8a1e54dbabcead040781d0c61dbebe1b9 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 11 Sep 2014 22:22:46 +0000 Subject: [PATCH 5/9] Load link widget javascript --- IPython/html/static/widgets/js/init.js | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/html/static/widgets/js/init.js b/IPython/html/static/widgets/js/init.js index cfbd13465..1dc9d7f3e 100644 --- a/IPython/html/static/widgets/js/init.js +++ b/IPython/html/static/widgets/js/init.js @@ -9,6 +9,7 @@ define([ "widgets/js/widget_float", "widgets/js/widget_image", "widgets/js/widget_int", + "widgets/js/widget_link", "widgets/js/widget_selection", "widgets/js/widget_selectioncontainer", "widgets/js/widget_string", From 9d96319f7690b318dedf15547f22553b27c08fb8 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 11 Sep 2014 22:58:44 +0000 Subject: [PATCH 6/9] Load the link models --- IPython/html/static/widgets/js/init.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/IPython/html/static/widgets/js/init.js b/IPython/html/static/widgets/js/init.js index 1dc9d7f3e..4a46a89a5 100644 --- a/IPython/html/static/widgets/js/init.js +++ b/IPython/html/static/widgets/js/init.js @@ -3,20 +3,25 @@ define([ "widgets/js/manager", + "widgets/js/widget_link", "widgets/js/widget_bool", "widgets/js/widget_button", "widgets/js/widget_box", "widgets/js/widget_float", "widgets/js/widget_image", "widgets/js/widget_int", - "widgets/js/widget_link", "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]); From 1204af464b8b08d75c26ccfc72fd9fc762e4b01e Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 26 Sep 2014 19:04:55 +0000 Subject: [PATCH 7/9] on("destroy",...) -> once("destroy",...) so we don't keep a reference to it, preventing gc Thanks to Sylvain Corlay for the suggestion. --- IPython/html/static/widgets/js/widget_link.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/html/static/widgets/js/widget_link.js b/IPython/html/static/widgets/js/widget_link.js index 7ee00d498..ba76dbd36 100644 --- a/IPython/html/static/widgets/js/widget_link.js +++ b/IPython/html/static/widgets/js/widget_link.js @@ -11,7 +11,7 @@ define([ this.update_bindings(model.previous("widgets") || [], value); this.update_value(this.get("widgets")[0]); }, this); - this.on("destroy", function(model, collection, options) { + this.once("destroy", function(model, collection, options) { this.update_bindings(this.get("widgets"), []); }, this); }, @@ -46,7 +46,7 @@ define([ var DirectionalLinkModel = widget.WidgetModel.extend({ initialize: function() { this.on("change", this.update_bindings, this); - this.on("destroy", function() { + this.once("destroy", function() { if (this.source) { this.source[0].off("change:" + this.source[1], null, this); } From 4496bfc6bd1d99cf03ae0e30099cae4c887a85ca Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Tue, 9 Dec 2014 22:48:21 +0000 Subject: [PATCH 8/9] Fix a bug in using promises with comms: this -> that --- IPython/html/static/services/kernels/comm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/html/static/services/kernels/comm.js b/IPython/html/static/services/kernels/comm.js index 4af2ba62e..cffd66f19 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) { From 8cf5972f57151fa359f2767e5ee42b2675d75442 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Tue, 9 Dec 2014 22:50:05 +0000 Subject: [PATCH 9/9] Add the unlink method to javascript links to maintain compatibility with traitlet links --- IPython/html/widgets/widget_link.py | 13 ++++++++--- .../Interactive Widgets/Widget Events.ipynb | 23 ++++++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/IPython/html/widgets/widget_link.py b/IPython/html/widgets/widget_link.py index 95970a3c1..d38c1163c 100644 --- a/IPython/html/widgets/widget_link.py +++ b/IPython/html/widgets/widget_link.py @@ -1,10 +1,9 @@ """Link and DirectionalLink classes. -Represents a button in the frontend using a widget. Allows user to listen for -click events on the button and trigger backend code when the clicks are fired. +Propagate changes between widgets on the javascript side """ #----------------------------------------------------------------------------- -# Copyright (c) 2013, the IPython Development Team. +# Copyright (c) 2014, the IPython Development Team. # # Distributed under the terms of the Modified BSD License. # @@ -31,6 +30,10 @@ class Link(Widget): 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) @@ -49,6 +52,10 @@ class DirectionalLink(Widget): 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 61df67917..75cd86d99 100644 --- a/examples/Interactive Widgets/Widget Events.ipynb +++ b/examples/Interactive Widgets/Widget Events.ipynb @@ -331,7 +331,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Function `widgets.link` returns a `Link` widget. The link can be broken by calling the `close` method." + "Function `widgets.link` returns a `Link` widget. The link can be broken by calling the `unlink` method." ] }, { @@ -342,7 +342,7 @@ }, "outputs": [], "source": [ - "# l.close()" + "# l.unlink()" ] }, { @@ -361,15 +361,22 @@ ] ], "kernelspec": { - "codemirror_mode": { - "name": "python", - "version": 2 - }, "display_name": "Python 2", - "language": "python", "name": "python2" }, - "signature": "sha256:05a3e92089b37f68e3134587ffef6ef73830e5f8b3c515ba24640d7c803820c3" + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.8" + }, + "signature": "sha256:b6eadc174d0d9c1907518d9f37760eb3dca3aec0ef1f3746e6f0537a36e99919" }, "nbformat": 4, "nbformat_minor": 0