1
"""
2
Defines various Select widgets which allow choosing one or more items
3
from a list of options.
4
"""
5 7
from __future__ import absolute_import, division, unicode_literals
6

7 7
import re
8

9 7
from collections import OrderedDict
10

11 7
import param
12

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

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

27

28 7
class SelectBase(Widget):
29

30 7
    options = param.ClassSelector(default=[], class_=(dict, list))
31

32 7
    __abstract = True
33

34 7
    @property
35 2
    def labels(self):
36 7
        return [as_unicode(o) for o in self.options]
37

38 7
    @property
39 2
    def values(self):
40 7
        if isinstance(self.options, dict):
41 7
            return list(self.options.values())
42
        else:
43 7
            return self.options
44

45 7
    @property
46 2
    def _items(self):
47 7
        return OrderedDict(zip(self.labels, self.values))
48

49

50

51 7
class SingleSelectBase(SelectBase):
52

53 7
    value = param.Parameter(default=None)
54

55 7
    _supports_embed = True
56

57 7
    __abstract = True
58

59 7
    def __init__(self, **params):
60 7
        super(SingleSelectBase, self).__init__(**params)
61 7
        values = self.values
62 7
        if self.value is None and None not in values and values:
63 7
            self.value = values[0]
64

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

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

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

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

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

126

127 7
class Select(SingleSelectBase):
128

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

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

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

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

149

150 7
class _MultiSelectBase(SingleSelectBase):
151

152 7
    value = param.List(default=[])
153

154 7
    _supports_embed = False
155

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

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

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

178

179 7
class MultiSelect(_MultiSelectBase):
180

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

185 7
    _widget_type = _BkMultiSelect
186

187

188 7
class MultiChoice(_MultiSelectBase):
189

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

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

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

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

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

205 7
    _widget_type = _BkMultiChoice
206

207

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

213 7
class AutocompleteInput(Widget):
214

215 7
    min_characters = param.Integer(default=2, doc="""
216
        The number of characters a user must type before
217
        completions are presented.""")
218

219 7
    options = param.List(default=[])
220

221 7
    placeholder = param.String(default='')
222

223 7
    value = param.Parameter(default=None)
224
    
225 7
    case_sensitive = param.Boolean(default=True)
226

227 7
    restrict = param.Boolean(default=True)
228

229 7
    _widget_type = _BkAutocompleteInput
230

231 7
    _rename = _AutocompleteInput_rename
232

233

234 7
class _RadioGroupBase(SingleSelectBase):
235

236 7
    _supports_embed = False
237

238 7
    _rename = {'name': None, 'options': 'labels', 'value': 'active'}
239

240 7
    _source_transforms = {'value': "source.labels[value]"}
241

242 7
    _target_transforms = {'value': "target.labels.indexOf(value)"}
243

244 7
    __abstract = True
245

246 7
    def _process_param_change(self, msg):
247 7
        msg = super(SingleSelectBase, self)._process_param_change(msg)
248 7
        values = self.values
249 7
        if 'active' in msg:
250 7
            value = msg['active']
251 7
            if value in values:
252 7
                msg['active'] = indexOf(value, values)
253
            else:
254 7
                if self.value is not None:
255 0
                    self.value = None
256 7
                msg['active'] = None
257

258 7
        if 'labels' in msg:
259 7
            msg['labels'] = self.labels
260 7
            value = self.value
261 7
            if not isIn(value, values):
262 7
                self.value = None
263 7
        return msg
264

265 7
    def _process_property_change(self, msg):
266 7
        msg = super(SingleSelectBase, self)._process_property_change(msg)
267 7
        if 'value' in msg:
268 7
            index = msg['value']
269 7
            if index is None:
270 0
                msg['value'] = None
271
            else:
272 7
                msg['value'] = list(self.values)[index]
273 7
        return msg
274

275 7
    def _get_embed_state(self, root, values=None, max_opts=3):
276 0
        if values is None:
277 0
            values = self.values
