diff --git a/Source/GUI/Knob.cpp b/Source/GUI/Knob.cpp index 05914af..4597f46 100644 --- a/Source/GUI/Knob.cpp +++ b/Source/GUI/Knob.cpp @@ -21,4 +21,66 @@ juce::String Knob::getTextFromValue (double value) return formatPercent ((float) value); } +void Knob::mouseDown (const juce::MouseEvent& e) +{ + juce::Slider::mouseDown (e); + showValuePopup (e); +} + +void Knob::mouseDrag (const juce::MouseEvent& e) +{ + juce::Slider::mouseDrag (e); + showValuePopup (e); +} + +void Knob::mouseUp (const juce::MouseEvent& e) +{ + juce::Slider::mouseUp (e); + hideValuePopup(); +} + +void Knob::mouseExit (const juce::MouseEvent& e) +{ + juce::Slider::mouseExit (e); + hideValuePopup(); +} + +// --------------------------------------------------------------------------- +// The floating readout lives as a child of the top-level component so it can +// follow the cursor outside this knob's own bounds without being clipped, and +// it never intercepts mouse clicks so it can't block interaction. +// --------------------------------------------------------------------------- +void Knob::showValuePopup (const juce::MouseEvent& e) +{ + auto* topLevel = getTopLevelComponent(); + if (topLevel == nullptr) + return; + + if (valuePopup == nullptr) + { + valuePopup = std::make_unique(); + valuePopup->setAlwaysOnTop (true); + valuePopup->setInterceptsMouseClicks (false, false); + valuePopup->setColour (juce::Label::backgroundColourId, juce::Colours::transparentBlack); + valuePopup->setColour (juce::Label::textColourId, theme::text); + valuePopup->setFont (juce::Font (14.0f, juce::Font::bold)); + valuePopup->setJustificationType (juce::Justification::centred); + topLevel->addAndMakeVisible (valuePopup.get()); + } + + valuePopup->setText (getTextFromValue (getValue()), juce::dontSendNotification); + + constexpr int w = 84; + constexpr int h = 24; + constexpr int gap = 10; + + const auto cursor = topLevel->getLocalPoint (nullptr, e.getScreenPosition()); + valuePopup->setBounds (cursor.getX() - w / 2, cursor.getY() - h - gap, w, h); +} + +void Knob::hideValuePopup() +{ + valuePopup.reset(); +} + } // namespace serum diff --git a/Source/GUI/Knob.h b/Source/GUI/Knob.h index 75cd11f..58f4142 100644 --- a/Source/GUI/Knob.h +++ b/Source/GUI/Knob.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "../Resources.h" namespace serum @@ -9,6 +10,10 @@ namespace serum // =========================================================================== // Rotary knob — a juce::Slider styled by SerumLookAndFeel. The value readout is // formatted by a user-provided function via getTextFromValue(). +// +// While the knob is being dragged it also shows a small floating label near the +// mouse cursor with the live (formatted) value, giving immediate feedback +// without changing the slider's normal behaviour. // =========================================================================== class Knob : public juce::Slider { @@ -20,8 +25,17 @@ public: juce::String getTextFromValue (double value) override; + void mouseDown (const juce::MouseEvent& e) override; + void mouseDrag (const juce::MouseEvent& e) override; + void mouseUp (const juce::MouseEvent& e) override; + void mouseExit (const juce::MouseEvent& e) override; + private: + void showValuePopup (const juce::MouseEvent& e); + void hideValuePopup(); + std::function formatter; + std::unique_ptr valuePopup; }; } // namespace serum diff --git a/Source/GUI/ToggleButton.h b/Source/GUI/ToggleButton.h index bc4b6b8..05ea149 100644 --- a/Source/GUI/ToggleButton.h +++ b/Source/GUI/ToggleButton.h @@ -9,8 +9,13 @@ namespace serum // =========================================================================== // LED-style toggle (power) button that drives a float parameter (0/1) or an // optional callback (used by RAVE). +// +// Inherits SettableTooltipClient so setTooltip() works exactly like it does on +// juce::Button / juce::Slider / juce::ComboBox (juce::Component itself has no +// tooltip support). // =========================================================================== -class ToggleButton : public juce::Component +class ToggleButton : public juce::Component, + public juce::SettableTooltipClient { public: explicit ToggleButton (const juce::String& label = {}); diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 825755b..3327f39 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -5,6 +5,57 @@ namespace serum namespace { + // ----------------------------------------------------------------------- + // Consistent spacing system. Every panel, row and control position below is + // derived from these constants so the tabs share identical margins, padding + // and row pitches. + // ----------------------------------------------------------------------- + namespace layout + { + constexpr int margin = 10; // panels inset from the tab view edge + constexpr int padding = 8; // controls inset inside panels + row rhythm + constexpr int gap = 6; // horizontal spacing between standard knobs + constexpr int knobSize = 76; // standard knob footprint (incl. label) + constexpr int knobSmall = 44; // small knob width (env / LFO rows) + constexpr int rowHeight = knobSize + padding; // vertical pitch of knob rows + constexpr int panelSpacing = 10; // gap between adjacent panels + constexpr int labelHeight = 16; // label strip height + + // Derived from the visual style (panel title bar and control sizes) so the + // content grid stays aligned across every tab. + constexpr int titleHeight = 22; // panel title bar height + constexpr int comboHeight = 24; // combo box height + constexpr int buttonHeight = 26; // text button height + constexpr int toggleWidth = 40; // "On"/"Sync" toggle width + constexpr int toggleHeight = 36; // toggle height (LED + visible label) + constexpr int headerHeight = toggleHeight; // top control row height + constexpr int displayHeight = 66; // waveform / filter display height + constexpr int envDisplayHeight = 56; // envelope display height + constexpr int lfoDisplayHeight = 64; // LFO display height + constexpr int smallKnobHeight = 60; // height of 44px-wide small knobs + constexpr int smallGap = 3; // horizontal spacing between small knobs + constexpr int arrowWidth = 26; // FX up/down arrow button width + constexpr int macroKnobSize = 120; // macro knob footprint + } + + // Horizontal position of a grid column (0-based). + inline int gridX (int col, int cellW, int cellGap) + { + return layout::margin + col * (cellW + cellGap); + } + + // Y position of a combo box centred on a panel's header row. + inline int comboRowY() + { + return layout::titleHeight + (layout::headerHeight - layout::comboHeight) / 2; + } + + // Y position of the first content row below a panel's header row. + inline int bodyTop() + { + return layout::titleHeight + layout::headerHeight + layout::padding; + } + juce::StringArray wavetableItems() { juce::StringArray a; @@ -37,7 +88,7 @@ namespace juce::String fmtSlope (float v) { const int s = (int) std::llround (v * 2.0f); return juce::String (s == 0 ? 6 : (s == 1 ? 12 : 24)) + " dB"; } juce::String fmtDepth (float v) { return juce::String (v, 2); } - void placeKnobs (std::vector& knobs, int x, int y, int w, int h, int gap = 6) + void placeKnobs (std::vector& knobs, int x, int y, int w, int h, int gap = layout::gap) { for (int i = 0; i < (int) knobs.size(); ++i) knobs[(size_t) i]->setBounds (x + i * (w + gap), y, w, h); @@ -60,8 +111,13 @@ namespace // --------------------------------------------------------------------------- PluginEditor::PluginEditor (SerumAltAudioProcessor& p) : AudioProcessorEditor (p), processor (p), + tooltipWindow (this, 500), masterKnob ("Master", formatPercent) { + // Tab / Shift+Tab cycling is handled by keyPressed(); this lets the editor + // receive keyboard focus when the user clicks anywhere on it. + setWantsKeyboardFocus (true); + root.setBounds (0, 0, kBaseW, kBaseH); addAndMakeVisible (root); @@ -104,18 +160,20 @@ void PluginEditor::resized() // --------------------------------------------------------------------------- Knob* PluginEditor::makeKnob (juce::Component* parent, const juce::String& name, - const juce::String& paramId, std::function fmt) + const juce::String& paramId, std::function fmt, + const juce::String& tooltip) { auto* k = new Knob (name, std::move (fmt)); parent->addAndMakeVisible (k); if (paramId.isNotEmpty()) sliderAttachments.push_back (std::make_unique ( processor.parameters, paramId, *k)); + k->setTooltip (tooltip.isNotEmpty() ? tooltip : name); return k; } juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::String& paramId, - const juce::StringArray& items) + const juce::StringArray& items, const juce::String& tooltip) { auto* c = new juce::ComboBox(); c->addItemList (items, 1); @@ -124,16 +182,19 @@ juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::St if (paramId.isNotEmpty()) comboAttachments.push_back (std::make_unique ( processor.parameters, paramId, *c)); + if (tooltip.isNotEmpty()) + c->setTooltip (tooltip); return c; } ToggleButton* PluginEditor::makeToggle (juce::Component* parent, const juce::String& label, - const juce::String& paramId) + const juce::String& paramId, const juce::String& tooltip) { auto* t = new ToggleButton (label); parent->addAndMakeVisible (t); if (paramId.isNotEmpty()) t->attach (processor.parameters, paramId); + t->setTooltip (tooltip.isNotEmpty() ? tooltip : label); return t; } @@ -149,6 +210,7 @@ void PluginEditor::buildTopBar() presetCombo.onChange = [this] { processor.setCurrentProgram (presetCombo.getSelectedItemIndex()); }; root.addAndMakeVisible (presetCombo); presetCombo.setBounds (200, 16, 220, 24); + presetCombo.setTooltip ("Select a factory preset"); // UI scale. scaleCombo.addItemList ({ "75%", "100%", "125%", "150%", "200%" }, 1); @@ -156,18 +218,25 @@ void PluginEditor::buildTopBar() scaleCombo.onChange = [this] { processor.setUiScaleIndex (scaleCombo.getSelectedItemIndex()); }; root.addAndMakeVisible (scaleCombo); scaleCombo.setBounds (980, 16, 70, 24); + scaleCombo.setTooltip ("Interface scale (75% - 200%)"); // RAVE. raveButton.setLabel ("RAVE"); raveButton.setOnColour (theme::raveGlow); raveButton.setBounds (1062, 2, 50, 56); root.addAndMakeVisible (raveButton); + raveButton.setTooltip ("RAVE one-click boost (unison, width, drive, OTT, reverb)"); // RAVE uses a callback rather than a direct parameter attachment. raveButton.setOnClick ([this] (bool on) { processor.setRaveEnabled (on); }); // Tab bar. juce::TextButton* tabs[5] = { &tabOsc, &tabFilter, &tabMod, &tabFx, &tabMacro }; const char* tabNames[5] = { "OSC", "FILTER", "MOD", "FX", "MACRO" }; + const char* tabTips[5] = { "Oscillators (Tab cycles sections)", + "Filters (Tab cycles sections)", + "Modulation: envelopes, LFOs and matrix (Tab cycles sections)", + "Effects rack (Tab cycles sections)", + "Macros (Tab cycles sections)" }; for (int i = 0; i < 5; ++i) { tabs[i]->setButtonText (tabNames[i]); @@ -175,11 +244,13 @@ void PluginEditor::buildTopBar() tabs[i]->setRadioGroupId (1001); tabs[i]->setBounds (10 + i * 92, 36, 84, 22); tabs[i]->onClick = [this, i] { setTab (i); }; + tabs[i]->setTooltip (tabTips[i]); root.addAndMakeVisible (tabs[i]); } // Master. masterKnob.setBounds (906, 0, 64, 60); + masterKnob.setTooltip ("Master output volume (0-100%)"); root.addAndMakeVisible (masterKnob); sliderAttachments.push_back (std::make_unique ( processor.parameters, ids::master, masterKnob)); @@ -194,79 +265,90 @@ void PluginEditor::buildOscTab() oscView.setBounds (0, 60, kBaseW, kBaseH - 60); root.addChildComponent (oscView); - oscAPanel.setBounds (10, 8, 545, 300); - oscBPanel.setBounds (565, 8, 545, 300); - subPanel.setBounds (10, 318, 360, 150); - noisePanel.setBounds (380, 318, 360, 150); + constexpr int colW = 545; // two equal columns filling kBaseW + constexpr int oscH = 308; + constexpr int subH = 150; + + oscAPanel.setBounds (gridX (0, colW, layout::panelSpacing), layout::margin, colW, oscH); + oscBPanel.setBounds (gridX (1, colW, layout::panelSpacing), layout::margin, colW, oscH); + subPanel.setBounds (gridX (0, colW, layout::panelSpacing), layout::margin + oscH + layout::panelSpacing, colW, subH); + noisePanel.setBounds (gridX (1, colW, layout::panelSpacing), layout::margin + oscH + layout::panelSpacing, colW, subH); for (auto* panel : { &oscAPanel, &oscBPanel, &subPanel, &noisePanel }) oscView.addAndMakeVisible (panel); + const int comboY = comboRowY(); + const int displayY = bodyTop(); // below header row + const int row1Y = displayY + layout::displayHeight + layout::padding; + const int row2Y = row1Y + layout::rowHeight; + // --- Oscillator A --- - makeToggle (&oscAPanel, "On", ids::oscAOn)->setBounds (10, 8, 40, 28); - makeCombo (&oscAPanel, ids::oscAWave, wavetableItems())->setBounds (56, 10, 200, 24); - makeCombo (&oscAPanel, ids::oscAWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" })->setBounds (262, 10, 130, 24); + makeToggle (&oscAPanel, "On", ids::oscAOn, "Enable oscillator A")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); + makeCombo (&oscAPanel, ids::oscAWave, wavetableItems(), "Oscillator A wavetable")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 200, layout::comboHeight); + makeCombo (&oscAPanel, ids::oscAWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" }, "Oscillator A warp mode") + ->setBounds (layout::padding + layout::toggleWidth + layout::gap + 200 + layout::gap, comboY, 130, layout::comboHeight); oscAWave.setWavetables (&processor.engine.getWavetables()); oscAPanel.addAndMakeVisible (oscAWave); - oscAWave.setBounds (10, 44, 525, 66); + oscAWave.setBounds (layout::padding, displayY, colW - 2 * layout::padding, layout::displayHeight); std::vector rowA1 = { - makeKnob (&oscAPanel, "WT Pos", ids::oscAWtPos, formatPercent), - makeKnob (&oscAPanel, "Warp Amt", ids::oscAWarpAmt, formatPercent), - makeKnob (&oscAPanel, "Coarse", ids::oscACoarse, formatSemis), - makeKnob (&oscAPanel, "Fine", ids::oscAFine, formatCents), - makeKnob (&oscAPanel, "Level", ids::oscALevel, formatPercent), - makeKnob (&oscAPanel, "Pan", ids::oscAPan, formatPan) + makeKnob (&oscAPanel, "Wavetable Position", ids::oscAWtPos, formatPercent, "Wavetable position (0-100%)"), + makeKnob (&oscAPanel, "Warp Amount", ids::oscAWarpAmt, formatPercent, "Warp amount (0-100%)"), + makeKnob (&oscAPanel, "Coarse", ids::oscACoarse, formatSemis, "Pitch coarse (-24 to +24 semitones)"), + makeKnob (&oscAPanel, "Fine", ids::oscAFine, formatCents, "Pitch fine (-100 to +100 cents)"), + makeKnob (&oscAPanel, "Level", ids::oscALevel, formatPercent, "Oscillator A level (0-100%)"), + makeKnob (&oscAPanel, "Pan", ids::oscAPan, formatPan, "Oscillator A pan (L100-R100)") }; - placeKnobs (rowA1, 10, 118, 76, 76, 6); + placeKnobs (rowA1, layout::padding, row1Y, layout::knobSize, layout::knobSize); std::vector rowA2 = { - makeKnob (&oscAPanel, "Unison", ids::oscAUnison, fmtUnison), - makeKnob (&oscAPanel, "Detune", ids::oscADetune, formatPercent), - makeKnob (&oscAPanel, "Spread", ids::oscASpread, formatPercent), - makeKnob (&oscAPanel, "Phase", ids::oscAPhase, formatPercent), - makeKnob (&oscAPanel, "Rand Ph", ids::oscARandPh, formatPercent) + makeKnob (&oscAPanel, "Unison", ids::oscAUnison, fmtUnison, "Unison voices (1-16)"), + makeKnob (&oscAPanel, "Detune", ids::oscADetune, formatPercent, "Unison detune (0-100%)"), + makeKnob (&oscAPanel, "Spread", ids::oscASpread, formatPercent, "Unison spread (0-100%)"), + makeKnob (&oscAPanel, "Phase", ids::oscAPhase, formatPercent, "Phase (0-100%)"), + makeKnob (&oscAPanel, "Random Phase", ids::oscARandPh, formatPercent, "Random phase (0-100%)") }; - placeKnobs (rowA2, 10, 198, 76, 76, 6); + placeKnobs (rowA2, layout::padding, row2Y, layout::knobSize, layout::knobSize); // --- Oscillator B --- - makeToggle (&oscBPanel, "On", ids::oscBOn)->setBounds (10, 8, 40, 28); - makeCombo (&oscBPanel, ids::oscBWave, wavetableItems())->setBounds (56, 10, 200, 24); - makeCombo (&oscBPanel, ids::oscBWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" })->setBounds (262, 10, 130, 24); + makeToggle (&oscBPanel, "On", ids::oscBOn, "Enable oscillator B")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); + makeCombo (&oscBPanel, ids::oscBWave, wavetableItems(), "Oscillator B wavetable")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 200, layout::comboHeight); + makeCombo (&oscBPanel, ids::oscBWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" }, "Oscillator B warp mode") + ->setBounds (layout::padding + layout::toggleWidth + layout::gap + 200 + layout::gap, comboY, 130, layout::comboHeight); oscBWave.setWavetables (&processor.engine.getWavetables()); oscBPanel.addAndMakeVisible (oscBWave); - oscBWave.setBounds (10, 44, 525, 66); + oscBWave.setBounds (layout::padding, displayY, colW - 2 * layout::padding, layout::displayHeight); std::vector rowB1 = { - makeKnob (&oscBPanel, "WT Pos", ids::oscBWtPos, formatPercent), - makeKnob (&oscBPanel, "Warp Amt", ids::oscBWarpAmt, formatPercent), - makeKnob (&oscBPanel, "Coarse", ids::oscBCoarse, formatSemis), - makeKnob (&oscBPanel, "Fine", ids::oscBFine, formatCents), - makeKnob (&oscBPanel, "Level", ids::oscBLevel, formatPercent), - makeKnob (&oscBPanel, "Pan", ids::oscBPan, formatPan) + makeKnob (&oscBPanel, "Wavetable Position", ids::oscBWtPos, formatPercent, "Wavetable position (0-100%)"), + makeKnob (&oscBPanel, "Warp Amount", ids::oscBWarpAmt, formatPercent, "Warp amount (0-100%)"), + makeKnob (&oscBPanel, "Coarse", ids::oscBCoarse, formatSemis, "Pitch coarse (-24 to +24 semitones)"), + makeKnob (&oscBPanel, "Fine", ids::oscBFine, formatCents, "Pitch fine (-100 to +100 cents)"), + makeKnob (&oscBPanel, "Level", ids::oscBLevel, formatPercent, "Oscillator B level (0-100%)"), + makeKnob (&oscBPanel, "Pan", ids::oscBPan, formatPan, "Oscillator B pan (L100-R100)") }; - placeKnobs (rowB1, 10, 118, 76, 76, 6); + placeKnobs (rowB1, layout::padding, row1Y, layout::knobSize, layout::knobSize); std::vector rowB2 = { - makeKnob (&oscBPanel, "Unison", ids::oscBUnison, fmtUnison), - makeKnob (&oscBPanel, "Detune", ids::oscBDetune, formatPercent), - makeKnob (&oscBPanel, "Spread", ids::oscBSpread, formatPercent), - makeKnob (&oscBPanel, "Phase", ids::oscBPhase, formatPercent), - makeKnob (&oscBPanel, "Rand Ph", ids::oscBRandPh, formatPercent) + makeKnob (&oscBPanel, "Unison", ids::oscBUnison, fmtUnison, "Unison voices (1-16)"), + makeKnob (&oscBPanel, "Detune", ids::oscBDetune, formatPercent, "Unison detune (0-100%)"), + makeKnob (&oscBPanel, "Spread", ids::oscBSpread, formatPercent, "Unison spread (0-100%)"), + makeKnob (&oscBPanel, "Phase", ids::oscBPhase, formatPercent, "Phase (0-100%)"), + makeKnob (&oscBPanel, "Random Phase", ids::oscBRandPh, formatPercent, "Random phase (0-100%)") }; - placeKnobs (rowB2, 10, 198, 76, 76, 6); + placeKnobs (rowB2, layout::padding, row2Y, layout::knobSize, layout::knobSize); // --- Sub --- - makeToggle (&subPanel, "On", ids::subOn)->setBounds (10, 10, 40, 28); - makeCombo (&subPanel, ids::subShape, { "Sine", "Triangle" })->setBounds (56, 12, 130, 24); - makeCombo (&subPanel, ids::subOct, { "-2 oct", "-1 oct", "0 oct" })->setBounds (192, 12, 90, 24); - makeKnob (&subPanel, "Level", ids::subLevel, formatPercent)->setBounds (10, 46, 76, 76); + makeToggle (&subPanel, "On", ids::subOn, "Enable sub oscillator")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); + makeCombo (&subPanel, ids::subShape, { "Sine", "Triangle" }, "Sub oscillator waveform")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 130, layout::comboHeight); + makeCombo (&subPanel, ids::subOct, { "-2 oct", "-1 oct", "0 oct" }, "Sub oscillator octave")->setBounds (layout::padding + layout::toggleWidth + layout::gap + 130 + layout::gap, comboY, 90, layout::comboHeight); + makeKnob (&subPanel, "Level", ids::subLevel, formatPercent, "Sub oscillator level (0-100%)")->setBounds (layout::padding, displayY, layout::knobSize, layout::knobSize); // --- Noise --- - makeToggle (&noisePanel, "On", ids::noiseOn)->setBounds (10, 10, 40, 28); - makeCombo (&noisePanel, ids::noiseType, { "White", "Pink" })->setBounds (56, 12, 130, 24); - makeKnob (&noisePanel, "Level", ids::noiseLevel, formatPercent)->setBounds (10, 46, 76, 76); + makeToggle (&noisePanel, "On", ids::noiseOn, "Enable noise")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); + makeCombo (&noisePanel, ids::noiseType, { "White", "Pink" }, "Noise type")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 130, layout::comboHeight); + makeKnob (&noisePanel, "Level", ids::noiseLevel, formatPercent, "Noise level (0-100%)")->setBounds (layout::padding, displayY, layout::knobSize, layout::knobSize); } void PluginEditor::buildFilterTab() @@ -274,49 +356,56 @@ void PluginEditor::buildFilterTab() filterView.setBounds (0, 60, kBaseW, kBaseH - 60); root.addChildComponent (filterView); - filter1Panel.setBounds (10, 8, 360, 300); - filter2Panel.setBounds (380, 8, 360, 300); - routingPanel.setBounds (750, 8, 360, 300); + constexpr int colW = 360; + constexpr int panelH = 300; // retains the original panel height (content top-aligned) + + filter1Panel.setBounds (gridX (0, colW, layout::panelSpacing), layout::margin, colW, panelH); + filter2Panel.setBounds (gridX (1, colW, layout::panelSpacing), layout::margin, colW, panelH); + routingPanel.setBounds (gridX (2, colW, layout::panelSpacing), layout::margin, colW, panelH); for (auto* panel : { &filter1Panel, &filter2Panel, &routingPanel }) filterView.addAndMakeVisible (panel); const juce::StringArray filterTypes = { "Ladder LP", "Ladder HP", "Ladder BP", "Diode", "Comb", "Formant", "Screamer" }; const juce::StringArray slopes = { "6 dB", "12 dB", "24 dB" }; + const int comboY = comboRowY(); + const int displayY = bodyTop(); + const int knobY = displayY + layout::displayHeight + layout::padding; // aligns with OSC row 1 + // Filter 1 - makeToggle (&filter1Panel, "On", ids::f1On)->setBounds (10, 8, 40, 28); - makeCombo (&filter1Panel, ids::f1Type, filterTypes)->setBounds (56, 10, 180, 24); - makeCombo (&filter1Panel, ids::f1Slope, slopes)->setBounds (242, 10, 100, 24); + makeToggle (&filter1Panel, "On", ids::f1On, "Enable filter 1")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); + makeCombo (&filter1Panel, ids::f1Type, filterTypes, "Filter 1 type")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 170, layout::comboHeight); + makeCombo (&filter1Panel, ids::f1Slope, slopes, "Filter 1 slope")->setBounds (layout::padding + layout::toggleWidth + layout::gap + 170 + layout::gap, comboY, 100, layout::comboHeight); filter1Panel.addAndMakeVisible (filter1Display); - filter1Display.setBounds (10, 44, 340, 70); + filter1Display.setBounds (layout::padding, displayY, colW - 2 * layout::padding, layout::displayHeight); std::vector f1 = { - makeKnob (&filter1Panel, "Cutoff", ids::f1Cutoff, formatHz), - makeKnob (&filter1Panel, "Res", ids::f1Res, formatPercent), - makeKnob (&filter1Panel, "Drive", ids::f1Drive, formatPercent), - makeKnob (&filter1Panel, "Keytrack", ids::f1Key, formatPercent) + makeKnob (&filter1Panel, "Cutoff", ids::f1Cutoff, formatHz, "Filter 1 cutoff (20 Hz - 20 kHz)"), + makeKnob (&filter1Panel, "Resonance", ids::f1Res, formatPercent, "Filter 1 resonance (0-100%)"), + makeKnob (&filter1Panel, "Drive", ids::f1Drive, formatPercent, "Filter 1 drive (0-100%)"), + makeKnob (&filter1Panel, "Keytrack", ids::f1Key, formatPercent, "Filter 1 keytrack (0-100%)") }; - placeKnobs (f1, 10, 124, 76, 76, 6); + placeKnobs (f1, layout::padding, knobY, layout::knobSize, layout::knobSize); // Filter 2 - makeToggle (&filter2Panel, "On", ids::f2On)->setBounds (10, 8, 40, 28); - makeCombo (&filter2Panel, ids::f2Type, filterTypes)->setBounds (56, 10, 180, 24); - makeCombo (&filter2Panel, ids::f2Slope, slopes)->setBounds (242, 10, 100, 24); + makeToggle (&filter2Panel, "On", ids::f2On, "Enable filter 2")->setBounds (layout::padding, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); + makeCombo (&filter2Panel, ids::f2Type, filterTypes, "Filter 2 type")->setBounds (layout::padding + layout::toggleWidth + layout::gap, comboY, 170, layout::comboHeight); + makeCombo (&filter2Panel, ids::f2Slope, slopes, "Filter 2 slope")->setBounds (layout::padding + layout::toggleWidth + layout::gap + 170 + layout::gap, comboY, 100, layout::comboHeight); filter2Panel.addAndMakeVisible (filter2Display); - filter2Display.setBounds (10, 44, 340, 70); + filter2Display.setBounds (layout::padding, displayY, colW - 2 * layout::padding, layout::displayHeight); std::vector f2 = { - makeKnob (&filter2Panel, "Cutoff", ids::f2Cutoff, formatHz), - makeKnob (&filter2Panel, "Res", ids::f2Res, formatPercent), - makeKnob (&filter2Panel, "Drive", ids::f2Drive, formatPercent), - makeKnob (&filter2Panel, "Keytrack", ids::f2Key, formatPercent) + makeKnob (&filter2Panel, "Cutoff", ids::f2Cutoff, formatHz, "Filter 2 cutoff (20 Hz - 20 kHz)"), + makeKnob (&filter2Panel, "Resonance", ids::f2Res, formatPercent, "Filter 2 resonance (0-100%)"), + makeKnob (&filter2Panel, "Drive", ids::f2Drive, formatPercent, "Filter 2 drive (0-100%)"), + makeKnob (&filter2Panel, "Keytrack", ids::f2Key, formatPercent, "Filter 2 keytrack (0-100%)") }; - placeKnobs (f2, 10, 124, 76, 76, 6); + placeKnobs (f2, layout::padding, knobY, layout::knobSize, layout::knobSize); // Routing - makeCombo (&routingPanel, ids::fRoute, { "Serial", "Parallel", "Split" })->setBounds (10, 10, 150, 24); - makeKnob (&routingPanel, "Mix", ids::fMix, formatPercent)->setBounds (10, 50, 76, 76); - makeKnob (&routingPanel, "Output", ids::fOut, formatPercent)->setBounds (100, 50, 76, 76); + makeCombo (&routingPanel, ids::fRoute, { "Serial", "Parallel", "Split" }, "Filter routing (serial / parallel / split)")->setBounds (layout::padding, comboY, 160, layout::comboHeight); + makeKnob (&routingPanel, "Mix", ids::fMix, formatPercent, "Filter mix (0-100%)")->setBounds (layout::padding, displayY, layout::knobSize, layout::knobSize); + makeKnob (&routingPanel, "Output", ids::fOut, formatPercent, "Filter output (0-100%)")->setBounds (layout::padding + layout::knobSize + layout::gap, displayY, layout::knobSize, layout::knobSize); } void PluginEditor::buildModTab() @@ -326,15 +415,20 @@ void PluginEditor::buildModTab() const juce::StringArray lfoShapes = { "Sine", "Triangle", "Saw", "Square", "S&H", "Step", "Freehand" }; + constexpr int colW = 269; // four equal columns: 4*269 + 3*8 = 1100 + constexpr int colGap = 8; + constexpr int envH = 154; + constexpr int lfoH = 206; + // Envelopes (4 across). for (int i = 0; i < kNumEnvelopes; ++i) { - const int x = 10 + i * 275; - envPanels[(size_t) i].setBounds (x, 8, 265, 150); + envPanels[(size_t) i].setBounds (gridX (i, colW, colGap), layout::margin, colW, envH); modView.addAndMakeVisible (envPanels[(size_t) i]); envPanels[(size_t) i].addAndMakeVisible (envDisplays[(size_t) i]); - envDisplays[(size_t) i].setBounds (8, 8, 249, 56); + envDisplays[(size_t) i].setBounds (layout::padding, layout::titleHeight, + colW - 2 * layout::padding, layout::envDisplayHeight); const char* idsA[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A }; const char* idsD[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D }; @@ -343,20 +437,22 @@ void PluginEditor::buildModTab() const char* idsC[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve }; std::vector knobs = { - makeKnob (&envPanels[(size_t) i], "A", idsA[i], formatSeconds), - makeKnob (&envPanels[(size_t) i], "D", idsD[i], formatSeconds), - makeKnob (&envPanels[(size_t) i], "S", idsS[i], formatPercent), - makeKnob (&envPanels[(size_t) i], "R", idsR[i], formatSeconds), - makeKnob (&envPanels[(size_t) i], "Curve", idsC[i], formatPercent) + makeKnob (&envPanels[(size_t) i], "Attack", idsA[i], formatSeconds, "Attack time (0.5 ms - 12 s)"), + makeKnob (&envPanels[(size_t) i], "Decay", idsD[i], formatSeconds, "Decay time (0.5 ms - 12 s)"), + makeKnob (&envPanels[(size_t) i], "Sustain", idsS[i], formatPercent, "Sustain level (0-100%)"), + makeKnob (&envPanels[(size_t) i], "Release", idsR[i], formatSeconds, "Release time (0.5 ms - 12 s)"), + makeKnob (&envPanels[(size_t) i], "Curve", idsC[i], formatPercent, "Envelope curve (0-100%)") }; - placeKnobs (knobs, 8, 72, 44, 60, 3); + placeKnobs (knobs, layout::padding, + layout::titleHeight + layout::envDisplayHeight + layout::padding, + layout::knobSmall, layout::smallKnobHeight, layout::smallGap); } // LFOs (4 across). + const int lfoTop = layout::margin + envH + layout::panelSpacing; for (int i = 0; i < kNumLfos; ++i) { - const int x = 10 + i * 275; - lfoPanels[(size_t) i].setBounds (x, 168, 265, 180); + lfoPanels[(size_t) i].setBounds (gridX (i, colW, colGap), lfoTop, colW, lfoH); modView.addAndMakeVisible (lfoPanels[(size_t) i]); const char* rate[4] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate }; @@ -367,20 +463,22 @@ void PluginEditor::buildModTab() const char* fade[4] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade }; const char* delay[4] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay }; - makeCombo (&lfoPanels[(size_t) i], shape[i], lfoShapes)->setBounds (8, 8, 110, 22); - makeToggle (&lfoPanels[(size_t) i], "Sync", sync[i])->setBounds (126, 4, 40, 28); + makeCombo (&lfoPanels[(size_t) i], shape[i], lfoShapes, "LFO shape")->setBounds (layout::padding, comboRowY(), 110, layout::comboHeight); + makeToggle (&lfoPanels[(size_t) i], "Sync", sync[i], "Tempo-sync LFO")->setBounds (layout::padding + 110 + layout::gap, layout::titleHeight, layout::toggleWidth, layout::toggleHeight); lfoPanels[(size_t) i].addAndMakeVisible (lfoDisplays[(size_t) i]); - lfoDisplays[(size_t) i].setBounds (8, 38, 249, 64); + lfoDisplays[(size_t) i].setBounds (layout::padding, bodyTop(), colW - 2 * layout::padding, layout::lfoDisplayHeight); std::vector knobs = { - makeKnob (&lfoPanels[(size_t) i], "Rate", rate[i], formatPercent), - makeKnob (&lfoPanels[(size_t) i], "Beat", beat[i], formatPercent), - makeKnob (&lfoPanels[(size_t) i], "Phase", phase[i], formatPercent), - makeKnob (&lfoPanels[(size_t) i], "Fade", fade[i], formatPercent), - makeKnob (&lfoPanels[(size_t) i], "Delay", delay[i], formatPercent) + makeKnob (&lfoPanels[(size_t) i], "Rate", rate[i], formatPercent, "LFO rate (0.02 Hz - 30 Hz)"), + makeKnob (&lfoPanels[(size_t) i], "Beat", beat[i], formatPercent, "Beat division (1/32 - 4 bars)"), + makeKnob (&lfoPanels[(size_t) i], "Phase", phase[i], formatPercent, "Phase (0-100%)"), + makeKnob (&lfoPanels[(size_t) i], "Fade", fade[i], formatPercent, "Fade in (0-100%)"), + makeKnob (&lfoPanels[(size_t) i], "Delay", delay[i], formatPercent, "Start delay (0-100%)") }; - placeKnobs (knobs, 8, 106, 44, 60, 3); + placeKnobs (knobs, layout::padding, + bodyTop() + layout::lfoDisplayHeight + layout::padding, + layout::knobSmall, layout::smallKnobHeight, layout::smallGap); lfoDisplays[(size_t) i].setOnShapeEdited ([this, i] (const std::vector& data, int steps) { @@ -389,34 +487,46 @@ void PluginEditor::buildModTab() } // Modulation matrix. - matrixPanel.setBounds (10, 356, 1100, 268); + const int matrixTop = lfoTop + lfoH + layout::panelSpacing; + const int matrixH = kBaseH - 60 - matrixTop - layout::margin; + matrixPanel.setBounds (layout::margin, matrixTop, kBaseW - 2 * layout::margin, matrixH); modView.addAndMakeVisible (matrixPanel); std::vector targetEnums; const juce::StringArray targetItems = modTargetItems (targetEnums); + const int mCenter = layout::titleHeight + 30; // vertical centre of the (60px) depth knob + modSourceCombo.addItemList (modSourceItems(), 1); modSourceCombo.setSelectedItemIndex (0, juce::dontSendNotification); - modSourceCombo.setBounds (10, 22, 170, 24); + modSourceCombo.setBounds (layout::padding, mCenter - layout::comboHeight / 2, 160, layout::comboHeight); + modSourceCombo.setTooltip ("Modulation source"); matrixPanel.addAndMakeVisible (modSourceCombo); modTargetCombo.addItemList (targetItems, 1); modTargetCombo.setSelectedItemIndex (0, juce::dontSendNotification); - modTargetCombo.setBounds (190, 22, 200, 24); + modTargetCombo.setBounds (layout::padding + 160 + layout::gap, mCenter - layout::comboHeight / 2, 200, layout::comboHeight); + modTargetCombo.setTooltip ("Modulation destination"); matrixPanel.addAndMakeVisible (modTargetCombo); + const int depthX = layout::padding + 160 + layout::gap + 200 + layout::gap; modDepthKnob.setRange (-1.0, 1.0); modDepthKnob.setValue (0.5, juce::dontSendNotification); modDepthKnob.setFormatter (fmtDepth); - modDepthKnob.setBounds (400, 4, 76, 60); + modDepthKnob.setBounds (depthX, layout::titleHeight, layout::knobSize, 60); + modDepthKnob.setTooltip ("Modulation depth (-1 to +1)"); matrixPanel.addAndMakeVisible (modDepthKnob); - modBipolarToggle.setBounds (486, 10, 56, 48); + modBipolarToggle.setLabel ("Bipolar"); + modBipolarToggle.setBounds (depthX + layout::knobSize + layout::gap, mCenter - layout::toggleHeight / 2, 60, layout::toggleHeight); + modBipolarToggle.setTooltip ("Bipolar modulation depth"); matrixPanel.addAndMakeVisible (modBipolarToggle); modBipolarToggle.setToggleState (true); + const int addX = depthX + layout::knobSize + layout::gap + 60 + layout::gap; modAddButton.setButtonText ("Add"); - modAddButton.setBounds (550, 22, 70, 26); + modAddButton.setBounds (addX, mCenter - layout::buttonHeight / 2, 70, layout::buttonHeight); + modAddButton.setTooltip ("Add modulation connection"); modAddButton.onClick = [this, targetEnums] { const ModSource src = (ModSource) modSourceCombo.getSelectedItemIndex(); @@ -432,7 +542,8 @@ void PluginEditor::buildModTab() matrixPanel.addAndMakeVisible (modAddButton); modRemoveButton.setButtonText ("Remove Last"); - modRemoveButton.setBounds (626, 22, 110, 26); + modRemoveButton.setBounds (addX + 70 + layout::gap, mCenter - layout::buttonHeight / 2, 110, layout::buttonHeight); + modRemoveButton.setTooltip ("Remove last modulation connection"); modRemoveButton.onClick = [this] { processor.engine.getMatrix().removeConnection (processor.engine.getMatrix().size() - 1); @@ -441,7 +552,8 @@ void PluginEditor::buildModTab() matrixPanel.addAndMakeVisible (modRemoveButton); modClearButton.setButtonText ("Clear"); - modClearButton.setBounds (742, 22, 70, 26); + modClearButton.setBounds (addX + 70 + layout::gap + 110 + layout::gap, mCenter - layout::buttonHeight / 2, 70, layout::buttonHeight); + modClearButton.setTooltip ("Clear all modulation connections"); modClearButton.onClick = [this] { processor.engine.getMatrix().clear(); @@ -449,7 +561,10 @@ void PluginEditor::buildModTab() }; matrixPanel.addAndMakeVisible (modClearButton); - modList.setBounds (10, 56, 1080, 200); + const int listTop = layout::titleHeight + 60 + layout::padding; + modList.setBounds (layout::padding, listTop, + kBaseW - 2 * layout::margin - 2 * layout::padding, + matrixH - listTop - layout::padding); modList.setReadOnly (true); modList.setMultiLine (true, false); modList.setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentBlack); @@ -466,37 +581,44 @@ void PluginEditor::buildFxTab() const juce::StringArray fxTypes = { "Off", "Hyper", "Chorus", "Flanger", "Phaser", "Distortion", "EQ", "Compressor", "Delay", "Reverb" }; + constexpr int colW = 545; + constexpr int slotH = 150; + for (int i = 0; i < kNumFxSlots; ++i) { const int col = i % 2; const int row = i / 2; - const int x = 10 + col * 555; - const int y = 8 + row * 156; + const int x = gridX (col, colW, layout::panelSpacing); + const int y = layout::margin + row * (slotH + layout::panelSpacing); - fxPanels[(size_t) i].setBounds (x, y, 545, 148); + fxPanels[(size_t) i].setBounds (x, y, colW, slotH); fxView.addAndMakeVisible (fxPanels[(size_t) i]); - fxTypeCombos[(size_t) i] = makeCombo (&fxPanels[(size_t) i], kFxType[i], fxTypes); - fxTypeCombos[(size_t) i]->setBounds (8, 8, 160, 24); + fxTypeCombos[(size_t) i] = makeCombo (&fxPanels[(size_t) i], kFxType[i], fxTypes, + "FX slot " + juce::String (i + 1) + " effect type"); + fxTypeCombos[(size_t) i]->setBounds (layout::padding, comboRowY(), 160, layout::comboHeight); + const int upX = layout::padding + 160 + layout::gap; fxUp[(size_t) i] = new juce::TextButton ("\xe2\x86\x91"); - fxUp[(size_t) i]->setBounds (176, 8, 26, 24); + fxUp[(size_t) i]->setBounds (upX, comboRowY(), layout::arrowWidth, layout::comboHeight); + fxUp[(size_t) i]->setTooltip ("Move effect up"); fxPanels[(size_t) i].addAndMakeVisible (fxUp[(size_t) i]); fxDown[(size_t) i] = new juce::TextButton ("\xe2\x86\x93"); - fxDown[(size_t) i]->setBounds (204, 8, 26, 24); + fxDown[(size_t) i]->setBounds (upX + layout::arrowWidth + layout::gap, comboRowY(), layout::arrowWidth, layout::comboHeight); + fxDown[(size_t) i]->setTooltip ("Move effect down"); fxPanels[(size_t) i].addAndMakeVisible (fxDown[(size_t) i]); fxUp[(size_t) i]->onClick = [this, i] { if (i > 0) swapFxSlots (i, i - 1); }; fxDown[(size_t) i]->onClick = [this, i] { if (i < kNumFxSlots - 1) swapFxSlots (i, i + 1); }; std::vector knobs = { - makeKnob (&fxPanels[(size_t) i], "Mix", kFxMix[i], formatPercent), - makeKnob (&fxPanels[(size_t) i], "P1", kFxP1[i], formatPercent), - makeKnob (&fxPanels[(size_t) i], "P2", kFxP2[i], formatPercent), - makeKnob (&fxPanels[(size_t) i], "P3", kFxP3[i], formatPercent), - makeKnob (&fxPanels[(size_t) i], "P4", kFxP4[i], formatPercent) + makeKnob (&fxPanels[(size_t) i], "Mix", kFxMix[i], formatPercent, "Effect mix (0-100%)"), + makeKnob (&fxPanels[(size_t) i], "P1", kFxP1[i], formatPercent, "Effect parameter 1 (0-100%)"), + makeKnob (&fxPanels[(size_t) i], "P2", kFxP2[i], formatPercent, "Effect parameter 2 (0-100%)"), + makeKnob (&fxPanels[(size_t) i], "P3", kFxP3[i], formatPercent, "Effect parameter 3 (0-100%)"), + makeKnob (&fxPanels[(size_t) i], "P4", kFxP4[i], formatPercent, "Effect parameter 4 (0-100%)") }; - placeKnobs (knobs, 8, 40, 76, 76, 6); + placeKnobs (knobs, layout::padding, bodyTop(), layout::knobSize, layout::knobSize); fxKnobs[(size_t) i] = knobs; } } @@ -506,40 +628,56 @@ void PluginEditor::buildMacroTab() macroView.setBounds (0, 60, kBaseW, kBaseH - 60); root.addChildComponent (macroView); + constexpr int colW = 269; // four equal columns: 4*269 + 3*8 = 1100 + constexpr int colGap = 8; + constexpr int macroH = 158; + for (int i = 0; i < kNumMacros; ++i) { - const int x = 10 + i * 275; - macroPanels[(size_t) i].setBounds (x, 8, 265, 240); + macroPanels[(size_t) i].setBounds (gridX (i, colW, colGap), layout::margin, colW, macroH); macroView.addAndMakeVisible (macroPanels[(size_t) i]); const char* ids[4] = { ids::macro1, ids::macro2, ids::macro3, ids::macro4 }; - macroKnobs[(size_t) i] = makeKnob (¯oPanels[(size_t) i], MacroControls::macroName (i), ids[i], formatPercent); - macroKnobs[(size_t) i]->setBounds (70, 20, 120, 120); + macroKnobs[(size_t) i] = makeKnob (¯oPanels[(size_t) i], MacroControls::macroName (i), ids[i], formatPercent, + MacroControls::macroName (i) + " macro (0-100%)"); + macroKnobs[(size_t) i]->setBounds ((colW - layout::macroKnobSize) / 2, + layout::titleHeight + layout::padding, + layout::macroKnobSize, layout::macroKnobSize); } // Macro assignment editor. - macroAssignPanel.setBounds (10, 256, 1100, 380); + const int assignTop = layout::margin + macroH + layout::panelSpacing; + const int assignH = kBaseH - 60 - assignTop - layout::margin; + macroAssignPanel.setBounds (layout::margin, assignTop, kBaseW - 2 * layout::margin, assignH); macroView.addAndMakeVisible (macroAssignPanel); std::vector targetEnums; const juce::StringArray targetItems = modTargetItems (targetEnums); + const int mCenter = layout::titleHeight + 30; // vertical centre of the (60px) depth knob + macroAssignIndex.addItemList ({ "Macro 1", "Macro 2", "Macro 3", "Macro 4" }, 1); macroAssignIndex.setSelectedItemIndex (0, juce::dontSendNotification); - macroAssignIndex.setBounds (10, 24, 140, 24); + macroAssignIndex.setBounds (layout::padding, mCenter - layout::comboHeight / 2, 140, layout::comboHeight); + macroAssignIndex.setTooltip ("Macro to assign"); macroAssignPanel.addAndMakeVisible (macroAssignIndex); macroAssignTarget.addItemList (targetItems, 1); macroAssignTarget.setSelectedItemIndex (0, juce::dontSendNotification); - macroAssignTarget.setBounds (160, 24, 220, 24); + macroAssignTarget.setBounds (layout::padding + 140 + layout::gap, mCenter - layout::comboHeight / 2, 220, layout::comboHeight); + macroAssignTarget.setTooltip ("Destination parameter"); macroAssignPanel.addAndMakeVisible (macroAssignTarget); + const int depthX = layout::padding + 140 + layout::gap + 220 + layout::gap; macroDepthKnob.setFormatter (fmtDepth); - macroDepthKnob.setBounds (390, 6, 76, 60); + macroDepthKnob.setBounds (depthX, layout::titleHeight, layout::knobSize, 60); + macroDepthKnob.setTooltip ("Macro assignment depth (0 to 1)"); macroAssignPanel.addAndMakeVisible (macroDepthKnob); + const int assignBtnX = depthX + layout::knobSize + layout::gap; macroAssignButton.setButtonText ("Assign"); - macroAssignButton.setBounds (476, 24, 80, 26); + macroAssignButton.setBounds (assignBtnX, mCenter - layout::buttonHeight / 2, 80, layout::buttonHeight); + macroAssignButton.setTooltip ("Assign destination to the selected macro"); macroAssignButton.onClick = [this, targetEnums] { const int macro = macroAssignIndex.getSelectedItemIndex(); @@ -554,7 +692,8 @@ void PluginEditor::buildMacroTab() macroAssignPanel.addAndMakeVisible (macroAssignButton); macroClearButton.setButtonText ("Clear All"); - macroClearButton.setBounds (562, 24, 80, 26); + macroClearButton.setBounds (assignBtnX + 80 + layout::gap, mCenter - layout::buttonHeight / 2, 80, layout::buttonHeight); + macroClearButton.setTooltip ("Clear all macro assignments"); macroClearButton.onClick = [this] { processor.engine.getMacros().clear(); @@ -562,7 +701,10 @@ void PluginEditor::buildMacroTab() }; macroAssignPanel.addAndMakeVisible (macroClearButton); - macroList.setBounds (10, 58, 1080, 310); + const int listTop = layout::titleHeight + 60 + layout::padding; + macroList.setBounds (layout::padding, listTop, + kBaseW - 2 * layout::margin - 2 * layout::padding, + assignH - listTop - layout::padding); macroList.setReadOnly (true); macroList.setMultiLine (true, false); macroList.setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentBlack); @@ -608,6 +750,35 @@ void PluginEditor::setTab (int index) tabMacro.setToggleState (currentTab == 4, juce::dontSendNotification); } +void PluginEditor::cycleTab (int delta) +{ + setTab ((currentTab + delta + 5) % 5); +} + +bool PluginEditor::keyPressed (const juce::KeyPress& key) +{ + // Only handle plain Tab / Shift+Tab (never Ctrl/Cmd/Alt+Tab). + if (key.getKeyCode() == juce::KeyPress::tabKey + && ! key.getModifiers().isCtrlDown() + && ! key.getModifiers().isAltDown() + && ! key.getModifiers().isCommandDown()) + { + // Leave Tab alone while a text editor or combo box is being edited so we + // don't break typing / focus traversal. + for (auto* c = juce::Component::getCurrentlyFocusedComponent(); c != nullptr; c = c->getParentComponent()) + { + if (dynamic_cast (c) != nullptr + || dynamic_cast (c) != nullptr) + return false; + } + + cycleTab (key.getModifiers().isShiftDown() ? -1 : +1); + return true; + } + + return false; +} + void PluginEditor::applyUiScale() { const float scale = processor.getUiScale(); diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 19f3732..1045089 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -30,6 +30,7 @@ public: void paint (juce::Graphics&) override; void resized() override; void timerCallback() override; + bool keyPressed (const juce::KeyPress& key) override; void setTab (int index); @@ -42,6 +43,9 @@ private: juce::Component root; + // --- tooltips --- + juce::TooltipWindow tooltipWindow; + // --- top bar --- std::unique_ptr logo; juce::ComboBox presetCombo; @@ -103,9 +107,12 @@ private: // --- helpers --- Knob* makeKnob (juce::Component* parent, const juce::String& name, const juce::String& paramId, - std::function fmt = {}); - juce::ComboBox* makeCombo (juce::Component* parent, const juce::String& paramId, const juce::StringArray& items); - ToggleButton* makeToggle (juce::Component* parent, const juce::String& label, const juce::String& paramId); + std::function fmt = {}, const juce::String& tooltip = {}); + juce::ComboBox* makeCombo (juce::Component* parent, const juce::String& paramId, const juce::StringArray& items, + const juce::String& tooltip = {}); + ToggleButton* makeToggle (juce::Component* parent, const juce::String& label, const juce::String& paramId, + const juce::String& tooltip = {}); + void cycleTab (int delta); void buildTopBar(); void buildOscTab();