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)
};