How do i make a custom text box for a slider

hi! I’ve posted about this plugin before but didn’t have my client request this until now. the plugin is pretty simple. it’s a gain knob that goes from -100 decibels (basically complete silence) to +60 db.

the gui he requested is pretty simple but he wanted the textbox in the middle as well as a custom font. and colour for the text.
image
how would I go about doing this?

2 Likes

btw this is the current gui:
image

If you just need the text in the middle, then you can use the same Slider::LookAndFeelMethods::drawRotarySlider as you are already using for the purple arc. juce::Graphics has functions to draw text.

If you also need the label to be editable, that’s going to be a little more complex, as the hit-box of the slider & text-box would intersect. Which means, it’s unclear where mouse events should go. Slider::LookAndFeelMethods::getSliderLayout is the other functioon you should look at. It positions the slider & textbox.

Hope this helps.

ok so since it’s a in the LookAndFeel structure I’m guessing It’d be in the .cpp or .h file of where I have my lookandfeel defined, correct?

also I’m using LAFV4, if I also use v2 for the Slider::LookAndFeelMethods::getSliderLayout, would that mess up any code?

likely you have to come up with your own combination of label and slider thingie cause the label that is part of slider by design can only be placed next to it but not on top. if you wanna have a cool knob you need to functionality to click on the label to enter a text value tho, so that’s why you can’t just draw it on the knob with look and feel, despite it looking the same on the surface

so you’re saying that I’d have to rewrite some of my code to add the text box and make it editable?

To elaborate @tobante 's answer, it can be as easy as putting this in your custom LookAndFeel:

Slider::SliderLayout getSliderLayout (Slider& slider) override
{
    Slider::SliderLayout layout;
    layout.sliderBounds = slider.getLocalBounds();
    layout.textBoxBounds = slider.getLocalBounds().withSizeKeepingCentre (120, 24);
    return layout;
}

getSliderLayout is also in LAFV4, you don’t have to worry about it. Just override it as daniel suggests.

ok i figured that out, now for the custom font… which I am very confused on…

// create typeface pointer and store it somewhere
// probably as a member in your LookAndFeel
juce::Typeface::Ptr typeface = juce::Typeface::createSystemTypefaceFor(
    BinaryData::MyFont_ttf, 
    BinaryData::MyFont_ttfSize
);

// in paint function
g.setFont(juce::Font{typeface}.withHeight(12.0F));
g.drawText(...);

Override LookAndFeel::getLabelFont():

Tried to insert (add) this into my custom LookAndFeel.

This by itself produced no change.

Do I call getSliderLayout in my drawRotarySlider?

Also when I create my slider, do I need to specify something here;

slider.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 50, 15);

First make sure, that you didn’t override slider’s resized() method. In fact you shouldn’t inherit from Slider at all, but if you do, be very careful.

Here your getSliderLayout() is called:

If you tackle such things, always use full-text search for the method in question.
And next put a breakpoint in your getSliderLayout() to figure out, if it is even called.

Next thing is to use juce’s implementation as a reference:

