#include "PluginEditor.h" #include "BuildInfo.h" #include RackEditor::RackEditor (RMX19AudioProcessor& p) : AudioProcessorEditor (p) , proc (p) , display (p) { face = juce::Colour (0xff16130f); backgroundImage = juce::ImageFileFormat::loadFrom (BinaryData::b2_png, BinaryData::b2_pngSize); up = new NavButton (-1, "^", "UP"); down = new NavButton (1, "v", "DOWN"); left = new NavButton (-1, "<", "LEFT"); right = new NavButton (1, ">", "RIGHT"); enterB = new OkButton ("ENTER", true); backB = new OkButton ("BACK", false); for (auto* b : { (juce::Button*) up, (juce::Button*) down, (juce::Button*) left, (juce::Button*) right, (juce::Button*) enterB, (juce::Button*) backB }) addAndMakeVisible (b); up->onClick = [this] { display.navUp(); }; down->onClick = [this] { display.navDown(); }; left->onClick = [this] { display.navLeft(); }; right->onClick = [this] { display.navRight(); }; enterB->onClick = [this] { display.navEnter(); }; backB->onClick = [this] { display.navExit(); }; addAndMakeVisible (display); scaleBox.addItem ("50%", 1); scaleBox.addItem ("75%", 2); scaleBox.addItem ("100%", 3); scaleBox.addItem ("125%", 4); scaleBox.addItem ("150%", 5); scaleBox.addItem ("200%", 6); scaleBox.addItem ("250%", 7); scaleBox.addItem ("300%", 8); scaleBox.setSelectedId (5, juce::dontSendNotification); scaleBox.setJustificationType (juce::Justification::centred); scaleBox.setTextWhenNothingSelected ("UI scale"); scaleBox.setColour (juce::ComboBox::backgroundColourId, juce::Colour (0xff221d18)); scaleBox.setColour (juce::ComboBox::outlineColourId, juce::Colour (0xff6b5f4e)); scaleBox.setColour (juce::ComboBox::textColourId, juce::Colour (0xffd8cdb9)); scaleBox.setColour (juce::ComboBox::arrowColourId, juce::Colour (0xffd8cdb9)); scaleBox.setColour (juce::PopupMenu::backgroundColourId, juce::Colour (0xff1c1813)); scaleBox.setColour (juce::PopupMenu::textColourId, juce::Colour (0xffd8cdb9)); scaleBox.setColour (juce::PopupMenu::highlightedBackgroundColourId, juce::Colour (0xff3a332b)); scaleBox.onChange = [this] { switch (scaleBox.getSelectedId()) { case 1: setScale (0.5f); break; case 2: setScale (0.75f); break; case 3: setScale (1.0f); break; case 4: setScale (1.25f); break; case 5: setScale (1.5f); break; case 6: setScale (2.0f); break; case 7: setScale (2.5f); break; case 8: setScale (3.0f); break; default: break; } }; addAndMakeVisible (scaleBox); setWantsKeyboardFocus (true); setScale (1.5f); startTimerHz (18); } RackEditor::~RackEditor() { stopTimer(); } void RackEditor::paint (juce::Graphics& g) { // full-area backdrop so scaled edges stay covered g.fillAll (juce::Colour (0xff0d0b09)); const auto b = juce::Rectangle (0, 0, baseW, baseH); // --- metal rack face (b2.png background, cover-scaled) --- if (backgroundImage.isValid()) { const float imageAspect = (float) backgroundImage.getWidth() / (float) backgroundImage.getHeight(); const float boxAspect = (float) b.getWidth() / (float) b.getHeight(); int sx = 0, sy = 0, sw = backgroundImage.getWidth(), sh = backgroundImage.getHeight(); if (imageAspect > boxAspect) { sw = (int) (backgroundImage.getHeight() * boxAspect); sx = (backgroundImage.getWidth() - sw) / 2; } else { sh = (int) (backgroundImage.getWidth() / boxAspect); sy = (backgroundImage.getHeight() - sh) / 2; } g.drawImage (backgroundImage, b.getX(), b.getY(), juce::roundToInt (b.getWidth() * uiScale), juce::roundToInt (b.getHeight() * uiScale), sx, sy, sw, sh); } else { g.setColour (face); g.fillRect (b); } g.setColour (juce::Colour (0xff08)); g.addTransform (juce::AffineTransform::scale (uiScale)); // brushed texture g.setColour (juce::Colour::fromRGBA (255, 255, 255, 7)); for (int x = 0; x < b.getWidth(); x += 3) g.fillRect (x, 0, 1, b.getHeight()); // rack rails (top/bottom) g.setColour (juce::Colour (0xff221e1a)); g.fillRect (0, 0, b.getWidth(), 4); g.fillRect (0, b.getHeight() - 4, b.getWidth(), 4); g.setColour (juce::Colour (0xff5a5347)); g.fillRect (0, 4, b.getWidth(), 1); g.fillRect (0, b.getHeight() - 5, b.getWidth(), 1); // brand plate g.setColour (juce::Colour (0xff614f3a)); g.fillRect (12, 6, 150, 12); g.setColour (juce::Colour (0xffc9a86f)); g.setFont (juce::FontOptions ("Arial", 10.0f, juce::Font::bold)); g.drawFittedText ("RMX-19", 12, 6, 150, 12, juce::Justification::centred, 1); // rear engraving text (kept clear of the UI-scale dropdown at the bottom right) g.setColour (juce::Colour (0xff6e6657)); g.setFont (juce::FontOptions ("Courier New", 8.0f, juce::Font::plain)); const juce::String ser = "RMX-19 S/N 0001-1943-04-19 24BIT/96K ULTRA SYNTH MODULE"; g.drawFittedText (ser, 12, b.getHeight() - 16, b.getWidth() - 128, 11, juce::Justification::centred, 1); } void RackEditor::resized() { // UI-scale dropdown lives in real (unscaled) coordinates, bottom right. scaleBox.setBounds (getWidth() - 112, getHeight() - 30, 104, 20); // everything else renders in base 656x248 space, scaled by an affine transform const juce::AffineTransform tf = juce::AffineTransform::scale (uiScale); // LCD recessed module const juce::Rectangle lcdArea (14, 24, 356, 168); display.setTransform (tf); display.setBounds (lcdArea); // nav diamond + enter/back to the right of the LCD const juce::Rectangle navArea (lcdArea.getRight() + 16, 26, baseW - lcdArea.getRight() - 30, 164); const int bw = 58, bh = 20; const int cx = navArea.getX() + (navArea.getWidth() - bw) / 2; const int rowY = navArea.getY(); up->setTransform (tf); up->setBounds (cx + 6, rowY + 1, bw + 3, bh); left->setTransform (tf); left->setBounds (navArea.getX() + 1, rowY + bh + 6, bw + 2, bh); down->setTransform (tf); down->setBounds (cx + 6, rowY + bh + 6 + 1, bw + 3, bh); right->setTransform (tf); right->setBounds (navArea.getRight() - bw + 3, rowY + bh + 6, bw - 3, bh); enterB->setTransform (tf); enterB->setBounds (navArea.getX() + 2, rowY + 2 * (bh + 6) + 4, bw + 2, bh); backB->setTransform (tf); backB->setBounds (navArea.getRight() - bw + 3, rowY + 2 * (bh + 6) + 4, bw - 3, bh); } void RackEditor::setScale (float s) { uiScale = s; setSize (juce::roundToInt (baseW * s), juce::roundToInt (baseH * s)); resized(); // ensure child layouts refresh at the new size } void RackEditor::timerCallback() { powerPulse = 0.5f + 0.5f * std::sin (juce::Time::getMillisecondCounter() * 0.0022f); // only the pulsing power LED needs redraws; everything else is static const int ledX = getWidth() - 20, ledY = 18; repaint (ledX - 26, ledY - 7, 30, 14); } bool RackEditor::keyPressed (const juce::KeyPress& k) { if (display.keyTrap (k)) { repaint(); return true; } return false; } // ---- buttons ------------------------------------------------------------- void RackEditor::NavButton::paintButton (juce::Graphics& g, bool, bool down) { const auto b = getLocalBounds(); const juce::Colour base = juce::Colour (0xff221d18); const juce::Colour edge = juce::Colour (0xff6b5f4e); const juce::Colour text = juce::Colour (0xfffff3d8); if (down) { g.setColour (edge.darker (0.4f)); g.fillRect (b); g.setColour (base.brighter (0.1f)); g.fillRect (b.reduced (2, 2)); } else { g.setColour (base); g.fillRect (b); g.setColour (edge); g.drawRect (b, 1); } // glyph g.setColour (text); g.setFont (juce::FontOptions ("Arial", 12.0f, juce::Font::bold)); g.drawFittedText (mark, b.withTrimmedBottom (9), juce::Justification::centred, 1); g.setFont (juce::FontOptions ("Arial", 7.0f, juce::Font::plain)); g.drawFittedText (getName(), b.withTrimmedTop (b.getHeight() - 8), juce::Justification::centred, 1); } void RackEditor::OkButton::paintButton (juce::Graphics& g, bool over, bool down) { const auto b = getLocalBounds(); const juce::Colour base = enter ? juce::Colour (0xff263a28) : juce::Colour (0xff332a20); const juce::Colour edge = juce::Colour (0xff8a9b6a); if (down) { g.setColour (edge.darker (0.35f)); g.fillRect (b); g.setColour (base.brighter (0.12f)); g.fillRect (b.reduced (2, 2)); } else { g.setColour (base); g.fillRect (b); g.setColour (edge); g.drawRect (b, 1); } g.setColour (over ? juce::Colour (0xffffd977) : juce::Colour (0xffd8d2c6)); g.setFont (juce::FontOptions ("Arial", 9.0f, juce::Font::bold)); g.drawFittedText (getName(), b, juce::Justification::centred, 1); }