holoviz / panel
1
"""
2
Defines various Select widgets which allow choosing one or more items
3
from a list of options.
4
"""
5 2
import re
6

7 2
from collections import OrderedDict
8

9 2
import param
10

11 2
from bokeh.models.widgets import (
12
    AutocompleteInput as _BkAutocompleteInput, CheckboxGroup as _BkCheckboxGroup,
13
    CheckboxButtonGroup as _BkCheckboxButtonGroup, MultiSelect as _BkMultiSelect,
14
    RadioButtonGroup as _BkRadioButtonGroup, RadioGroup as _BkRadioBoxGroup,
15
    Select as _BkSelect, MultiChoice as _BkMultiChoice
16
)
17

18 2
from ..layout import Column, VSpacer
19 2
from ..models import SingleSelect as _BkSingleSelect
20 2
from ..util import as_unicode, isIn, indexOf, bokeh_version
21 2
from .base import Widget, CompositeWidget
22 2
from .button import _ButtonBase, Button
23 2
from .input import TextInput, TextAreaInput
24

25

26 2
class SelectBase(Widget):
27

28 2
    options = param.ClassSelector(default=[], class_=(dict, list))
29

30 2
    __abstract = True
31

32 2
    @property
33 1
    def labels(self):
34 2
        return [as_unicode(o) for o in self.options]
35

36 2
    @property
37 1
    def values(self):
38 2
        if isinstance(self.options, dict):
39 2
            return list(self.options.values())
40
        else:
41 2
            return self.options
42

43 2
    @property
44 1
    def _items(self):
45 2
        return OrderedDict(zip(self.labels, self.values))
46

47

48

49 2
class SingleSelectBase(SelectBase):
50

51 2
    value = param.Parameter(default=None)
52

53 2
    _supports_embed = True
54

55 2
    __abstract = True
56

57 2
    def __init__(self, **params):
58 2
        super().__init__(**params)
59 2
        values = self.values
60 2
        if self.value is None and None not in values and values:
61 2
            self.value = values[0]
62

63 2
    def _process_param_change(self, msg):
64 2
        msg = super()._process_param_change(msg)
65 2
        labels, values = self.labels, self.values
66 2
        unique = len(set(self.unicode_values)) == len(labels)
67 2
        if 'value' in msg:
68 2
            val = msg['value']
69 2
            if isIn(val, values):
70 2
                unicode_values = self.unicode_values if unique else labels
71 2
                msg['value'] = unicode_values[indexOf(val, values)]
72 2
            elif values:
73 2
                self.value = self.values[0]
74
            else:
75 2
                self.value = None
76 2
                msg['value'] = ''
77

78 2
        if 'options' in msg:
79 2
            if isinstance(self.options, dict):
80 2
                if unique:
81 2
                    options = [(v, l) for l,v in zip(labels, self.unicode_values)]
82
                else:
83 0
                    options = labels
84 2
                msg['options'] = options
85
            else:
86 2
                msg['options'] = self.unicode_values
87 2
            val = self.value
88 2
            if values:
89 2
                if not isIn(val, values):
90 2
                    self.value = values[0]
91
            else:
92 2
                self.value = None
93 2
        return msg
94

95 2
    @property
96 1
    def unicode_values(self):
97 2
        return [as_unicode(v) for v in self.values]
98

99 2
    def _process_property_change(self, msg):
100 2
        msg = super()._process_property_change(msg)
101 2
        if 'value' in msg:
102 2
            if not self.values:
103 0
                pass
104 2
            elif msg['value'] == '':
105 0
                msg['value'] = self.values[0] if self.values else None
106
            else:
107 2
                if isIn(msg['value'], self.unicode_values):
108 2
                    idx = indexOf(msg['value'], self.unicode_values)
109
                else:
110 0
                    idx = indexOf(msg['value'], self.labels)
111 2
                msg['value'] = self._items[self.labels[idx]]
112 2
        msg.pop('options', None)
113 2
        return msg
114

115 2
    def _get_embed_state(self, root, values=None, max_opts=3):
116 2
        if values is None:
117 2
            values = self.values
118 2
        elif any(v not in self.values for v in values):
119 2
            raise ValueError("Supplied embed states were not found "
120
                             "in the %s widgets values list." %
121
                             type(self).__name__)
122 2
        return (self, self._models[root.ref['id']][0], values,
123
                lambda x: x.value, 'value', 'cb_obj.value')
124

125

