Merge pull request #6890 from bollwyvl/widget-select-multiple

SelectMultiple widget
pull/37/head
Jonathan Frederic 11 years ago
commit 6d248f258a

@ -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 = $('<ul />')
.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();
},
});
@ -430,7 +431,8 @@ define([
this.$listbox = $('<select />')
.addClass('widget-listbox form-control')
.attr('size', 6)
.appendTo(this.$el);
.appendTo(this.$el)
.on('change', $.proxy(this.handle_change, this));
this.update();
},
@ -443,7 +445,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 +453,14 @@ define([
$('<option />')
.text(item)
.attr('data-value', encodeURIComponent(item))
.attr('value_name', item)
.appendTo(that.$listbox)
.on('click', $.proxy(that.handle_click, that));
.attr('selected_label', item)
.on("click", $.proxy(that.handle_click, that))
.appendTo(that.$listbox);
}
});
// 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');
@ -504,20 +506,68 @@ define([
handle_click: function (e) {
/**
* Handle when a value is clicked.
* Handle when a new value is clicked.
*/
this.$listbox.val($(e.target).val()).change();
},
handle_change: function (e) {
/**
* Handle when a new value is selected.
*
* 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', this.$listbox.val(), {updated_view: this});
this.touch();
},
},
});
var SelectMultipleView = SelectView.extend({
render: function(){
/**
* Called when view is rendered.
*/
SelectMultipleView.__super__.render.apply(this);
this.$el.removeClass('widget-select')
.addClass('widget-select-multiple');
this.$listbox.attr('multiple', true)
.on('change', $.proxy(this.handle_change, this));
return this;
},
update: function(){
/**
* Update the contents of this view
*
* Called when the model is changed. The model may have been
* changed by another view or by a state update from the back-end.
*/
SelectMultipleView.__super__.update.apply(this, arguments);
this.$listbox.val(this.model.get('selected_labels'));
},
handle_change: function (e) {
/**
* Handle when a new value is selected.
*
* 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 {
'DropdownView': DropdownView,
'RadioButtonsView': RadioButtonsView,
'ToggleButtonsView': ToggleButtonsView,
'SelectView': SelectView,
'SelectMultipleView': SelectMultipleView,
};
});

@ -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){

@ -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

@ -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):

@ -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})

@ -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')

@ -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)"
]

@ -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,10 +421,44 @@
"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 <kbd>shift</kbd> and <kbd>ctrl</kbd> 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": {

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

Loading…
Cancel
Save