You can see, it uses the information of TextBoxAbove, if you don’t reflect that in your getSliderLayout() you don’t need to set it (except NoTextBox might switch off visibility somewhere else, maybe lookAndFeelChanged(), this is left as an exercise for the reader :wink:

Ok thanks for that information.

Still do I specify a textbox or not when creating rotary slider?

slider.setTextBoxStyle (Slider::TextEntryBoxPosition::NoTextBox, 0, 0, 0);

or

slider.setTextBoxStyle (Slider::TextBoxBelow, true, 50, 15);

Can you describe what you want to achieve?
It’s hard to guess, since different people with different issues already contributed to this thread.

Well my synth’s sliders are 99% small knobs, in order to keep all parameters visible instead of hidden menus.

For some of these rotary knobs, it’s very difficult to dial in the needed exact value, so I need a way to be able to enter the value manually.

My knobs are highly customized with the value in the center of the small knob, and title/name below.

I understand it night be difficult to implement double clicking value to edit, as it is in the center of the knob, value barely fitting the knob.

If I implement a regular textbox below center, the knob becomes too small.

I thought of implementing a way to enlarge the knob area to about twice the size, when I hoover over it.

But I’m also ok with Ctrl-clicking knob to make a small component appear containing a text entry box, problem is I need to make the SliderValueChanged fire when I enter a value.

A few years you helped me with “MySlider” a way to open a component right-clicking a slider. That I could expand to also incorporate a component popping up when Ctrl-cliking. Here’s what I got;

/*
  ====================================================================

    MySlider.h
    Created: 1 Jan 2021 6:58:12pm
  ====================================================================
*/

#pragma once

#include <JuceHeader.h>

#include "ParameterEntryComponent.h"
#include "SetModifierTarget.h"

//====================================================================
class MySlider : public juce::Slider {
public:
    MySlider () = default;

    MySlider (const juce::String& componentName) : 
      juce::Slider (componentName) {}

    MySlider (juce::Slider::SliderStyle style, 
      juce::Slider::TextEntryBoxPosition textBoxPosition) : 
        juce::Slider (style, textBoxPosition) {}

    void mouseDown (const juce::MouseEvent& event) override
    {
        if (juce::ModifierKeys::currentModifiers.isRightButtonDown ())
        {
            modifierTargetPtr->setModifierTargetStatus 
              (Slider::getComponentID (), Slider::getTooltip ());
        }
        else if (juce::ModifierKeys::currentModifiers.isCtrlDown ())
        {
            parameterEntryPtr->setEntryTargetStatus 
              (Slider::getComponentID (), 
                Slider::getTooltip (), Slider::getValue ());
        }
        else
            juce::Slider::mouseDown (event);
    }

    void ParameterEntryComponentPtr (ParameterEntryComponent* ptr)
    {
        parameterEntryPtr = ptr;
    }

    void setModifierTargetComponentPtr (SetModifierTarget* ptr)
    {
        modifierTargetPtr = ptr;
    }

private:
    ParameterEntryComponent* parameterEntryPtr = nullptr;
    SetModifierTarget* modifierTargetPtr = nullptr;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MySlider)
};

However in the cpp file although I’m able to enter a value, it won’t go “back” to the slider and fire SliderValueChanged. Perhaps if I could do something like this;

parameterEntryPtr->setEntryTargetStatus (Slider&);

Instead of this;

parameterEntryPtr->setEntryTargetStatus (Slider::getComponentID (), 
  Slider::getTooltip (), Slider::getValue ());

Ok I figured out how to get it to work;

#pragma once

#include <JuceHeader.h>

#include "ParameterEntryComponent.h"
#include "SetModifierTarget.h"

//====================================================================
class MySlider : public juce::Slider {
public:
    MySlider () = default;

    MySlider (const juce::String& componentName) : 
      juce::Slider (componentName) {}

    MySlider (juce::Slider::SliderStyle style, 
      juce::Slider::TextEntryBoxPosition textBoxPosition)
        : juce::Slider (style, textBoxPosition) {}

    void mouseDown (const juce::MouseEvent& event) override
    {
        if (juce::ModifierKeys::currentModifiers.isRightButtonDown ())
        {
            modifierTargetPtr->setModifierTargetStatus 
              (Slider::getComponentID (), Slider::getTooltip ());
        }
        else if (juce::ModifierKeys::currentModifiers.isCtrlDown ())
        {
            sliderPtr = this;

            parameterEntryPtr->setEntryTargetStatus 
              (Slider::getComponentID (), Slider::getTooltip (), 
                Slider::getValue (), sliderPtr);
        }
        else
            juce::Slider::mouseDown (event);
    }

    void ParameterEntryComponentPtr (ParameterEntryComponent* ptr)
    {
        parameterEntryPtr = ptr;
    }

