UI changes, layout redefinition

This commit is contained in:
Armin 2026-08-15 18:31:51 +02:00
commit 1d9ba772fe
12 changed files with 536 additions and 528 deletions

View file

@ -3,13 +3,12 @@
#include "BuildInfo.h"
static constexpr int Y_HEADER = 8;
static constexpr int Y_ALGO = 48;
static constexpr int Y_SLABEL1 = 86;
static constexpr int Y_ROW1 = 104;
static constexpr int Y_SLABEL2 = 204;
static constexpr int Y_ROW2 = 222;
static constexpr int Y_SEP = 322;
static constexpr int Y_VIZ = 326;
static constexpr int Y_SLABEL1 = 46;
static constexpr int Y_ROW1 = 64;
static constexpr int Y_SLABEL2 = 164;
static constexpr int Y_ROW2 = 182;
static constexpr int Y_SEP = 282;
static constexpr int Y_VIZ = 286;
static constexpr int SEC_TIME = 8;
static constexpr int SEC_FREQUENCY = 254;
@ -21,28 +20,26 @@ static constexpr int SEP_FD = 409;
static constexpr int SEP_DS = 655;
static constexpr int SEP_SC = 737;
// -----------------------------------------------------------------------------
// EditorContent
// -----------------------------------------------------------------------------
// constructor
// -----------------------------------------------------------------------------
FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
: AudioProcessorEditor(&p),
audioProcessor(p),
algoSelector(p.apvts),
vuIn("IN", VUMeter::Side::Input),
EditorContent::EditorContent(FDNReverbAudioProcessor& p, AmbivalenceLookAndFeel& laf)
: vuIn("IN", VUMeter::Side::Input),
vuOut("OUT", VUMeter::Side::Output)
{
setLookAndFeel(&laf);
setSize(W, H);
setOpaque(true);
lookAndFeel = &laf;
// -- Title --
titleLabel.setText("AMBIVALENCE 1.1", juce::dontSendNotification);
titleLabel.setText("AMBIVALENCE", juce::dontSendNotification);
titleLabel.setFont(juce::Font(juce::FontOptions(
"Helvetica Neue", 14.f, juce::Font::bold)));
"Helvetica Neue", 24.f, juce::Font::bold)));
titleLabel.setColour(juce::Label::textColourId, AmbivalenceColors::TextPrimary);
titleLabel.setJustificationType(juce::Justification::centredLeft);
addAndMakeVisible(titleLabel);
addAndMakeVisible(algoSelector);
auto BK = [&](ArcKnob& k, const char* id, const char* lbl) {
k.build(p.apvts, id, lbl, this, laf);
};
@ -68,7 +65,7 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
BK(kHiCutNorm, "hicut", "HI CUT");
// -- ProMode button --
proModeButton.setButtonText("PRO");
proModeButton.setButtonText("EQ");
proModeButton.setClickingTogglesState(true);
proModeButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::Accent);
proModeButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
@ -79,8 +76,16 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
new juce::AudioProcessorValueTreeState::ButtonAttachment(
p.apvts, "promode", proModeButton));
// -- Zoom menu button --
zoomButton.setButtonText(juce::String(
juce::roundToInt(EditorContent::kDefaultZoom * 100.f)) + "%");
zoomButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
zoomButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
zoomButton.setTooltip("UI zoom");
addAndMakeVisible(zoomButton);
// -- ER SOLO button --
erSoloButton.setButtonText("ER SOLO");
erSoloButton.setButtonText("ER ONLY");
erSoloButton.setClickingTogglesState(true);
erSoloButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::AccentBlue);
erSoloButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
@ -124,82 +129,8 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
BK(kLoCutPro, "locut", "LO CUT");
BK(kHiCutPro, "hicut", "HI CUT");
// -------------------------------------------------------------------------
// preset UI
// -------------------------------------------------------------------------
presetManager = std::make_unique<PresetManager>(p);
// < PREV
presetPrevButton.setButtonText("<");
presetPrevButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
presetPrevButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
addAndMakeVisible(presetPrevButton);
presetPrevButton.onClick = [this] {
presetManager->loadPrevPreset();
};
// preset name combo
presetCombo.setLookAndFeel(&laf);
addAndMakeVisible(presetCombo);
presetCombo.onChange = [this] {
int idx = presetCombo.getSelectedItemIndex();
auto names = presetManager->getPresetNames();
if (idx >= 0 && idx < names.size())
presetManager->loadPreset(names[idx]);
};
// > NEXT
presetNextButton.setButtonText(">");
presetNextButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
presetNextButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
addAndMakeVisible(presetNextButton);
presetNextButton.onClick = [this] {
presetManager->loadNextPreset();
};
// SAVE
presetSaveButton.setButtonText("SAVE");
presetSaveButton.setColour(juce::TextButton::buttonColourId,
AmbivalenceColors::Accent.withAlpha(0.75f));
presetSaveButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::Background);
addAndMakeVisible(presetSaveButton);
presetSaveButton.onClick = [this] { savePresetWithDialog(); };
// --- after changes ---
// LOAD
presetLoadButton.setButtonText("LOAD");
presetLoadButton.setColour(juce::TextButton::buttonColourId,
AmbivalenceColors::AccentBlue.withAlpha(0.75f));
presetLoadButton.setColour(juce::TextButton::textColourOffId,
AmbivalenceColors::Background);
addAndMakeVisible(presetLoadButton);
presetLoadButton.onClick = [this] {
auto names = presetManager->getPresetNames();
int idx = presetCombo.getSelectedItemIndex();
if (idx >= 0 && idx < names.size())
presetManager->loadPreset(names[idx]);
};
// DELETE
presetDeleteButton.setButtonText("DELETE");
presetDeleteButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
presetDeleteButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary);
addAndMakeVisible(presetDeleteButton);
presetDeleteButton.onClick = [this] { deleteCurrentPreset(); };
// callback setup
// in the constructor callback setup
presetManager->onPresetListChanged = [this] { refreshPresetCombo(); };
// * fix : whenever the preset name changes Processor notify
presetManager->onPresetLoaded = [this](const juce::String& name) {
audioProcessor.setLastSavedPresetName(name);
refreshPresetCombo();
};
// -- Visualizers --
rt60Viz.setProcessor(&p);
decayCurveViz.setProcessor(&p);
addAndMakeVisible(rt60Viz);
addAndMakeVisible(decayCurveViz);
addAndMakeVisible(vuIn);
@ -254,75 +185,32 @@ FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
"Helvetica Neue", 8.5f, juce::Font::plain)));
statusLabel.setColour(juce::Label::textColourId,
AmbivalenceColors::TextSecondary.withAlpha(0.6f));
statusLabel.setJustificationType(juce::Justification::centred);
statusLabel.setJustificationType(juce::Justification::centredRight);
addAndMakeVisible(statusLabel);
// --- after changes ---
refreshPresetCombo(); // added: initialize the combo at startup
// -- Preset UI (wiring is done by the editor) --
addAndMakeVisible(presetPrevButton);
addAndMakeVisible(presetCombo);
addAndMakeVisible(presetNextButton);
addAndMakeVisible(presetSaveButton);
addAndMakeVisible(presetLoadButton);
addAndMakeVisible(presetDeleteButton);
updatePanelVisibility();
startTimerHz(60);
}
// -----------------------------------------------------------------------------
// destructor
// EditorContent::updatePanelVisibility
// -----------------------------------------------------------------------------
FDNReverbEditor::~FDNReverbEditor() {
stopTimer();
setLookAndFeel(nullptr);
satTypeCombo.setLookAndFeel(nullptr);
presetCombo.setLookAndFeel(nullptr);
}
// -----------------------------------------------------------------------------
// timerCallback
// -----------------------------------------------------------------------------
void FDNReverbEditor::timerCallback()
{
vuIn.setLevels(audioProcessor.getInputRMSL(),
audioProcessor.getInputRMSR());
vuOut.setLevels(audioProcessor.getOutputRMSL(),
audioProcessor.getOutputRMSR());
vuIn.repaint();
vuOut.repaint();
static int metricsCounter = 0;
if (++metricsCounter >= 2) {
metricsCounter = 0;
labelD50Value.setText(
juce::String(audioProcessor.getD50() * 100.0f, 1) + "%",
juce::dontSendNotification);
labelC50Value.setText(
juce::String(audioProcessor.getC50(), 1) + "dB",
juce::dontSendNotification);
labelC80Value.setText(
juce::String(audioProcessor.getC80(), 1) + "dB",
juce::dontSendNotification);
labelEDTValue.setText(
juce::String(audioProcessor.getEDT(), 2) + "s",
juce::dontSendNotification);
}
bool newProMode = (*audioProcessor.apvts.getRawParameterValue("promode") > 0.5f);
if (newProMode != isProMode) {
isProMode = newProMode;
updatePanelVisibility();
resized();
repaint();
}
}
// -----------------------------------------------------------------------------
// updatePanelVisibility
// -----------------------------------------------------------------------------
void FDNReverbEditor::updatePanelVisibility()
void EditorContent::updatePanelVisibility()
{
auto setKnob = [](ArcKnob& k, bool vis) {
k.slider.setVisible(vis);
k.label.setVisible(vis);
};
const bool showNormal = !isProMode;
const bool showPro = isProMode;
const bool showNormal = !proMode;
const bool showPro = proMode;
setKnob(kPreDelay, showNormal);
setKnob(kRoomSize, showNormal);
@ -357,37 +245,100 @@ void FDNReverbEditor::updatePanelVisibility()
presetPrevButton.setVisible(true);
presetCombo.setVisible(true);
presetNextButton.setVisible(true);
// --- after changes ---
presetSaveButton.setVisible(true);
presetLoadButton.setVisible(true); // * added
presetLoadButton.setVisible(true);
presetDeleteButton.setVisible(true);
}
// -----------------------------------------------------------------------------
// resized
// EditorContent::setProMode
// -----------------------------------------------------------------------------
void FDNReverbEditor::resized()
void EditorContent::setProMode(bool pro)
{
titleLabel.setBounds(PAD, Y_HEADER, 180, 32);
proModeButton.setBounds(196, Y_HEADER + 5, 52, 22);
erSoloButton.setBounds(256, Y_HEADER + 5, 72, 22);
vuIn.setBounds(W - 220, Y_HEADER + 2, 96, 28);
vuOut.setBounds(W - 120, Y_HEADER + 2, 96, 28);
if (proMode == pro)
return;
algoSelector.setBounds(PAD, Y_ALGO, W - PAD * 2, 30);
proMode = pro;
updatePanelVisibility();
resized();
repaint();
}
// -----------------------------------------------------------------------------
// EditorContent::resized
// -----------------------------------------------------------------------------
void EditorContent::resized()
{
uiScale = getWidth() > 0 ? (float)getWidth() / (float)W : 1.0f;
// scale fonts that are set explicitly (knob/button/combo fonts handled
// via the LookAndFeel which already has the same uiScale pushed to it).
auto labelFont = [this](float h, int style = juce::Font::plain) {
return juce::Font(juce::FontOptions("Helvetica Neue", h * uiScale, style));
};
titleLabel.setFont(labelFont(24.f, juce::Font::bold));
satTypeLabel.setFont(labelFont(9.f));
labelMetricsTitle.setFont(labelFont(8.5f, juce::Font::bold));
labelD50Caption.setFont(labelFont(8.f));
labelC50Caption.setFont(labelFont(8.f));
labelC80Caption.setFont(labelFont(8.f));
labelEDTCaption.setFont(labelFont(8.f));
labelD50Value.setFont(labelFont(9.5f, juce::Font::bold));
labelC50Value.setFont(labelFont(9.5f, juce::Font::bold));
labelC80Value.setFont(labelFont(9.5f, juce::Font::bold));
labelEDTValue.setFont(labelFont(9.5f, juce::Font::bold));
statusLabel.setFont(labelFont(8.5f));
ArcKnob* knobs[] = {
&kPreDelay, &kRoomSize, &kDecay,
&kHFDamp, &kLFAbsorb,
&kDiffusion, &kModAmt, &kModRate,
&kStereoW,
&kERLevel, &kSaturation,
&kWet, &kDry,
&kDuckAmt, &kDuckThr, &kDuckAtt, &kDuckRel,
&kLoCutNorm, &kHiCutNorm,
&kRTBands[0], &kRTBands[1], &kRTBands[2], &kRTBands[3], &kRTBands[4],
&kRTBands[5], &kRTBands[6], &kRTBands[7], &kRTBands[8], &kRTBands[9],
&kTiltLow, &kTiltMid, &kTiltHigh,
&kLoCutPro, &kHiCutPro
};
for (auto* k : knobs)
k->label.setFont(labelFont(9.f));
if (lookAndFeel != nullptr)
lookAndFeel->zoomScale = uiScale;
decayCurveViz.setZoomScale(uiScale);
// layout is written in fixed design coords (W x H); S() maps to the
// actual component size so no host-hostile setTransform is needed.
auto S = [this](int x, int y, int w, int h) {
return juce::Rectangle<int>(juce::roundToInt(x * uiScale),
juce::roundToInt(y * uiScale),
juce::roundToInt(w * uiScale),
juce::roundToInt(h * uiScale));
};
titleLabel.setBounds(S(PAD, Y_HEADER - 6, 300, 44));
proModeButton.setBounds(S(316, Y_HEADER + 5, 52, 22));
erSoloButton.setBounds(S(376, Y_HEADER + 5, 72, 22));
vuIn.setBounds(S(W - 220, Y_HEADER + 2, 96, 28));
vuOut.setBounds(S(W - 120, Y_HEADER + 2, 96, 28));
zoomButton.setBounds(S(W - 272, Y_HEADER + 5, 48, 22));
auto place1 = [&](ArcKnob& k, int& x, int y) {
k.label.setBounds(x, y, KNOB_W, KNOB_LBL_H);
k.slider.setBounds(x, y + KNOB_LBL_H, KNOB_W, KNOB_H);
k.label.setBounds(S(x, y, KNOB_W, KNOB_LBL_H));
k.slider.setBounds(S(x, y + KNOB_LBL_H, KNOB_W, KNOB_H));
x += KNOB_W + ROW1_GAP;
};
auto place2 = [&](ArcKnob& k, int& x, int y) {
k.label.setBounds(x, y, KNOB_W, KNOB_LBL_H);
k.slider.setBounds(x, y + KNOB_LBL_H, KNOB_W, KNOB_H);
k.label.setBounds(S(x, y, KNOB_W, KNOB_LBL_H));
k.slider.setBounds(S(x, y + KNOB_LBL_H, KNOB_W, KNOB_H));
x += KNOB_W + PAD;
};
if (!isProMode) {
if (!proMode) {
// -- Row 1 --
int kx = PAD;
place1(kPreDelay, kx, Y_ROW1);
@ -424,8 +375,8 @@ void FDNReverbEditor::resized()
// -- ProMode row 2 --
int kx2 = PAD;
satTypeLabel.setBounds(kx2, Y_SLABEL2, KNOB_W, KNOB_LBL_H);
satTypeCombo.setBounds(kx2, Y_SLABEL2 + KNOB_LBL_H + 2, KNOB_W + PAD, 24);
satTypeLabel.setBounds(S(kx2, Y_SLABEL2, KNOB_W, KNOB_LBL_H));
satTypeCombo.setBounds(S(kx2, Y_SLABEL2 + KNOB_LBL_H + 2, KNOB_W + PAD, 24));
kx2 += KNOB_W + PAD + PAD + 8;
place2(kTiltLow, kx2, Y_ROW2);
place2(kTiltMid, kx2, Y_ROW2);
@ -439,10 +390,10 @@ void FDNReverbEditor::resized()
// preset UI (always, mode-independent)
// -------------------------------------------------------------------------
// layout (PRESET_PANEL_X=632 starting from ):
//
//
// top row Y=Y_ROW2: [<(26)] gap4 [combo(154)] gap4 [>(26)] -> right edge 848
// bottom row Y=Y_ROW2+34:[SAVE(104)] gap8 [DELETE(104)] -> right edge 848
//
//
// separator vertical line : PRESET_PANEL_X - 9 = 623
// -------------------------------------------------------------------------
{
@ -450,31 +401,26 @@ void FDNReverbEditor::resized()
const int btnH = 26;
// top row
presetPrevButton.setBounds(px, Y_ROW2, 26, btnH);
presetCombo.setBounds(px + 30, Y_ROW2, 154, btnH);
presetNextButton.setBounds(px + 188, Y_ROW2, 26, btnH);
presetPrevButton.setBounds(S(px, Y_ROW2, 26, btnH));
presetCombo.setBounds(S(px + 30, Y_ROW2, 154, btnH));
presetNextButton.setBounds(S(px + 188, Y_ROW2, 26, btnH));
// --- after changes ---
// bottom row : [SAVE(68)] [LOAD(68)] [DELETE(68)]
presetSaveButton.setBounds(px, Y_ROW2 + 34, 68, btnH);
presetLoadButton.setBounds(px + 72, Y_ROW2 + 34, 68, btnH);
presetDeleteButton.setBounds(px + 144, Y_ROW2 + 34, 68, btnH);
// bottom row : [SAVE(68)] [LOAD(68)] [DELETE(68)]
presetSaveButton.setBounds(S(px, Y_ROW2 + 34, 68, btnH));
presetLoadButton.setBounds(S(px + 72, Y_ROW2 + 34, 68, btnH));
presetDeleteButton.setBounds(S(px + 144, Y_ROW2 + 34, 68, btnH));
}
// -- Visualizers --
const int vizTotalH = H - Y_VIZ - STATUS_H - PAD;
const int rt60Height = vizTotalH / 2 - 2;
const int decayHeight = vizTotalH / 2 - 2;
const int decayY = Y_VIZ + rt60Height + 4;
// -- Visualizer --
const int decayHeight = H - Y_VIZ - STATUS_H - PAD;
rt60Viz.setBounds(PAD, Y_VIZ, W - PAD * 2, rt60Height);
decayCurveViz.setBounds(PAD, decayY, W - PAD * 2, decayHeight);
decayCurveViz.setBounds(S(PAD, Y_VIZ, W - PAD * 2, decayHeight));
// -- AcousticMetrics --
const int metricsRight = W - PAD - 8;
const int metricsW = 280;
const int metricsLeft = metricsRight - metricsW;
const int metricsTop = decayY + 6;
const int metricsTop = Y_VIZ + 6;
const int metricsRowH = 14;
const int metricsRow1Y = metricsTop + 14;
const int metricsRow2Y = metricsRow1Y + metricsRowH;
@ -483,37 +429,33 @@ void FDNReverbEditor::resized()
const int colSpacing = 8;
const int colW = captionW + valueW;
labelMetricsTitle.setBounds(metricsLeft, metricsTop, 80, 12);
labelD50Caption.setBounds(metricsLeft, metricsRow1Y, captionW, metricsRowH);
labelD50Value.setBounds(metricsLeft + captionW, metricsRow1Y, valueW, metricsRowH);
labelC50Caption.setBounds(metricsLeft + colW + colSpacing, metricsRow1Y, captionW, metricsRowH);
labelC50Value.setBounds(metricsLeft + colW + colSpacing + captionW, metricsRow1Y, valueW, metricsRowH);
labelC80Caption.setBounds(metricsLeft, metricsRow2Y, captionW, metricsRowH);
labelC80Value.setBounds(metricsLeft + captionW, metricsRow2Y, valueW, metricsRowH);
labelEDTCaption.setBounds(metricsLeft + colW + colSpacing, metricsRow2Y, captionW, metricsRowH);
labelEDTValue.setBounds(metricsLeft + colW + colSpacing + captionW, metricsRow2Y, valueW, metricsRowH);
labelMetricsTitle.setBounds(S(metricsLeft, metricsTop, 80, 12));
labelD50Caption.setBounds(S(metricsLeft, metricsRow1Y, captionW, metricsRowH));
labelD50Value.setBounds(S(metricsLeft + captionW, metricsRow1Y, valueW, metricsRowH));
labelC50Caption.setBounds(S(metricsLeft + colW + colSpacing, metricsRow1Y, captionW, metricsRowH));
labelC50Value.setBounds(S(metricsLeft + colW + colSpacing + captionW, metricsRow1Y, valueW, metricsRowH));
labelC80Caption.setBounds(S(metricsLeft, metricsRow2Y, captionW, metricsRowH));
labelC80Value.setBounds(S(metricsLeft + captionW, metricsRow2Y, valueW, metricsRowH));
labelEDTCaption.setBounds(S(metricsLeft + colW + colSpacing, metricsRow2Y, captionW, metricsRowH));
labelEDTValue.setBounds(S(metricsLeft + colW + colSpacing + captionW, metricsRow2Y, valueW, metricsRowH));
// -- Git build-info status bar --
statusLabel.setBounds(PAD, H - STATUS_H, W - PAD * 2, STATUS_H);
statusLabel.setBounds(S(PAD, H - STATUS_H, W - PAD - 16, STATUS_H));
}
// -----------------------------------------------------------------------------
// paint
// EditorContent::paint
// -----------------------------------------------------------------------------
void FDNReverbEditor::paint(juce::Graphics& g)
void EditorContent::paint(juce::Graphics& g)
{
g.fillAll(AmbivalenceColors::Background);
g.addTransform(juce::AffineTransform::scale(uiScale));
juce::ColourGradient grad(
AmbivalenceColors::Surface.withAlpha(0.12f), 0.f, 0.f,
AmbivalenceColors::Background, 0.f, (float)H, false);
g.setGradientFill(grad);
g.fillAll();
g.setFont(juce::Font(juce::FontOptions(8.f)));
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.35f));
g.drawText("16ch FDN | SAPF | ISM-ER | 44.1-192kHz",
PAD + 336, Y_HEADER + 10, W / 2, 12,
juce::Justification::centredLeft);
g.fillRect(0.f, 0.f, (float)W, (float)H);
g.setColour(AmbivalenceColors::Separator);
g.drawHorizontalLine(Y_SEP, (float)PAD, (float)(W - PAD));
@ -525,7 +467,7 @@ void FDNReverbEditor::paint(juce::Graphics& g)
g.drawText(t, x, y, 200, 14, juce::Justification::centredLeft);
};
if (!isProMode) {
if (!proMode) {
// -- Row 1 separator --
g.setColour(AmbivalenceColors::Separator);
g.drawVerticalLine(SEP_TF, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H));
@ -583,12 +525,197 @@ void FDNReverbEditor::paint(juce::Graphics& g)
g.drawHorizontalLine(H - STATUS_H - 1, (float)PAD, (float)(W - PAD));
}
// -----------------------------------------------------------------------------
// EditorContent::Zoom control
// -----------------------------------------------------------------------------
void EditorContent::showZoomMenu()
{
static constexpr float zoomLevels[] = { 0.75f, 1.0f, 1.5f, 2.0f, 2.5f, 3.0f };
juce::PopupMenu menu;
for (int i = 0; i < 6; ++i) {
const int pct = juce::roundToInt(zoomLevels[i] * 100.f);
menu.addItem(juce::String(pct) + "%", true,
juce::approximatelyEqual(zoomLevels[i], currentZoom),
[this, level = zoomLevels[i]] { setZoomLevel(level); });
}
menu.showMenuAsync(juce::PopupMenu::Options()
.withTargetComponent(&zoomButton)
.withStandardItemHeight(22));
}
void EditorContent::setZoomLevel(float scale)
{
currentZoom = juce::jlimit(0.75f, 3.0f, scale);
zoomButton.setButtonText(
juce::String(juce::roundToInt(currentZoom * 100.f)) + "%");
if (onZoomChange)
onZoomChange(currentZoom);
}
// -----------------------------------------------------------------------------
// constructor
// -----------------------------------------------------------------------------
FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
: AudioProcessorEditor(&p),
audioProcessor(p)
{
setLookAndFeel(&laf);
setSize(juce::roundToInt((float)EditorContent::W * EditorContent::kDefaultZoom),
juce::roundToInt((float)EditorContent::H * EditorContent::kDefaultZoom));
content = std::make_unique<EditorContent>(p, laf);
addAndMakeVisible(content.get());
// setSize() in the constructor runs resized() while content is still
// null, and hosts open the editor at exactly this size, so resized()
// would otherwise never run again - leaving content at 0x0 (black
// canvas). Lay it out explicitly here.
resized();
// when the user picks a zoom level, resize the (untransformed) editor so
// it exactly matches the scaled content. resized() derives the content
// scale from the actual editor width, so hosts that clamp the window
// still end up with a correct, fully-covered UI.
content->onZoomChange = [this](float z) {
setSize(juce::roundToInt((float)EditorContent::W * z),
juce::roundToInt((float)EditorContent::H * z));
};
// -- Preset UI wiring --
presetManager = std::make_unique<PresetManager>(p);
content->zoomButton.onClick = [this] { content->showZoomMenu(); };
content->presetPrevButton.setButtonText("<");
content->presetPrevButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
content->presetPrevButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
content->presetPrevButton.onClick = [this] {
presetManager->loadPrevPreset();
};
content->presetCombo.setLookAndFeel(&laf);
content->presetCombo.onChange = [this] {
int idx = content->presetCombo.getSelectedItemIndex() - FDNReverb::NUM_ALGORITHMS;
auto names = presetManager->getPresetNames();
if (idx >= 0 && idx < names.size())
presetManager->loadPreset(names[idx]);
};
content->presetNextButton.setButtonText(">");
content->presetNextButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
content->presetNextButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
content->presetNextButton.onClick = [this] {
presetManager->loadNextPreset();
};
content->presetSaveButton.setButtonText("SAVE");
content->presetSaveButton.setColour(juce::TextButton::buttonColourId,
AmbivalenceColors::Accent.withAlpha(0.75f));
content->presetSaveButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::Background);
content->presetSaveButton.onClick = [this] { savePresetWithDialog(); };
content->presetLoadButton.setButtonText("LOAD");
content->presetLoadButton.setColour(juce::TextButton::buttonColourId,
AmbivalenceColors::AccentBlue.withAlpha(0.75f));
content->presetLoadButton.setColour(juce::TextButton::textColourOffId,
AmbivalenceColors::Background);
content->presetLoadButton.onClick = [this] {
int idx = content->presetCombo.getSelectedItemIndex() - FDNReverb::NUM_ALGORITHMS;
auto names = presetManager->getPresetNames();
if (idx >= 0 && idx < names.size())
presetManager->loadPreset(names[idx]);
};
content->presetDeleteButton.setButtonText("DELETE");
content->presetDeleteButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
content->presetDeleteButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary);
content->presetDeleteButton.onClick = [this] { deleteCurrentPreset(); };
// callback setup
presetManager->onPresetListChanged = [this] { refreshPresetCombo(); };
// * fix : whenever the preset name changes Processor notify
presetManager->onPresetLoaded = [this](const juce::String& name) {
audioProcessor.setLastSavedPresetName(name);
refreshPresetCombo();
};
refreshPresetCombo();
startTimerHz(60);
}
// -----------------------------------------------------------------------------
// destructor
// -----------------------------------------------------------------------------
FDNReverbEditor::~FDNReverbEditor() {
stopTimer();
setLookAndFeel(nullptr);
content->satTypeCombo.setLookAndFeel(nullptr);
content->presetCombo.setLookAndFeel(nullptr);
}
// -----------------------------------------------------------------------------
// timerCallback
// -----------------------------------------------------------------------------
void FDNReverbEditor::timerCallback()
{
content->vuIn.setLevels(audioProcessor.getInputRMSL(),
audioProcessor.getInputRMSR());
content->vuOut.setLevels(audioProcessor.getOutputRMSL(),
audioProcessor.getOutputRMSR());
content->vuIn.repaint();
content->vuOut.repaint();
static int metricsCounter = 0;
if (++metricsCounter >= 2) {
metricsCounter = 0;
content->labelD50Value.setText(
juce::String(audioProcessor.getD50() * 100.0f, 1) + "%",
juce::dontSendNotification);
content->labelC50Value.setText(
juce::String(audioProcessor.getC50(), 1) + "dB",
juce::dontSendNotification);
content->labelC80Value.setText(
juce::String(audioProcessor.getC80(), 1) + "dB",
juce::dontSendNotification);
content->labelEDTValue.setText(
juce::String(audioProcessor.getEDT(), 2) + "s",
juce::dontSendNotification);
}
bool newProMode = (*audioProcessor.apvts.getRawParameterValue("promode") > 0.5f);
content->setProMode(newProMode);
}
// -----------------------------------------------------------------------------
// resized
// -----------------------------------------------------------------------------
void FDNReverbEditor::resized()
{
if (content == nullptr)
return;
// No setTransform here: scaling is done by EditorContent applying
// uiScale to every child bounds and to its own paint() graphics, which
// renders correctly in all hosts (some hosts, e.g. FL Studio, fail to
// composite a transformed top-level child and show a blank frame).
content->setBounds(getLocalBounds());
}
// -----------------------------------------------------------------------------
// paint
// -----------------------------------------------------------------------------
void FDNReverbEditor::paint(juce::Graphics& g)
{
g.fillAll(AmbivalenceColors::Background);
}
// -----------------------------------------------------------------------------
// Preset UI helpers
// -----------------------------------------------------------------------------
void FDNReverbEditor::refreshPresetCombo()
{
presetCombo.clear(juce::dontSendNotification);
content->presetCombo.clear(juce::dontSendNotification);
auto names = presetManager->getPresetNames();
// ---------------------------------------------------------------------
@ -606,29 +733,41 @@ void FDNReverbEditor::refreshPresetCombo()
presetManager->setCurrentPresetName(saved);
}
// -- built-in algorithm presets --
content->presetCombo.addSectionHeading("ALGORITHM");
static const char* algoNames[FDNReverb::NUM_ALGORITHMS] = {
"ROOM1", "ROOM2", "HALL1", "HALL2", "PLATE", "SPRING", "GOLDFOIL"
};
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i)
content->presetCombo.addItem(algoNames[i], i + 1);
// -- saved user presets --
if (names.isEmpty()) {
presetCombo.addItem("-- No Presets --", 1);
presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
presetDeleteButton.setEnabled(false);
presetLoadButton.setEnabled(false);
presetPrevButton.setEnabled(false);
presetNextButton.setEnabled(false);
const int placeholderId = FDNReverb::NUM_ALGORITHMS + 1;
content->presetCombo.addItem("-- No Presets --", placeholderId);
content->presetCombo.setSelectedId(placeholderId, juce::dontSendNotification);
content->presetDeleteButton.setEnabled(false);
content->presetLoadButton.setEnabled(false);
content->presetPrevButton.setEnabled(false);
content->presetNextButton.setEnabled(false);
return;
}
// user-preset IDs start after the algorithm IDs so no two combo items
// share an ID (JUCE ticks every item whose ID matches the selection).
for (int i = 0; i < names.size(); ++i)
presetCombo.addItem(names[i], i + 1);
content->presetCombo.addItem(names[i], FDNReverb::NUM_ALGORITHMS + i + 1);
int idx = presetManager->getCurrentPresetIndex();
if (idx >= 0)
presetCombo.setSelectedItemIndex(idx, juce::dontSendNotification);
content->presetCombo.setSelectedItemIndex(idx, juce::dontSendNotification);
else
presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
content->presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
presetDeleteButton.setEnabled(true);
presetLoadButton.setEnabled(true);
presetPrevButton.setEnabled(names.size() > 1);
presetNextButton.setEnabled(names.size() > 1);
content->presetDeleteButton.setEnabled(true);
content->presetLoadButton.setEnabled(true);
content->presetPrevButton.setEnabled(names.size() > 1);
content->presetNextButton.setEnabled(names.size() > 1);
}
void FDNReverbEditor::savePresetWithDialog()
@ -687,4 +826,4 @@ void FDNReverbEditor::deleteCurrentPreset()
}),
true
);
}
}