widget_selection.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. # Copyright (c) Jupyter Development Team.
  2. # Distributed under the terms of the Modified BSD License.
  3. """Selection classes.
  4. Represents an enumeration using a widget.
  5. """
  6. try:
  7. from collections.abc import Iterable, Mapping
  8. except ImportError:
  9. from collections import Iterable, Mapping # py2
  10. try:
  11. from itertools import izip
  12. except ImportError: #python3.x
  13. izip = zip
  14. from itertools import chain
  15. from .widget_description import DescriptionWidget, DescriptionStyle
  16. from .valuewidget import ValueWidget
  17. from .widget_core import CoreWidget
  18. from .widget_style import Style
  19. from .trait_types import InstanceDict, TypedTuple
  20. from .widget import register, widget_serialization
  21. from .docutils import doc_subst
  22. from traitlets import (Unicode, Bool, Int, Any, Dict, TraitError, CaselessStrEnum,
  23. Tuple, Union, observe, validate)
  24. from ipython_genutils.py3compat import unicode_type
  25. _doc_snippets = {}
  26. _doc_snippets['selection_params'] = """
  27. options: list
  28. The options for the dropdown. This can either be a list of values, e.g.
  29. ``['Galileo', 'Brahe', 'Hubble']`` or ``[0, 1, 2]``, or a list of
  30. (label, value) pairs, e.g.
  31. ``[('Galileo', 0), ('Brahe', 1), ('Hubble', 2)]``.
  32. index: int
  33. The index of the current selection.
  34. value: any
  35. The value of the current selection. When programmatically setting the
  36. value, a reverse lookup is performed among the options to check that
  37. the value is valid. The reverse lookup uses the equality operator by
  38. default, but another predicate may be provided via the ``equals``
  39. keyword argument. For example, when dealing with numpy arrays, one may
  40. set ``equals=np.array_equal``.
  41. label: str
  42. The label corresponding to the selected value.
  43. disabled: bool
  44. Whether to disable user changes.
  45. description: str
  46. Label for this input group. This should be a string
  47. describing the widget.
  48. """
  49. _doc_snippets['multiple_selection_params'] = """
  50. options: dict or list
  51. The options for the dropdown. This can either be a list of values, e.g.
  52. ``['Galileo', 'Brahe', 'Hubble']`` or ``[0, 1, 2]``, a list of
  53. (label, value) pairs, e.g.
  54. ``[('Galileo', 0), ('Brahe', 1), ('Hubble', 2)]``,
  55. or a dictionary mapping the labels to the values, e.g. ``{'Galileo': 0,
  56. 'Brahe': 1, 'Hubble': 2}``. The labels are the strings that will be
  57. displayed in the UI, representing the actual Python choices, and should
  58. be unique. If this is a dictionary, the order in which they are
  59. displayed is not guaranteed.
  60. index: iterable of int
  61. The indices of the options that are selected.
  62. value: iterable
  63. The values that are selected. When programmatically setting the
  64. value, a reverse lookup is performed among the options to check that
  65. the value is valid. The reverse lookup uses the equality operator by
  66. default, but another predicate may be provided via the ``equals``
  67. keyword argument. For example, when dealing with numpy arrays, one may
  68. set ``equals=np.array_equal``.
  69. label: iterable of str
  70. The labels corresponding to the selected value.
  71. disabled: bool
  72. Whether to disable user changes.
  73. description: str
  74. Label for this input group. This should be a string
  75. describing the widget.
  76. """
  77. _doc_snippets['slider_params'] = """
  78. orientation: str
  79. Either ``'horizontal'`` or ``'vertical'``. Defaults to ``horizontal``.
  80. readout: bool
  81. Display the current label next to the slider. Defaults to ``True``.
  82. continuous_update: bool
  83. If ``True``, update the value of the widget continuously as the user
  84. holds the slider. Otherwise, the model is only updated after the
  85. user has released the slider. Defaults to ``True``.
  86. """
  87. def _make_options(x):
  88. """Standardize the options tuple format.
  89. The returned tuple should be in the format (('label', value), ('label', value), ...).
  90. The input can be
  91. * an iterable of (label, value) pairs
  92. * an iterable of values, and labels will be generated
  93. """
  94. # Check if x is a mapping of labels to values
  95. if isinstance(x, Mapping):
  96. import warnings
  97. warnings.warn("Support for mapping types has been deprecated and will be dropped in a future release.", DeprecationWarning)
  98. return tuple((unicode_type(k), v) for k, v in x.items())
  99. # only iterate once through the options.
  100. xlist = tuple(x)
  101. # Check if x is an iterable of (label, value) pairs
  102. if all((isinstance(i, (list, tuple)) and len(i) == 2) for i in xlist):
  103. return tuple((unicode_type(k), v) for k, v in xlist)
  104. # Otherwise, assume x is an iterable of values
  105. return tuple((unicode_type(i), i) for i in xlist)
  106. def findvalue(array, value, compare = lambda x, y: x == y):
  107. "A function that uses the compare function to return a value from the list."
  108. try:
  109. return next(x for x in array if compare(x, value))
  110. except StopIteration:
  111. raise ValueError('%r not in array'%value)
  112. class _Selection(DescriptionWidget, ValueWidget, CoreWidget):
  113. """Base class for Selection widgets
  114. ``options`` can be specified as a list of values, list of (label, value)
  115. tuples, or a dict of {label: value}. The labels are the strings that will be
  116. displayed in the UI, representing the actual Python choices, and should be
  117. unique. If labels are not specified, they are generated from the values.
  118. When programmatically setting the value, a reverse lookup is performed
  119. among the options to check that the value is valid. The reverse lookup uses
  120. the equality operator by default, but another predicate may be provided via
  121. the ``equals`` keyword argument. For example, when dealing with numpy arrays,
  122. one may set equals=np.array_equal.
  123. """
  124. value = Any(None, help="Selected value", allow_none=True)
  125. label = Unicode(None, help="Selected label", allow_none=True)
  126. index = Int(None, help="Selected index", allow_none=True).tag(sync=True)
  127. options = Any((),
  128. help="""Iterable of values, (label, value) pairs, or a mapping of {label: value} pairs that the user can select.
  129. The labels are the strings that will be displayed in the UI, representing the
  130. actual Python choices, and should be unique.
  131. """)
  132. _options_full = None
  133. # This being read-only means that it cannot be changed by the user.
  134. _options_labels = TypedTuple(trait=Unicode(), read_only=True, help="The labels for the options.").tag(sync=True)
  135. disabled = Bool(help="Enable or disable user changes").tag(sync=True)
  136. def __init__(self, *args, **kwargs):
  137. self.equals = kwargs.pop('equals', lambda x, y: x == y)
  138. # We have to make the basic options bookkeeping consistent
  139. # so we don't have errors the first time validators run
  140. self._initializing_traits_ = True
  141. options = _make_options(kwargs.get('options', ()))
  142. self._options_full = options
  143. self.set_trait('_options_labels', tuple(i[0] for i in options))
  144. self._options_values = tuple(i[1] for i in options)
  145. # Select the first item by default, if we can
  146. if 'index' not in kwargs and 'value' not in kwargs and 'label' not in kwargs:
  147. nonempty = (len(options) > 0)
  148. kwargs['index'] = 0 if nonempty else None
  149. kwargs['label'], kwargs['value'] = options[0] if nonempty else (None, None)
  150. super(_Selection, self).__init__(*args, **kwargs)
  151. self._initializing_traits_ = False
  152. @validate('options')
  153. def _validate_options(self, proposal):
  154. # if an iterator is provided, exhaust it
  155. if isinstance(proposal.value, Iterable) and not isinstance(proposal.value, Mapping):
  156. proposal.value = tuple(proposal.value)
  157. # throws an error if there is a problem converting to full form
  158. self._options_full = _make_options(proposal.value)
  159. return proposal.value
  160. @observe('options')
  161. def _propagate_options(self, change):
  162. "Set the values and labels, and select the first option if we aren't initializing"
  163. options = self._options_full
  164. self.set_trait('_options_labels', tuple(i[0] for i in options))
  165. self._options_values = tuple(i[1] for i in options)
  166. if self._initializing_traits_ is not True:
  167. if len(options) > 0:
  168. if self.index == 0:
  169. # Explicitly trigger the observers to pick up the new value and
  170. # label. Just setting the value would not trigger the observers
  171. # since traitlets thinks the value hasn't changed.
  172. self._notify_trait('index', 0, 0)
  173. else:
  174. self.index = 0
  175. else:
  176. self.index = None
  177. @validate('index')
  178. def _validate_index(self, proposal):
  179. if proposal.value is None or 0 <= proposal.value < len(self._options_labels):
  180. return proposal.value
  181. else:
  182. raise TraitError('Invalid selection: index out of bounds')
  183. @observe('index')
  184. def _propagate_index(self, change):
  185. "Propagate changes in index to the value and label properties"
  186. label = self._options_labels[change.new] if change.new is not None else None
  187. value = self._options_values[change.new] if change.new is not None else None
  188. if self.label is not label:
  189. self.label = label
  190. if self.value is not value:
  191. self.value = value
  192. @validate('value')
  193. def _validate_value(self, proposal):
  194. value = proposal.value
  195. try:
  196. return findvalue(self._options_values, value, self.equals) if value is not None else None
  197. except ValueError:
  198. raise TraitError('Invalid selection: value not found')
  199. @observe('value')
  200. def _propagate_value(self, change):
  201. if change.new is None:
  202. index = None
  203. elif self.index is not None and self._options_values[self.index] == change.new:
  204. index = self.index
  205. else:
  206. index = self._options_values.index(change.new)
  207. if self.index != index:
  208. self.index = index
  209. @validate('label')
  210. def _validate_label(self, proposal):
  211. if (proposal.value is not None) and (proposal.value not in self._options_labels):
  212. raise TraitError('Invalid selection: label not found')
  213. return proposal.value
  214. @observe('label')
  215. def _propagate_label(self, change):
  216. if change.new is None:
  217. index = None
  218. elif self.index is not None and self._options_labels[self.index] == change.new:
  219. index = self.index
  220. else:
  221. index = self._options_labels.index(change.new)
  222. if self.index != index:
  223. self.index = index
  224. def _repr_keys(self):
  225. keys = super(_Selection, self)._repr_keys()
  226. # Include options manually, as it isn't marked as synced:
  227. for key in sorted(chain(keys, ('options',))):
  228. if key == 'index' and self.index == 0:
  229. # Index 0 is default when there are options
  230. continue
  231. yield key
  232. class _MultipleSelection(DescriptionWidget, ValueWidget, CoreWidget):
  233. """Base class for multiple Selection widgets
  234. ``options`` can be specified as a list of values, list of (label, value)
  235. tuples, or a dict of {label: value}. The labels are the strings that will be
  236. displayed in the UI, representing the actual Python choices, and should be
  237. unique. If labels are not specified, they are generated from the values.
  238. When programmatically setting the value, a reverse lookup is performed
  239. among the options to check that the value is valid. The reverse lookup uses
  240. the equality operator by default, but another predicate may be provided via
  241. the ``equals`` keyword argument. For example, when dealing with numpy arrays,
  242. one may set equals=np.array_equal.
  243. """
  244. value = TypedTuple(trait=Any(), help="Selected values")
  245. label = TypedTuple(trait=Unicode(), help="Selected labels")
  246. index = TypedTuple(trait=Int(), help="Selected indices").tag(sync=True)
  247. options = Any((),
  248. help="""Iterable of values, (label, value) pairs, or a mapping of {label: value} pairs that the user can select.
  249. The labels are the strings that will be displayed in the UI, representing the
  250. actual Python choices, and should be unique.
  251. """)
  252. _options_full = None
  253. # This being read-only means that it cannot be changed from the frontend!
  254. _options_labels = TypedTuple(trait=Unicode(), read_only=True, help="The labels for the options.").tag(sync=True)
  255. disabled = Bool(help="Enable or disable user changes").tag(sync=True)
  256. def __init__(self, *args, **kwargs):
  257. self.equals = kwargs.pop('equals', lambda x, y: x == y)
  258. # We have to make the basic options bookkeeping consistent
  259. # so we don't have errors the first time validators run
  260. self._initializing_traits_ = True
  261. options = _make_options(kwargs.get('options', ()))
  262. self._full_options = options
  263. self.set_trait('_options_labels', tuple(i[0] for i in options))
  264. self._options_values = tuple(i[1] for i in options)
  265. super(_MultipleSelection, self).__init__(*args, **kwargs)
  266. self._initializing_traits_ = False
  267. @validate('options')
  268. def _validate_options(self, proposal):
  269. if isinstance(proposal.value, Iterable) and not isinstance(proposal.value, Mapping):
  270. proposal.value = tuple(proposal.value)
  271. # throws an error if there is a problem converting to full form
  272. self._options_full = _make_options(proposal.value)
  273. return proposal.value
  274. @observe('options')
  275. def _propagate_options(self, change):
  276. "Unselect any option"
  277. options = self._options_full
  278. self.set_trait('_options_labels', tuple(i[0] for i in options))
  279. self._options_values = tuple(i[1] for i in options)
  280. if self._initializing_traits_ is not True:
  281. self.index = ()
  282. @validate('index')
  283. def _validate_index(self, proposal):
  284. "Check the range of each proposed index."
  285. if all(0 <= i < len(self._options_labels) for i in proposal.value):
  286. return proposal.value
  287. else:
  288. raise TraitError('Invalid selection: index out of bounds')
  289. @observe('index')
  290. def _propagate_index(self, change):
  291. "Propagate changes in index to the value and label properties"
  292. label = tuple(self._options_labels[i] for i in change.new)
  293. value = tuple(self._options_values[i] for i in change.new)
  294. # we check equality so we can avoid validation if possible
  295. if self.label != label:
  296. self.label = label
  297. if self.value != value:
  298. self.value = value
  299. @validate('value')
  300. def _validate_value(self, proposal):
  301. "Replace all values with the actual objects in the options list"
  302. try:
  303. return tuple(findvalue(self._options_values, i, self.equals) for i in proposal.value)
  304. except ValueError:
  305. raise TraitError('Invalid selection: value not found')
  306. @observe('value')
  307. def _propagate_value(self, change):
  308. index = tuple(self._options_values.index(i) for i in change.new)
  309. if self.index != index:
  310. self.index = index
  311. @validate('label')
  312. def _validate_label(self, proposal):
  313. if any(i not in self._options_labels for i in proposal.value):
  314. raise TraitError('Invalid selection: label not found')
  315. return proposal.value
  316. @observe('label')
  317. def _propagate_label(self, change):
  318. index = tuple(self._options_labels.index(i) for i in change.new)
  319. if self.index != index:
  320. self.index = index
  321. def _repr_keys(self):
  322. keys = super(_MultipleSelection, self)._repr_keys()
  323. # Include options manually, as it isn't marked as synced:
  324. for key in sorted(chain(keys, ('options',))):
  325. yield key
  326. @register
  327. class ToggleButtonsStyle(DescriptionStyle, CoreWidget):
  328. """Button style widget.
  329. Parameters
  330. ----------
  331. button_width: str
  332. The width of each button. This should be a valid CSS
  333. width, e.g. '10px' or '5em'.
  334. font_weight: str
  335. The text font weight of each button, This should be a valid CSS font
  336. weight unit, for example 'bold' or '600'
  337. """
  338. _model_name = Unicode('ToggleButtonsStyleModel').tag(sync=True)
  339. button_width = Unicode(help="The width of each button.").tag(sync=True)
  340. font_weight = Unicode(help="Text font weight of each button.").tag(sync=True)
  341. @register
  342. @doc_subst(_doc_snippets)
  343. class ToggleButtons(_Selection):
  344. """Group of toggle buttons that represent an enumeration.
  345. Only one toggle button can be toggled at any point in time.
  346. Parameters
  347. ----------
  348. {selection_params}
  349. tooltips: list
  350. Tooltip for each button. If specified, must be the
  351. same length as `options`.
  352. icons: list
  353. Icons to show on the buttons. This must be the name
  354. of a font-awesome icon. See `http://fontawesome.io/icons/`
  355. for a list of icons.
  356. button_style: str
  357. One of 'primary', 'success', 'info', 'warning' or
  358. 'danger'. Applies a predefined style to every button.
  359. style: ToggleButtonsStyle
  360. Style parameters for the buttons.
  361. """
  362. _view_name = Unicode('ToggleButtonsView').tag(sync=True)
  363. _model_name = Unicode('ToggleButtonsModel').tag(sync=True)
  364. tooltips = TypedTuple(Unicode(), help="Tooltips for each button.").tag(sync=True)
  365. icons = TypedTuple(Unicode(), help="Icons names for each button (FontAwesome names without the fa- prefix).").tag(sync=True)
  366. style = InstanceDict(ToggleButtonsStyle).tag(sync=True, **widget_serialization)
  367. button_style = CaselessStrEnum(
  368. values=['primary', 'success', 'info', 'warning', 'danger', ''],
  369. default_value='', allow_none=True, help="""Use a predefined styling for the buttons.""").tag(sync=True)
  370. @register
  371. @doc_subst(_doc_snippets)
  372. class Dropdown(_Selection):
  373. """Allows you to select a single item from a dropdown.
  374. Parameters
  375. ----------
  376. {selection_params}
  377. """
  378. _view_name = Unicode('DropdownView').tag(sync=True)
  379. _model_name = Unicode('DropdownModel').tag(sync=True)
  380. @register
  381. @doc_subst(_doc_snippets)
  382. class RadioButtons(_Selection):
  383. """Group of radio buttons that represent an enumeration.
  384. Only one radio button can be toggled at any point in time.
  385. Parameters
  386. ----------
  387. {selection_params}
  388. """
  389. _view_name = Unicode('RadioButtonsView').tag(sync=True)
  390. _model_name = Unicode('RadioButtonsModel').tag(sync=True)
  391. @register
  392. @doc_subst(_doc_snippets)
  393. class Select(_Selection):
  394. """
  395. Listbox that only allows one item to be selected at any given time.
  396. Parameters
  397. ----------
  398. {selection_params}
  399. rows: int
  400. The number of rows to display in the widget.
  401. """
  402. _view_name = Unicode('SelectView').tag(sync=True)
  403. _model_name = Unicode('SelectModel').tag(sync=True)
  404. rows = Int(5, help="The number of rows to display.").tag(sync=True)
  405. @register
  406. @doc_subst(_doc_snippets)
  407. class SelectMultiple(_MultipleSelection):
  408. """
  409. Listbox that allows many items to be selected at any given time.
  410. The ``value``, ``label`` and ``index`` attributes are all iterables.
  411. Parameters
  412. ----------
  413. {multiple_selection_params}
  414. rows: int
  415. The number of rows to display in the widget.
  416. """
  417. _view_name = Unicode('SelectMultipleView').tag(sync=True)
  418. _model_name = Unicode('SelectMultipleModel').tag(sync=True)
  419. rows = Int(5, help="The number of rows to display.").tag(sync=True)
  420. class _SelectionNonempty(_Selection):
  421. """Selection that is guaranteed to have a value selected."""
  422. # don't allow None to be an option.
  423. value = Any(help="Selected value")
  424. label = Unicode(help="Selected label")
  425. index = Int(help="Selected index").tag(sync=True)
  426. def __init__(self, *args, **kwargs):
  427. if len(kwargs.get('options', ())) == 0:
  428. raise TraitError('options must be nonempty')
  429. super(_SelectionNonempty, self).__init__(*args, **kwargs)
  430. @validate('options')
  431. def _validate_options(self, proposal):
  432. if isinstance(proposal.value, Iterable) and not isinstance(proposal.value, Mapping):
  433. proposal.value = tuple(proposal.value)
  434. self._options_full = _make_options(proposal.value)
  435. if len(self._options_full) == 0:
  436. raise TraitError("Option list must be nonempty")
  437. return proposal.value
  438. @validate('index')
  439. def _validate_index(self, proposal):
  440. if 0 <= proposal.value < len(self._options_labels):
  441. return proposal.value
  442. else:
  443. raise TraitError('Invalid selection: index out of bounds')
  444. class _MultipleSelectionNonempty(_MultipleSelection):
  445. """Selection that is guaranteed to have an option available."""
  446. def __init__(self, *args, **kwargs):
  447. if len(kwargs.get('options', ())) == 0:
  448. raise TraitError('options must be nonempty')
  449. super(_MultipleSelectionNonempty, self).__init__(*args, **kwargs)
  450. @validate('options')
  451. def _validate_options(self, proposal):
  452. if isinstance(proposal.value, Iterable) and not isinstance(proposal.value, Mapping):
  453. proposal.value = tuple(proposal.value)
  454. # throws an error if there is a problem converting to full form
  455. self._options_full = _make_options(proposal.value)
  456. if len(self._options_full) == 0:
  457. raise TraitError("Option list must be nonempty")
  458. return proposal.value
  459. @register
  460. @doc_subst(_doc_snippets)
  461. class SelectionSlider(_SelectionNonempty):
  462. """
  463. Slider to select a single item from a list or dictionary.
  464. Parameters
  465. ----------
  466. {selection_params}
  467. {slider_params}
  468. """
  469. _view_name = Unicode('SelectionSliderView').tag(sync=True)
  470. _model_name = Unicode('SelectionSliderModel').tag(sync=True)
  471. orientation = CaselessStrEnum(
  472. values=['horizontal', 'vertical'], default_value='horizontal',
  473. help="Vertical or horizontal.").tag(sync=True)
  474. readout = Bool(True,
  475. help="Display the current selected label next to the slider").tag(sync=True)
  476. continuous_update = Bool(True,
  477. help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
  478. @register
  479. @doc_subst(_doc_snippets)
  480. class SelectionRangeSlider(_MultipleSelectionNonempty):
  481. """
  482. Slider to select multiple contiguous items from a list.
  483. The index, value, and label attributes contain the start and end of
  484. the selection range, not all items in the range.
  485. Parameters
  486. ----------
  487. {multiple_selection_params}
  488. {slider_params}
  489. """
  490. _view_name = Unicode('SelectionRangeSliderView').tag(sync=True)
  491. _model_name = Unicode('SelectionRangeSliderModel').tag(sync=True)
  492. value = Tuple(help="Min and max selected values")
  493. label = Tuple(help="Min and max selected labels")
  494. index = Tuple((0,0), help="Min and max selected indices").tag(sync=True)
  495. @observe('options')
  496. def _propagate_options(self, change):
  497. "Select the first range"
  498. options = self._options_full
  499. self.set_trait('_options_labels', tuple(i[0] for i in options))
  500. self._options_values = tuple(i[1] for i in options)
  501. if self._initializing_traits_ is not True:
  502. self.index = (0, 0)
  503. @validate('index')
  504. def _validate_index(self, proposal):
  505. "Make sure we have two indices and check the range of each proposed index."
  506. if len(proposal.value) != 2:
  507. raise TraitError('Invalid selection: index must have two values, but is %r'%(proposal.value,))
  508. if all(0 <= i < len(self._options_labels) for i in proposal.value):
  509. return proposal.value
  510. else:
  511. raise TraitError('Invalid selection: index out of bounds: %s'%(proposal.value,))
  512. orientation = CaselessStrEnum(
  513. values=['horizontal', 'vertical'], default_value='horizontal',
  514. help="Vertical or horizontal.").tag(sync=True)
  515. readout = Bool(True,
  516. help="Display the current selected label next to the slider").tag(sync=True)
  517. continuous_update = Bool(True,
  518. help="Update the value of the widget as the user is holding the slider.").tag(sync=True)