126 2
class Select(SingleSelectBase):
127

128 2
    size = param.Integer(default=1, bounds=(1, None), doc="""
129
        Declares how many options are displayed at the same time.
130
        If set to 1 displays options as dropdown otherwise displays
131
        scrollable area.""")
132

133 2
    _source_transforms = {'size': None}
134

135 2
    @property
136 1
    def _widget_type(self):
137 2
        return _BkSelect if self.size == 1 else _BkSingleSelect
138

139 2
    def __init__(self, **params):
140 2
        super().__init__(**params)
141 2
        if self.size == 1:
142 2
            self.param.size.constant = True
143

144 2
    def _process_param_change(self, msg):
145 2
        msg = super()._process_param_change(msg)
146 2
        if msg.get('size') == 1:
147 2
            msg.pop('size')
148 2
        return msg
149

150

151 2
class _MultiSelectBase(SingleSelectBase):
152

153 2
    value = param.List(default=[])
154

155 2
    _supports_embed = False
156

157 2
    def _process_param_change(self, msg):
158 2
        msg = super(SingleSelectBase, self)._process_param_change(msg)
159 2
        labels, values = self.labels, self.values
160 2
        if 'value' in msg:
161 2
            msg['value'] = [labels[indexOf(v, values)] for v in msg['value']
162
                            if isIn(v, values)]
163

164 2
        if 'options' in msg:
165 2
            msg['options'] = labels
166 2
            if any(not isIn(v, values) for v in self.value):
167 2
                self.value = [v for v in self.value if isIn(v, values)]
168 2
        return msg
169

170 2
    def _process_property_change(self, msg):
171 2
        msg = super(SingleSelectBase, self)._process_property_change(msg)
172 2
        if 'value' in msg:
173 2
            labels = self.labels
174 2
            msg['value'] = [self._items[v] for v in msg['value']
175
                            if v in labels]
176 2
        msg.pop('options', None)
177 2
        return msg
178

179

180 2
class MultiSelect(_MultiSelectBase):
181

182 2
    size = param.Integer(default=4, doc="""
183
        The number of items displayed at once (i.e. determines the
184
        widget height).""")
185

186 2
    _widget_type = _BkMultiSelect
187

188

189 2
class MultiChoice(_MultiSelectBase):
190

191 2
    delete_button = param.Boolean(default=True, doc="""
192
        Whether to display a button to delete a selected option.""")
193

194 2
    max_items = param.Integer(default=None, bounds=(1, None), doc="""
195
        Maximum number of options that can be selected.""")
196

197 2
    option_limit = param.Integer(default=None, bounds=(1, None), doc="""
198
        Maximum number of options to display at once.""")
199

200 2
    placeholder = param.String(default='', doc="""
201
        String displayed when no selection has been made.""")
202

203 2
    solid = param.Boolean(default=True, doc="""
204
        Whether to display widget with solid or light style.""")
205

206 2
    _widget_type = _BkMultiChoice
207

208

209 2
_AutocompleteInput_rename = {'name': 'title', 'options': 'completions'}
210 2
if bokeh_version < '2.3.0':
211
    # disable restrict keyword
212 0
    _AutocompleteInput_rename['restrict'] = None
213

214 2
class AutocompleteInput(Widget):
215

216 2
    case_sensitive = param.Boolean(default=True, doc="""
217
        Enable or disable case sensitivity.""")
218

219 2
    min_characters = param.Integer(default=2, doc="""
220
        The number of characters a user must type before
221
        completions are presented.""")
222

223 2
    options = param.List(default=[], doc="""
224
        A list of completion strings. This will be used to guide the
225
        user upon typing the beginning of a desired value.""")
226

227 2
    placeholder = param.String(default='', doc="""
228
        Placeholder for empty input field.""")
229

230 2
    restrict = param.Boolean(default=True, doc="""
231
        Set to False in order to allow users to enter text that is not
232
        present in the list of completion strings.""")
233

234 2
    value = param.String(default='', allow_None=True, doc="""
235
      Initial or entered text value updated when <enter> key is pressed.""")
236

237 2
    value_input = param.String(default='', allow_None=True, doc="""
238
      Initial or entered text value updated on every key press.""")
239

240 2
    _widget_type = _BkAutocompleteInput
241

242 2
    _rename = _AutocompleteInput_rename
243

244

245 2
class _RadioGroupBase(SingleSelectBase):
246

247 2
    _supports_embed = False
248

