monostep/Source/PluginEditor.cpp

613 lines
20 KiB
C++
Raw Normal View History

#include "PluginEditor.h"
#include "PluginProcessor.h"
#include "Theme.h"
#include "BuildInfo.h"
using namespace monostep;
//==============================================================================
EditorLookAndFeel::EditorLookAndFeel()
{
setColour (juce::Slider::textBoxBackgroundColourId, colours::panel);
setColour (juce::Slider::textBoxTextColourId, colours::text);
setColour (juce::Slider::textBoxOutlineColourId, colours::grid);
}
void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w, int h,
float sliderPos, float rotaryStartAngle,
float rotaryEndAngle, juce::Slider&)
{
const auto bounds = juce::Rectangle<float> ((float) x, (float) y, (float) w, (float) h);
const auto centre = bounds.getCentre();
const float radius = juce::jmin (bounds.getWidth(), bounds.getHeight()) * 0.5f;
const float start = rotaryStartAngle;
const float end = rotaryEndAngle;
const float angle = start + sliderPos * (end - start);
g.setColour (colours::grid);
g.fillEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f);
g.setColour (colours::gridLight);
g.drawEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f, 1.0f);
if (end - start < 6.0f)
{
juce::Path valueArc;
valueArc.addCentredArc (centre.getX(), centre.getY(),
radius * 0.85f, radius * 0.85f,
0.0f, start, angle, true);
g.setColour (colours::accent);
g.strokePath (valueArc, juce::PathStrokeType (2.5f));
}
// JUCE measures rotary angles clockwise from 12 o'clock; the pointer is
// drawn with the same convention so it matches the arc and mouse dragging.
const float tickLen = radius * 0.62f;
g.setColour (colours::accent);
g.drawLine (centre.getX(), centre.getY(),
centre.getX() + tickLen * std::sin (angle),
centre.getY() - tickLen * std::cos (angle),
2.5f);
g.fillEllipse (centre.getX() - 2.5f, centre.getY() - 2.5f, 5.0f, 5.0f);
}
void EditorLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
bool, bool)
{
const auto bounds = button.getLocalBounds().toFloat();
g.setColour (button.getToggleState() ? colours::accent : colours::grid);
g.fillRoundedRectangle (bounds, 4.0f);
g.setFont (font (11.0f));
g.setColour (button.getToggleState() ? colours::bg : colours::text);
g.drawText (button.getButtonText(), bounds, juce::Justification::centred);
}
void EditorLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& button,
const juce::Colour&, bool highlighted, bool)
{
const auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
if (button.getToggleState())
g.setColour (colours::accent);
else
g.setColour (highlighted ? colours::gridLight : colours::grid);
g.fillRoundedRectangle (bounds, 4.0f);
}
void EditorLookAndFeel::drawButtonText (juce::Graphics& g, juce::TextButton& button, bool, bool)
{
g.setFont (font (10.5f));
g.setColour (button.getToggleState() ? colours::bg : colours::text);
g.drawText (button.getButtonText(), button.getLocalBounds(), juce::Justification::centred);
}
//==============================================================================
Knob::Knob (const juce::String& title)
{
slider.setSliderStyle (juce::Slider::RotaryVerticalDrag);
slider.setRotaryParameters (juce::MathConstants<float>::pi * 1.25f,
juce::MathConstants<float>::pi * 2.75f,
true);
slider.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
slider.onValueChange = [this]
{
updateValueLabel();
if (customOnValueChange != nullptr)
customOnValueChange ((float) slider.getValue());
};
addAndMakeVisible (slider);
titleLabel.setText (title, juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centred);
titleLabel.setFont (font (10.0f));
titleLabel.setColour (juce::Label::textColourId, colours::text);
addAndMakeVisible (titleLabel);
valueLabel.setJustificationType (juce::Justification::centred);
valueLabel.setFont (font (9.0f));
valueLabel.setColour (juce::Label::textColourId, colours::accent);
addAndMakeVisible (valueLabel);
updateValueLabel();
}
void Knob::resized()
{
const int w = getWidth();
titleLabel.setBounds (0, 0, w, 14);
slider.setBounds (4, 15, w - 8, w - 8);
valueLabel.setBounds (0, w - 8 + 15, w, 12);
}
void Knob::updateValueLabel()
{
const float value = slider.getValue();
if (format)
valueLabel.setText (format (value), juce::dontSendNotification);
else
valueLabel.setText (juce::String (value, 2), juce::dontSendNotification);
}
//==============================================================================
ChoiceBar::ChoiceBar (juce::AudioProcessorValueTreeState& apvtsRef,
const juce::String& id,
const juce::StringArray& choices)
: apvts (&apvtsRef), paramId (id)
{
for (int i = 0; i < choices.size(); ++i)
{
auto* button = buttons.add (new juce::TextButton (choices[i]));
button->setClickingTogglesState (true);
button->setRadioGroupId (1);
button->onClick = [this, i] { setIndex (i); };
addAndMakeVisible (button);
}
apvts->addParameterListener (paramId, this);
refreshFromParam();
}
ChoiceBar::ChoiceBar (const juce::StringArray& choices,
std::function<void (int)> onSelected)
: manualHandler (std::move (onSelected))
{
for (int i = 0; i < choices.size(); ++i)
{
auto* button = buttons.add (new juce::TextButton (choices[i]));
button->setClickingTogglesState (true);
button->setRadioGroupId (1);
button->onClick = [this, i] { setIndex (i); };
addAndMakeVisible (button);
}
buttons.getFirst()->setToggleState (true, juce::dontSendNotification);
}
ChoiceBar::~ChoiceBar()
{
if (apvts != nullptr)
apvts->removeParameterListener (paramId, this);
}
void ChoiceBar::parameterChanged (const juce::String&, float)
{
juce::MessageManager::callAsync ([this] { refreshFromParam(); });
}
void ChoiceBar::refreshFromParam()
{
if (apvts == nullptr)
return;
if (auto* parameter = apvts->getParameter (paramId))
{
const int index = juce::roundToInt (parameter->convertFrom0to1 (parameter->getValue()));
for (int i = 0; i < buttons.size(); ++i)
buttons[i]->setToggleState (i == index, juce::dontSendNotification);
}
}
void ChoiceBar::setIndex (int index)
{
if (apvts != nullptr)
{
if (auto* parameter = apvts->getParameter (paramId))
{
parameter->beginChangeGesture();
parameter->setValueNotifyingHost (parameter->convertTo0to1 ((float) index));
parameter->endChangeGesture();
}
return;
}
if (manualHandler != nullptr)
manualHandler (index);
}
void ChoiceBar::resized()
{
const int count = buttons.size();
const float spacing = 3.0f;
const float w = ((float) getWidth() - spacing * (count - 1)) / (float) count;
for (int i = 0; i < count; ++i)
buttons[i]->setBounds (juce::roundToInt (i * (w + spacing)), 0,
juce::roundToInt (w), getHeight());
}
//==============================================================================
StepPanel::StepPanel (MonoStepAudioProcessor& processorRef)
: processor (processorRef),
fineKnob ("Fine")
{
titleLabel.setText ("Step", juce::dontSendNotification);
titleLabel.setJustificationType (juce::Justification::centred);
titleLabel.setFont (font (12.0f));
titleLabel.setColour (juce::Label::textColourId, colours::text);
addAndMakeVisible (titleLabel);
noteLabel.setJustificationType (juce::Justification::centred);
noteLabel.setFont (font (20.0f));
noteLabel.setColour (juce::Label::textColourId, colours::accent);
addAndMakeVisible (noteLabel);
gateLabel.setJustificationType (juce::Justification::centred);
gateLabel.setFont (font (10.0f));
gateLabel.setColour (juce::Label::textColourId, colours::textDim);
addAndMakeVisible (gateLabel);
auto& fineSlider = fineKnob.getSlider();
fineSlider.setRange (-50.0, 50.0, 1.0);
fineSlider.setValue (0.0, juce::dontSendNotification);
fineKnob.setFormat ([] (float v) { return juce::String (v, 0) + " ct"; });
fineKnob.setOnValueChanged ([this] (float v)
{
if (! updating)
processor.setStepCents (selectedStep, v);
});
addAndMakeVisible (fineKnob);
prevButton.setButtonText ("<");
prevButton.onClick = [this] { setSelectedStep (selectedStep - 1); };
addAndMakeVisible (prevButton);
nextButton.setButtonText (">");
nextButton.onClick = [this] { setSelectedStep (selectedStep + 1); };
addAndMakeVisible (nextButton);
hintLabel.setText ("click a column in the grid or use the arrows to pick the step to fine-tune", juce::dontSendNotification);
hintLabel.setJustificationType (juce::Justification::centred);
hintLabel.setFont (font (8.5f));
hintLabel.setColour (juce::Label::textColourId, colours::textDim);
addAndMakeVisible (hintLabel);
clearButton.setButtonText ("Clear Pattern");
clearButton.onClick = [this] { clearPattern(); };
addAndMakeVisible (clearButton);
setSelectedStep (0);
}
void StepPanel::setSelectedStep (int stepIdx)
{
if (stepIdx < 0 || stepIdx >= numSteps)
return;
if (stepIdx != selectedStep)
{
selectedStep = stepIdx;
if (onStepSelected)
onStepSelected (stepIdx);
}
refresh();
}
void StepPanel::refresh()
{
const auto& s = processor.getSequencer().step (selectedStep);
updating = true;
titleLabel.setText ("Step " + juce::String (selectedStep + 1), juce::dontSendNotification);
noteLabel.setText (processor.getStepNoteName (selectedStep)
+ (std::fabs (s.cents) > 0.5f ? " (" + formatCents (s.cents) + ")" : ""),
juce::dontSendNotification);
gateLabel.setText (s.gate ? "gate: on" : "gate: off", juce::dontSendNotification);
fineKnob.getSlider().setValue (s.cents, juce::dontSendNotification);
updating = false;
}
void StepPanel::clearPattern()
{
for (int i = 0; i < numSteps; ++i)
{
processor.setStepGate (i, false);
processor.setStepNote (i, 0);
processor.setStepCents (i, 0.0f);
processor.setStepSlide (i, false);
}
refresh();
}
void StepPanel::resized()
{
const int w = getWidth();
titleLabel.setBounds (0, 4, w, 16);
noteLabel.setBounds (0, 22, w, 28);
gateLabel.setBounds (0, 50, w, 14);
const int knobX = (w - 74) / 2;
const int knobY = 68;
fineKnob.setBounds (knobX, knobY, 74, 74);
const int btnY = knobY + 25;
prevButton.setBounds (knobX - 32, btnY, 24, 24);
nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24);
hintLabel.setBounds (8, knobY + 82, w - 16, 14);
clearButton.setBounds (10, knobY + 106, w - 20, 26);
}
//==============================================================================
MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcessor& processor)
: AudioProcessorEditor (processor),
processorRef (processor),
content (*this),
lnf (std::make_unique<EditorLookAndFeel>()),
lengthKnob ("Length"),
swingKnob ("Swing"),
rootKnob ("Root"),
gateKnob ("Gate"),
masterKnob ("Master"),
detuneKnob ("Detune"),
mixKnob ("Mix"),
cutoffKnob ("Cutoff"),
resKnob ("Res"),
glideKnob ("Glide"),
attackKnob ("Attack"),
decayKnob ("Decay"),
sustainKnob ("Sustain"),
releaseKnob ("Release"),
rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }),
octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }),
zoomBar ({ "100%", "125%", "150%", "175%", "200%" }, [] (int) {}),
wave1Bar (processor.getAPVTS(), "osc1Wave", { "Sin", "Tri", "Saw", "Sq" }),
wave2Bar (processor.getAPVTS(), "osc2Wave", { "Sin", "Tri", "Saw", "Sq" }),
matrix (processor),
stepPanel (processor)
{
setLookAndFeel (lnf.get());
setSize (baseWidth, baseHeight);
auto& apvts = processor.getAPVTS();
addAndMakeVisible (content);
content.addAndMakeVisible (rateBar);
content.addAndMakeVisible (octaveBar);
content.addAndMakeVisible (zoomBar);
content.addAndMakeVisible (wave1Bar);
content.addAndMakeVisible (wave2Bar);
static constexpr float zoomValues[] = { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
zoomBar.setManualHandler ([this] (int idx)
{
setZoomFactor (zoomValues[juce::jlimit (0, 4, idx)]);
});
auto addKnob = [&apvts, this] (Knob& knob, const juce::String& id, std::function<juce::String (float)> fmt)
{
content.addAndMakeVisible (knob);
knob.attach (apvts, id);
knob.setFormat (std::move (fmt));
};
addKnob (lengthKnob, "seqLen", [] (float v) { return juce::String ((int) v); });
addKnob (swingKnob, "seqSwing", [] (float v) { return juce::String (v, 2); });
addKnob (rootKnob, "seqRoot", [] (float v) { return juce::String ((int) v); });
addKnob (gateKnob, "seqGateLen", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (masterKnob, "master", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
addKnob (detuneKnob, "detune", [] (float v) { return juce::String (juce::roundToInt (v)) + " ct"; });
addKnob (mixKnob, "mix", [] (float v) { return juce::String (v, 2); });
addKnob (cutoffKnob, "cutoff", [] (float v) { return v >= 1000.0f ? juce::String (juce::roundToInt (v / 100.0f) * 100) + " Hz" : juce::String (juce::roundToInt (v)) + " Hz"; });
addKnob (resKnob, "res", [] (float v) { return juce::String (v, 2); });
addKnob (glideKnob, "glide", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (attackKnob, "attack", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (decayKnob, "decay", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (sustainKnob, "sustain", [] (float v) { return juce::String (v, 2); });
addKnob (releaseKnob, "release", [] (float v) { return juce::String (v, 2) + "s"; });
content.addAndMakeVisible (matrix);
content.addAndMakeVisible (stepPanel);
matrix.onStepSelected = [this] (int stepIdx)
{
stepPanel.setSelectedStep (stepIdx);
};
stepPanel.onStepSelected = [this] (int stepIdx)
{
matrix.setSelectedStep (stepIdx);
};
lengthKnob.setOnValueChanged ([this] (float)
{
matrix.repaint();
});
matrix.onGateChanged = [this] (int stepIdx, bool gate)
{
processorRef.setStepGate (stepIdx, gate);
stepPanel.refresh();
};
matrix.onNoteChanged = [this] (int stepIdx, int semitone)
{
processorRef.setStepNote (stepIdx, semitone);
stepPanel.refresh();
};
matrix.onFineChanged = [this] (int stepIdx, float cents)
{
processorRef.setStepCents (stepIdx, cents);
stepPanel.refresh();
};
matrix.onSlideChanged = [this] (int stepIdx, bool slide)
{
processorRef.setStepSlide (stepIdx, slide);
stepPanel.refresh();
};
processorRef.onPatternChanged = [this] { refreshEditor(); };
startTimerHz (30);
refreshEditor();
}
MonoStepAudioProcessorEditor::~MonoStepAudioProcessorEditor()
{
stopTimer();
processorRef.onPatternChanged = nullptr;
setLookAndFeel (nullptr);
}
void MonoStepAudioProcessorEditor::timerCallback()
{
const int step = processorRef.getCurrentStep();
if (step != lastPlayStep)
{
lastPlayStep = step;
matrix.setPlayPosition (step);
}
}
void MonoStepAudioProcessorEditor::refreshEditor()
{
matrix.setSelectedStep (matrix.getSelectedStep());
stepPanel.refresh();
matrix.repaint();
}
void MonoStepAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (colours::bg);
}
void MonoStepAudioProcessorEditor::paintContent (juce::Graphics& g)
{
g.setFont (font (22.0f));
g.setColour (colours::text);
g.drawText ("MonoStep", 16, 12, 130, 24, juce::Justification::centredLeft);
g.setFont (font (9.0f));
g.setColour (colours::textDim);
g.drawText ("2 OSC - step sequencer", 16, 34, 130, 12, juce::Justification::centredLeft);
const auto drawCaption = [&] (const juce::Component& component, const juce::String& text)
{
g.drawText (text,
component.getX(), component.getY() - 13,
component.getWidth(), 12,
juce::Justification::centred);
};
drawCaption (rateBar, "RATE");
drawCaption (octaveBar, "OCTAVE");
drawCaption (zoomBar, "ZOOM");
drawCaption (wave1Bar, "OSC 1");
drawCaption (wave2Bar, "OSC 2");
const int footY = baseHeight - 20;
g.setColour (colours::grid);
g.fillRect (0, footY, baseWidth, 20);
g.setColour (colours::panel);
g.drawHorizontalLine (footY, 0, (float) baseWidth);
g.setFont (font (9.0f));
g.setColour (colours::textDim);
g.drawText (buildinfo::footerText(), 16, footY + 4, baseWidth - 32, 12, juce::Justification::centredLeft);
}
void MonoStepAudioProcessorEditor::setZoomFactor (float newZoom)
{
zoomFactor = newZoom;
setSize (juce::roundToInt (baseWidth * newZoom), juce::roundToInt (baseHeight * newZoom));
}
void MonoStepAudioProcessorEditor::resized()
{
const int targetW = juce::roundToInt (baseWidth * zoomFactor);
const int targetH = juce::roundToInt (baseHeight * zoomFactor);
if (getWidth() != targetW || getHeight() != targetH)
{
setSize (targetW, targetH);
return;
}
content.setBounds (0, 0, baseWidth, baseHeight);
content.setTransform (juce::AffineTransform::scale (zoomFactor));
// All layout below happens in design units; the content transform scales it.
const int w = baseWidth;
const int h = baseHeight;
const int topBarH = 58;
const int bottomBarH = 96;
const int pad = 12;
lengthKnob.setBounds (128, 2, 54, 56);
rateBar.setBounds (192, 18, 210, 26);
swingKnob.setBounds (422, 2, 54, 56);
rootKnob.setBounds (482, 2, 54, 56);
octaveBar.setBounds (548, 18, 170, 26);
zoomBar.setBounds (w - pad - 54 - 8 - 236, 18, 236, 26);
masterKnob.setBounds (w - pad - 54, 2, 54, 56);
const int panelW = 200;
const int midTop = topBarH;
const int midH = h - topBarH - bottomBarH - pad;
stepPanel.setBounds (w - pad - panelW, midTop, panelW, midH);
matrix.setBounds (pad, midTop, w - pad * 2 - panelW - pad, midH);
const int bottomY = h - bottomBarH - pad;
int x = 16;
const int barW = 112;
wave1Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 8;
wave2Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 16;
const int knobW = 56;
const int knobGap = 72;
const int knobY = bottomY + 2;
const auto placeKnob = [&] (Knob& knob)
{
knob.setBounds (x, knobY, knobW, 82);
x += knobGap;
};
placeKnob (detuneKnob);
placeKnob (mixKnob);
placeKnob (cutoffKnob);
placeKnob (resKnob);
placeKnob (glideKnob);
placeKnob (attackKnob);
placeKnob (decayKnob);
placeKnob (sustainKnob);
placeKnob (releaseKnob);
}