    void setModifierTargetComponentPtr (SetModifierTarget* ptr)
    {
        modifierTargetPtr = ptr;
    }

private:
    Slider* sliderPtr = nullptr;
    ParameterEntryComponent* parameterEntryPtr = nullptr;
    SetModifierTarget* modifierTargetPtr = nullptr;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MySlider)
};
#pragma once

#include <JuceHeader.h>

//====================================================================
/*
*/
class ParameterEntryComponent : public juce::Component,
    public Button::Listener, public Label::Listener
{
public:
    ParameterEntryComponent();
    ~ParameterEntryComponent() override;

    void setEntryTargetStatus (String id, String toolTip, double value, 
      Slider* ptr);

    void paint (juce::Graphics&) override;
    void resized() override;

    int appWidth = 100, appHeight = 10;
    int modifierTargetX = 100, modifierTargetY = 10;

private:
    void buttonClicked (Button* button) override;
    bool initializeObjects ();
    void labelTextChanged (Label* label) override;

    Slider* sliderPtr = nullptr;

    int windowWidth = 0;
    int windowHeight = 0;

    String targetSliderID; // Knob's or switch's ID
    String targetToolTip; // Knob's tooltip

    float titleFontSize = 0;

    bool componentConstructed = false;

    TextButton closeButton; // Close target window

    Label manualParamField;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ParameterEntryComponent)
};
#include "ParameterEntryComponent.h"

#include "globalVars.h"

//====================================================================
ParameterEntryComponent::ParameterEntryComponent()
{
}

ParameterEntryComponent::~ParameterEntryComponent()
{
}

//====================================================================
void ParameterEntryComponent::buttonClicked (Button* button)
{
    String id = button->getComponentID ();

    if (id == "Close") setVisible (false);
}

//====================================================================
bool ParameterEntryComponent::initializeObjects ()
{
    closeButton.setComponentID ("Close");
    closeButton.setColour (TextButton::buttonColourId, Colours::darkred);
    closeButton.setButtonText ("x");
    closeButton.getProperties ().set ("Label", 0);
    closeButton.addListener (this);
    addAndMakeVisible (closeButton);

    manualParamField.setComponentID ("Manual Entry Field");
    manualParamField.setColour (TextButton::textColourOffId, Colours::black);
    manualParamField.setColour (TextButton::buttonOnColourId, statusCS[15]);
    manualParamField.setColour (TextButton::textColourOnId, Colours::black);
    manualParamField.setTooltip ("Manual Parameter Entry Field\n\nEnter value and press 'Enter'.");
    manualParamField.setJustificationType (Justification::centred);
    manualParamField.setWantsKeyboardFocus (true);
    manualParamField.setEditable (true);
    addAndMakeVisible (manualParamField);
    manualParamField.addListener (this);

    return true;
}

void ParameterEntryComponent::setEntryTargetStatus (String id, String toolTip, double value, Slider* ptr)
{
    sliderPtr = ptr;

    auto& desktop = Desktop::getInstance ();
    auto mouseSource = desktop.getMainMouseSource ();

    auto mousePos = mouseSource.getScreenPosition ();
    int mouseX = roundToInt (mousePos.getX ());
    int mouseY = roundToInt (mousePos.getY ());

    auto window = getTopLevelComponent ();
    int appX = window->getScreenBounds ().getX ();
    int appY = window->getScreenBounds ().getY ();

    // Details Parameter?
    targetSliderID = id;
    targetSliderID = targetSliderID.replace (" - Strip", "", false);

    targetToolTip = toolTip;

    // Set window height
    // Title
    int height = window->getScreenBounds ().getHeight () / 4;

    setBounds (modifierTargetX, modifierTargetY, 
      window->getScreenBounds ().getWidth () / 7, height);

    // Move component position close to calling knob's position
    int x = mouseX - appX - windowWidth / 2;
    int y = mouseY - appY + 30;

    // Adjust x position if part exceeds app
    if (x < 0)
        x = 2;
    else if (x + windowWidth > appWidth)
        x = appWidth - windowWidth - 2;

    // Adjust y position if part exceeds app
    if (y < 0)
        y = 2;
    else if (y + windowHeight > appHeight)
        y = appHeight - windowHeight - 2;

    // Now save position in case app is resized while component is visible
    modifierTargetX = x;
    modifierTargetY = y;

    setBounds (modifierTargetX, modifierTargetY, windowWidth, windowHeight);
    setVisible (true);
    resized ();

    manualParamField.setText (String (value), dontSendNotification);
    manualParamField.showEditor ();
}

