Reversible Slider


#1

Hi,

I am trying to implement a reversible slider adapter, using the Slider::proportionOfLengthToValue and Slider::valueToProportionOfLength methods.

When reversed, the slider should slide from max to min instead of min to max.

I only managed to get this working with rotary sliders, though.
Linear sliders behave quite strangely on extreme values: they jump back to the other side.

Is there something I missed there?

template < class ConcreteSlider = Slider >
class ReversibleSlider : public ConcreteSlider
{
public:
    using ConcreteSlider::getMinimum;
    using ConcreteSlider::getMaximum;
    using ConcreteSlider::repaint;

public:
    ReversibleSlider (const String& componentName) :
      Slider(componentName),
          reversed(false)
      {}

      ~ ReversibleSlider () {}

public:
    void setReverse (bool shouldBeReversed)
    {
        if (reversed != shouldBeReversed)
        {
            reversed = shouldBeReversed;
            repaint();
        }
    }

public: // Slider
    double proportionOfLengthToValue (double proportion)
    {
        double ret = 0;
        if (reversed)
            ret = getMaximum() + getMinimum() - ConcreteSlider::proportionOfLengthToValue(proportion);
        else
            ret = ConcreteSlider::proportionOfLengthToValue(proportion);

        Logger::outputDebugPrintf(T("proportionOfLengthToValue(%f) == %f"), proportion, ret);
        return ret;
    }

    double valueToProportionOfLength (double value)
    {
        double ret = 0;
        if (reversed)
            ret = jlimit(0., 1., 1.0 - ConcreteSlider::valueToProportionOfLength(value));                
        else
            ret = ConcreteSlider::valueToProportionOfLength(value);

        Logger::outputDebugPrintf(T("valueToProportionOfLength(%f) == %f"), value, ret);
        return ret;
    }

private:
    bool reversed;
};

class ReversibleSliderTestComponent :
    public Component,
    public ButtonListener,
    public ComboBoxListener,
    public SliderListener
{
public:
    ReversibleSliderTestComponent ()
    {
        addAndMakeVisible(reverseButton = new ToggleButton("Reversed"));
        reverseButton->addButtonListener(this);

        addAndMakeVisible(styleComboBox = new ComboBox("styleComboBox"));
        styleComboBox->addListener(this);

        struct StyleData
        {
            Slider::SliderStyle id;
            const char* name;
        }
        styles [] =
        {
            { Slider::LinearHorizontal, "LinearHorizontal" },
            { Slider::LinearVertical, "LinearVertical" }, 
            { Slider::LinearBar, "LinearBar" }, 
            { Slider::Rotary, "Rotary" },
            { Slider::RotaryHorizontalDrag, "RotaryHorizontalDrag" },
            { Slider::RotaryVerticalDrag, "RotaryVerticalDrag" },
            { Slider::IncDecButtons, "IncDecButtons" },
            { Slider::TwoValueHorizontal, "TwoValueHorizontal" },
            { Slider::TwoValueVertical, "TwoValueVertical" },
            { Slider::ThreeValueHorizontal, "ThreeValueHorizontal" },
            { Slider::ThreeValueVertical, "ThreeValueVertical" }
        };
        size_t stylesSize = sizeof(styles) / sizeof(StyleData);

        for (size_t i = 0; i < stylesSize; ++i)
            styleComboBox->addItem(styles[i].name, toId(styles[i].id));

        addAndMakeVisible(reversibleSlider = new ReversibleSlider<>("reversibleSlider"));
        reversibleSlider->setSliderStyle(Slider::Rotary);
        reversibleSlider->addListener(this);

    }

public: // Component
    virtual void resized ()
    {
        reverseButton->setBoundsRelative(0.f, 0.f, 0.5f, 0.5f);
        styleComboBox->setBoundsRelative(0.5f, 0.f, 0.5f, 0.5f);
        reversibleSlider->setBoundsRelative(0.f, 0.5f, 1.f, 0.5f);
    }

public: // ButtonListener
    virtual void buttonClicked(Button* button)
    {
        if (button == reverseButton)
            reversibleSlider->setReverse(reverseButton->getToggleState());
    }

public: // ComboBoxListener
     virtual void comboBoxChanged (ComboBox* comboBox)
     {
         if (comboBox = styleComboBox)
             reversibleSlider->setSliderStyle(toSliderStyle(styleComboBox->getSelectedId()));
     }

public: // SliderListener
    virtual void sliderValueChanged(Slider* slider)
    {
        if (slider == reversibleSlider)
            Logger::outputDebugString(String("Slider: ") << slider->getValue());
    }

private:
    static int toId (Slider::SliderStyle style)
    {
        return style + 1;
    }

    static Slider::SliderStyle toSliderStyle (int id)
    {
        return static_cast<Slider::SliderStyle>(id - 1);
    }

private:
    ToggleButton* reverseButton;
    ComboBox* styleComboBox;
    ReversibleSlider<Slider>* reversibleSlider;
};

#2

Hmm - yes, I guess that would cause a few problems! The code kind of expects the maximum to be the value at the top end, and the minimum the one at the bottom end. It all gets very confusing if these are swapped, and I’m not sure what would be the best way to make it cope with that…


#3

I managed to make it work. It requires the following changes in Slider::getLinearSliderPos (const double) :

--- juce_Slider.1.43.cpp	Wed Jul 25 10:33:42 2007
+++ juce_Slider.cpp	Wed Jul 25 10:29:05 2007
@@ -657,15 +657,15 @@
 {
     double sliderPosProportional;
 
     if (maximum > minimum)
     {
-        if (value <= minimum)
+        if (value < minimum)
         {
             sliderPosProportional = 0.0;
         }
-        else if (value >= maximum)
+        else if (value > maximum)
         {
             sliderPosProportional = 1.0;
         }
         else
         {

#4

Is that all?! I’m surprised it’s not messier than that!


#5

Well, it’s all that’s required to get the drawing correct and suppress the jumping.

But I also spotted some weirdness in the mouse wheel handling.

Here’s the fix I used in Slider::mouseWheelMove():

@@ -1257,14 +1265,16 @@
 
         const double proportionDelta = (wheelIncrementX != 0 ? -wheelIncrementX : wheelIncrementY) * 0.15f;
         const double currentPos = valueToProportionOfLength (currentValue);
         const double newValue = proportionOfLengthToValue (jlimit (0.0, 1.0, currentPos + proportionDelta));
 
-        double delta = jmax (fabs (newValue - currentValue), interval);
+        double delta = 0.;
 
-        if (proportionDelta < 0)
-            delta = -delta;
+        if (currentValue < newValue)
+            delta = jmax (newValue - currentValue, interval);
+        else if (currentValue > newValue)
+            delta = -jmax (currentValue - newValue, interval);
 
         sendDragStart();
         setValue (snapValue (currentValue + delta, false), true, true);
         sendDragEnd();
     }

Are there any other Slider aspects to watch for?


#6

Nice one, thanks. Can’t think of any other places that’d give trouble, but the slider class is a complex beastie.


#7

BTW, I guess this works as well?

[code] double delta = jmax (fabs (newValue - currentValue), interval);

    if (currentValue > newValue)
        delta = -delta;

[/code]


#8

It almost works :slight_smile:

It doesn’t properly handle the case where currentValue == newValue and interval is non-zero. This can happen when the slider is stuck at min and the wheel tries to move it further down.

This causes a strange oscillation between min and (min + interval).


#9

ok, so

[code] double delta = (newValue != currentValue)
? jmax (fabs (newValue - currentValue), interval) : 0;

[/code]


#10

Works a charm.

Thanks :slight_smile:


#11