249 2
    _rename = {'name': None, 'options': 'labels', 'value': 'active'}
250

251 2
    _source_transforms = {'value': "source.labels[value]"}
252

253 2
    _target_transforms = {'value': "target.labels.indexOf(value)"}
254

255 2
    __abstract = True
256

257 2
    def _process_param_change(self, msg):
258 2
        msg = super(SingleSelectBase, self)._process_param_change(msg)
259 2
        values = self.values
260 2
        if 'active' in msg:
261 2
            value = msg['active']
262 2
            if value in values:
263 2
                msg['active'] = indexOf(value, values)
264
            else:
265 2
                if self.value is not None:
266 0
                    self.value = None
267 2
                msg['active'] = None
268

269 2
        if 'labels' in msg:
270 2
            msg['labels'] = self.labels
271 2
            value = self.value
272 2
            if not isIn(value, values):
273 2
                self.value = None
274 2
        return msg
275

276 2
    def _process_property_change(self, msg):
277 2
        msg = super(SingleSelectBase, self)._process_property_change(msg)
278 2
        if 'value' in msg:
279 2
            index = msg['value']
280 2
            if index is None:
281 0
                msg['value'] = None
282
            else:
283 2
                msg['value'] = list(self.values)[index]
284 2
        return msg
285

286 2
    def _get_embed_state(self, root, values=None, max_opts=3):
287 0
        if values is None:
288 0
            values = self.values
289 0
        elif any(v not in self.values for v in values):
290 0
            raise ValueError("Supplied embed states were not found in "
291
                             "the %s widgets values list." %
292
                             type(self).__name__)
293 0
        return (self, self._models[root.ref['id']][0], values,
294
                lambda x: x.active, 'active', 'cb_obj.active')
295

296

297

298 2
class RadioButtonGroup(_RadioGroupBase, _ButtonBase):
299

300 2
    _widget_type = _BkRadioButtonGroup
301

302 2
    _supports_embed = True
303

304

305

306 2
class RadioBoxGroup(_RadioGroupBase):
307

308 2
    inline = param.Boolean(default=False, doc="""
309
        Whether the items be arrange vertically (``False``) or
310
        horizontally in-line (``True``).""")
311

312 2
    _supports_embed = True
313

314 2
    _widget_type = _BkRadioBoxGroup
315

316

317

318 2
class _CheckGroupBase(SingleSelectBase):
319

320 2
    value = param.List(default=[])
321

322 2
    _rename = {'name': None, 'options': 'labels', 'value': 'active'}
323

324 2
    _source_transforms = {'value': "value.map((index) => source.labels[index])"}
325

326 2
    _target_transforms = {'value': "value.map((label) => target.labels.indexOf(label))"}
327

328 2
    _supports_embed = False
329

330 2
    __abstract = True
331

332 2
    def _process_param_change(self, msg):
333 2
        msg = super(SingleSelectBase, self)._process_param_change(msg)
334 2
        values = self.values
335 2
        if 'active' in msg:
336 2
            msg['active'] = [indexOf(v, values) for v in msg['active']
337
                             if isIn(v, values)]
338 2
        if 'labels' in msg:
339 2
            msg['labels'] = self.labels
340 2
            if any(not isIn(v, values) for v in self.value):
341 0
                self.value = [v for v in self.value if isIn(v, values)]
342 2
        msg.pop('title', None)
343 2
        return msg
344

345 2
    def _process_property_change(self, msg):
346 2
        msg = super(SingleSelectBase, self)._process_property_change(msg)
347 2
        if 'value' in msg:
348 2
            values = self.values
349 2
            msg['value'] = [values[a] for a in msg['value']]
350 2
        return msg
351

352

353

354 2
class CheckButtonGroup(_CheckGroupBase, _ButtonBase):
355

356 2
    _widget_type = _BkCheckboxButtonGroup
357

358

359 2
class CheckBoxGroup(_CheckGroupBase):
360

361 2
    inline = param.Boolean(default=False, doc="""
362
        Whether the items be arrange vertically (``False``) or
363
        horizontally in-line (``True``).""")
364

365 2
    _widget_type = _BkCheckboxGroup
366

367

368

369 2
class ToggleGroup(SingleSelectBase):
370
    """This class is a factory of ToggleGroup widgets.
371

372
    A ToggleGroup is a group of widgets which can be switched 'on' or 'off'.
373

374
    Two types of widgets are available through the widget_type argument :
375
        * `'button'` (default)
376
        * `'box'`
377

378
    Two different behaviors are available through behavior argument:
379
        * 'check' (default) : boolean
380
           Any number of widgets can be selected. In this case value
381
           is a 'list' of objects.
382
        * 'radio' : boolean
383
           One and only one widget is switched on. In this case value
384
           is an 'object'.
385
    """
