Implementing a true fine mode for my sliders

I’m trying to implement a true fine mode for my sliders (which are actually just textboxes). I disabled velocity mode and kept two values of drag sensitivity, one for coarse and one for fine. On mouseDrag, I check the modifier keys and setMouseDragSensitivity accordingly. It works, but with a defect: if the mode is changed during a drag, the value jumps. This happens because the full drag extent has changed but the drag start position hasn’t. The only fix I could think of is calling mouseDown, which has the defect of starting a new gesture and a new undoable action:

void MySlider::setFineMode(bool fineMode)
{
    fineMode_ = fineMode;
    setMouseDragSensitivity(fineMode ? fineSens_ : coarseSens_);
}
void MySlider::mouseDown(const MouseEvent& event)
{
    setFineMode(event.mods.isAnyModifierKeyDown());
    Slider::mouseDown(event);
}
void MySlider::mouseDrag(const MouseEvent& event)
{
    if (fineMode_ != event.mods.isAnyModifierKeyDown())
    {
        setFineMode(!fineMode_);
        Slider::mouseDown(event);
    }
    else Slider::mouseDrag(event);
}

Has anyone found a solution that solves both issues without modifying Slider?

Well, this is my current solution. It’s a horrible hack, but it’s less horrible than fine tuning with velocity mode.

struct MySlider : public Slider
{
    // ...
    UndoManager* undoManager{}; // set somewhere
private:
    void setFineMode(bool fine);
    bool fine_{}, modeChanged_{};
    int sens_[2]{ 500, 5000 }; // say
};

MySlider::MySlider()
{
    // ...
    setVelocityModeParameters(1., 1, 0., false);
    onDragStart = [this]()
    {
        if (modeChanged_)
        {
            modeChanged_ = false;
            if (undoManager) undoManager->undo();
        }
    };
}

void MySlider::setFineMode(bool fine)
{
    setMouseDragSensitivity(sens_[fine_ = fine]);
}

void MySlider::mouseDown(const MouseEvent& event)
{
    setFineMode(event.mods.isAnyModifierKeyDown());
    Slider::mouseDown(event);
}

void MySlider::mouseDrag(const MouseEvent& event)
{
    if (fine_ != event.mods.isAnyModifierKeyDown())
    {
        setFineMode(!fine_);
        modeChanged_ = true;
        Slider::mouseDown(event);
    }
    else Slider::mouseDrag(event);
}

I keep undoManager as a pointer because some sliders are not attached. mouseDown sets the initial mode. If it changes while dragging, a flag is raised and Slider::mouseDown is called. If the slider is attached, this begins a new undo transaction (through sliderDragStarted in SliderAttachment::Pimpl). After that, onDragStart is called, which drops the flag and undoes the transaction. This works because


onDragStart is called after all listeners. This is brittle, but I can’t think of other way with all the relevant parts being hidden in pimpls.

JUCE Sliders do have a fine mode, if you hold down the Ctrl key.
But maybe that’s not what you are looking for?

Could someone take a cursory look to see if I’m doing something silly and this will explode at the fourth run? Many people asked for a true fine mode in other threads, I guessed someone may have tried something like this before.

Sorry, I wrote before your message. That would be velocity mode. The problem with velocity mode is that it depends on mouse speed. If you move too little, it stalls. Then you move faster and it runs. The use case would be: you want to slide from 0 to 51.4, so you coarse drag to 50, then fine drag to 51.4. Both drags should be absolute, but with different sensitivities. The problem is that absolute drags are computed with respect to the drag start point, which is set is mouseDown. So you have to call mouseDown after changing the sensitivity, but it also triggers a new undo transaction. You end up with many transactions for a single drag, one for every mode change.

We managed to implement the same thing by modify Slider::setMouseDragSensitivity to the following:

void Slider::setMouseDragSensitivity (int distanceForFullScaleDrag)
{
    jassert (distanceForFullScaleDrag > 0);

    pimpl->pixelsForFullDragExtent = distanceForFullScaleDrag;

    if (pimpl->currentDrag != nullptr)
    {
        pimpl->mouseDragStartPos = pimpl->mousePosWhenLastDragged;
        pimpl->valueOnMouseDown = pimpl->valueWhenLastDragged;
    }
}

And only calling that function when the modifiers, and therefore the sensitivity you want to use, changes. With this you won’t need to manually call Slider::mouseDown in MySlider::mouseDrag. You should also remove the else statement from MySlider::mouseDrag.

2 Likes

I was trying to avoid modifying Slider, but it looks like the reasonable thing to do, it’s just a simple mod. I think I’ll go with it. Thanks

Just for reference, I’ve changed the edit a bit with the corresponding lines from Pimpl::mouseDown:

if (pimpl->currentDrag != nullptr)
{
    pimpl->mouseDragStartPos = pimpl->mousePosWhenLastDragged;
    pimpl->valueWhenLastDragged = (pimpl->sliderBeingDragged == 2 ? pimpl->valueMax
                                : (pimpl->sliderBeingDragged == 1 ? pimpl->valueMin
                                                                  : pimpl->currentValue)).getValue();
    pimpl->valueOnMouseDown = pimpl->valueWhenLastDragged;
}

This is necessary in my case because I’m using snapValue(), so valueWhenLastDragged != currentValue.

2 Likes