GetDunne Wiki

Notes from the desk of Shane Dunne, software development consultant

User Tools

Site Tools


the_plugin_editor

The VanillaJuce plugin Editor (GUI)

The VanillaJuceAudioProcessorEditor class defined in PluginEditor.h is very simple, and little changed from what was automatically generated by the Projucer:

class VanillaJuceAudioProcessorEditor
    : public AudioProcessorEditor
    , public ChangeListener
{
public:
    VanillaJuceAudioProcessorEditor (VanillaJuceAudioProcessor&);
    ~VanillaJuceAudioProcessorEditor();
 
    void paint (Graphics&) override;
    void resized() override;
 
    virtual void changeListenerCallback(ChangeBroadcaster* source);
 
private:
    VanillaJuceAudioProcessor& processor;
    GuiTabs guiTabs;
 
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VanillaJuceAudioProcessorEditor)
};

The editor GUI is entirely built from JUCE Component-class objects. It consists of a single GuiTabs object (the guiTabs member variable above), which itself owns and contains all the other Components. As you'll see below, all of my Gui… classes take a SynthSound* pointer as a constructor argument, and implement a void notify() member function which gets called whenever any parameter is changed. Here is the entire PluginEditor.cpp file (except for the #include lines at the top):

VanillaJuceAudioProcessorEditor::VanillaJuceAudioProcessorEditor (VanillaJuceAudioProcessor& p)
    : AudioProcessorEditor (&p)
    , processor (p)
    , guiTabs(p.getSound())
{
    setSize (600, 300);
    addAndMakeVisible(&guiTabs);
    p.addChangeListener(this);
}
 
VanillaJuceAudioProcessorEditor::~VanillaJuceAudioProcessorEditor()
{
    processor.removeChangeListener(this);
}
 
void VanillaJuceAudioProcessorEditor::paint (Graphics& g)
{
    ignoreUnused(g);
}
 
void VanillaJuceAudioProcessorEditor::resized()
{
    guiTabs.setBounds(0, 0, getWidth(), getHeight());
}
 
void VanillaJuceAudioProcessorEditor::changeListenerCallback(ChangeBroadcaster* source)
{
    ignoreUnused(source);
    guiTabs.notify();
}

The constructor calls the processor's getSound() function to get a pointer to the one shared SynthSound object, which it passes to the constructor for guiTabs. It also sets the GUI window size, calls juce::Component::addAndMakeVisible() to add the tabs object as a child Component of the editor, and registers the editor as a change-listener to the processor. The destructor unregisters it.

The resized() function sets the bounds of the guiTabs object so it completely fills the editor window. As a consequence, the paint() function has nothing to do, because the tabs object will render the entire window contents.

The editor inherits from juce::ChangeListener, hence it implements changeListenerCallback(), which simply calls guiTabs.notify() (which will propagate the change notification to all of its child GUI components.)

The GuiTabs object

The GuiTabs class is a container for a juce::TabbedComponent object, and three “tab” Components which are added to it:

class GuiTabs  : public Component
{
public:
    GuiTabs (SynthSound* pSynthSound);
    ~GuiTabs();
 
    void paint (Graphics& g) override;
    void resized() override;
 
    void notify();
 
private:
    ScopedPointer<TabbedComponent> tabbedComponent;
    GuiMainTab* pMainTab;
    GuiOscTab* pOscTab;
    GuiEgTab* pAmpEgTab;
 
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GuiTabs)
};
 
GuiTabs::GuiTabs (SynthSound* pSynthSound)
{
    addAndMakeVisible (tabbedComponent = new TabbedComponent (TabbedButtonBar::TabsAtTop));
    tabbedComponent->setTabBarDepth (32);
    tabbedComponent->addTab(TRANS("Main"), Colours::lightgrey, pMainTab = new GuiMainTab(pSynthSound), true);
    tabbedComponent->addTab (TRANS("Osc"), Colours::lightgrey, pOscTab = new GuiOscTab(pSynthSound), true);
    tabbedComponent->addTab(TRANS("AmpEG"), Colours::lightgrey, pAmpEgTab = new GuiEgTab(pSynthSound), true);
    tabbedComponent->setCurrentTabIndex(0);
}
 
GuiTabs::~GuiTabs()
{
}
 
//==============================================================================
void GuiTabs::paint (Graphics& g)
{
    g.fillAll (Colour (0xff323e44));
}
 
void GuiTabs::resized()
{
    tabbedComponent->setBounds (0, 0, getWidth(), getHeight());
}
 
