#include "PluginEditor.h" #include "GitVersion.h" BeamgridAudioProcessorEditor::BeamgridAudioProcessorEditor (BeamgridAudioProcessor& p) : AudioProcessorEditor (&p), processorRef (p), analyserComponent (p.getAnalyser(), p.getParametersState(), lookAndFeel) { setLookAndFeel (&lookAndFeel); setSize (980, 480); // Make the editor freely resizable (with a corner resizer). The plugin // reports IPlugView::canResize() == true to the host, which is what FL // Studio (and other hosts) use to enable their native maximize button. setResizable (true, true); setResizeLimits (800, 260, 4000, 4000); analyserComponent.setLookAndFeel (&lookAndFeel); addAndMakeVisible (analyserComponent); auto setupKnob = [&] (juce::Slider& s, const juce::String& name) { s.setSliderStyle (juce::Slider::RotaryHorizontalVerticalDrag); s.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 80, 18); s.setColour (juce::Slider::textBoxTextColourId, juce::Colours::lightgrey); s.setTextValueSuffix (" " + name); s.setLookAndFeel (&lookAndFeel); addAndMakeVisible (s); }; setupKnob (graceSlider, "ms"); setupKnob (falloffSlider, ""); setupKnob (barfalloffSlider, ""); setupKnob (barsSlider, ""); setupKnob (hueSlider, ""); setupKnob (modeSlider, ""); setupKnob (ledsSlider, ""); setupKnob (smoothSlider, ""); setupKnob (gridSlider, ""); barsSlider.setNumDecimalPlacesToDisplay (0); modeSlider.setNumDecimalPlacesToDisplay (0); ledsSlider.setNumDecimalPlacesToDisplay (0); graceLabel.setText ("PEAK GRACE", juce::dontSendNotification); graceLabel.setJustificationType (juce::Justification::centred); graceLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible (graceLabel); falloffLabel.setText ("PEAK FALLOFF", juce::dontSendNotification); falloffLabel.setJustificationType (juce::Justification::centred); falloffLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible (falloffLabel); barfalloffLabel.setText ("BAR FALLOFF", juce::dontSendNotification); barfalloffLabel.setJustificationType (juce::Justification::centred); barfalloffLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible (barfalloffLabel); barsLabel.setText ("BARS", juce::dontSendNotification); barsLabel.setJustificationType (juce::Justification::centred); barsLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible (barsLabel); hueLabel.setText ("HUE", juce::dontSendNotification); hueLabel.setJustificationType (juce::Justification::centred); hueLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible (hueLabel); modeLabel.setText ("MODE", juce::dontSendNotification); modeLabel.setJustificationType (juce::Justification::centred); modeLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible (modeLabel); ledsLabel.setText ("LEDS", juce::dontSendNotification); ledsLabel.setJustificationType (juce::Justification::centred); ledsLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible (ledsLabel); smoothLabel.setText ("SMOOTH", juce::dontSendNotification); smoothLabel.setJustificationType (juce::Justification::centred); smoothLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible (smoothLabel); gridLabel.setText ("GRID", juce::dontSendNotification); gridLabel.setJustificationType (juce::Justification::centred); gridLabel.setColour (juce::Label::textColourId, juce::Colours::lightgrey); addAndMakeVisible (gridLabel); scaleBox.addItemList (juce::StringArray ("75%", "100%", "150%", "200%", "250%", "300%"), 1); scaleBox.setSelectedItemIndex (1); // 100% scaleBox.setColour (juce::ComboBox::backgroundColourId, juce::Colours::black.brighter (0.15f)); scaleBox.setColour (juce::ComboBox::textColourId, juce::Colours::lightgrey); scaleBox.setColour (juce::ComboBox::arrowColourId, juce::Colours::lightgrey); scaleBox.onChange = [this] { const double pct = scaleBox.getText().retainCharacters ("0123456789.").getDoubleValue(); const double scale = juce::jlimit (0.5, 3.0, pct / 100.0); setScaleFactor (scale); processorRef.getParametersState().state.setProperty ("uiScale", scale, nullptr); }; addAndMakeVisible (scaleBox); titleLabel.setText ("BEAMGRID", juce::dontSendNotification); titleLabel.setJustificationType (juce::Justification::centredRight); titleLabel.setFont (juce::FontOptions (20.0f, juce::Font::bold)); titleLabel.setColour (juce::Label::textColourId, lookAndFeel.getTeal()); addAndMakeVisible (titleLabel); statusLabel.setFont (juce::FontOptions (11.0f)); statusLabel.setColour (juce::Label::textColourId, juce::Colours::grey); statusLabel.setJustificationType (juce::Justification::centredRight); juce::String status; status << "git " << juce::String (BEAMGRID_GIT_COMMIT).substring (0, 7); status << " [" << (BEAMGRID_GIT_DIRTY ? "dirty" : "clean") << "]"; status << " build " << BEAMGRID_BUILD_DATE; statusLabel.setText (status, juce::dontSendNotification); addAndMakeVisible (statusLabel); graceAttachment = std::make_unique( processorRef.getParametersState(), "grace", graceSlider); falloffAttachment = std::make_unique( processorRef.getParametersState(), "falloff", falloffSlider); barfalloffAttachment = std::make_unique( processorRef.getParametersState(), "barfalloff", barfalloffSlider); barsAttachment = std::make_unique( processorRef.getParametersState(), "bars", barsSlider); hueAttachment = std::make_unique( processorRef.getParametersState(), "hue", hueSlider); modeAttachment = std::make_unique( processorRef.getParametersState(), "mode", modeSlider); ledsAttachment = std::make_unique( processorRef.getParametersState(), "leds", ledsSlider); smoothAttachment = std::make_unique( processorRef.getParametersState(), "smooth", smoothSlider); gridAttachment = std::make_unique( processorRef.getParametersState(), "grid", gridSlider); // Set these AFTER the attachments: SliderAttachment installs its own // textFromValueFunction, so assigning afterwards makes ours take effect. barsSlider.textFromValueFunction = [] (double v) { return juce::String (juce::roundToInt (v)); }; modeSlider.textFromValueFunction = [] (double v) { const int m = juce::roundToInt (v); return m == 2 ? juce::String ("SPECTRUM") : m == 3 ? juce::String ("LED") : juce::String ("BARS"); }; // Force an immediate repaint of the text boxes now that our formatters // are installed (otherwise they keep the attachment's initial text). barsSlider.updateText(); modeSlider.updateText(); processorRef.getParametersState().addParameterListener ("hue", this); // Restore the saved UI size + scale from the plugin state (the host saves // this state with the project), so the window reopens the same way. auto& uiState = processorRef.getParametersState().state; const double storedScale = uiState.getProperty ("uiScale", 1.0); const double scales[] = { 0.75, 1.0, 1.5, 2.0, 2.5, 3.0 }; int scaleIdx = 1; for (int i = 0; i < 6; ++i) if (std::abs (scales[i] - storedScale) < 0.001) scaleIdx = i; scaleBox.setSelectedItemIndex (scaleIdx); // triggers onChange -> setScaleFactor const int storedW = juce::jmax (800, static_cast (uiState.getProperty ("uiWidth", getWidth()))); const int storedH = uiState.getProperty ("uiHeight", getHeight()); setSize (storedW, storedH); } BeamgridAudioProcessorEditor::~BeamgridAudioProcessorEditor() { processorRef.getParametersState().removeParameterListener ("hue", this); setLookAndFeel (nullptr); } void BeamgridAudioProcessorEditor::parameterChanged (const juce::String& parameterID, float newValue) { if (parameterID == "hue") { // Update the shared LookAndFeel immediately and repaint the whole // editor so every rotary knob's arc (and the title) recolours. lookAndFeel.setHueTurns (newValue - 0.5f); repaint(); } } void BeamgridAudioProcessorEditor::paint (juce::Graphics& g) { g.fillAll (juce::Colours::black); // Keep the title colour in sync with the HUE knob. titleLabel.setColour (juce::Label::textColourId, lookAndFeel.getTeal()); } void BeamgridAudioProcessorEditor::resized() { const int panelH = 114; const int knob = 64; const int y = getHeight() - panelH + 14; // 32px blank bar across the very top of the plugin (the dropdown sits // 16px below the top edge so it isn't glued to it). 30px blank strip on // the right so the spectrum doesn't touch the right border. const int topBarH = 32; const int rightPad = 30; analyserComponent.setBounds (0, topBarH, getWidth() - rightPad, getHeight() - panelH - topBarH); const int margin = 24; const int gap = 14; const int count = 9; const int titleW = 220; // Available width for the knob row (leave room on the right for the title). const int avail = getWidth() - margin * 2 - titleW; int spacing = knob + gap; const int minSpacing = knob + 4; if ((count - 1) * spacing + knob > avail) spacing = juce::jmax (minSpacing, (avail - knob) / (count - 1)); struct KnobRow { juce::Slider& s; juce::Label& l; }; const KnobRow knobs[] = { { graceSlider, graceLabel }, { falloffSlider, falloffLabel }, { barfalloffSlider, barfalloffLabel }, { barsSlider, barsLabel }, { hueSlider, hueLabel }, { modeSlider, modeLabel }, { ledsSlider, ledsLabel }, { smoothSlider, smoothLabel }, { gridSlider, gridLabel }, }; const juce::Font labelFont (juce::FontOptions (11.0f)); // Knobs are drawn 16px higher than the panel baseline; the title and // status bar below stay anchored to the original baseline. const int knobY = y - 16; for (int i = 0; i < count; ++i) { const int x = margin + i * spacing; knobs[i].s.setBounds (x, knobY, knob, knob); // Let the label span the full spacing (wider than the knob) so longer // names like "BAR FALLOFF" aren't clipped, centred under the knob. const int labelW = spacing; const int labelX = x + (knob - labelW) / 2; knobs[i].l.setFont (labelFont); knobs[i].l.setBounds (labelX, knobY + knob + 2, labelW, 18); } titleLabel.setBounds (getWidth() - titleW - margin, y - 6, titleW, knob + 24); statusLabel.setBounds (16, getHeight() - 16, getWidth() - 42, 14); // UI scaling dropdown, top-right (16px below the top edge). scaleBox.setBounds (getWidth() - 96 - margin, 16, 96, 16); // Remember the current window size so it can be restored with the project. processorRef.getParametersState().state.setProperty ("uiWidth", getWidth(), nullptr); processorRef.getParametersState().state.setProperty ("uiHeight", getHeight(), nullptr); }