Merge pull request #5012 from minrk/selection-dict

Make `SelectionWidget.values` a dict
This commit is contained in:
Brian E. Granger 2014-02-04 15:27:23 -08:00
commit 350705075b
3 changed files with 82 additions and 56 deletions

View File

@ -57,14 +57,14 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
// changed by another view or by a state update from the back-end. // changed by another view or by a state update from the back-end.
if (options === undefined || options.updated_view != this) { if (options === undefined || options.updated_view != this) {
var selected_item_text = this.model.get('_value'); var selected_item_text = this.model.get('value_name');
if (selected_item_text.length === 0) { if (selected_item_text.length === 0) {
this.$droplabel.text(' '); this.$droplabel.text(' ');
} else { } else {
this.$droplabel.text(selected_item_text); this.$droplabel.text(selected_item_text);
} }
var items = this.model.get('labels'); var items = this.model.get('value_names');
var $replace_droplist = $('<ul />') var $replace_droplist = $('<ul />')
.addClass('dropdown-menu'); .addClass('dropdown-menu');
var that = this; var that = this;
@ -107,7 +107,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
// Calling model.set will trigger all of the other views of the // Calling model.set will trigger all of the other views of the
// model to update. // model to update.
this.model.set('_value', $(e.target).text(), {updated_view: this}); this.model.set('value_name', $(e.target).text(), {updated_view: this});
this.touch(); this.touch();
}, },
@ -139,7 +139,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
// changed by another view or by a state update from the back-end. // changed by another view or by a state update from the back-end.
if (options === undefined || options.updated_view != this) { if (options === undefined || options.updated_view != this) {
// Add missing items to the DOM. // Add missing items to the DOM.
var items = this.model.get('labels'); var items = this.model.get('value_names');
var disabled = this.model.get('disabled'); var disabled = this.model.get('disabled');
var that = this; var that = this;
_.each(items, function(item, index) { _.each(items, function(item, index) {
@ -159,7 +159,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
} }
var $item_element = that.$container.find(item_query); var $item_element = that.$container.find(item_query);
if (that.model.get('_value') == item) { if (that.model.get('value_name') == item) {
$item_element.prop('checked', true); $item_element.prop('checked', true);
} else { } else {
$item_element.prop('checked', false); $item_element.prop('checked', false);
@ -199,14 +199,14 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
// Calling model.set will trigger all of the other views of the // Calling model.set will trigger all of the other views of the
// model to update. // model to update.
this.model.set('_value', $(e.target).val(), {updated_view: this}); this.model.set('value_name', $(e.target).val(), {updated_view: this});
this.touch(); this.touch();
}, },
}); });
WidgetManager.register_widget_view('RadioButtonsView', RadioButtonsView); WidgetManager.register_widget_view('RadioButtonsView', RadioButtonsView);
var ToggleButtonsView = IPython.DOMWidgetView.extend({ var ToggleButtonsView = IPython.DOMWidgetView.extend({
render : function(){ render : function(){
// Called when view is rendered. // Called when view is rendered.
this.$el this.$el
@ -230,7 +230,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
// changed by another view or by a state update from the back-end. // changed by another view or by a state update from the back-end.
if (options === undefined || options.updated_view != this) { if (options === undefined || options.updated_view != this) {
// Add missing items to the DOM. // Add missing items to the DOM.
var items = this.model.get('labels'); var items = this.model.get('value_names');
var disabled = this.model.get('disabled'); var disabled = this.model.get('disabled');
var that = this; var that = this;
_.each(items, function(item, index) { _.each(items, function(item, index) {
@ -245,7 +245,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
} }
var $item_element = that.$buttongroup.find(item_query); var $item_element = that.$buttongroup.find(item_query);
if (that.model.get('_value') == item) { if (that.model.get('value_name') == item) {
$item_element.addClass('active'); $item_element.addClass('active');
} else { } else {
$item_element.removeClass('active'); $item_element.removeClass('active');
@ -285,7 +285,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
// Calling model.set will trigger all of the other views of the // Calling model.set will trigger all of the other views of the
// model to update. // model to update.
this.model.set('_value', $(e.target).text(), {updated_view: this}); this.model.set('value_name', $(e.target).text(), {updated_view: this});
this.touch(); this.touch();
}, },
}); });
@ -316,21 +316,21 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
// changed by another view or by a state update from the back-end. // changed by another view or by a state update from the back-end.
if (options === undefined || options.updated_view != this) { if (options === undefined || options.updated_view != this) {
// Add missing items to the DOM. // Add missing items to the DOM.
var items = this.model.get('labels'); var items = this.model.get('value_names');
var that = this; var that = this;
_.each(items, function(item, index) { _.each(items, function(item, index) {
var item_query = ' :contains("' + item + '")'; var item_query = ' :contains("' + item + '")';
if (that.$listbox.find(item_query).length === 0) { if (that.$listbox.find(item_query).length === 0) {
$('<option />') $('<option />')
.text(item) .text(item)
.attr('_value', item) .attr('value_name', item)
.appendTo(that.$listbox) .appendTo(that.$listbox)
.on('click', $.proxy(that.handle_click, that)); .on('click', $.proxy(that.handle_click, that));
} }
}); });
// Select the correct element // Select the correct element
this.$listbox.val(this.model.get('_value')); this.$listbox.val(this.model.get('value_name'));
// Disable listbox if needed // Disable listbox if needed
var disabled = this.model.get('disabled'); var disabled = this.model.get('disabled');
@ -368,7 +368,7 @@ define(["notebook/js/widgets/widget"], function(WidgetManager){
// Calling model.set will trigger all of the other views of the // Calling model.set will trigger all of the other views of the
// model to update. // model to update.
this.model.set('_value', $(e.target).text(), {updated_view: this}); this.model.set('value_name', $(e.target).text(), {updated_view: this});
this.touch(); this.touch();
}, },
}); });

View File

@ -76,7 +76,7 @@ casper.notebook_test(function () {
'Widget list exists.'); 'Widget list exists.');
// Verify that no items are selected. // Verify that no items are selected.
this.test.assert(verify_selection(this, -1), 'No items selected.'); this.test.assert(verify_selection(this, 0), 'Default first item selected.');
}); });
index = this.append_cell( index = this.append_cell(
@ -123,7 +123,9 @@ casper.notebook_test(function () {
index = this.append_cell( index = this.append_cell(
'for widget in selection:\n' + 'for widget in selection:\n' +
' widget.values = list(widget.values) + ["z"]\n' + ' d = widget.values.copy()\n' +
' d["z"] = "z"\n' +
' widget.values = d\n' +
'selection[0].value = "z"'); 'selection[0].value = "z"');
this.execute_cell_then(index, function(index){ this.execute_cell_then(index, function(index){

View File

@ -1,4 +1,4 @@
"""SelectionWidget class. """SelectionWidget classes.
Represents an enumeration using a widget. Represents an enumeration using a widget.
""" """
@ -13,68 +13,92 @@ Represents an enumeration using a widget.
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Imports # Imports
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
from collections import OrderedDict
from threading import Lock from threading import Lock
from .widget import DOMWidget from .widget import DOMWidget
from IPython.utils.traitlets import Unicode, List, Bool, Any, Dict from IPython.utils.traitlets import Unicode, List, Bool, Any, Dict, TraitError
from IPython.utils.py3compat import unicode_type
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# SelectionWidget # SelectionWidget
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class _SelectionWidget(DOMWidget): class _SelectionWidget(DOMWidget):
value = Any(help="Selected value") """Base class for Selection widgets
values = List(help="List of values the user can select")
labels = List(help="""List of string representations for each value. ``values`` can be specified as a list or dict. If given as a list,
These string representations are used to display the values in the it will be transformed to a dict of the form ``{str(value):value}``.
front-end.""", sync=True) # Only synced to the back-end. """
value = Any(help="Selected value")
values = Dict(help="""Dictionary of {name: value} the user can select.
The keys of this dictionary are the strings that will be displayed in the UI,
representing the actual Python choices.
The keys of this dictionary are also available as value_names.
""")
value_name = Unicode(help="The name of the selected value", sync=True)
value_names = List(Unicode, help="""Read-only list of names for each value.
If values is specified as a list, this is the string representation of each element.
Otherwise, it is the keys of the values dictionary.
These strings are used to display the choices in the front-end.""", sync=True)
disabled = Bool(False, help="Enable or disable user changes", sync=True) disabled = Bool(False, help="Enable or disable user changes", sync=True)
description = Unicode(help="Description of the value this widget represents", sync=True) description = Unicode(help="Description of the value this widget represents", sync=True)
_value = Unicode(sync=True) # Bi-directionally synced. def __init__(self, *args, **kwargs):
def __init__(self, *pargs, **kwargs):
"""Constructor"""
self.value_lock = Lock() self.value_lock = Lock()
self.on_trait_change(self._string_value_set, ['_value']) self._in_values_changed = False
DOMWidget.__init__(self, *pargs, **kwargs) if 'values' in kwargs:
values = kwargs['values']
# convert list values to an dict of {str(v):v}
if isinstance(values, list):
# preserve list order with an OrderedDict
kwargs['values'] = OrderedDict((unicode_type(v), v) for v in values)
DOMWidget.__init__(self, *args, **kwargs)
def _values_changed(self, name, old, new):
"""Handles when the values dict has been changed.
def _labels_changed(self, name=None, old=None, new=None): Setting values implies setting value names from the keys of the dict.
"""Handles when the value_names Dict has been changed. """
self._in_values_changed = True
This method sets the _reverse_value_names Dict to the inverse of the new try:
value for the value_names Dict.""" self.value_names = list(new.keys())
if len(new) != len(self.values): finally:
raise TypeError('Labels list must be the same size as the values list.') self._in_values_changed = False
def _values_changed(self, name=None, old=None, new=None): # ensure that the chosen value is one of the choices
"""Handles when the value_names Dict has been changed. if self.value not in new.values():
self.value = next(iter(new.values()))
This method sets the _reverse_value_names Dict to the inverse of the new
value for the value_names Dict.""" def _value_names_changed(self, name, old, new):
if len(new) != len(self.labels): if not self._in_values_changed:
self.labels = [(self.labels[i] if i < len(self.labels) else str(v)) for i, v in enumerate(new)] raise TraitError("value_names is a read-only proxy to values.keys(). Use the values dict instead.")
def _value_changed(self, name, old, new): def _value_changed(self, name, old, new):
"""Called when value has been changed""" """Called when value has been changed"""
if self.value_lock.acquire(False): if self.value_lock.acquire(False):
try: try:
# Make sure the value is in the list of values. # Reverse dictionary lookup for the value name
if new in self.values: for k,v in self.values.items():
# Set the string version of the value. if new == v:
self._value = self.labels[self.values.index(new)] # set the selected value name
else: self.value_name = k
raise TypeError('Value must be a value in the values list.') return
raise KeyError(new)
finally: finally:
self.value_lock.release() self.value_lock.release()
def _string_value_set(self, name, old, new): def _value_name_changed(self, name, old, new):
"""Called when _value has been changed.""" """Called when the value name has been changed (typically by the frontend)."""
if self.value_lock.acquire(False): if self.value_lock.acquire(False):
try: try:
if new in self.labels: self.value = self.values[new]
self.value = self.values[self.labels.index(new)]
else:
self.value = None
finally: finally:
self.value_lock.release() self.value_lock.release()