void GuiTabs::notify()
{
    pMainTab->notify();
    pOscTab->notify();
    pAmpEgTab->notify();
}

The constructor creates the tabbedComponent object and calls addAndMakeVisible() on it, sets the depth of the tabs bar to 32 pixels, creates the three tab objects and calls tabbedComponent→addTab, and sets the first (zeroth) tab to be the initially-selected one.

Note there are no deletes in the destructor, despite the use of new in the constructor. Setting the last argument of addTab() to true tells tabbedComponent to take ownership of the three tab objects, so it will delete them. The tabbedComponent member variable itself is declared as a JUCE ScopedPointer, so the object it points to (the tabs object), will get deleted automatically when the GuiTabs object itself gets deleted.

The paint() member function fills the window with the background color. This is necessary, despite the fact that the resized() sets the bounds of tabbedComponent to the full size of the GUI window, because when tabbedComponent draws itself, it draws only the three tabs at the top and their contents below—it does not draw anything into the space to the right of the third tab. The fillAll() call in paint() ensures this gets filled in, by filling the entire GUI window with the background color before tabbedComponent renders.

The individual tabs

The three tab classes GuiMainTab, GuiOscTab, and GuiEgTab are all very similar, so let's just look at GuiMainTab which is the simplest of the three. Here's the class declaration:

class GuiMainTab : public Component, public SliderListener
{
public:
    GuiMainTab (SynthSound* pSynthSound);
    ~GuiMainTab();
 
    void paint (Graphics& g) override;
    void resized() override;
    void sliderValueChanged (Slider* sliderThatWasMoved) override;
 
    void notify();
 
private:
    SynthSound* pSound;
 
    ScopedPointer<Label> masterLevelLabel;
    ScopedPointer<Slider> masterLevelSlider;
    ScopedPointer<Label> pbUpLabel;
    ScopedPointer<Slider> pbUpSlider;
    ScopedPointer<Label> pbDownLabel;
    ScopedPointer<Slider> pbDownSlider;
 
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GuiMainTab)
};

This class inherits from juce::SliderListener as well as from juce::Component, to ensure that it gets notified (via a call to sliderValueChanged()) whenever any of its three slider controls changes.

The constructor takes a SynthSound* argument, which it saves in the member variable pSound. The GuiTabs object didn't need to do this, because it only needed to pass the SynthSound* pointer on to the constructors of the tab-objects. The tab objects themselves do need to store a copy of the SynthSound pointer, so they can use it in two ways:

  1. To read and write the parameters of the current patch, the tab object can dereference the pSound pointer to get at its pParams member, which points to the SynthParams structure.
  2. Whenever any of the tab's controls changes any parameter value, it must call pSound→parameterChanged() to notify the Synth object, which in turn will notify all active synthesizer voices.

Note the use of ScopedPointers for all of the contained objects. This ensures we don't have to put a series of deletes into the destructor. In fact, the destructor for GuiMainTab is empty.

The bulk of the constructor code just creates and sets up the three slider controls and their labels. I developed this code by using the Projucer to create a stand-alone GUI program, using its interactive GUI editor, then I looked at the code it produced and adapted that for use here.

GuiMainTab::GuiMainTab (SynthSound* pSynthSound)
    : pSound(pSynthSound)
{
    addAndMakeVisible (masterLevelLabel = new Label ("master level label", TRANS("Master Volume")));
    masterLevelLabel->setFont (Font (15.00f, Font::plain).withTypefaceStyle ("Regular"));
    masterLevelLabel->setJustificationType (Justification::centredRight);
    masterLevelLabel->setEditable (false, false, false);
    masterLevelLabel->setColour (TextEditor::textColourId, Colours::black);
    masterLevelLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
 
    addAndMakeVisible (masterLevelSlider = new Slider ("Master Volume"));
    masterLevelSlider->setRange (0, 10, 0);
    masterLevelSlider->setSliderStyle (Slider::LinearHorizontal);
    masterLevelSlider->setTextBoxStyle (Slider::TextBoxRight, false, 80, 20);
    masterLevelSlider->addListener (this);
 
    addAndMakeVisible(pbUpLabel = new Label("PB up semis", TRANS("P.Bend up (semi)")));
    pbUpLabel->setFont(Font(15.00f, Font::plain).withTypefaceStyle("Regular"));
    pbUpLabel->setJustificationType(Justification::centredRight);
    pbUpLabel->setEditable(false, false, false);
    pbUpLabel->setColour(TextEditor::textColourId, Colours::black);
    pbUpLabel->setColour(TextEditor::backgroundColourId, Colour(0x00000000));
 
    addAndMakeVisible(pbUpSlider = new Slider("PB up semis"));
    pbUpSlider->setRange(0, 12, 1);
    pbUpSlider->setSliderStyle(Slider::LinearHorizontal);
    pbUpSlider->setTextBoxStyle(Slider::TextBoxRight, false, 80, 20);
    pbUpSlider->addListener(this);
 
    addAndMakeVisible(pbDownLabel = new Label("PB down semis", TRANS("P.Bend down (semi)")));
    pbDownLabel->setFont(Font(15.00f, Font::plain).withTypefaceStyle("Regular"));
    pbDownLabel->setJustificationType(Justification::centredRight);
    pbDownLabel->setEditable(false, false, false);
    pbDownLabel->setColour(TextEditor::textColourId, Colours::black);
    pbDownLabel->setColour(TextEditor::backgroundColourId, Colour(0x00000000));
 
    addAndMakeVisible(pbDownSlider = new Slider("PB down semis"));
    pbDownSlider->setRange(0, 12, 1);
    pbDownSlider->setSliderStyle(Slider::LinearHorizontal);
    pbDownSlider->setTextBoxStyle(Slider::TextBoxRight, false, 80, 20);
    pbDownSlider->addListener(this);
 
    notify();
}