//====================================================================
void ParameterEntryComponent::labelTextChanged (Label* label)
{
    sliderPtr->setValue (label->getTextValue ().getValue ());
    manualParamField.showEditor ();
}

//====================================================================
void ParameterEntryComponent::paint (juce::Graphics& g)
{
    g.fillAll (innerColor.brighter (0.2f));

    // Border
    g.setColour (sectionTitleColorSelected);
    g.drawRect (0, 0, windowWidth, windowHeight);

    // Parameter title
    int yPos = roundToInt (titleFontSize);
    g.setColour (sectionTitleColorSelected);
    g.setFont (titleFontSize * 1.25f);
    String title = targetSliderID;
    title = title.replace ("TG ", "Tone Generator ", false);
    title = title.replace (" Engine ", " ", false);
    g.drawFittedText (title, windowWidth / 27, 0, 
      windowWidth - windowWidth / 9, yPos, 
      Justification::horizontallyCentred, 1, 0.5f);

    // Sub Title
    yPos += roundToInt (titleFontSize);
    g.setColour (sectionTitleColorSelected.darker (0.5f));
    g.setFont (titleFontSize);
    g.drawSingleLineText ("Manual Parameter Entry", 
      windowWidth / 2, yPos, Justification::horizontallyCentred);   

    yPos += roundToInt (titleFontSize * 6);
    g.setColour (sectionTitleColorSelected.darker (0.25f));
    g.setFont (titleFontSize * 0.85f);
    g.drawFittedText (targetToolTip, windowWidth / 27, yPos, 
      windowWidth - windowWidth / 9, roundToInt (titleFontSize * 3), 
      Justification::horizontallyCentred, 5, 0.5f);
}

void ParameterEntryComponent::resized()
{
    windowWidth = getWidth ();
    windowHeight = getHeight ();

    titleFontSize = float (windowWidth / 15);

    if (componentConstructed == false) componentConstructed = initializeObjects ();

    // Buttons and knobs
    int buttonSize = windowWidth / 15;
    closeButton.setBounds (windowWidth - 1 - buttonSize, 1, buttonSize, buttonSize);

    manualParamField.setBounds (roundToInt (windowWidth * 0.325f), 
      windowHeight / 6, windowWidth / 3, windowHeight / 6);
}

And in PlugInEditor reSized;

if (setEntryTargetComponent.modifierTargetX != setEntryTargetComponent.getScreenX ())
{
	float scale = float (setEntryTargetComponent.modifierTargetX) / float (setEntryTargetComponent.getScreenX ());
	setEntryTargetComponent.modifierTargetX = roundToInt (float (setEntryTargetComponent.modifierTargetX) / scale);
	setEntryTargetComponent.modifierTargetY = roundToInt (float (setEntryTargetComponent.modifierTargetY) / scale);
}
setEntryTargetComponent.setBounds (setEntryTargetComponent.modifierTargetX, setEntryTargetComponent.modifierTargetY, sectionGeneralPtr->doubleSectionMargin + roundToInt (sectionGeneralPtr->knobSizeXOffset[1] * 4.45f), sectionGeneralPtr->doubleSectionMargin + roundToInt (sectionGeneralPtr->knobSizeXOffset[1] * 4.5f));