386

387 2
    _widgets_type = ['button', 'box']
388 2
    _behaviors = ['check', 'radio']
389

390 2
    def __new__(cls, widget_type='button', behavior='check', **params):
391

392 2
        if widget_type not in ToggleGroup._widgets_type:
393 2
            raise ValueError('widget_type {} is not valid. Valid options are {}'
394
                             .format(widget_type, ToggleGroup._widgets_type))
395 2
        if behavior not in ToggleGroup._behaviors:
396 2
            raise ValueError('behavior {} is not valid. Valid options are {}'
397
                             .format(widget_type, ToggleGroup._behaviors))
398

399 2
        if behavior == 'check':
400 2
            if widget_type == 'button':
401 2
                return CheckButtonGroup(**params)
402
            else:
403 2
                return CheckBoxGroup(**params)
404
        else:
405 2
            if isinstance(params.get('value'), list):
406 2
                raise ValueError('Radio buttons require a single value, '
407
                                 'found: %s' % params['value'])
408 2
            if widget_type == 'button':
409 2
                return RadioButtonGroup(**params)
410
            else:
411 2
                return RadioBoxGroup(**params)
412

413

414

415 2
class CrossSelector(CompositeWidget, MultiSelect):
416
    """
417
    A composite widget which allows selecting from a list of items
418
    by moving them between two lists. Supports filtering values by
419
    name to select them in bulk.
420
    """
421

422 2
    width = param.Integer(default=600, allow_None=True, doc="""
423
        The number of options shown at once (note this is the
424
        only way to control the height of this widget)""")
425

426 2
    height = param.Integer(default=200, allow_None=True, doc="""
427
        The number of options shown at once (note this is the
428
        only way to control the height of this widget)""")
429

430 2
    filter_fn = param.Callable(default=re.search, doc="""
431
        The filter function applied when querying using the text
432
        fields, defaults to re.search. Function is two arguments, the
433
        query or pattern and the item label.""")
434

435 2
    size = param.Integer(default=10, doc="""
436
        The number of options shown at once (note this is the only way
437
        to control the height of this widget)""")
438

439 2
    definition_order = param.Integer(default=True, doc="""
440
       Whether to preserve definition order after filtering. Disable
441
       to allow the order of selection to define the order of the
442
       selected list.""")
443

444 2
    def __init__(self, **params):
445 2
        super().__init__(**params)
446
        # Compute selected and unselected values
447

448 2
        labels, values = self.labels, self.values
449 2
        selected = [labels[indexOf(v, values)] for v in params.get('value', [])
450
                    if isIn(v, values)]
451 2
        unselected = [k for k in labels if k not in selected]
452 2
        layout = dict(sizing_mode='stretch_both', background=self.background, margin=0)
453 2
        self._lists = {
454
            False: MultiSelect(options=unselected, size=self.size, **layout),
455
            True: MultiSelect(options=selected, size=self.size, **layout)
456
        }
457 2
        self._lists[False].param.watch(self._update_selection, 'value')
458 2
        self._lists[True].param.watch(self._update_selection, 'value')
459

460
        # Define buttons
461 2
        self._buttons = {False: Button(name='<<', width=50),
462
                         True: Button(name='>>', width=50)}
463

464 2
        self._buttons[False].param.watch(self._apply_selection, 'clicks')
465 2
        self._buttons[True].param.watch(self._apply_selection, 'clicks')
466

467
        # Define search
468 2
        self._search = {
469
            False: TextInput(placeholder='Filter available options',
470
                             margin=(0, 0, 10, 0), width_policy='max'),
471
            True: TextInput(placeholder='Filter selected options',
472
                            margin=(0, 0, 10, 0), width_policy='max')
473
        }
474 2
        self._search[False].param.watch(self._filter_options, 'value')
475 2
        self._search[True].param.watch(self._filter_options, 'value')
476

477 2
        self._placeholder = TextAreaInput(
478
            placeholder=("To select an item highlight it on the left "
479
                         "and use the arrow button to move it to the right."),
480
            disabled=True, **layout
481
        )
482 2
        right = self._lists[True] if self.value else self._placeholder
483

484
        # Define Layout
