It would be useful if the ToggleButtons widget would have an on_click() method, just like the usual Button.
Use case: use this on_click() event to re-evaluate an interactive function. This makes sense when the ToggleButtons represent actions rather than a selection. These actions might be non-idempotent (clicking the button twice is not the same as clicking it once).
I believe buttons only have click because they are the only stateless widget. ToggleButtons use .value, so you can observe changes to .value, which will be triggered on every click:
toggle = w.ToggleButton(description='click me')
def on_click(change):
print(change['new'])
toggle.observe(on_click, 'value')
and you can use it with interact, as well:
@interact
def clicked(a=ToggleButton()):
print(a)
@minrk I think what @jdemeyer means is that the buttons should be allowed to mimic a stateless widget. In that, they may be clicked without the value being changed and you would still like to trigger an action.
In this example, repeatedly clicking hello doesn't trigger the print, which if you attach changed to on_click should do:

@minrk I think what @jdemeyer means is that the buttons should be allowed to mimic a stateless widget. In that, they may be clicked without the value being changed and you would still like to trigger an action.
That's exactly what I mean indeed. If you click the same button twice, .value is not changed the second time but you may still want to intercept the second click (and call the interactive function again).
Sorry, I was thinking of ToggleButton, where by definition each click toggles the state, I forgot about ToggleButtons. In interacts, you can't trigger re-evaluation without changing the value of any other widget, so I'm not quite sure why a choice-selection should be the exception. Should every widget send all their click/interaction events that don't change state?
In interacts, you can't trigger re-evaluation without changing the value of any other widget
Exactly. And this is annoying because such re-evaluation without changing the value could be useful for
Should every widget send all their click/interaction events that don't change state?
Well, maybe not every widget but clicking a button is a very typical way to indicate an explicit action.
OK. @SylvainCorlay how feasible is it to add the click event to ToggleButton(s)?
In the meantime, interact_manual gives you a button to re-evaluate, regardless of whether inputs have changed.
I would be easy but I wanted to take advantage of the fact that we still have few stateless things in ipywidgets to get back into https://github.com/ipython/ipywidgets/pull/46 (signals and slots).
This is not so important for ipywidgets, but we have felt the need for something like this in downstream projects like bqplot, and @abelnation (who is doing a massive rework on pythreejs) was also proposing frontend to backend callbacks with argument serialization.
I would be happy if you only added support for the on_click() event itself. Wiring up this event to the interact is something I can easily do myself.
Let me add that this feature is used quite often in practice in SageMath interacts. I consider it the most important bug when porting SageMath interacts to ipywidgets.
I think a semantically, a selection container for ToggleButtons is different than a row of buttons that invoke events when clicked. One is selecting a value from a set of choices, the other is presenting a list of convenient actions. A huge UI difference is that the selection container has the current selection highlighted, while the row of buttons does not have a 'pressed' button highlighted.
I would call the row of buttons a 'ButtonBar', and think it should be a different widget. Semantically, it's the same as an HBox of buttons, but renders the buttons close together. Would an HBox of buttons work just as well?
Would an HBox of buttons work just as well?
No because I still want the ButtonBar to have a value attribute indicating which button was pressed.
I think a semantically, a selection container for ToggleButtons is different than a row of buttons that invoke events when clicked.
The problem is that Sage uses the same widget for both things. So if we want to be compatible with Sage, we really need to have one widget which supports both.
I don't think we ought to pull in things from Sage that would be a bad thing moving forward, though. If anything, Sage could have a small compatibility wrapper that it loads by default, maybe with a deprecation warning.
For this issue, I'm simply asking to be able to intercept the on_click() event. I am not saying that ipywidgets should by default do something with this event. So that would be 100% backwards compatible for ipywidgets.
For this issue, I'm simply asking to be able to intercept the on_click() event. I am not saying that ipywidgets should by default do something with this event. So that would be 100% backwards compatible for ipywidgets.
Okay. Can you submit a PR?
Okay. Can you submit a PR?
No, this is beyond my skill set (otherwise I would have done it already).
Moving to 6.1
This should do the job:
import ipywidgets as widgets
class ClickResponsiveToggleButtons(widgets.ToggleButtons):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._click_handlers = widgets.CallbackDispatcher()
self.on_msg(self._handle_button_msg)
pass
def on_click(self, callback, remove=False):
"""Register a callback to execute when the button is clicked.
The callback will be called with one argument, the clicked button
widget instance.
Parameters
----------
remove: bool (optional)
Set to true to remove the callback from the list of callbacks.
"""
self._click_handlers.register_callback(callback, remove=remove)
def _handle_button_msg(self, _, content, buffers):
"""Handle a msg from the front-end.
Parameters
----------
content: dict
Content of the msg.
"""
if content.get('event', '') == 'click':
self._click_handlers(self)
Most helpful comment
I believe buttons only have click because they are the only stateless widget. ToggleButtons use .value, so you can observe changes to .value, which will be triggered on every click:
and you can use it with interact, as well: