Combine Slider with Button

Hi all!

I’d like to create something like a clickable Slider bar.
Or a TextButton representing a parameter’s state plus value.
I’m not talking about dis-/enabling the Slider.
I want to de/activate a property being able to see the Value.
Similar to pressing the cap of a rotary encoder.

Many Attempts keep me struggling:

  • take a Component with a Slider and TextButton as Children? How on earth should I set the different setInterceptsMouseClicks() routines?
  • same, and trigger their mouseDown’s and Up’s? Can’t access those of the Button!
  • derive from Slider or Button? no mouse action on the other as well, reinventing the other half of the wheel…

Other posts I found didn’t match my needs..
I hope somebody gets my needs and please can head me to some useful direction?

Thanks!!

This sounds like the kind of situation where I would build a custom component from scratch. I have found that trying to merge or overly customize the pre-existing components for specific behaviour leads to more problems than it solves.

3 Likes

Phew, really? :smile:
I was hoping something like an encoder for i.e. simultaneous volume/mute would have been there before…

Can you give a screenshot/mockup of the sort of component you mean?

I feel like inheritting from juce::Slider and adding some additional MouseEvent checks for clicks could do what you need.

Something to consider though is accessibility - would someone using a screenreader understand how to use that component? Especially if you go the route of writing a custom component altogether - you’d want to make sure you also write a suitable juce::AccessibilityHandler and keyboard shortcuts etc. to get the same control as using the mouse.

I need it to enhance an environment that already handles with Buttons, so it’d be nice to derive from the Button class…
But I’ll try implementing some drag funcionality.

Actually I’d be fine if both Slider and TextButton would react on mouse events.
But however I use setInterceptsMouseClicks(), only one of them is useable…

Or is this not the function to use?

Yeah that sounds right.

Inherit from juce::Slider and override mouseUp? Something like this:

void mouseUp (const MouseEvent& event) override
{
    if (event.getDistanceFromDragStart() == 0)
        // toggle property or whatever
    else
        Slider::mouseUp (event);
}

You might of course want to experiment with that getDistanceFromDragStart threshold, and allow for a slight amount of wiggle when the user clicks on it.

I understand the dilemma. Do you extend, or do you re-compose?

I had to do this with a project which needed an endless slider - kind of like a mod wheel, only infinite/unconstrained, and I wanted to render an image according to the condition, mimicking real hardware rotary knobs perpendicular to the users perspective.

I started with juce::Slider, since it was the ‘closest’ to what I needed, and then I extended it (note however, that I also intended to make it somewhat useful as a class in a non-JUCE UI framework - thus the callbacks instead of paramChanged messages).

This is not necessarily the ‘right’ way to do it - but it is a functional example for how you can get started with a juce:: UI component, and transmogrify it into strange and interesting territory.

Apropos having “standard DAW” controls, JUCE does have most of the standard controls, you may find, in the DemoRunner - they’re just not pretty and obvious at first glance as being the basic UI elements for a lot of much prettier apps/plugins out there. And of course, there are budding 3rd-party JUCE UI control libraries out there, commercial ones, even .. to consider.

#ifndef EndlessSlider_h
#define EndlessSlider_h

#include "MainLookAndFeel.h"

class EndlessSlider : public Slider {
public:
    EndlessSlider () :
    Slider()
    {
        currentMoved = 0;
        lastMoved = 0;
        prevMoved = 0;
        lastFilledElem = 17;
    }

    ~EndlessSlider() override {}

    // set these callbacks where you use this class in order to get inc/dec messages
    std::function<void()> sliderValueSet;
    std::function<void()> sliderReset;
    
    // calculate whether to callback to an increment or decrement, and update UI
    void mouseDrag(const MouseEvent &e) override
    {
        if (e.mouseWasDraggedSinceMouseDown())
        {
            currentMoved = e.getDistanceFromDragStartY() * 1.0f;
            lastMoved = currentMoved + prevMoved;
            sliderValue = jmap(lastMoved, static_cast<float>(proportionOfHeight(0.48f)), 
                              (-1)*static_cast<float>(proportionOfHeight(0.52f)), 
                              -0.5f, 1.f);
            sliderValueSet();
            lastMovedPoportion = (lastMoved / (getHeight() * 1.0f));
            repaint();
        }
    }

    void mouseWheelMove(const MouseEvent& event, const MouseWheelDetails& wheel) override
    {
        (void)event;
        currentMoved = -10*wheel.deltaY;
        lastMoved = currentMoved + prevMoved;
        sliderValue = jmap(lastMoved, static_cast<float>(proportionOfHeight(0.48f)),
            (-1) * static_cast<float>(proportionOfHeight(0.52f)),
            -0.5f, 1.f);
        sliderValueSet();
        lastMovedPoportion = static_cast<float>(lastMoved / getHeight());
        prevMoved = lastMoved;
        repaint();
    }

    void paint (Graphics&g) override
    {
        g.fillAll(Colours::black);

        Rectangle<float> bounds = getLocalBounds().toFloat();
        float height = bounds.getHeight();
        int numElem = 34;
        float spaceBetween = height / static_cast<float>(numElem);
        float y = lastMoved;
        int mappedY = 0;
        int elemWidth = 0;
        int counter = 0;
        int r = static_cast<int>(sqrt(((height * height)) / 2)); // circle radius

        ColourGradient cg = ColourGradient(mainLaF.trimSliderMainColor, 
                                           bounds.getWidth() / 2, 
                                           bounds.getHeight() / 2, 
                                           Colours::black, 
                                           bounds.getWidth() / 2, 0, true);
        g.setGradientFill(cg);
        g.fillRect(bounds.reduced(5, 5));

        for (int i = 0; i < numElem; i++)
        {
            if (i == 0)
            {
                y += spaceBetween/2.f; // place first element
            }
            else
            {
                y += spaceBetween;
            }
            // calculate y when mouse out of component
            if (y > height)
            {
                counter = static_cast<int>(std::abs(y / height));
                y -= height *counter;
            }
            else if (y < 0)
            {
                counter = static_cast<int>(std::abs(y / height) + 1);
                y += height * counter;
            }
            // calculate y when mousePos in component
            if (y < height /2)
            {
                mappedY = static_cast<int>((-1) * (height / 2) + y);
            }
            else if (juce::approximatelyEqual(y, height / 2))
            {
                mappedY = 0;
            }
            else if (y > height / 2)
            {
                mappedY = static_cast<int>(y - height / 2);
            }
            // calculate width change with circle equation
            elemWidth = static_cast<int>(sqrt(r*r - (mappedY*mappedY)));

            auto rect = Rectangle<float>(bounds.getWidth() * 0.22f,
                y - (elemWidth / (numElem * 2)) / 2.0f,
                bounds.getWidth() * 0.55f,
                elemWidth / (numElem * 2.0f));

            if (i == lastFilledElem)
                filledRect = rect;
            else
            {
                g.setColour(Colours::black);
                g.fillRoundedRectangle(rect, 2.f);
            }

        }
        g.setColour(Colours::grey);
        g.fillRoundedRectangle(filledRect, 2.f);
    }

    void mouseExit (const MouseEvent& e) override
    {
        (void)e;
        repaint();
    }

    void mouseDoubleClick(const MouseEvent& e) override
    {
        (void)e;
        lastMoved = 0;
        prevMoved = 0;
        sliderReset();
        repaint();
    }

    void mouseUp(const MouseEvent& e) override
    {
        (void)e;
        prevMoved = lastMoved;
    }

    void resized() override
    {
       if (!juce::approximatelyEqual(lastMoved, 0.0f))
       {
           lastMoved = proportionOfHeight(lastMovedPoportion) * 1.0f;
           prevMoved = lastMoved;
       }
       repaint();
    }

    float getCurrentSliderValue()
    {
        // Set precision 0.00 for sliderValue
        sliderValue = std::round(sliderValue * 100.f) / 100.f;
        return sliderValue;
    }

private:
    float lastMoved;
    float currentMoved;
    int lastFilledElem;
    float prevMoved;

//    bool dragStarted;
//    bool isMouseUp;
    float lastMovedPoportion = 0;

    float sliderValue;
    Rectangle<float> filledRect;
    MainLookAndFeel mainLaF;
};

#endif /* EndlessSlider_h */

EDIT: To your specific bullet points, you’d compose an outer ‘container’ component which has all the children embedded in it, and maintain the state by way of associated parameters in your AVTS .. the UI components don’t have to be clever, they just have to know how to render the parameters - put all the state logic in a parameter that you process elsewhere …

Sorry for my late reply, must have overseen the notification…

Yes, I tried this last approach with Button and Slider living on a third “mother”-component.
It actually represents my first idea (which makes me feel on the right path).
The slider in front of the button could be transparent, but the problem here:
The children eat those mouse messages, regardless how setInterceptsMouseClicks() is called.
Unless I use it the wrong way…?
So, again, I can either slide or toggle

You could possibly set both the slider and button to ignore mouse events (setInterceptsMouseClicks(false)), then in the parent component overrride the mouse event callbacks you need (mouseDown(), mouseUp(), etc.) and forward the events to both components.

Correct, there’s not a way (at least, not an easy/graceful way) to make two Components overlap, and have them both get the same mouse events.

Which is why, if you scroll up a few replies, I suggested using one Component, overriding mouseUp, and using that to check the value of event.getDistanceFromDragStart().

