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