Add more controls to the Play widget
Opened this issue · 4 comments
Summary
Add first/last/previous/next buttons to the Play widget. This can then be hooked up to the slider widget if needed. We might also add speed up/slow down buttons, and maybe a reverse button that reverses the iteration direction (to play backwards...)
For example, here is how Mathematica does it:
https://reference.wolfram.com/language/tutorial/Files/IntroductionToManipulate.en/3.png (we have next/previous/play/repeat, as well as speed up and slow down, I think...)
Original issue
I use the SelectionSlider
widget a lot, and often it's convenient to have buttons like first/prev/next/last, as well as being able to jump to a specific position. It looks like a very general case, so may be it's possible to add such widget to ipywidgets
?
I implemented such widget myself, based on existing ones (so it's python-only, no javascript):
class SelectionSliderBtns(iw.Box):
description = traitlets.Unicode()
value = traitlets.Any()
options = traitlets.Union([traitlets.List(), traitlets.Dict()])
def __init__(self, **kwargs):
super().__init__(**kwargs)
slider = iw.SelectionSlider()
buttons = []
for icon, ind_func in [
('fa-step-backward', lambda ind: 0), ('fa-chevron-left', lambda ind: ind - 1),
('fa-chevron-right', lambda ind: ind + 1), ('fa-step-forward', lambda ind: -1)]:
btn = iw.Button(icon=icon)
buttons.append(btn)
btn.layout.width = '32px'
@btn.on_click
def _(*_, ind_func=ind_func):
ind = self.options.index(self.value)
self.value = self.options[ind_func(ind)]
index_input = iw.BoundedIntText(layout=Layout(width='50px'))
len_label = iw.HTML(layout=Layout(padding='5px 0 0 0'))
traitlets.link((self, 'options'), (slider, 'options'))
try:
traitlets.link((self, 'value'), (slider, 'value'))
except:
traitlets.link((slider, 'value'), (self, 'value'))
traitlets.link((self, 'description'), (slider, 'description'))
@observe(self, 'value')
def _(*_):
buttons[1].disabled = (not self.options or self.value == self.options[0])
buttons[2].disabled = (not self.options or self.value == self.options[-1])
for btn in buttons:
btn.button_style = '' if btn.disabled else 'success'
try:
ind = self.options.index(self.value) + 1
except:
ind = 0
index_input.value = ind
@observe(self, 'options')
def _(*_):
len_label.value = '/ {}'.format(len(self.options))
index_input.min = 0
index_input.max = len(self.options)
@observe(index_input, 'value')
def _(*_):
self.value = self.options[index_input.value - 1]
self.children = [iw.HBox([*buttons[:2], slider, index_input, len_label, *buttons[2:]])]
self.add_class('panel')
self.add_class('panel-default')
self.children[0].add_class('panel-body')
It uses my implementation of observe
for convenience (see #716):
def observe(widget, trait_name):
def wrapper(func):
widget.observe(func, trait_name)
func()
return wrapper
Nice workaround. This could be a good idea in general for any slider.
This seems very much like the current play widget. Perhaps first/last/next/previous buttons can be added to the play widget.
Hmm, I have a question that seems related to this Issue... I can't currently use Play
with the selection widget. Here is a minimal example below. Is this a problem on my end?
import ipywidgets as widgets
play = widgets.Play(value=0,min=0, max=5, step=1)
slider = widgets.SelectionSlider(options=[0,1,2,3,4,5])
widgets.jslink((play, 'value'), (slider, 'value'))
widgets.HBox([play, slider])
Results in:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-17fa565e009a> in <module>()
5 slider = widgets.SelectionSlider(options=[0,1,2,3,4,5])
6
----> 7 widgets.jslink((play, 'value'), (slider, 'value'))
8 widgets.HBox([play, slider])
~/anaconda3/lib/python3.6/site-packages/ipywidgets/widgets/widget_link.py in jslink(attr1, attr2)
73 >>> c = link((widget1, 'value'), (widget2, 'value'))
74 """
---> 75 return Link(attr1, attr2)
76
77
~/anaconda3/lib/python3.6/site-packages/ipywidgets/widgets/widget_link.py in __init__(self, source, target, **kwargs)
50 kwargs['source'] = source
51 kwargs['target'] = target
---> 52 super(Link, self).__init__(**kwargs)
53
54 # for compatibility with traitlet links
~/anaconda3/lib/python3.6/site-packages/ipywidgets/widgets/widget.py in __init__(self, **kwargs)
409 """Public constructor"""
410 self._model_id = kwargs.pop('model_id', None)
--> 411 super(Widget, self).__init__(**kwargs)
412
413 Widget._call_widget_constructed(self)
~/anaconda3/lib/python3.6/site-packages/traitlets/traitlets.py in __init__(self, *args, **kwargs)
995 for key, value in kwargs.items():
996 if self.has_trait(key):
--> 997 setattr(self, key, value)
998 else:
999 # passthrough args that don't set traits to super
~/anaconda3/lib/python3.6/site-packages/traitlets/traitlets.py in __set__(self, obj, value)
583 raise TraitError('The "%s" trait is read-only.' % self.name)
584 else:
--> 585 self.set(obj, value)
586
587 def _validate(self, obj, value):
~/anaconda3/lib/python3.6/site-packages/traitlets/traitlets.py in set(self, obj, value)
557
558 def set(self, obj, value):
--> 559 new_value = self._validate(obj, value)
560 try:
561 old_value = obj._trait_values[self.name]
~/anaconda3/lib/python3.6/site-packages/traitlets/traitlets.py in _validate(self, obj, value)
589 return value
590 if hasattr(self, 'validate'):
--> 591 value = self.validate(obj, value)
592 if obj._cross_validation_lock is False:
593 value = self._cross_validate(obj, value)
~/anaconda3/lib/python3.6/site-packages/traitlets/traitlets.py in validate(self, obj, value)
2240 return value
2241
-> 2242 value = self.validate_elements(obj, value)
2243
2244 return value
~/anaconda3/lib/python3.6/site-packages/ipywidgets/widgets/widget_link.py in validate_elements(self, obj, value)
31 raise TypeError("No such trait: %s" % trait_repr)
32 elif not trait.get_metadata('sync'):
---> 33 raise TypeError("%s cannot be synced" % trait_repr)
34 return value
35
TypeError: SelectionSlider.value cannot be synced
value lives only on the kernel side, you can either use ipywidgets.link (alias of traitlets.link) or link it to index, which lives in the front end.