After all, what is the difference between a Slider and a Button, in terms of the user interaction via a mouse? With a Button, the user clicks the mouse, doesn’t drag the mouse, and then releases the mouse. With a Slider, the user clicks the mouse, drags the mouse, and then releases the mouse.

By checking MouseEvent::getDistanceFromDragStart you should be able to differentiate between these two types of interaction, using a single Component.

Here’s a potential method to solve the dilemma - use a class like this, have the parent set "shouldConsume-* to true, and any children it contains, to false:

class SharedMouseComponent : public juce::Component
{
public:
    void mouseDown(const juce::MouseEvent& e) override
    {
        if (!shouldConsumeMouseDown(e))
        {
            // Pass to parent by calling base implementation
            Component::mouseDown(e);
            return;
        }
        
        // Handle locally
        onMouseDown(e);
    }

    void mouseMove(const juce::MouseEvent& e) override
    {
        if (!shouldConsumeMouseMove(e))
        {
            Component::mouseMove(e);
            return;
        }
        
        onMouseMove(e);
    }

protected:
    virtual bool shouldConsumeMouseDown(const juce::MouseEvent& e) = 0;
    virtual bool shouldConsumeMouseMove(const juce::MouseEvent& e) = 0;
    virtual void onMouseDown(const juce::MouseEvent& e) {}
    virtual void onMouseMove(const juce::MouseEvent& e) {}
};

The fundamental issue is that the events are getting consumed by the Child components when they should be propagating back down to the Parent ..

Seems like a convoluted alternative to just calling setInterceptsMouseClicks (false, false) on the two child components.

That issue is easily solved. But once the mouse events are passed up to the parent component, then what? See my posts above about how to differentiate between click vs click and drag actions.

And further, for the OP’s case, where they want a “clickable Slider bar”, it begs the question of why use separate child components at all. They don’t want two different components drawn on screen, they want one component that will respond differently to two different types of mouse action.

I have tested and used the approach that I posted above, it is not just a theoretical solution.

OK, many thanks for the input,I’ll give it a go! :+1:

But actually I don’t get why a Slider’s mouseDown() may be triggered externally, while the the TextButton’s may not (same to most mouse functions).
I think they all should be at least protected…

Generally the answer to those sorts of questions is simply because that’s how it’s always been and changing it now would likely cause lots of breaking changes so it’s best to leave as-is.

I’d also recommend caution with methods like those that aren’t defined by the base class directly. The mouse event callbacks come from juce::MouseListener which juce::Component inherits from. I’d suggest having a separate MyMouseListener that inherits from juce::MouseListener and listens to the components you need so you can avoid altering their existing behaviour.

1 Like

Can you explain the reason behind this caveat more?

And if we’re talking about Slider as a base class for the OP’s custom Component, then Slider::mouseDown and its other mouse event methods are defined by the base class directly.

I would just generally prefer to avoid modifying the behaviour of a base class when I can solve the problem in other ways, especially more complex classes like juce::Slider that have quite niche behaviour. I’ve just had one too many cases where I change the base class’s behaviour to solve one problem, but introduce another problem in doing so and end up in wack-a-mole mode.

1 Like

An improvement would be to use mouseWasDraggedSinceMouseDown()

The reason why buttons react to mouseUp rather than mouseDown is, because only then you know, if the user was dragging or only clicked.

You can also duplicate the mouse events (but might add another can of worms) by adding a mouseListener to the front most component.

Be aware, that any Component is already a mouseListener.

2 Likes

For many cases, I agree. But I thought it would be more instructional to get the OP thinking about using “distance from drag start” to distinguish between user actions, rather than move right to the higher level (gesture-aware) mouseWasDraggedSinceMouseDown.

Plus mouseWasDraggedSinceMouseDown brings a timing element into play, i.e. if you hold the mouse down for more than 300 ms, it will return true even if you didn’t move the mouse at all. So again, for someone learning how to use mouse events, I thought it better to stick with the plainer stuff.

I will throw in one tweak to the code I posted above – instead of using getDistanceFromDragStart, it would be more correct to use either getDistanceFromDragStartX or getDistanceFromDragStartY, depending on whether the slider is horizontal or vertical in orientation.

I made It!

And after extracting all parts regarding my usecase (added sophisticated DragAndDrop’ping :smiley: ), the more or less tidied code even looks less complex than expected.

As expected I ended up with a new Component class containing an alpha’d TextButton and a Slider as Members, forwarding the mouse events.
To look nice, it took a custom LookAndFeel, because by default Buttons have rounded corners but SliderBoxes don’t.

Fingers crossed I find the time to head it to gitHub…

Thanks for your support, folks!

@Benicz Could you share your code here?