I’ve been working on new plugin where the ability to resize is quite significant. The desired behavior is to drag the bottom right corner to snap to several different predefined sizes.
To do this I’m using a custom ComponentBoundsConstrainer within my PluginEditor. In AU & VST this works fine. However the VST3 builds seem to cause glitchy issues in certain hosts. I’ve tested on a fair amount of hosts and these are the ones that seem to exhibit the issue:
macOS:
• Cubase Pro 9: Constrainer doesn’t snap. Resizing occurs smoothly.
• WaveLab Pro 9: Hard to describe. A lot of the drawing bounds get messed up.
• Studio One: Flickering/glitching when resizing.
• Bitwig: Constrainer doesn’t snap. Resizing occurs smoothly.
Windows:
• Studio One
• Cubase Elements 9 (didn’t have the pro version to test)
Note: the VST2 equivalents in these hosts work fine!
Here is an example of how I’m customizing the ComponentBoundsConstrainer. This one snaps between two sizes:
class Constrainer : public ComponentBoundsConstrainer
{
public:
void checkBounds (Rectangle<int>& bounds,
const Rectangle<int>& previousBounds,
const Rectangle<int>& limits,
bool isStretchingTop,
bool isStretchingLeft,
bool isStretchingBottom,
bool isStretchingRight) override
{
if (bounds.getHeight() > 350)
{
bounds.setHeight (600);
bounds.setWidth (400);
}
else
{
bounds.setHeight (200);
bounds.setWidth (200);
}
}
};
To reproduce the issue:
- Pull the latest changes in the JUCE develop branch
- In the JuceDemoPlugin project replace PluginEditor.h with the following code (use
git diff
to see the difference):
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#pragma once
#include "../JuceLibraryCode/JuceHeader.h"
#include "PluginProcessor.h"
class Constrainer : public ComponentBoundsConstrainer
{
public:
void checkBounds (Rectangle<int>& bounds,
const Rectangle<int>& previousBounds,
const Rectangle<int>& limits,
bool isStretchingTop,
bool isStretchingLeft,
bool isStretchingBottom,
bool isStretchingRight) override
{
if (bounds.getHeight() > 350)
{
bounds.setHeight (600);
bounds.setWidth (400);
}
else
{
bounds.setHeight (200);
bounds.setWidth (200);
}
}
};
//==============================================================================
/** This is the editor component that our filter will display.
*/
class JuceDemoPluginAudioProcessorEditor : public AudioProcessorEditor,
private Timer
{
public:
JuceDemoPluginAudioProcessorEditor (JuceDemoPluginAudioProcessor&);
~JuceDemoPluginAudioProcessorEditor();
//==============================================================================
void paint (Graphics&) override;
void resized() override;
void timerCallback() override;
void hostMIDIControllerIsAvailable (bool) override;
void updateTrackProperties();
private:
class ParameterSlider;
MidiKeyboardComponent midiKeyboard;
Label timecodeDisplayLabel, gainLabel, delayLabel;
ScopedPointer<ParameterSlider> gainSlider, delaySlider;
Colour backgroundColour;
Constrainer constrain;
//==============================================================================
JuceDemoPluginAudioProcessor& getProcessor() const
{
return static_cast<JuceDemoPluginAudioProcessor&> (processor);
}
void updateTimecodeDisplay (AudioPlayHead::CurrentPositionInfo);
};
- Replace PluginEditor.cpp with the following code:
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#include "PluginProcessor.h"
#include "PluginEditor.h"
//==============================================================================
// This is a handy slider subclass that controls an AudioProcessorParameter
// (may move this class into the library itself at some point in the future..)
class JuceDemoPluginAudioProcessorEditor::ParameterSlider : public Slider,
private Timer
{
public:
ParameterSlider (AudioProcessorParameter& p)
: Slider (p.getName (256)), param (p)
{
setRange (0.0, 1.0, 0.0);
startTimerHz (30);
updateSliderPos();
}
void valueChanged() override { param.setValueNotifyingHost ((float) Slider::getValue()); }
void timerCallback() override { updateSliderPos(); }
void startedDragging() override { param.beginChangeGesture(); }
void stoppedDragging() override { param.endChangeGesture(); }
double getValueFromText (const String& text) override { return param.getValueForText (text); }
String getTextFromValue (double value) override { return param.getText ((float) value, 1024); }
void updateSliderPos()
{
const float newValue = param.getValue();
if (newValue != (float) Slider::getValue() && ! isMouseButtonDown())
Slider::setValue (newValue, NotificationType::dontSendNotification);
}
AudioProcessorParameter& param;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ParameterSlider)
};
//==============================================================================
JuceDemoPluginAudioProcessorEditor::JuceDemoPluginAudioProcessorEditor (JuceDemoPluginAudioProcessor& owner)
: AudioProcessorEditor (owner),
midiKeyboard (owner.keyboardState, MidiKeyboardComponent::horizontalKeyboard),
timecodeDisplayLabel (String()),
gainLabel (String(), "Throughput level:"),
delayLabel (String(), "Delay:")
{
// add some sliders..
addAndMakeVisible (gainSlider = new ParameterSlider (*owner.gainParam));
gainSlider->setSliderStyle (Slider::Rotary);
addAndMakeVisible (delaySlider = new ParameterSlider (*owner.delayParam));
delaySlider->setSliderStyle (Slider::Rotary);
// add some labels for the sliders..
gainLabel.attachToComponent (gainSlider, false);
gainLabel.setFont (Font (11.0f));
delayLabel.attachToComponent (delaySlider, false);
delayLabel.setFont (Font (11.0f));
// add the midi keyboard component..
addAndMakeVisible (midiKeyboard);
// add a label that will display the current timecode and status..
addAndMakeVisible (timecodeDisplayLabel);
timecodeDisplayLabel.setFont (Font (Font::getDefaultMonospacedFontName(), 15.0f, Font::plain));
// set resize limits for this plug-in
// setResizeLimits (400, 200, 1024, 700);
setConstrainer (&constrain);
setResizable (true, true);
// set our component's initial size to be the last one that was stored in the filter's settings
setSize (owner.lastUIWidth,
owner.lastUIHeight);
updateTrackProperties();
// start a timer which will keep our timecode display updated
startTimerHz (30);
}
JuceDemoPluginAudioProcessorEditor::~JuceDemoPluginAudioProcessorEditor()
{
}
//==============================================================================
void JuceDemoPluginAudioProcessorEditor::paint (Graphics& g)
{
g.setColour (backgroundColour);
g.fillAll();
}
void JuceDemoPluginAudioProcessorEditor::resized()
{
// This lays out our child components...
Rectangle<int> r (getLocalBounds().reduced (8));
timecodeDisplayLabel.setBounds (r.removeFromTop (26));
midiKeyboard.setBounds (r.removeFromBottom (70));
r.removeFromTop (20);
Rectangle<int> sliderArea (r.removeFromTop (60));
gainSlider->setBounds (sliderArea.removeFromLeft (jmin (180, sliderArea.getWidth() / 2)));
delaySlider->setBounds (sliderArea.removeFromLeft (jmin (180, sliderArea.getWidth())));
getProcessor().lastUIWidth = getWidth();
getProcessor().lastUIHeight = getHeight();
}
//==============================================================================
void JuceDemoPluginAudioProcessorEditor::timerCallback()
{
updateTimecodeDisplay (getProcessor().lastPosInfo);
}
void JuceDemoPluginAudioProcessorEditor::hostMIDIControllerIsAvailable (bool controllerIsAvailable)
{
midiKeyboard.setVisible (! controllerIsAvailable);
}
void JuceDemoPluginAudioProcessorEditor::updateTrackProperties ()
{
auto trackColour = getProcessor().trackProperties.colour;
auto& lf = getLookAndFeel();
backgroundColour = (trackColour == Colour() ? lf.findColour (ResizableWindow::backgroundColourId)
: trackColour.withAlpha (1.0f).withBrightness (0.266f));
repaint();
}
//==============================================================================
// quick-and-dirty function to format a timecode string
static String timeToTimecodeString (double seconds)
{
const int millisecs = roundToInt (seconds * 1000.0);
const int absMillisecs = std::abs (millisecs);
return String::formatted ("%02d:%02d:%02d.%03d",
millisecs / 3600000,
(absMillisecs / 60000) % 60,
(absMillisecs / 1000) % 60,
absMillisecs % 1000);
}
// quick-and-dirty function to format a bars/beats string
static String quarterNotePositionToBarsBeatsString (double quarterNotes, int numerator, int denominator)
{
if (numerator == 0 || denominator == 0)
return "1|1|000";
const int quarterNotesPerBar = (numerator * 4 / denominator);
const double beats = (fmod (quarterNotes, quarterNotesPerBar) / quarterNotesPerBar) * numerator;
const int bar = ((int) quarterNotes) / quarterNotesPerBar + 1;
const int beat = ((int) beats) + 1;
const int ticks = ((int) (fmod (beats, 1.0) * 960.0 + 0.5));
return String::formatted ("%d|%d|%03d", bar, beat, ticks);
}
// Updates the text in our position label.
void JuceDemoPluginAudioProcessorEditor::updateTimecodeDisplay (AudioPlayHead::CurrentPositionInfo pos)
{
MemoryOutputStream displayText;
displayText << "[" << SystemStats::getJUCEVersion() << "] "
<< String (pos.bpm, 2) << " bpm, "
<< pos.timeSigNumerator << '/' << pos.timeSigDenominator
<< " - " << timeToTimecodeString (pos.timeInSeconds)
<< " - " << quarterNotePositionToBarsBeatsString (pos.ppqPosition,
pos.timeSigNumerator,
pos.timeSigDenominator);
if (pos.isRecording)
displayText << " (recording)";
else if (pos.isPlaying)
displayText << " (playing)";
timecodeDisplayLabel.setText (displayText.toString(), dontSendNotification);
}
- Open the VST3 plugin in one of the above listed hosts. Drag the bottom right corner to snap between the two different sizes.
Many thanks!