485 2
        self._unselected = Column(self._search[False], self._lists[False], **layout)
486 2
        self._selected = Column(self._search[True], right, **layout)
487 2
        buttons = Column(self._buttons[True], self._buttons[False], margin=(0, 5))
488

489 2
        self._composite[:] = [
490
            self._unselected, Column(VSpacer(), buttons, VSpacer()), self._selected
491
        ]
492

493 2
        self._selections = {False: [], True: []}
494 2
        self._query = {False: '', True: ''}
495

496 2
    @param.depends('size', watch=True)
497 1
    def _update_size(self):
498 0
        self._lists[False].size = self.size
499 0
        self._lists[True].size = self.size
500

501 2
    @param.depends('disabled', watch=True)
502 1
    def _update_disabled(self):
503 0
        self._buttons[False].disabled = self.disabled
504 0
        self._buttons[True].disabled = self.disabled
505

506 2
    @param.depends('value', watch=True)
507 1
    def _update_value(self):
508 2
        labels, values = self.labels, self.values
509 2
        selected = [labels[indexOf(v, values)] for v in self.value
510
                    if isIn(v, values)]
511 2
        unselected = [k for k in labels if k not in selected]
512 2
        self._lists[True].options = selected
513 2
        self._lists[True].value = []
514 2
        self._lists[False].options = unselected
515 2
        self._lists[False].value = []
516 2
        if len(self._lists[True].options) and self._selected[-1] is not self._lists[True]:
517 0
            self._selected[-1] = self._lists[True]
518 2
        elif not len(self._lists[True].options) and self._selected[-1] is not self._placeholder:
519 2
            self._selected[-1] = self._placeholder
520

521 2
    @param.depends('options', watch=True)
522 1
    def _update_options(self):
523
        """
524
        Updates the options of each of the sublists after the options
525
        for the whole widget are updated.
526
        """
527 2
        self._selections[False] = []
528 2
        self._selections[True] = []
529 2
        self._update_value()
530

531 2
    def _apply_filters(self):
532 2
        self._apply_query(False)
533 2
        self._apply_query(True)
534

535 2
    def _filter_options(self, event):
536
        """
537
        Filters unselected options based on a text query event.
538
        """
539 2
        selected = event.obj is self._search[True]
540 2
        self._query[selected] = event.new
541 2
        self._apply_query(selected)
542

543 2
    def _apply_query(self, selected):
544 2
        query = self._query[selected]
545 2
        other = self._lists[not selected].labels
546 2
        labels = self.labels
547 2
        if self.definition_order:
548 2
            options = [k for k in labels if k not in other]
549
        else:
550 2
            options = self._lists[selected].values
551 2
        if not query:
552 2
            self._lists[selected].options = options
553 2
            self._lists[selected].value = []
554
        else:
555 2
            try:
556 2
                matches = [o for o in options if self.filter_fn(query, o)]
557 0
            except Exception:
558 0
                matches = []
559 2
            self._lists[selected].options = options if options else []
560 2
            self._lists[selected].value = [m for m in matches]
561

562 2
    def _update_selection(self, event):
563
        """
564
        Updates the current selection in each list.
565
        """
566 2
        selected = event.obj is self._lists[True]
567 2
        self._selections[selected] = [v for v in event.new if v != '']
568

569 2
    def _apply_selection(self, event):
570
        """
571
        Applies the current selection depending on which button was
572
        pressed.
573
        """
574 2
        selected = event.obj is self._buttons[True]
575

576 2
        new = OrderedDict([(k, self._items[k]) for k in self._selections[not selected]])
577 2
        old = self._lists[selected].options
578 2
        other = self._lists[not selected].options
579

580 2
        merged = OrderedDict([(k, k) for k in list(old)+list(new)])
581 2
        leftovers = OrderedDict([(k, k) for k in other if k not in new])
582 2
        self._lists[selected].options = merged if merged else {}
583 2
        self._lists[not selected].options = leftovers if leftovers else {}
584 2
        if len(self._lists[True].options):
585 2
            self._selected[-1] = self._lists[True]
586
        else:
587 2
            self._selected[-1] = self._placeholder
588 2
        self.value = [self._items[o] for o in self._lists[True].options if o != '']
589 2
        self._apply_filters()
590

591 2
    def _get_model(self, doc, root=None, parent=None, comm=None):
592 0
        return self._composite._get_model(doc, root, parent, comm)

Read our documentation on viewing source code .

Loading