| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637 |
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- """Selection classes.
- Represents an enumeration using a widget.
- """
- try:
- from collections.abc import Iterable, Mapping
- except ImportError:
- from collections import Iterable, Mapping # py2
- try:
- from itertools import izip
- except ImportError: #python3.x
- izip = zip
- from itertools import chain
- from .widget_description import DescriptionWidget, DescriptionStyle
- from .valuewidget import ValueWidget
- from .widget_core import CoreWidget
- from .widget_style import Style
- from .trait_types import InstanceDict, TypedTuple
- from .widget import register, widget_serialization
- from .docutils import doc_subst
- from traitlets import (Unicode, Bool, Int, Any, Dict, TraitError, CaselessStrEnum,
- Tuple, Union, observe, validate)
- from ipython_genutils.py3compat import unicode_type
- _doc_snippets = {}
- _doc_snippets['selection_params'] = """
- options: list
- The options for the dropdown. This can either be a list of values, e.g.
- ``['Galileo', 'Brahe', 'Hubble']`` or ``[0, 1, 2]``, or a list of
- (label, value) pairs, e.g.
- ``[('Galileo', 0), ('Brahe', 1), ('Hubble', 2)]``.
- index: int
- The index of the current selection.
- value: any
- The value of the current selection. When programmatically setting the
- value, a reverse lookup is performed among the options to check that
- the value is valid. The reverse lookup uses the equality operator by
- default, but another predicate may be provided via the ``equals``
- keyword argument. For example, when dealing with numpy arrays, one may
- set ``equals=np.array_equal``.
- label: str
- The label corresponding to the selected value.
- disabled: bool
- Whether to disable user changes.
- description: str
- Label for this input group. This should be a string
- describing the widget.
- """
- _doc_snippets['multiple_selection_params'] = """
- options: dict or list
- The options for the dropdown. This can either be a list of values, e.g.
- ``['Galileo', 'Brahe', 'Hubble']`` or ``[0, 1, 2]``, a list of
- (label, value) pairs, e.g.
- ``[('Galileo', 0), ('Brahe', 1), ('Hubble', 2)]``,
- or a dictionary mapping the labels to the values, e.g. ``{'Galileo': 0,
- 'Brahe': 1, 'Hubble': 2}``. The labels are the strings that will be
- displayed in the UI, representing the actual Python choices, and should
- be unique. If this is a dictionary, the order in which they are
- displayed is not guaranteed.
- index: iterable of int
- The indices of the options that are selected.
- value: iterable
- The values that are selected. When programmatically setting the
- value, a reverse lookup is performed among the options to check that
- the value is valid. The reverse lookup uses the equality operator by
- default, but another predicate may be provided via the ``equals``
- keyword argument. For example, when dealing with numpy arrays, one may
- set ``equals=np.array_equal``.
- label: iterable of str
- The labels corresponding to the selected value.
- disabled: bool
- Whether to disable user changes.
- description: str
- Label for this input group. This should be a string
- describing the widget.
- """
- _doc_snippets['slider_params'] = """
- orientation: str
- Either ``'horizontal'`` or ``'vertical'``. Defaults to ``horizontal``.
- readout: bool
- Display the current label next to the slider. Defaults to ``True``.
- continuous_update: bool
- If ``True``, update the value of the widget continuously as the user
- holds the slider. Otherwise, the model is only updated after the
- user has released the slider. Defaults to ``True``.
- """
- def _make_options(x):
- """Standardize the options tuple format.
- The returned tuple should be in the format (('label', value), ('label', value), ...).
- The input can be
- * an iterable of (label, value) pairs
- * an iterable of values, and labels will be generated
- """
- # Check if x is a mapping of labels to values
- if isinstance(x, Mapping):
- import warnings
- warnings.warn("Support for mapping types has been deprecated and will be dropped in a future release.", DeprecationWarning)
- return tuple((unicode_type(k), v) for k, v in x.items())
- # only iterate once through the options.
- xlist = tuple(x)
- # Check if x is an iterable of (label, value) pairs
- if all((isinstance(i, (list, tuple)) and len(i) == 2) for i in xlist):
- return tuple((unicode_type(k), v) for k, v in xlist)
- # Otherwise, assume x is an iterable of values
- return tuple((unicode_type(i), i) for i in xlist)
- def findvalue(array, value, compare = lambda x, y: x == y):
- "A function that uses the compare function to return a value from the list."
- try:
- return next(x for x in array if compare(x, value))
- except StopIteration:
- raise ValueError('%r not in array'%value)
- class _Selection(DescriptionWidget, ValueWidget, CoreWidget):
- """Base class for Selection widgets
- ``options`` can be specified as a list of values, list of (label, value)
- tuples, or a dict of {label: value}. The labels are the strings that will be
- displayed in the UI, representing the actual Python choices, and should be
- unique. If labels are not specified, they are generated from the values.
- When programmatically setting the value, a reverse lookup is performed
- among the options to check that the value is valid. The reverse lookup uses
- the equality operator by default, but another predicate may be provided via
- the ``equals`` keyword argument. For example, when dealing with numpy arrays,
- one may set equals=np.array_equal.
- """
- value = Any(None, help="Selected value", allow_none=True)
- label = Unicode(None, help="Selected label", allow_none=True)
- index = Int(None, help="Selected index", allow_none=True).tag(sync=True)
- options = Any((),
- help="""Iterable of values, (label, value) pairs, or a mapping of {label: value} pairs that the user can select.
- The labels are the strings that will be displayed in the UI, representing the
- actual Python choices, and should be unique.
- """)
- _options_full = None
- # This being read-only means that it cannot be changed by the user.
- _options_labels = TypedTuple(trait=Unicode(), read_only=True, help="The labels for the options.").tag(sync=True)
- disabled = Bool(help="Enable or disable user changes").tag(sync=True)
- def __init__(self, *args, **kwargs):
- self.equals = kwargs.pop('equals', lambda x, y: x == y)
- # We have to make the basic options bookkeeping consistent
- # so we don't have errors the first time validators run
- self._initializing_traits_ = True
- options = _make_options(kwargs.get('options', ()))
- self._options_full = options
- self.set_trait('_options_labels', tuple(i[0] for i in options))
- self._options_values = tuple(i[1] for i in options)
- # Select the first item by default, if we can
- if 'index' not in kwargs and 'value' not in kwargs and 'label' not in kwargs:
- nonempty = (len(options) > 0)
- kwargs['index'] = 0 if nonempty else None
- kwargs['label'], kwargs['value'] = options[0] if nonempty else (None, None)
- super(_Selection, self).__init__(*args, **kwargs)
- self._initializing_traits_ = False
- @validate('options')
- def _validate_options(self, proposal):
- # if an iterator is provided, exhaust it
- if isinstance(proposal.value, Iterable) and not isinstance(proposal.value, Mapping):
- proposal.value = tuple(proposal.value)
- # throws an error if there is a problem converting to full form
- self._options_full = _make_options(proposal.value)
- return proposal.value
- @observe('options')
- def _propagate_options(self, change):
- "Set the values and labels, and select the first option if we aren't initializing"
- options = self._options_full
- self.set_trait('_options_labels', tuple(i[0] for i in options))
- self._options_values = tuple(i[1] for i in options)
- if self._initializing_traits_ is not True:
- if len(options) > 0:
- if self.index == 0:
- # Explicitly trigger the observers to pick up the new value and
- # label. Just setting the value would not trigger the observers
- # since traitlets thinks the value hasn't changed.
- self._notify_trait('index', 0, 0)
- else:
- self.index = 0
- else:
- self.index = None
- @validate('index')
- def _validate_index(self, proposal):
- if proposal.value is None or 0 <= proposal.value < len(self._options_labels):
- return proposal.value
- else:
- raise TraitError('Invalid selection: index out of bounds')
- @observe('index')
- def _propagate_index(self, change):
- "Propagate changes in index to the value and label properties"
- label = self._options_labels[change.new] if change.new is not None else None
- value = self._options_values[change.new] if change.new is not None else None
- if self.label is not label:
- self.label = label
- if self.value is not value:
- self.value = value
- @validate('value')
- def _validate_value(self, proposal):
- value = proposal.value
- try:
- return findvalue(self._options_values, value, self.equals) if value is not None else None
- except ValueError:
- raise TraitError('Invalid selection: value not found')
- @observe('value')
- def _propagate_value(self, change):
- if change.new is None:
- index = None
- elif self.index is not None and self._options_values[self.index] == change.new:
- index = self.index
- else:
- index = self._options_values.index(change.new)
- if self.index != index:
- self.index = index
- @validate('label')
- def _validate_label(self, proposal):
- if (proposal.value is not None) and (proposal.value not in self._options_labels):
- raise TraitError('Invalid selection: label not found')
- return proposal.value
- @observe('label')
- def _propagate_label(self, change):
- if change.new is None:
- index = None
- elif self.index is not None and self._options_labels[self.index] == change.new:
- index = self.index
- else:
- index = self._options_labels.index(change.new)
- if self.index != index:
- self.index = index
- def _repr_keys(self):
- keys = super(_Selection, self)._repr_keys()
- # Include options manually, as it isn't marked as synced:
- for key in sorted(chain(keys, ('options',))):
- if key == 'index' and self.index == 0:
- # Index 0 is default when there are options
- continue
- yield key
- class _MultipleSelection(DescriptionWidget, ValueWidget, CoreWidget):
- """Base class for multiple Selection widgets
- ``options`` can be specified as a list of values, list of (label, value)
- tuples, or a dict of {label: value}. The labels are the strings that will be
- displayed in the UI, representing the actual Python choices, and should be
- unique. If labels are not specified, they are generated from the values.
- When programmatically setting the value, a reverse lookup is performed
- among the options to check that the value is valid. The reverse lookup uses
- the equality operator by default, but another predicate may be provided via
- the ``equals`` keyword argument. For example, when dealing with numpy arrays,
- one may set equals=np.array_equal.
- """
- value = TypedTuple(trait=Any(), help="Selected values")
- label = TypedTuple(trait=Unicode(), help="Selected labels")
- index = TypedTuple(trait=Int(), help="Selected indices").tag(sync=True)
- options = Any((),
- help="""Iterable of values, (label, value) pairs, or a mapping of {label: value} pairs that the user can select.
- The labels are the strings that will be displayed in the UI, representing the
- actual Python choices, and should be unique.
- """)
- _options_full = None
- # This being read-only means that it cannot be changed from the frontend!
- _options_labels = TypedTuple(trait=Unicode(), read_only=True, help="The labels for the options.").tag(sync=True)
- disabled = Bool(help="Enable or disable user changes").tag(sync=True)
- def __init__(self, *args, **kwargs):
- self.equals = kwargs.pop('equals', lambda x, y: x == y)
- # We have to make the basic options bookkeeping consistent
- # so we don't have errors the first time validators run
- self._initializing_traits_ = True
- options = _make_options(kwargs.get('options', ()))
- self._full_options = options
- self.set_trait('_options_labels', tuple(i[0] for i in options))
- self._options_values = tuple(i[1] for i in options)
- super(_MultipleSelection, self).__init__(*args, **kwargs)
- self._initializing_traits_ = False
- @validate('options')
- def _validate_options(self, proposal):
- if isinstance(proposal.value, Iterable) and not isinstance(proposal.value, Mapping):
- proposal.value = tuple(proposal.value)
- # throws an error if there is a problem converting to full form
- self._options_full = _make_options(proposal.value)
- return proposal.value
- @observe('options')
- def _propagate_options(self, change):
- "Unselect any option"
- options = self._options_full
- self.set_trait('_options_labels', tuple(i[0] for i in options))
- self._options_values = tuple(i[1] for i in options)
- if self._initializing_traits_ is not True:
- self.index = ()
- @validate('index')
- def _validate_index(self, proposal):
- "Check the range of each proposed index."
- if all(0 <= i < len(self._options_labels) for i in proposal.value):
- return proposal.value
- else:
- raise TraitError('Invalid selection: index out of bounds')
- @observe('index')
- def _propagate_index(self, change):
- "Propagate changes in index to the value and label properties"
- label = tuple(self._options_labels[i] for i in change.new)
- value = tuple(self._options_values[i] for i in change.new)
- # we check equality so we can avoid validation if possible
- if self.label != label:
- self.label = label
- if self.value != value:
- self.value = value
- @validate('value')
- def _validate_value(self, proposal):
- "Replace all values with the actual objects in the options list"
- try:
- return tuple(findvalue(self._options_values, i, self.equals) for i in proposal.value)
- except ValueError:
- raise TraitError('Invalid selection: value not found')
- @observe('value')
- def _propagate_value(self, change):
- index = tuple(self._options_values.index(i) for i in change.new)
- if self.index != index:
- self.index = index
- @validate('label')
- def _validate_label(self, proposal):
- if any(i not in self._options_labels for i in proposal.value):
- raise TraitError('Invalid selection: label not found')
- return proposal.value
- @observe('label')
- def _propagate_label(self, change):
- index = tuple(self._options_labels.index(i) for i in change.new)
- if self.index != index:
- self.index = index
- def _repr_keys(self):
- keys = super(_MultipleSelection, self)._repr_keys()
- # Include options manually, as it isn't marked as synced:
- for key in sorted(chain(keys, ('options',))):
- yield key
- @register
- class ToggleButtonsStyle(DescriptionStyle, CoreWidget):
- """Button style widget.
- Parameters
- ----------
- button_width: str
- The width of each button. This should be a valid CSS
- width, e.g. '10px' or '5em'.
- font_weight: str
- The text font weight of each button, This should be a valid CSS font
- weight unit, for example 'bold' or '600'
- """
- _model_name = Unicode('ToggleButtonsStyleModel').tag(sync=True)
- button_width = Unicode(help="The width of each button.").tag(sync=True)
- font_weight = Unicode(help="Text font weight of each button.").tag(sync=True)
- @register
- @doc_subst(_doc_snippets)
- class ToggleButtons(_Selection):
- """Group of toggle buttons that represent an enumeration.
- Only one toggle button can be toggled at any point in time.
- Parameters
- ----------
- {selection_params}
- tooltips: list
- Tooltip for each button. If specified, must be the
- same length as `options`.
- icons: list
- Icons to show on the buttons. This must be the name
- of a font-awesome icon. See `http://fontawesome.io/icons/`
- for a list of icons.
- button_style: str
- One of 'primary', 'success', 'info', 'warning' or
- 'danger'. Applies a predefined style to every button.
- style: ToggleButtonsStyle
- Style parameters for the buttons.
- """
- _view_name = Unicode('ToggleButtonsView').tag(sync=True)
- _model_name = Unicode('ToggleButtonsModel').tag(sync=True)
- tooltips = TypedTuple(Unicode(), help="Tooltips for each button.").tag(sync=True)
- icons = TypedTuple(Unicode(), help="Icons names for each button (FontAwesome names without the fa- prefix).").tag(sync=True)
- style = InstanceDict(ToggleButtonsStyle).tag(sync=True, **widget_serialization)
- button_style = CaselessStrEnum(
- values=['primary', 'success', 'info', 'warning', 'danger', ''],
- default_value='', allow_none=True, help="""Use a predefined styling for the buttons.""").tag(sync=True)
- @register
- @doc_subst(_doc_snippets)
- class Dropdown(_Selection):
- """Allows you to select a single item from a dropdown.
- Parameters
- ----------
- {selection_params}
- """
- _view_name = Unicode('DropdownView').tag(sync=True)
- _model_name = Unicode('DropdownModel').tag(sync=True)
- @register
- @doc_subst(_doc_snippets)
- class RadioButtons(_Selection):
- """Group of radio buttons that represent an enumeration.
- Only one radio button can be toggled at any point in time.
- Parameters
- ----------
- {selection_params}
- """
- _view_name = Unicode('RadioButtonsView').tag(sync=True)
- _model_name = Unicode('RadioButtonsModel').tag(sync=True)
- @register
- @doc_subst(_doc_snippets)
- class Select(_Selection):
- """
- Listbox that only allows one item to be selected at any given time.
- Parameters
- ----------
- {selection_params}
- rows: int
- The number of rows to display in the widget.
- """
- _view_name = Unicode('SelectView').tag(sync=True)
- _model_name = Unicode('SelectModel').tag(sync=True)
- rows = Int(5, help="The number of rows to display.").tag(sync=True)
- @register
- @doc_subst(_doc_snippets)
- class SelectMultiple(_MultipleSelection):
- """
- Listbox that allows many items to be selected at any given time.
- The ``value``, ``label`` and ``index`` attributes are all iterables.
- Parameters
- ----------
- {multiple_selection_params}
- rows: int
- The number of rows to display in the widget.
- """
- _view_name = Unicode('SelectMultipleView').tag(sync=True)
- _model_name = Unicode('SelectMultipleModel').tag(sync=True)
- rows = Int(5, help="The number of rows to display.").tag(sync=True)
- class _SelectionNonempty(_Selection):
- """Selection that is guaranteed to have a value selected."""
- # don't allow None to be an option.
- value = Any(help="Selected value")
- label = Unicode(help="Selected label")
- index = Int(help="Selected index").tag(sync=True)
- def __init__(self, *args, **kwargs):
- if len(kwargs.get('options', ())) == 0:
- raise TraitError('options must be nonempty')
- super(_SelectionNonempty, self).__init__(*args, **kwargs)
- @validate('options')
- def _validate_options(self, proposal):
- if isinstance(proposal.value, Iterable) and not isinstance(proposal.value, Mapping):
- proposal.value = tuple(proposal.value)
- self._options_full = _make_options(proposal.value)
- if len(self._options_full) == 0:
- raise TraitError("Option list must be nonempty")
- return proposal.value
- @validate('index')
- def _validate_index(self, proposal):
- if 0 <= proposal.value < len(self._options_labels):
- return proposal.value
- else:
- raise TraitError('Invalid selection: index out of bounds')
- class _MultipleSelectionNonempty(_MultipleSelection):
- """Selection that is guaranteed to have an option available."""
- def __init__(self, *args, **kwargs):
- if len(kwargs.get('options', ())) == 0:
- raise TraitError('options must be nonempty')
- super(_MultipleSelectionNonempty, self).__init__(*args, **kwargs)
- @validate('options')
- def _validate_options(self, proposal):
- if isinstance(proposal.value, Iterable) and not isinstance(proposal.value, Mapping):
- proposal.value = tuple(proposal.value)
- # throws an error if there is a problem converting to full form
- self._options_full = _make_options(proposal.value)
- if len(self._options_full) == 0:
- raise TraitError("Option list must be nonempty")
- return proposal.value
- @register
- @doc_subst(_doc_snippets)
- class SelectionSlider(_SelectionNonempty):
- """
- Slider to select a single item from a list or dictionary.
- Parameters
- ----------
- {selection_params}
- {slider_params}
- """
- _view_name = Unicode('SelectionSliderView').tag(sync=True)
- _model_name = Unicode('SelectionSliderModel').tag(sync=True)
- orientation = CaselessStrEnum(
- values=['horizontal', 'vertical'], default_value='horizontal',
- help="Vertical or horizontal.").tag(sync=True)
- readout = Bool(True,
- help="Display the current selected label next to the slider").tag(sync=True)
- continuous_update = Bool(True,
- help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
- @register
- @doc_subst(_doc_snippets)
- class SelectionRangeSlider(_MultipleSelectionNonempty):
- """
- Slider to select multiple contiguous items from a list.
-
- The index, value, and label attributes contain the start and end of
- the selection range, not all items in the range.
- Parameters
- ----------
- {multiple_selection_params}
- {slider_params}
- """
- _view_name = Unicode('SelectionRangeSliderView').tag(sync=True)
- _model_name = Unicode('SelectionRangeSliderModel').tag(sync=True)
- value = Tuple(help="Min and max selected values")
- label = Tuple(help="Min and max selected labels")
- index = Tuple((0,0), help="Min and max selected indices").tag(sync=True)
- @observe('options')
- def _propagate_options(self, change):
- "Select the first range"
- options = self._options_full
- self.set_trait('_options_labels', tuple(i[0] for i in options))
- self._options_values = tuple(i[1] for i in options)
- if self._initializing_traits_ is not True:
- self.index = (0, 0)
- @validate('index')
- def _validate_index(self, proposal):
- "Make sure we have two indices and check the range of each proposed index."
- if len(proposal.value) != 2:
- raise TraitError('Invalid selection: index must have two values, but is %r'%(proposal.value,))
- if all(0 <= i < len(self._options_labels) for i in proposal.value):
- return proposal.value
- else:
- raise TraitError('Invalid selection: index out of bounds: %s'%(proposal.value,))
- orientation = CaselessStrEnum(
- values=['horizontal', 'vertical'], default_value='horizontal',
- help="Vertical or horizontal.").tag(sync=True)
- readout = Bool(True,
- help="Display the current selected label next to the slider").tag(sync=True)
- continuous_update = Bool(True,
- help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
|