278 0
        elif any(v not in self.values for v in values):
279 0
            raise ValueError("Supplied embed states were not found in "
280
                             "the %s widgets values list." %
281
                             type(self).__name__)
282 0
        return (self, self._models[root.ref['id']][0], values,
283
                lambda x: x.active, 'active', 'cb_obj.active')
284

285

286

287 7
class RadioButtonGroup(_RadioGroupBase, _ButtonBase):
288

289 7
    _widget_type = _BkRadioButtonGroup
290

291 7
    _supports_embed = True
292

293

294

295 7
class RadioBoxGroup(_RadioGroupBase):
296

297 7
    inline = param.Boolean(default=False, doc="""
298
        Whether the items be arrange vertically (``False``) or
299
        horizontally in-line (``True``).""")
300

301 7
    _supports_embed = True
302

303 7
    _widget_type = _BkRadioBoxGroup
304

305

306

307 7
class _CheckGroupBase(SingleSelectBase):
308

309 7
    value = param.List(default=[])
310

311 7
    _rename = {'name': None, 'options': 'labels', 'value': 'active'}
312

313 7
    _source_transforms = {'value': "value.map((index) => source.labels[index])"}
314

315 7
    _target_transforms = {'value': "value.map((label) => target.labels.indexOf(label))"}
316

317 7
    _supports_embed = False
318

319 7
    __abstract = True
320

321 7
    def _process_param_change(self, msg):
322 7
        msg = super(SingleSelectBase, self)._process_param_change(msg)
323 7
        values = self.values
324 7
        if 'active' in msg:
325 7
            msg['active'] = [indexOf(v, values) for v in msg['active']
326
                             if isIn(v, values)]
327 7
        if 'labels' in msg:
328 7
            msg['labels'] = self.labels
329 7
            if any(not isIn(v, values) for v in self.value):
330 0
                self.value = [v for v in self.value if isIn(v, values)]
331 7
        msg.pop('title', None)
332 7
        return msg
333

334 7
    def _process_property_change(self, msg):
335 7
        msg = super(SingleSelectBase, self)._process_property_change(msg)
336 7
        if 'value' in msg:
337 7
            values = self.values
338 7
            msg['value'] = [values[a] for a in msg['value']]
339 7
        return msg
340

341

342

343 7
class CheckButtonGroup(_CheckGroupBase, _ButtonBase):
344

345 7
    _widget_type = _BkCheckboxButtonGroup
346

347

348 7
class CheckBoxGroup(_CheckGroupBase):
349

350 7
    inline = param.Boolean(default=False, doc="""
351
        Whether the items be arrange vertically (``False``) or
352
        horizontally in-line (``True``).""")
353

354 7
    _widget_type = _BkCheckboxGroup
355

356

357

358 7
class ToggleGroup(SingleSelectBase):
359
    """This class is a factory of ToggleGroup widgets.
360

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

363
    Two types of widgets are available through the widget_type argument :
364
        * `'button'` (default)
365
        * `'box'`
366

367
    Two different behaviors are available through behavior argument:
368
        * 'check' (default) : boolean
369
           Any number of widgets can be selected. In this case value
370
           is a 'list' of objects.
371
        * 'radio' : boolean
372
           One and only one widget is switched on. In this case value
373
           is an 'object'.
374
    """
375

376 7
    _widgets_type = ['button', 'box']
377 7
    _behaviors = ['check', 'radio']
378

379 7
    def __new__(cls, widget_type='button', behavior='check', **params):
380

381 7
        if widget_type not in ToggleGroup._widgets_type:
382 7
            raise ValueError('widget_type {} is not valid. Valid options are {}'
383
                             .format(widget_type, ToggleGroup._widgets_type))
384 7
        if behavior not in ToggleGroup._behaviors:
385 7
            raise ValueError('behavior {} is not valid. Valid options are {}'
386
                             .format(widget_type, ToggleGroup._behaviors))
387

388 7
        if behavior == 'check':
389 7
            if widget_type == 'button':
