Round 1 findings and fixes, all previously uncommitted: - FXProcessor per-slot DSP history repair (HEAD does not compile without it) - SynthVoice::getModulationValue, editor control-lock guard - ExactCompare.h naming the deliberate exact float comparisons - KnobLookAndFeel replacing the never-installed SerumLookAndFeel - 55 first-party compiler warnings to 0 - AGENT.md renamed to AGENTS.md across Source/ - graft repository context graph and its hooks - JANITOR_TASKS.md and UI_UX_AUDIT.md records
155 lines
5.0 KiB
C++
155 lines
5.0 KiB
C++
#include "Knob.h"
|
|
|
|
namespace serum
|
|
{
|
|
|
|
namespace
|
|
{
|
|
// One shared look-and-feel for every knob. It outlives every Knob because
|
|
// it is a function-local static and the GUI owns the knobs.
|
|
KnobLookAndFeel& knobStyle()
|
|
{
|
|
static KnobLookAndFeel style;
|
|
return style;
|
|
}
|
|
}
|
|
|
|
Knob::Knob (const juce::String& name, std::function<juce::String (float)> fmt)
|
|
: formatter (std::move (fmt))
|
|
{
|
|
setSliderStyle (juce::Slider::RotaryVerticalDrag);
|
|
setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
|
|
setRange (0.0, 1.0);
|
|
setDoubleClickReturnValue (true, 0.5);
|
|
setName (name);
|
|
setComponentID (name);
|
|
|
|
// Draw the dial and the name strip through the theme, not through the
|
|
// shared default look-and-feel.
|
|
setLookAndFeel (&knobStyle());
|
|
}
|
|
|
|
juce::String Knob::getTextFromValue (double value)
|
|
{
|
|
if (formatter)
|
|
return formatter ((float) value);
|
|
return formatPercent ((float) value);
|
|
}
|
|
|
|
// The dial takes the top of the footprint and the name takes a strip at the
|
|
// bottom, so the name is always visible without a tooltip hover.
|
|
void Knob::resized()
|
|
{
|
|
juce::Slider::resized();
|
|
textArea = getLocalBounds().removeFromBottom (
|
|
juce::jmin (KnobLookAndFeel::labelHeight, getHeight() / 2));
|
|
}
|
|
|
|
void Knob::paint (juce::Graphics& g)
|
|
{
|
|
juce::Slider::paint (g);
|
|
|
|
if (textArea.isEmpty())
|
|
return;
|
|
|
|
// Dim while disabled, bright while hovered, otherwise the normal label
|
|
// colour, so the three states are distinguishable without colour alone.
|
|
const auto colour = isEnabled() ? (isMouseOverOrDragging() ? theme::text : theme::textDim)
|
|
: theme::outline;
|
|
|
|
g.setColour (colour);
|
|
g.setFont (juce::Font (11.0f));
|
|
g.drawText (getName(), textArea, juce::Justification::centred, false);
|
|
}
|
|
|
|
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;
|
|
|
|
constexpr int baseW = 84;
|
|
constexpr int baseH = 24;
|
|
constexpr int gap = 10;
|
|
|
|
// The interface scale is applied to a parent component, not to this slider.
|
|
// Measure it from the local-space distance between two neighbouring points,
|
|
// so a scaled interface does not leave the readout at a fixed size.
|
|
float scale = 1.0f;
|
|
if (auto* parent = getParentComponent())
|
|
{
|
|
const auto origin = getLocalPoint (parent, juce::Point<int> (0, 0)).toFloat();
|
|
const auto unitX = getLocalPoint (parent, juce::Point<int> (1, 0)).toFloat();
|
|
scale = origin.getDistanceFrom (unitX);
|
|
}
|
|
scale = juce::jlimit (0.5f, 2.5f, scale);
|
|
const int w = juce::roundToInt (baseW * scale);
|
|
const int h = juce::roundToInt (baseH * scale);
|
|
|
|
if (valuePopup == nullptr)
|
|
{
|
|
valuePopup = std::make_unique<juce::Label>();
|
|
valuePopup->setAlwaysOnTop (true);
|
|
valuePopup->setInterceptsMouseClicks (false, false);
|
|
valuePopup->setColour (juce::Label::backgroundColourId, juce::Colours::transparentBlack);
|
|
valuePopup->setColour (juce::Label::textColourId, theme::text);
|
|
valuePopup->setJustificationType (juce::Justification::centred);
|
|
topLevel->addAndMakeVisible (valuePopup.get());
|
|
}
|
|
|
|
valuePopup->setFont (juce::Font (14.0f * scale, juce::Font::bold));
|
|
valuePopup->setText (getTextFromValue (getValue()), juce::dontSendNotification);
|
|
|
|
// Keep the readout inside the window so a knob near an edge still shows it.
|
|
const auto popupPos = topLevel->getLocalPoint (nullptr, e.getScreenPosition());
|
|
valuePopup->setBounds (juce::jlimit (0, juce::jmax (0, topLevel->getWidth() - w),
|
|
popupPos.getX() - w / 2),
|
|
juce::jlimit (0, juce::jmax (0, topLevel->getHeight() - h),
|
|
popupPos.getY() - h - gap),
|
|
w, h);
|
|
}
|
|
|
|
void Knob::hideValuePopup()
|
|
{
|
|
valuePopup.reset();
|
|
}
|
|
|
|
// A section switch can hide the view holding this knob mid-drag. The popup is a
|
|
// sibling of that view, so it has to be taken down here or it stays on screen.
|
|
void Knob::visibilityChanged()
|
|
{
|
|
if (! isVisible())
|
|
hideValuePopup();
|
|
}
|
|
|
|
} // namespace serum
|