FR: Make AttachedControlBase in AudioProcessorValueTreeState.cpp a public facing class

I have a number of custom GUI objects for my plugins, it would be very useful to be able to create an attachment to AudioProcessorValueTreeState for them, along with an attachControl method.

I basically copied the ButtonAttachment classes and modified them for my custom classes.

3 Likes

+1 to making AttachedControlBase public. I’ve a few custom GUI objects which could do with having this class around to save re-implementing the functionality.

There was also mention of this in the JUCE discord recently.

5 Likes

Hey, here’s a class inspired by AttachedControlBase that you can use to control your custom GUI elements. Would be great if sth. like this would find its way into JUCE.

#pragma once

class ParameterAttachment : public AudioProcessorValueTreeState::Listener,
                            public AsyncUpdater
{
public:
    /** The Parameter Attachment can be added on any GUI or non GUI class, which needs control from or to a
     AudioProcessorValueTreeState Parameter. There's two different ways to use it. For Components it's not neccessary
     to use the addListener methods, it might be sufficent to use repaintOnParameterChange.*/
    ParameterAttachment (AudioProcessorValueTreeState& s, const String& p)
        : parameters (s), paramID (p), lastValue (0)
    {
        if (!(parameter = parameters.getParameter (paramID)))
            jassert (true); //paramID not found
        parameters.addParameterListener (paramID, this);
    }

    ~ParameterAttachment ()
    {
        parameters.removeParameterListener (paramID, this);
    }

    struct Listener
    {
        virtual ~Listener (){};

        virtual void unnormalisedParameterValueChanged (float, String){};
        virtual void normalisedParameterValueChanged (float, String){};
    };

    /** Takes any object and sends callbacks on parameter changes.*/
    void addListener (Listener* listener)
    {
        listeners.add (listener);
        sendInitialUpdate ();
    }

    void removeListener (Listener* listener)
    {
        listeners.remove (listener);
    }

    /** If you add a Component to this class, it will be repainted, whenever the parameter is changed and you can use the getUnnormalisedValue () or getNormalisedValue () to get the current value*/
    void repaintOnParameterChange (Component* component)
    {
        componentToRepaint = component;
        sendInitialUpdate ();
    }

    /** Call this to to tell the host you started editng this parameter*/
    void startParameterChange ()
    {
        if (!userIsControllingParameter)
        {
            userIsControllingParameter = true;
            parameter->beginChangeGesture ();
        }
    }

    /** Call this to to tell the host you ended editng this parameter*/
    void endParameterChange ()
    {
        if (userIsControllingParameter)
        {
            userIsControllingParameter = false;
            parameter->endChangeGesture ();
        }
    }

    /** Sets the unnormalised value and informs the host*/
    void setNewUnnormalisedValue (float newUnnormalisedValue)
    {
        jassert (userIsControllingParameter); // Always call startParameterChange() before changing

        const float newValue = parameters.getParameterRange (paramID)
                                   .convertTo0to1 (newUnnormalisedValue);
        if (parameter->getValue () != newValue)
            parameter->setValueNotifyingHost (newValue);
    }

    /** Sets the normalised value (0 to 1) and informs the host*/
    void setNewNormalisedValue (float newNormalisedValue)
    {
        jassert (userIsControllingParameter); // Always call startParameterChange() before changing

        const float newValue = jlimit (0.0f, 1.0f, newNormalisedValue);
        if (parameter->getValue () != newValue)
            parameter->setValueNotifyingHost (newValue);
    }

    float getUnnormalisedValue ()
    {
        return lastValue;
    }

    float getNormalisedValue ()
    {
        return parameters.getParameterRange (paramID).convertTo0to1 (lastValue);
    }
    
    String getText ()
    {
        return parameter->getText(parameter->getValue(), 40);
    }

private:
    void parameterChanged (const String&, float newValue) override
    {
        lastValue = newValue;

        if (MessageManager::getInstance ()->isThisTheMessageThread ())
        {
            cancelPendingUpdate ();
            if (!listeners.isEmpty())
            {
                listeners.call (&Listener::unnormalisedParameterValueChanged, newValue, paramID);
                float newNormalisedValue = parameters.getParameterRange (paramID).convertTo0to1 (newValue);
                listeners.call (&Listener::normalisedParameterValueChanged, newNormalisedValue, paramID);
            }
            if (componentToRepaint != nullptr)
            {
                componentToRepaint->repaint();
            }
        }
        else
        {
            triggerAsyncUpdate ();
        }
    }

    void handleAsyncUpdate () override
    {
        if (!listeners.isEmpty())
        {
            listeners.call (&Listener::unnormalisedParameterValueChanged, lastValue, paramID);
            float newNormalisedValue = parameters.getParameterRange (paramID).convertTo0to1 (lastValue);
            listeners.call (&Listener::normalisedParameterValueChanged, newNormalisedValue, paramID);
        }
        if (componentToRepaint != nullptr)
        {
            componentToRepaint->repaint();
        }
        
    }

    void sendInitialUpdate ()
    {
        if (float* v = parameters.getRawParameterValue (paramID))
            parameterChanged (paramID, *v);
    }

    AudioProcessorValueTreeState& parameters;
    AudioProcessorParameter* parameter;

    ListenerList<ParameterAttachment::Listener> listeners;

    Component* componentToRepaint{nullptr};

    String paramID;
    float lastValue;

    bool userIsControllingParameter = {false};

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ParameterAttachment)
};