diff --git a/IPython/html/static/widgets/js/widget_selection.js b/IPython/html/static/widgets/js/widget_selection.js
index f00e799..24aad9f 100644
--- a/IPython/html/static/widgets/js/widget_selection.js
+++ b/IPython/html/static/widgets/js/widget_selection.js
@@ -5,8 +5,9 @@ define([
"widgets/js/widget",
"base/js/utils",
"jquery",
+ "underscore",
"bootstrap",
-], function(widget, utils, $){
+], function(widget, utils, $, _){
var DropdownView = widget.DOMWidgetView.extend({
render : function(){
@@ -52,19 +53,19 @@ define([
/**
* Update the contents of this view
*
- * Called when the model is changed. The model may have been
+ * 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) {
- var selected_item_text = this.model.get('value_name');
+ var selected_item_text = this.model.get('selected_label');
if (selected_item_text.trim().length === 0) {
this.$droplabel.html(" ");
} else {
this.$droplabel.text(selected_item_text);
}
- var items = this.model.get('_value_names');
+ var items = this.model.get('_options_labels');
var $replace_droplist = $('
')
.addClass('dropdown-menu');
// Copy the style
@@ -150,7 +151,7 @@ define([
* Calling model.set will trigger all of the other views of the
* model to update.
*/
- this.model.set('value_name', $(e.target).text(), {updated_view: this});
+ this.model.set('selected_label', $(e.target).text(), {updated_view: this});
this.touch();
// Manually hide the droplist.
@@ -188,7 +189,7 @@ define([
*/
if (options === undefined || options.updated_view != this) {
// Add missing items to the DOM.
- var items = this.model.get('_value_names');
+ var items = this.model.get('_options_labels');
var disabled = this.model.get('disabled');
var that = this;
_.each(items, function(item, index) {
@@ -209,7 +210,7 @@ define([
}
var $item_element = that.$container.find(item_query);
- if (that.model.get('value_name') == item) {
+ if (that.model.get('selected_label') == item) {
$item_element.prop('checked', true);
} else {
$item_element.prop('checked', false);
@@ -263,7 +264,7 @@ define([
* Calling model.set will trigger all of the other views of the
* model to update.
*/
- this.model.set('value_name', $(e.target).val(), {updated_view: this});
+ this.model.set('selected_label', $(e.target).val(), {updated_view: this});
this.touch();
},
});
@@ -305,7 +306,7 @@ define([
*/
if (options === undefined || options.updated_view != this) {
// Add missing items to the DOM.
- var items = this.model.get('_value_names');
+ var items = this.model.get('_options_labels');
var disabled = this.model.get('disabled');
var that = this;
var item_html;
@@ -328,7 +329,7 @@ define([
.on('click', $.proxy(that.handle_click, that));
that.update_style_traits($item_element);
}
- if (that.model.get('value_name') == item) {
+ if (that.model.get('selected_label') == item) {
$item_element.addClass('active');
} else {
$item_element.removeClass('active');
@@ -410,7 +411,7 @@ define([
* Calling model.set will trigger all of the other views of the
* model to update.
*/
- this.model.set('value_name', $(e.target).attr('value'), {updated_view: this});
+ this.model.set('selected_label', $(e.target).attr('value'), {updated_view: this});
this.touch();
},
});
@@ -443,7 +444,7 @@ define([
*/
if (options === undefined || options.updated_view != this) {
// Add missing items to the DOM.
- var items = this.model.get('_value_names');
+ var items = this.model.get('_options_labels');
var that = this;
_.each(items, function(item, index) {
var item_query = 'option[data-value="' + encodeURIComponent(item) + '"]';
@@ -451,14 +452,14 @@ define([
$('')
.text(item)
.attr('data-value', encodeURIComponent(item))
- .attr('value_name', item)
+ .attr('selected_label', item)
.appendTo(that.$listbox)
.on('click', $.proxy(that.handle_click, that));
}
});
// Select the correct element
- this.$listbox.val(this.model.get('value_name'));
+ this.$listbox.val(this.model.get('selected_label'));
// Disable listbox if needed
var disabled = this.model.get('disabled');
@@ -509,9 +510,35 @@ define([
* Calling model.set will trigger all of the other views of the
* model to update.
*/
- this.model.set('value_name', $(e.target).text(), {updated_view: this});
+ this.model.set('selected_label', $(e.target).text(), {updated_view: this});
this.touch();
- },
+ },
+ });
+ var SelectMultipleView = SelectView.extend({
+ render: function(){
+ SelectMultipleView.__super__.render.apply(this);
+ this.$el.removeClass('widget-select')
+ .addClass('widget-select-multiple');
+ this.$listbox.attr('multiple', true)
+ .on('input', $.proxy(this.handle_click, this));
+ return this;
+ },
+
+ update: function(){
+ SelectMultipleView.__super__.update.apply(this, arguments);
+ this.$listbox.val(this.model.get('selected_labels'));
+ },
+
+ handle_click: function (e) {
+ // Handle when a value is clicked.
+
+ // Calling model.set will trigger all of the other views of the
+ // model to update.
+ this.model.set('selected_labels',
+ (this.$listbox.val() || []).slice(),
+ {updated_view: this});
+ this.touch();
+ },
});
return {
@@ -519,5 +546,6 @@ define([
'RadioButtonsView': RadioButtonsView,
'ToggleButtonsView': ToggleButtonsView,
'SelectView': SelectView,
+ 'SelectMultipleView': SelectMultipleView,
};
});
diff --git a/IPython/html/tests/widgets/widget_selection.js b/IPython/html/tests/widgets/widget_selection.js
index 94b433c..a3c5bcf 100644
--- a/IPython/html/tests/widgets/widget_selection.js
+++ b/IPython/html/tests/widgets/widget_selection.js
@@ -43,11 +43,11 @@ casper.notebook_test(function () {
//values=["' + selection_values + '"[i] for i in range(4)]
selection_index = this.append_cell(
- 'values=["' + selection_values + '"[i] for i in range(4)]\n' +
- 'selection = [widgets.Dropdown(values=values),\n' +
- ' widgets.ToggleButtons(values=values),\n' +
- ' widgets.RadioButtons(values=values),\n' +
- ' widgets.Select(values=values)]\n' +
+ 'options=["' + selection_values + '"[i] for i in range(4)]\n' +
+ 'selection = [widgets.Dropdown(options=options),\n' +
+ ' widgets.ToggleButtons(options=options),\n' +
+ ' widgets.RadioButtons(options=options),\n' +
+ ' widgets.Select(options=options)]\n' +
'[display(selection[i]) for i in range(4)]\n' +
'for widget in selection:\n' +
' def handle_change(name,old,new):\n' +
@@ -136,9 +136,9 @@ casper.notebook_test(function () {
index = this.append_cell(
'from copy import copy\n' +
'for widget in selection:\n' +
- ' d = copy(widget.values)\n' +
+ ' d = copy(widget.options)\n' +
' d.append("z")\n' +
- ' widget.values = d\n' +
+ ' widget.options = d\n' +
'selection[0].value = "z"');
this.execute_cell_then(index, function(index){
diff --git a/IPython/html/widgets/__init__.py b/IPython/html/widgets/__init__.py
index 837e945..8b19449 100644
--- a/IPython/html/widgets/__init__.py
+++ b/IPython/html/widgets/__init__.py
@@ -7,7 +7,7 @@ from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgres
from .widget_image import Image
from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress, IntRangeSlider
from .widget_output import Output
-from .widget_selection import RadioButtons, ToggleButtons, Dropdown, Select
+from .widget_selection import RadioButtons, ToggleButtons, Dropdown, Select, SelectMultiple
from .widget_selectioncontainer import Tab, Accordion
from .widget_string import HTML, Latex, Text, Textarea
from .interaction import interact, interactive, fixed, interact_manual
diff --git a/IPython/html/widgets/interaction.py b/IPython/html/widgets/interaction.py
index 3f30fd7..366a8fe 100644
--- a/IPython/html/widgets/interaction.py
+++ b/IPython/html/widgets/interaction.py
@@ -59,7 +59,7 @@ def _widget_abbrev_single_value(o):
if isinstance(o, string_types):
return Text(value=unicode_type(o))
elif isinstance(o, dict):
- return Dropdown(values=o)
+ return Dropdown(options=o)
elif isinstance(o, bool):
return Checkbox(value=o)
elif isinstance(o, float):
@@ -76,7 +76,7 @@ def _widget_abbrev(o):
float_or_int = (float, int)
if isinstance(o, (list, tuple)):
if o and all(isinstance(x, string_types) for x in o):
- return Dropdown(values=[unicode_type(k) for k in o])
+ return Dropdown(options=[unicode_type(k) for k in o])
elif _matches(o, (float_or_int, float_or_int)):
min, max, value = _get_min_max_value(o[0], o[1])
if all(isinstance(_, int) for _ in o):
diff --git a/IPython/html/widgets/tests/test_interaction.py b/IPython/html/widgets/tests/test_interaction.py
index 9d2a9eb..f1b0133 100644
--- a/IPython/html/widgets/tests/test_interaction.py
+++ b/IPython/html/widgets/tests/test_interaction.py
@@ -118,7 +118,7 @@ def test_single_value_dict():
check_widget(w,
cls=widgets.Dropdown,
description='d',
- values=d,
+ options=d,
value=next(iter(d.values())),
)
@@ -229,7 +229,7 @@ def test_list_tuple_str():
d = dict(
cls=widgets.Dropdown,
value=first,
- values=values
+ options=values
)
check_widgets(c, tup=d, lis=d)
@@ -287,12 +287,12 @@ def test_default_values():
),
h=dict(
cls=widgets.Dropdown,
- values={'a': 1, 'b': 2},
+ options={'a': 1, 'b': 2},
value=2
),
j=dict(
cls=widgets.Dropdown,
- values=['hi', 'there'],
+ options=['hi', 'there'],
value='there'
),
)
@@ -310,12 +310,12 @@ def test_default_out_of_bounds():
),
h=dict(
cls=widgets.Dropdown,
- values={'a': 1},
+ options={'a': 1},
value=1,
),
j=dict(
cls=widgets.Dropdown,
- values=['hi', 'there'],
+ options=['hi', 'there'],
value='hi',
),
)
@@ -634,3 +634,59 @@ def test_float_range_logic():
frsw(lower=5)
with nt.assert_raises(ValueError):
frsw(upper=5)
+
+
+def test_multiple_selection():
+ smw = widgets.SelectMultiple
+
+ # degenerate multiple select
+ w = smw()
+ check_widget(w, value=tuple(), options=None, selected_labels=tuple())
+
+ # don't accept random other value when no options
+ with nt.assert_raises(KeyError):
+ w.value = (2,)
+ check_widget(w, value=tuple(), selected_labels=tuple())
+
+ # basic multiple select
+ w = smw(options=[(1, 1)], value=[1])
+ check_widget(w, cls=smw, value=(1,), options=[(1, 1)])
+
+ # don't accept random other value
+ with nt.assert_raises(KeyError):
+ w.value = w.value + (2,)
+ check_widget(w, value=(1,), selected_labels=(1,))
+
+ # change options
+ w.options = w.options + [(2, 2)]
+ check_widget(w, options=[(1, 1), (2,2)])
+
+ # change value
+ w.value = w.value + (2,)
+ check_widget(w, value=(1, 2), selected_labels=(1, 2))
+
+ # change value name
+ w.selected_labels = (1,)
+ check_widget(w, value=(1,))
+
+ # don't accept random other names when no options
+ with nt.assert_raises(KeyError):
+ w.selected_labels = (3,)
+ check_widget(w, value=(1,))
+
+ # don't accept selected_label (from superclass)
+ with nt.assert_raises(AttributeError):
+ w.selected_label = 3
+
+ # don't return selected_label (from superclass)
+ with nt.assert_raises(AttributeError):
+ print(w.selected_label)
+
+ # dict style
+ w.options = {1: 1}
+ check_widget(w, options={1: 1})
+
+ # updating
+ with nt.assert_raises(KeyError):
+ w.value = (2,)
+ check_widget(w, options={1: 1})
diff --git a/IPython/html/widgets/widget_selection.py b/IPython/html/widgets/widget_selection.py
index 33be173..5193fb1 100644
--- a/IPython/html/widgets/widget_selection.py
+++ b/IPython/html/widgets/widget_selection.py
@@ -30,38 +30,38 @@ from IPython.utils.warn import DeprecatedClass
class _Selection(DOMWidget):
"""Base class for Selection widgets
- ``values`` can be specified as a list or dict. If given as a list,
+ ``options`` can be specified as a list or dict. If given as a list,
it will be transformed to a dict of the form ``{str(value):value}``.
"""
value = Any(help="Selected value")
- value_name = Unicode(help="The name of the selected value", sync=True)
- values = Any(help="""List of (key, value) tuples or dict of values that the
+ selected_label = Unicode(help="The label of the selected value", sync=True)
+ options = Any(help="""List of (key, value) tuples or dict of values that the
user can select.
The keys of this list are the strings that will be displayed in the UI,
representing the actual Python choices.
- The keys of this list are also available as _value_names.
+ The keys of this list are also available as _options_labels.
""")
- _values_dict = Dict()
- _value_names = Tuple(sync=True)
- _value_values = Tuple()
+ _options_dict = Dict()
+ _options_labels = Tuple(sync=True)
+ _options_values = Tuple()
disabled = Bool(False, help="Enable or disable user changes", sync=True)
description = Unicode(help="Description of the value this widget represents", sync=True)
def __init__(self, *args, **kwargs):
self.value_lock = Lock()
- self.values_lock = Lock()
- self.on_trait_change(self._values_readonly_changed, ['_values_dict', '_value_names', '_value_values', '_values'])
- if 'values' in kwargs:
- self.values = kwargs.pop('values')
+ self.options_lock = Lock()
+ self.on_trait_change(self._options_readonly_changed, ['_options_dict', '_options_labels', '_options_values', '_options'])
+ if 'options' in kwargs:
+ self.options = kwargs.pop('options')
DOMWidget.__init__(self, *args, **kwargs)
- self._value_in_values()
+ self._value_in_options()
- def _make_values(self, x):
+ def _make_options(self, x):
# If x is a dict, convert it to list format.
if isinstance(x, (OrderedDict, dict)):
return [(k, v) for k, v in x.items()]
@@ -70,7 +70,7 @@ class _Selection(DOMWidget):
if not isinstance(x, (list, tuple)):
raise ValueError('x')
- # If x is an ordinary list, use the values as names.
+ # If x is an ordinary list, use the option values as names.
for y in x:
if not isinstance(y, (list, tuple)) or len(y) < 2:
return [(i, i) for i in x]
@@ -78,42 +78,42 @@ class _Selection(DOMWidget):
# Value is already in the correct format.
return x
- def _values_changed(self, name, old, new):
- """Handles when the values tuple has been changed.
+ def _options_changed(self, name, old, new):
+ """Handles when the options tuple has been changed.
- Setting values implies setting value names from the keys of the dict.
- """
- if self.values_lock.acquire(False):
+ Setting options implies setting option labels from the keys of the dict.
+ """
+ if self.options_lock.acquire(False):
try:
- self.values = new
+ self.options = new
- values = self._make_values(new)
- self._values_dict = {i[0]: i[1] for i in values}
- self._value_names = [i[0] for i in values]
- self._value_values = [i[1] for i in values]
- self._value_in_values()
+ options = self._make_options(new)
+ self._options_dict = {i[0]: i[1] for i in options}
+ self._options_labels = [i[0] for i in options]
+ self._options_values = [i[1] for i in options]
+ self._value_in_options()
finally:
- self.values_lock.release()
+ self.options_lock.release()
- def _value_in_values(self):
+ def _value_in_options(self):
# ensure that the chosen value is one of the choices
- if self._value_values:
- if self.value not in self._value_values:
- self.value = next(iter(self._value_values))
- def _values_readonly_changed(self, name, old, new):
- if not self.values_lock.locked():
- raise TraitError("`.%s` is a read-only trait. Use the `.values` tuple instead." % name)
+ if self._options_values:
+ if self.value not in self._options_values:
+ self.value = next(iter(self._options_values))
+ def _options_readonly_changed(self, name, old, new):
+ if not self.options_lock.locked():
+ raise TraitError("`.%s` is a read-only trait. Use the `.options` tuple instead." % name)
def _value_changed(self, name, old, new):
"""Called when value has been changed"""
if self.value_lock.acquire(False):
try:
# Reverse dictionary lookup for the value name
- for k,v in self._values_dict.items():
+ for k, v in self._options_dict.items():
if new == v:
# set the selected value name
- self.value_name = k
+ self.selected_label = k
return
# undo the change, and raise KeyError
self.value = old
@@ -121,11 +121,68 @@ class _Selection(DOMWidget):
finally:
self.value_lock.release()
- def _value_name_changed(self, name, old, new):
+ def _selected_label_changed(self, name, old, new):
"""Called when the value name has been changed (typically by the frontend)."""
if self.value_lock.acquire(False):
try:
- self.value = self._values_dict[new]
+ self.value = self._options_dict[new]
+ finally:
+ self.value_lock.release()
+
+
+class _MultipleSelection(_Selection):
+ """Base class for MultipleSelection widgets.
+
+ As with ``_Selection``, ``options`` can be specified as a list or dict. If
+ given as a list, it will be transformed to a dict of the form
+ ``{str(value): value}``.
+
+ Despite their names, ``value`` (and ``selected_label``) will be tuples, even
+ if only a single option is selected.
+ """
+
+ value = Tuple(help="Selected values")
+ selected_labels = Tuple(help="The labels of the selected options",
+ sync=True)
+
+ @property
+ def selected_label(self):
+ raise AttributeError(
+ "Does not support selected_label, use selected_labels")
+
+ def _value_in_options(self):
+ # ensure that the chosen value is one of the choices
+ if self.options:
+ old_value = self.value or []
+ new_value = []
+ for value in old_value:
+ if value in self._options_dict.values():
+ new_value.append(value)
+ if new_value:
+ self.value = new_value
+ else:
+ self.value = [next(iter(self._options_dict.values()))]
+
+ def _value_changed(self, name, old, new):
+ """Called when value has been changed"""
+ if self.value_lock.acquire(False):
+ try:
+ self.selected_labels = [
+ self._options_labels[self._options_values.index(v)]
+ for v in new
+ ]
+ except:
+ self.value = old
+ raise KeyError(new)
+ finally:
+ self.value_lock.release()
+
+ def _selected_labels_changed(self, name, old, new):
+ """Called when the selected label has been changed (typically by the
+ frontend)."""
+ if self.value_lock.acquire(False):
+ try:
+ self.value = [self._options_dict[name] for name in new]
finally:
self.value_lock.release()
@@ -165,6 +222,15 @@ class Select(_Selection):
_view_name = Unicode('SelectView', sync=True)
+@register('IPython.SelectMultiple')
+class SelectMultiple(_MultipleSelection):
+ """Listbox that allows many items to be selected at any given time.
+ Despite their names, inherited from ``_Selection``, the currently chosen
+ option values, ``value``, or their labels, ``selected_labels`` must both be
+ updated with a list-like object."""
+ _view_name = Unicode('SelectMultipleView', sync=True)
+
+
# Remove in IPython 4.0
ToggleButtonsWidget = DeprecatedClass(ToggleButtons, 'ToggleButtonsWidget')
DropdownWidget = DeprecatedClass(Dropdown, 'DropdownWidget')
diff --git a/examples/Interactive Widgets/Export As (nbconvert).ipynb b/examples/Interactive Widgets/Export As (nbconvert).ipynb
index 4ba62ee..e672889 100644
--- a/examples/Interactive Widgets/Export As (nbconvert).ipynb
+++ b/examples/Interactive Widgets/Export As (nbconvert).ipynb
@@ -86,7 +86,7 @@
},
"outputs": [],
"source": [
- "exporter_names = widgets.Dropdown(values=get_export_names(), value='html')\n",
+ "exporter_names = widgets.Dropdown(options=get_export_names(), value='html')\n",
"export_button = widgets.Button(description=\"Export\")\n",
"download_link = widgets.HTML(visible=False)"
]
diff --git a/examples/Interactive Widgets/Widget List.ipynb b/examples/Interactive Widgets/Widget List.ipynb
index 2335868..9dc1209 100644
--- a/examples/Interactive Widgets/Widget List.ipynb
+++ b/examples/Interactive Widgets/Widget List.ipynb
@@ -274,7 +274,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "There are four widgets that can be used to display single selection lists. All four inherit from the same base class. You can specify the **enumeration of selectables by passing a list**. You can **also specify the enumeration as a dictionary**, in which case the **keys will be used as the item displayed** in the list and the corresponding **value will be returned** when an item is selected."
+ "There are four widgets that can be used to display single selection lists, and one that can be used to display multiple selection lists. All inherit from the same base class. You can specify the **enumeration of selectable options by passing a list**. You can **also specify the enumeration as a dictionary**, in which case the **keys will be used as the item displayed** in the list and the corresponding **value will be returned** when an item is selected."
]
},
{
@@ -298,7 +298,7 @@
"source": [
"from IPython.display import display\n",
"w = widgets.Dropdown(\n",
- " values=['1', '2', '3'],\n",
+ " options=['1', '2', '3'],\n",
" value='2',\n",
" description='Number:',\n",
")\n",
@@ -332,7 +332,7 @@
"outputs": [],
"source": [
"w = widgets.Dropdown(\n",
- " values={'One': 1, 'Two': 2, 'Three': 3},\n",
+ " options={'One': 1, 'Two': 2, 'Three': 3},\n",
" value=2,\n",
" description='Number:',\n",
")\n",
@@ -371,7 +371,7 @@
"source": [
"widgets.RadioButtons(\n",
" description='Pizza topping:',\n",
- " values=['pepperoni', 'pineapple', 'anchovies'],\n",
+ " options=['pepperoni', 'pineapple', 'anchovies'],\n",
")"
]
},
@@ -396,7 +396,7 @@
"source": [
"widgets.Select(\n",
" description='OS:',\n",
- " values=['Linux', 'Windows', 'OSX'],\n",
+ " options=['Linux', 'Windows', 'OSX'],\n",
")"
]
},
@@ -421,12 +421,46 @@
"source": [
"widgets.ToggleButtons(\n",
" description='Speed:',\n",
- " values=['Slow', 'Regular', 'Fast'],\n",
+ " options=['Slow', 'Regular', 'Fast'],\n",
")"
]
},
{
"cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### SelectMultiple\n",
+ "Multiple values can be selected with shift and ctrl pressed and mouse clicks or arrow keys."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "w = widgets.SelectMultiple(\n",
+ " description=\"Fruits\",\n",
+ " options=['Apples', 'Oranges', 'Pears']\n",
+ ")\n",
+ "display(w)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "w.value"
+ ]
+ },
+ {
+ "cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
diff --git a/examples/Interactive Widgets/Widget Styling.ipynb b/examples/Interactive Widgets/Widget Styling.ipynb
index f30f1b0..47ddadf 100644
--- a/examples/Interactive Widgets/Widget Styling.ipynb
+++ b/examples/Interactive Widgets/Widget Styling.ipynb
@@ -236,11 +236,11 @@
"outputs": [],
"source": [
"name = widgets.Text(description='Name:')\n",
- "color = widgets.Dropdown(description='Color:', values=['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'])\n",
+ "color = widgets.Dropdown(description='Color:', options=['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'])\n",
"page1 = widgets.Box(children=[name, color])\n",
"\n",
"age = widgets.IntSlider(description='Age:', min=0, max=120, value=50)\n",
- "gender = widgets.RadioButtons(description='Gender:', values=['male', 'female'])\n",
+ "gender = widgets.RadioButtons(description='Gender:', options=['male', 'female'])\n",
"page2 = widgets.Box(children=[age, gender])\n",
"\n",
"tabs = widgets.Tab(children=[page1, page2])\n",