##// END OF EJS Templates
Allow a comparison operator 'equals' to be set for reverse lookup
Sylvain Corlay -
Show More
@@ -1,238 +1,240 b''
1 """Selection classes.
1 """Selection classes.
2
2
3 Represents an enumeration using a widget.
3 Represents an enumeration using a widget.
4 """
4 """
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
6 # Copyright (c) 2013, the IPython Development Team.
7 #
7 #
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9 #
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 from collections import OrderedDict
17 from collections import OrderedDict
18 from threading import Lock
18 from threading import Lock
19
19
20 from .widget import DOMWidget, register
20 from .widget import DOMWidget, register
21 from IPython.utils.traitlets import (
21 from IPython.utils.traitlets import (
22 Unicode, Bool, Any, Dict, TraitError, CaselessStrEnum, Tuple
22 Unicode, Bool, Any, Dict, TraitError, CaselessStrEnum, Tuple
23 )
23 )
24 from IPython.utils.py3compat import unicode_type
24 from IPython.utils.py3compat import unicode_type
25 from IPython.utils.warn import DeprecatedClass
25 from IPython.utils.warn import DeprecatedClass
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # SelectionWidget
28 # SelectionWidget
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 class _Selection(DOMWidget):
30 class _Selection(DOMWidget):
31 """Base class for Selection widgets
31 """Base class for Selection widgets
32
32
33 ``options`` can be specified as a list or dict. If given as a list,
33 ``options`` can be specified as a list or dict. If given as a list,
34 it will be transformed to a dict of the form ``{str(value):value}``.
34 it will be transformed to a dict of the form ``{str(value):value}``.
35 """
35 """
36
36
37 value = Any(help="Selected value")
37 value = Any(help="Selected value")
38 selected_label = Unicode(help="The label of the selected value", sync=True)
38 selected_label = Unicode(help="The label of the selected value", sync=True)
39 options = Any(help="""List of (key, value) tuples or dict of values that the
39 options = Any(help="""List of (key, value) tuples or dict of values that the
40 user can select.
40 user can select.
41
41
42 The keys of this list are the strings that will be displayed in the UI,
42 The keys of this list are the strings that will be displayed in the UI,
43 representing the actual Python choices.
43 representing the actual Python choices.
44
44
45 The keys of this list are also available as _options_labels.
45 The keys of this list are also available as _options_labels.
46 """)
46 """)
47
47
48 _options_dict = Dict()
48 _options_dict = Dict()
49 _options_labels = Tuple(sync=True)
49 _options_labels = Tuple(sync=True)
50 _options_values = Tuple()
50 _options_values = Tuple()
51
51
52 disabled = Bool(False, help="Enable or disable user changes", sync=True)
52 disabled = Bool(False, help="Enable or disable user changes", sync=True)
53 description = Unicode(help="Description of the value this widget represents", sync=True)
53 description = Unicode(help="Description of the value this widget represents", sync=True)
54
54
55 def __init__(self, *args, **kwargs):
55 def __init__(self, *args, **kwargs):
56 self.value_lock = Lock()
56 self.value_lock = Lock()
57 self.options_lock = Lock()
57 self.options_lock = Lock()
58 self.equals = kwargs.pop('equals', lambda x, y: x == y)
58 self.on_trait_change(self._options_readonly_changed, ['_options_dict', '_options_labels', '_options_values', '_options'])
59 self.on_trait_change(self._options_readonly_changed, ['_options_dict', '_options_labels', '_options_values', '_options'])
59 if 'options' in kwargs:
60 if 'options' in kwargs:
60 self.options = kwargs.pop('options')
61 self.options = kwargs.pop('options')
61 DOMWidget.__init__(self, *args, **kwargs)
62 DOMWidget.__init__(self, *args, **kwargs)
62 self._value_in_options()
63 self._value_in_options()
63
64
64 def _make_options(self, x):
65 def _make_options(self, x):
65 # If x is a dict, convert it to list format.
66 # If x is a dict, convert it to list format.
66 if isinstance(x, (OrderedDict, dict)):
67 if isinstance(x, (OrderedDict, dict)):
67 return [(k, v) for k, v in x.items()]
68 return [(k, v) for k, v in x.items()]
68
69
69 # Make sure x is a list or tuple.
70 # Make sure x is a list or tuple.
70 if not isinstance(x, (list, tuple)):
71 if not isinstance(x, (list, tuple)):
71 raise ValueError('x')
72 raise ValueError('x')
72
73
73 # If x is an ordinary list, use the option values as names.
74 # If x is an ordinary list, use the option values as names.
74 for y in x:
75 for y in x:
75 if not isinstance(y, (list, tuple)) or len(y) < 2:
76 if not isinstance(y, (list, tuple)) or len(y) < 2:
76 return [(i, i) for i in x]
77 return [(i, i) for i in x]
77
78
78 # Value is already in the correct format.
79 # Value is already in the correct format.
79 return x
80 return x
80
81
81 def _options_changed(self, name, old, new):
82 def _options_changed(self, name, old, new):
82 """Handles when the options tuple has been changed.
83 """Handles when the options tuple has been changed.
83
84
84 Setting options implies setting option labels from the keys of the dict.
85 Setting options implies setting option labels from the keys of the dict.
85 """
86 """
86 if self.options_lock.acquire(False):
87 if self.options_lock.acquire(False):
87 try:
88 try:
88 self.options = new
89 self.options = new
89
90
90 options = self._make_options(new)
91 options = self._make_options(new)
91 self._options_dict = {i[0]: i[1] for i in options}
92 self._options_dict = {i[0]: i[1] for i in options}
92 self._options_labels = [i[0] for i in options]
93 self._options_labels = [i[0] for i in options]
93 self._options_values = [i[1] for i in options]
94 self._options_values = [i[1] for i in options]
94 self._value_in_options()
95 self._value_in_options()
95 finally:
96 finally:
96 self.options_lock.release()
97 self.options_lock.release()
97
98
98 def _value_in_options(self):
99 def _value_in_options(self):
99 # ensure that the chosen value is one of the choices
100 # ensure that the chosen value is one of the choices
100
101
101 if self._options_values:
102 if self._options_values:
102 if self.value not in self._options_values:
103 if self.value not in self._options_values:
103 self.value = next(iter(self._options_values))
104 self.value = next(iter(self._options_values))
104
105
105 def _options_readonly_changed(self, name, old, new):
106 def _options_readonly_changed(self, name, old, new):
106 if not self.options_lock.locked():
107 if not self.options_lock.locked():
107 raise TraitError("`.%s` is a read-only trait. Use the `.options` tuple instead." % name)
108 raise TraitError("`.%s` is a read-only trait. Use the `.options` tuple instead." % name)
109
108 def _value_changed(self, name, old, new):
110 def _value_changed(self, name, old, new):
109 """Called when value has been changed"""
111 """Called when value has been changed"""
110 if self.value_lock.acquire(False):
112 if self.value_lock.acquire(False):
111 try:
113 try:
112 # Reverse dictionary lookup for the value name
114 # Reverse dictionary lookup for the value name
113 for k, v in self._options_dict.items():
115 for k, v in self._options_dict.items():
114 if new == v:
116 if self.equals(new, v):
115 # set the selected value name
117 # set the selected value name
116 self.selected_label = k
118 self.selected_label = k
117 return
119 return
118 # undo the change, and raise KeyError
120 # undo the change, and raise KeyError
119 self.value = old
121 self.value = old
120 raise KeyError(new)
122 raise KeyError(new)
121 finally:
123 finally:
122 self.value_lock.release()
124 self.value_lock.release()
123
125
124 def _selected_label_changed(self, name, old, new):
126 def _selected_label_changed(self, name, old, new):
125 """Called when the value name has been changed (typically by the frontend)."""
127 """Called when the value name has been changed (typically by the frontend)."""
126 if self.value_lock.acquire(False):
128 if self.value_lock.acquire(False):
127 try:
129 try:
128 self.value = self._options_dict[new]
130 self.value = self._options_dict[new]
129 finally:
131 finally:
130 self.value_lock.release()
132 self.value_lock.release()
131
133
132
134
133 class _MultipleSelection(_Selection):
135 class _MultipleSelection(_Selection):
134 """Base class for MultipleSelection widgets.
136 """Base class for MultipleSelection widgets.
135
137
136 As with ``_Selection``, ``options`` can be specified as a list or dict. If
138 As with ``_Selection``, ``options`` can be specified as a list or dict. If
137 given as a list, it will be transformed to a dict of the form
139 given as a list, it will be transformed to a dict of the form
138 ``{str(value): value}``.
140 ``{str(value): value}``.
139
141
140 Despite their names, ``value`` (and ``selected_label``) will be tuples, even
142 Despite their names, ``value`` (and ``selected_label``) will be tuples, even
141 if only a single option is selected.
143 if only a single option is selected.
142 """
144 """
143
145
144 value = Tuple(help="Selected values")
146 value = Tuple(help="Selected values")
145 selected_labels = Tuple(help="The labels of the selected options",
147 selected_labels = Tuple(help="The labels of the selected options",
146 sync=True)
148 sync=True)
147
149
148 @property
150 @property
149 def selected_label(self):
151 def selected_label(self):
150 raise AttributeError(
152 raise AttributeError(
151 "Does not support selected_label, use selected_labels")
153 "Does not support selected_label, use selected_labels")
152
154
153 def _value_in_options(self):
155 def _value_in_options(self):
154 # ensure that the chosen value is one of the choices
156 # ensure that the chosen value is one of the choices
155 if self.options:
157 if self.options:
156 old_value = self.value or []
158 old_value = self.value or []
157 new_value = []
159 new_value = []
158 for value in old_value:
160 for value in old_value:
159 if value in self._options_dict.values():
161 if value in self._options_dict.values():
160 new_value.append(value)
162 new_value.append(value)
161 if new_value:
163 if new_value:
162 self.value = new_value
164 self.value = new_value
163 else:
165 else:
164 self.value = [next(iter(self._options_dict.values()))]
166 self.value = [next(iter(self._options_dict.values()))]
165
167
166 def _value_changed(self, name, old, new):
168 def _value_changed(self, name, old, new):
167 """Called when value has been changed"""
169 """Called when value has been changed"""
168 if self.value_lock.acquire(False):
170 if self.value_lock.acquire(False):
169 try:
171 try:
170 self.selected_labels = [
172 self.selected_labels = [
171 self._options_labels[self._options_values.index(v)]
173 self._options_labels[self._options_values.index(v)]
172 for v in new
174 for v in new
173 ]
175 ]
174 except:
176 except:
175 self.value = old
177 self.value = old
176 raise KeyError(new)
178 raise KeyError(new)
177 finally:
179 finally:
178 self.value_lock.release()
180 self.value_lock.release()
179
181
180 def _selected_labels_changed(self, name, old, new):
182 def _selected_labels_changed(self, name, old, new):
181 """Called when the selected label has been changed (typically by the
183 """Called when the selected label has been changed (typically by the
182 frontend)."""
184 frontend)."""
183 if self.value_lock.acquire(False):
185 if self.value_lock.acquire(False):
184 try:
186 try:
185 self.value = [self._options_dict[name] for name in new]
187 self.value = [self._options_dict[name] for name in new]
186 finally:
188 finally:
187 self.value_lock.release()
189 self.value_lock.release()
188
190
189
191
190 @register('IPython.ToggleButtons')
192 @register('IPython.ToggleButtons')
191 class ToggleButtons(_Selection):
193 class ToggleButtons(_Selection):
192 """Group of toggle buttons that represent an enumeration. Only one toggle
194 """Group of toggle buttons that represent an enumeration. Only one toggle
193 button can be toggled at any point in time."""
195 button can be toggled at any point in time."""
194 _view_name = Unicode('ToggleButtonsView', sync=True)
196 _view_name = Unicode('ToggleButtonsView', sync=True)
195
197
196 button_style = CaselessStrEnum(
198 button_style = CaselessStrEnum(
197 values=['primary', 'success', 'info', 'warning', 'danger', ''],
199 values=['primary', 'success', 'info', 'warning', 'danger', ''],
198 default_value='', allow_none=True, sync=True, help="""Use a
200 default_value='', allow_none=True, sync=True, help="""Use a
199 predefined styling for the buttons.""")
201 predefined styling for the buttons.""")
200
202
201 @register('IPython.Dropdown')
203 @register('IPython.Dropdown')
202 class Dropdown(_Selection):
204 class Dropdown(_Selection):
203 """Allows you to select a single item from a dropdown."""
205 """Allows you to select a single item from a dropdown."""
204 _view_name = Unicode('DropdownView', sync=True)
206 _view_name = Unicode('DropdownView', sync=True)
205
207
206 button_style = CaselessStrEnum(
208 button_style = CaselessStrEnum(
207 values=['primary', 'success', 'info', 'warning', 'danger', ''],
209 values=['primary', 'success', 'info', 'warning', 'danger', ''],
208 default_value='', allow_none=True, sync=True, help="""Use a
210 default_value='', allow_none=True, sync=True, help="""Use a
209 predefined styling for the buttons.""")
211 predefined styling for the buttons.""")
210
212
211 @register('IPython.RadioButtons')
213 @register('IPython.RadioButtons')
212 class RadioButtons(_Selection):
214 class RadioButtons(_Selection):
213 """Group of radio buttons that represent an enumeration. Only one radio
215 """Group of radio buttons that represent an enumeration. Only one radio
214 button can be toggled at any point in time."""
216 button can be toggled at any point in time."""
215 _view_name = Unicode('RadioButtonsView', sync=True)
217 _view_name = Unicode('RadioButtonsView', sync=True)
216
218
217
219
218
220
219 @register('IPython.Select')
221 @register('IPython.Select')
220 class Select(_Selection):
222 class Select(_Selection):
221 """Listbox that only allows one item to be selected at any given time."""
223 """Listbox that only allows one item to be selected at any given time."""
222 _view_name = Unicode('SelectView', sync=True)
224 _view_name = Unicode('SelectView', sync=True)
223
225
224
226
225 @register('IPython.SelectMultiple')
227 @register('IPython.SelectMultiple')
226 class SelectMultiple(_MultipleSelection):
228 class SelectMultiple(_MultipleSelection):
227 """Listbox that allows many items to be selected at any given time.
229 """Listbox that allows many items to be selected at any given time.
228 Despite their names, inherited from ``_Selection``, the currently chosen
230 Despite their names, inherited from ``_Selection``, the currently chosen
229 option values, ``value``, or their labels, ``selected_labels`` must both be
231 option values, ``value``, or their labels, ``selected_labels`` must both be
230 updated with a list-like object."""
232 updated with a list-like object."""
231 _view_name = Unicode('SelectMultipleView', sync=True)
233 _view_name = Unicode('SelectMultipleView', sync=True)
232
234
233
235
234 # Remove in IPython 4.0
236 # Remove in IPython 4.0
235 ToggleButtonsWidget = DeprecatedClass(ToggleButtons, 'ToggleButtonsWidget')
237 ToggleButtonsWidget = DeprecatedClass(ToggleButtons, 'ToggleButtonsWidget')
236 DropdownWidget = DeprecatedClass(Dropdown, 'DropdownWidget')
238 DropdownWidget = DeprecatedClass(Dropdown, 'DropdownWidget')
237 RadioButtonsWidget = DeprecatedClass(RadioButtons, 'RadioButtonsWidget')
239 RadioButtonsWidget = DeprecatedClass(RadioButtons, 'RadioButtonsWidget')
238 SelectWidget = DeprecatedClass(Select, 'SelectWidget')
240 SelectWidget = DeprecatedClass(Select, 'SelectWidget')
General Comments 0
You need to be logged in to leave comments. Login now