The constructor ends with a call to this tab's own notify() function, which is more commonly called in response to parameter changes within the plugin processor. It sets the values of all three sliders, based on the values of the corresponding parameters.

    SynthParameters* pParams = pSound->pParams;
    masterLevelSlider->setValue(10.0 * pParams->masterLevel);
    pbUpSlider->setValue(pParams->pitchBendUpSemitones);
    pbDownSlider->setValue(pParams->pitchBendDownSemitones);

The sliderValueChanged() callback does the reverse operation, changing the value of a parameter whenever the corresponding slider is adjusted:

void GuiMainTab::sliderValueChanged (Slider* sliderThatWasMoved)
{
    double value = sliderThatWasMoved->getValue();
    SynthParameters* pParams = pSound->pParams;
    if (sliderThatWasMoved == masterLevelSlider)
    {
        pParams->masterLevel = 0.1 * value;
    }
    else if (sliderThatWasMoved == pbUpSlider)
    {
        pParams->pitchBendUpSemitones = int(value);
    }
    else if (sliderThatWasMoved == pbDownSlider)
    {
        pParams->pitchBendDownSemitones = int(value);
    }
    pSound->parameterChanged();
}

The final pSound→parameterChanged() call propagates the change back to the Synth object and thence to any active synth voices.

The paint() member function takes care of filling the entire background behind the labels and sliders:

void GuiMainTab::paint (Graphics& g)
{
    g.fillAll (Colour (0xff323e44));
}

The resized() function completes the layout by calling setBounds() on all the child Component objects. Unfortunately, the present version of the Projucer doesn't write code like this; it simply puts numeric constant values for all four setBounds() arguments. I used the Projucer-generated code to glean key dimensional values, then rewrote the code to use these somewhat dynamically:

void GuiMainTab::resized()
{
    const int labelLeft = 16;
    const int controlLeft = 144;
    const int labelWidth = 120;
    const int sliderWidth = 420;
    const int controlHeight = 24;
    const int gapHeight = 8;
 
    int top = 20;
    masterLevelLabel->setBounds(labelLeft, top, labelWidth, controlHeight);
    masterLevelSlider->setBounds(controlLeft, top, sliderWidth, controlHeight);
    top += controlHeight + 5 * gapHeight;
    pbUpLabel->setBounds(labelLeft, top, labelWidth, controlHeight);
    pbUpSlider->setBounds(controlLeft, top, sliderWidth, controlHeight);
    top += controlHeight + gapHeight;
    pbDownLabel->setBounds(labelLeft, top, labelWidth, controlHeight);
    pbDownSlider->setBounds(controlLeft, top, sliderWidth, controlHeight);
}

The code above will lay out the labels and sliders so they are aligned nicely, with certain gaps between them. It does not make any reference to the tab object's own bounds. If you want to create a dynamic layout where, for example, the slider widths adjust automatically to fill the available horizontal space, you would need to compute the sliderWidth value dynamically based on the tab's own bounds (call the getBounds() function).

And that's pretty much it for the VanillaJuce GUI. The other two tabs don't differ in any meaningful way.

the_plugin_editor.txt · Last modified: 2017/10/02 19:38 by shane