jupyter-widgets/ipywidgets

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.

gully commented

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.