Button::onClick


#1

Just a heads-up that I’m adding a couple of new helper objects: Button::onClick and Button::onStateChange, which were an experiment in finding an easy way to attach lambdas to event callbacks. I did a quick whizz through the codebase to change a load of example code to use them, so you can take a look at how much it simplifies the code - in almost every case it was shorter, cleaner and nicer to use than Button::Listener.

Since I’m pleased with the result, I’ll roll out some more use of this style for other classes as we go on - requests for which ones you want added are welcome!

Should all be up on the develop branch shortly…


#2

Wait, was this quote of mine meant to go here?


#3

oh, how odd… I had two windows open… must have got mixed up!


#4

And sorry - scratch the bit about a new class called EventHandler… Just spotted a flaw in that idea which makes it no easier than using std::function directly, which I will do…


#5

we love lambdas!


#6

You can add lambda for Timer and AsyncUpdater as well so it remove the need to inherit and allow multiple of those in a single class :slight_smile:

Thanks !


#7

Timer lambda exists as callAfterDelay:

https://juce.com/doc/classTimer#ae7100e8b84d2a8d6adac3d14cbbd27d6

AsyncUpdater doesn’t really make sense when it comes to lambdas because the point is to have a single method (handleAsyncUpdate) that gets marked as needing to be called. If you’re looking for a method that is “call this lambda on the message thread at some async point in the future” look at MessageManager::callAsync. Just be aware that unlike AsyncUpdater it allocates on the message queue, so it’s not safe to call from a high priority thread.


#8

I think the two use cases requested here are different from Timer:: callAfterDelay and MessageManager::callAsync. Both of these are single trigger, deferred callbacks.

A LambdaTimer would be used to repeatedly call a lambda at a specific interval, a LambdaAsyncUpdater would be used to coalesce multiple calls in to a single callback. I gave examples with code of both in my ADC 2016 presentation: on pages 19 and 22 respectively.

class LambdaTimer   : public juce::Timer
{
public:
    LambdaTimer() = default;

    LambdaTimer (std::function<void()> newCallback)
    {
        callback = std::move (newCallback);
    }

    LambdaTimer& setCallback (std::function<void()> newCallback)
    {
        callback = std::move (newCallback);
        return *this;
    }

    void timerCallback() override
    {
        if (callback)
            callback();
    }

private:
    std::function<void()> callback;
};


//==============================================================================
/**
    Asyncronously call a function.
 */
struct LambdaAsyncUpdater  : public juce::AsyncUpdater
{
    /** Creates an empty LambdaAsyncUpdater. */
    LambdaAsyncUpdater() = default;

    /** Destructor. */
    ~LambdaAsyncUpdater()
    {
        cancelPendingUpdate();
    }

    /** Sets the function to call. */
    LambdaAsyncUpdater& setFunction (std::function<void()> f)
    {
        function = std::move (f);
        return *this;
    }

    /** @internal. */
    void handleAsyncUpdate() override
    {
        jassert (function);
        function();
    }

    std::function<void()> function;
};

#9

So, does this mark the death of the whole Listener design being used in JUCE?
relevant threads:



#10

Aha, similar to the class I posted the other day:


#11

No - we’ll leave the listeners in there because sometimes you need to attach multiple listeners or to a component, or manage adding/removing them in more complex situations. But for the 95% of normal situations where you just need to attach one quick, simple callback to something like a button, this pattern makes the code much simpler.


#12

an interesting design for this would be that the lambda can be a proxy listener list if you require multiple listeners. The lambda can capture the previous lambda and call it as well as the new one.


#13

Here is a design which combines Listeners & Lambdas, best of two worlds:


#14

perhaps we could attach a lambda to the ChangeBroadcaster, so that we don’t have to inherits ChangeListener every time?


#15

And with multiple listeners? You may override accidentally other “listeners” with this approach.

Here is another approach:


#16

Wouldn’t be a good idea to have
std::function<void(Component * c)> onClick
instead of
std::function<void()> onClick

No need to use listeners in simple cases, but we still would know which component is responsible for the action. I used to it after years of programming in Delphi where such form of callback was typical:
procedure TComponent.Click(TObject: Sender)

I’m not talking here about the buttons only, but generally about event callbacks we can attach to lambdas.


#17

You can capture the notifying component, the “this” pointer or some other relevant information needed in the lambda so the “sender” parameter isn’t really needed.

So for example, something like this :

for (int i=0;i<100;++i)
{
  TextButton* but = new TextButton;  
  addAndMakeVisible(but);
  but->onClick=[i,but]()
  { Logger::writeToLog("Button "+String(i)+" pressed. Button's pointer is "+String(but)); }; 
}

#18

This is exactly how I use them now. You’re right this parameter isn’t really needed, but - at least for me - it would be clearer and faster. I could send the same list of parameters to another function without additional coding. Maybe it’s a matter of habit :wink:


#19

In the old class-based listener paradigm it was helpful to have the component passed in as a parameter to the callback. But the whole point of lambdas is that they can capture whatever info your callback needs - I can think of several disadvantages there’d be in adding a parameter like you suggest, but no advantages at all to doing it.

(…but if you do for some reason have a use-case where you want lots of lambdas that take a pointer, you can implement exactly that yourself, by wrapping your own lambda in another one which calls it and passes the component pointer. Writing that would be a one-line free function.)


#20

Ok, I agree one should stick to a certain paradigm. So, according to that all new helpers we expect in the future releases will be without any parameters? For example we could have Viewport with a callback onVisibleAreaChanged (something I already use). So it would be logical to follow this pattern:

myViewport.onVisibleAreaChanged = [this, v = &this->myViewport] () { doSomething(v->getViewArea()); };

and not this one?

myViewport.onVisibleAreaChanged = [this, v = &this->myViewport] (const Rectangle<int>& newVisibleArea) { doSomething(newVisibleArea); };