390 7
                return CheckButtonGroup(**params)
391
            else:
392 7
                return CheckBoxGroup(**params)
393
        else:
394 7
            if isinstance(params.get('value'), list):
395 7
                raise ValueError('Radio buttons require a single value, '
396
                                 'found: %s' % params['value'])
397 7
            if widget_type == 'button':
398 7
                return RadioButtonGroup(**params)
399
            else:
400 7
                return RadioBoxGroup(**params)
401

402

403

404 7
class CrossSelector(CompositeWidget, MultiSelect):
405
    """
406
    A composite widget which allows selecting from a list of items
407
    by moving them between two lists. Supports filtering values by
408
    name to select them in bulk.
409
    """
410

411 7
    width = param.Integer(default=600, allow_None=True, doc="""
412
        The number of options shown at once (note this is the
413
        only way to control the height of this widget)""")
414

415 7
    height = param.Integer(default=200, allow_None=True, doc="""
416
        The number of options shown at once (note this is the
417
        only way to control the height of this widget)""")
418

419 7
    filter_fn = param.Callable(default=re.search, doc="""
420
        The filter function applied when querying using the text
421
        fields, defaults to re.search. Function is two arguments, the
422
        query or pattern and the item label.""")
423

424 7
    size = param.Integer(default=10, doc="""
425
        The number of options shown at once (note this is the only way
426
        to control the height of this widget)""")
427

428 7
    definition_order = param.Integer(default=True, doc="""
429
       Whether to preserve definition order after filtering. Disable
430
       to allow the order of selection to define the order of the
431
       selected list.""")
432

433 7
    def __init__(self, **params):
434 7
        super(CrossSelector, self).__init__(**params)
435
        # Compute selected and unselected values
436

437 7
        labels, values = self.labels, self.values
438 7
        selected = [labels[indexOf(v, values)] for v in params.get('value', [])
439
                    if isIn(v, values)]
440 7
        unselected = [k for k in labels if k not in selected]
441 7
        layout = dict(sizing_mode='stretch_both', background=self.background, margin=0)
442 7
        self._lists = {
443
            False: MultiSelect(options=unselected, size=self.size, **layout),
444
            True: MultiSelect(options=selected, size=self.size, **layout)
445
        }
446 7
        self._lists[False].param.watch(self._update_selection, 'value')
447 7
        self._lists[True].param.watch(self._update_selection, 'value')
448

449
        # Define buttons
450 7
        self._buttons = {False: Button(name='<<', width=50),
451
                         True: Button(name='>>', width=50)}
452

453 7
        self._buttons[False].param.watch(self._apply_selection, 'clicks')
454 7
        self._buttons[True].param.watch(self._apply_selection, 'clicks')
455

456
        # Define search
457 7
        self._search = {
458
            False: TextInput(placeholder='Filter available options',
459
                             margin=(0, 0, 10, 0), width_policy='max'),
460
            True: TextInput(placeholder='Filter selected options',
461
                            margin=(0, 0, 10, 0), width_policy='max')
462
        }
463 7
        self._search[False].param.watch(self._filter_options, 'value')
464 7
        self._search[True].param.watch(self._filter_options, 'value')
465

466 7
        self._placeholder = TextAreaInput(
467
            placeholder=("To select an item highlight it on the left "
468
                         "and use the arrow button to move it to the right."),
469
            disabled=True, **layout
470
        )
471 7
        right = self._lists[True] if self.value else self._placeholder
472

473
        # Define Layout
474 7
        self._unselected = Column(self._search[False], self._lists[False], **layout)
475 7
        self._selected = Column(self._search[True], right, **layout)
476 7
        buttons = Column(self._buttons[True], self._buttons[False], margin=(0, 5))
477

478 7
        self._composite[:] = [
479
            self._unselected, Column(VSpacer(), buttons, VSpacer()), self._selected
480
        ]
481

482 7
        self._selections = {False: [], True: []}
483 7
        self._query = {False: '', True: ''}
484

485 7
    @param.depends('size', watch=True)
486 2
    def _update_size(self):
487 0
        self._lists[False].size = self.size
488 0
        self._lists[True].size = self.size
489

490 7
    @param.depends('disabled', watch=True)
491 2
    def _update_disabled(self):
492 0
        self._buttons[False].disabled = self.disabled
493 0
        self._buttons[True].disabled = self.disabled
494

495 7
    @param.depends('value', watch=True)
496 2
    def _update_value(self):
497 7
        labels, values = self.labels, self.values
498 7
        selected = [labels[indexOf(v, values)] for v in self.value
499
                    if isIn(v, values)]
500 7
        unselected = [k for k in labels if k not in selected]
501 7
        self._lists[True].options = selected
502 7
        self._lists[True].value = []
503 7
        self._lists[False].options = unselected
504 7
        self._lists[False].value = []
505 7
        if len(self._lists[True].options) and self._selected[-1] is not self._lists[True]:
506 0
            self._selected[-1] = self._lists[True]
507 7
        elif not len(self._lists[True].options) and self._selected[-1] is not self._placeholder:
508 7
            self._selected[-1] = self._placeholder
509

510 7
    @param.depends('options', watch=True)
511 2
    def _update_options(self):
512
        """
513
        Updates the options of each of the sublists after the options
514
        for the whole widget are updated.
515
        """
516 7
        self._selections[False] = []
517 7
        self._selections[True] = []
518 7
        self._update_value()
519

520 7
    def _apply_filters(self):
521 7
        self._apply_query(False)
522 7
        self._apply_query(True)
523

524 7
    def _filter_options(self, event):
525
        """
526
        Filters unselected options based on a text query event.
527
        """
528 7
        selected = event.obj is self._search[True]
529 7
        self._query[selected] = event.new
530 7
        self._apply_query(selected)
531

532 7
    def _apply_query(self, selected):
533 7
        query = self._query[selected]
534 7
        other = self._lists[not selected].labels
535 7
        labels = self.labels
536 7
        if self.definition_order:
537 7
            options = [k for k in labels if k not in other]
538
        else:
539 7
            options = self._lists[selected].values
540 7
        if not query:
541 7
            self._lists[selected].options = options
542 7
            self._lists[selected].value = []
543
        else:
544 7
            try:
545 7
                matches = [o for o in options if self.filter_fn(query, o)]
546 0
            except Exception:
547 0
                matches = []
548 7
            self._lists[selected].options = options if options else []
549 7
            self._lists[selected].value = [m for m in matches]
550

551 7
    def _update_selection(self, event):
552
        """
553
        Updates the current selection in each list.
554
        """
555 7
        selected = event.obj is self._lists[True]
556 7
        self._selections[selected] = [v for v in event.new if v != '']
557

558 7
    def _apply_selection(self, event):
559
        """
560
        Applies the current selection depending on which button was
561
        pressed.
562
        """
563 7
        selected = event.obj is self._buttons[True]
564

565 7
        new = OrderedDict([(k, self._items[k]) for k in self._selections[not selected]])
566 7
        old = self._lists[selected].options
567 7
        other = self._lists[not selected].options
568

569 7
        merged = OrderedDict([(k, k) for k in list(old)+list(new)])
570 7
        leftovers = OrderedDict([(k, k) for k in other if k not in new])
571 7
        self._lists[selected].options = merged if merged else {}
572 7
        self._lists[not selected].options = leftovers if leftovers else {}
573 7
        if len(self._lists[True].options):
574 7
            self._selected[-1] = self._lists[True]
575
        else:
576 7
            self._selected[-1] = self._placeholder
577 7
        self.value = [self._items[o] for o in self._lists[True].options if o != '']
578 7
        self._apply_filters()
579

580 7
    def _get_model(self, doc, root=None, parent=None, comm=None):
581 0
        return self._composite._get_model(doc, root, parent, comm)

Read our documentation on viewing source code .

Loading