steinbergmedia / vstgui

A user interface toolkit mainly for audio plug-ins
Other
880 stars 124 forks source link

CAnimKnob not receiving mouse button release in some hosts #284

Closed mxa closed 1 year ago

mxa commented 1 year ago

Our project works on Mac but on Linux the mouse button release is never received resulting in unexpected behaviour of the GUI.

scheffle commented 1 year ago

@mxa , can you provide a little bit more information, which kind of project (standalone, vst3), which host, etc... Thank you

clwe commented 1 year ago

Hello @scheffle,

We build a VST3, the issue was tested with Bitwig Studio 4.3.10 on Linux. I subclassed CAnimKnob and implemented the mouse functions as follows:

namespace VSTGUI {

    CKickButtonHover::CKickButtonHover(const VSTGUI::CRect& size, IControlListener* listener, int32_t tag, CBitmap* background, CBitmap* triggerBitmap, const CPoint& offset)
    : CAnimKnob(size, listener, tag, background, offset)
    {}

    CMouseEventResult CKickButtonHover::onMouseDown (CPoint& where, const CButtonState& buttons)
    {
        LOG_DEBUG("mouse down on trigger view \n");
        if (!(buttons & kLButton))
            return kMouseEventNotHandled;

        value = 1.0;
        if (isDirty ())
        {
            invalid ();
            valueChanged ();
        }
        beginEdit ();
        return onMouseMoved(where, buttons);
    }

    CMouseEventResult CKickButtonHover::onMouseUp (CPoint& where, const CButtonState& buttons)
    {
        LOG_DEBUG("mouse up on trigger view \n");
        if (!(buttons & kLButton))
            return kMouseEventNotHandled;

        value = 0.5;
        if (isDirty ())
        {
            invalid ();
            valueChanged ();
        }
        endEdit ();
        return kMouseEventHandled;
    }

    CMouseEventResult CKickButtonHover::onMouseEntered (CPoint& where, const CButtonState& buttons)
    {
        LOG_DEBUG("mouse entered trigger view \n");
        value = 0.5;
        if (isDirty ())
        {
            invalid ();
            valueChanged ();
        }

        return kMouseEventHandled;
    }

    CMouseEventResult CKickButtonHover::onMouseExited (CPoint& where, const CButtonState& buttons)
    {
        LOG_DEBUG("mouse exited trigger view \n");
        value = 0.0;
        if (isDirty ())
        {
            invalid ();
            valueChanged ();
        }

        return kMouseEventHandled;
    }

    bool CKickButtonHover::onWheel (const CPoint& where, const CMouseWheelAxis& axis, const float &distance, const CButtonState &buttons)
    {   
        return false;
    }
}

I just re-tested after changes to the order of isDirty(), invalid() and valueChanged() and the mouse up event is correctly received. But the value of 1.0 of this event somehow doesn't get properly received in the GUI.

scheffle commented 1 year ago

Hi @clwe , do you get mouse ups on normal CAnimKnobs?

clwe commented 1 year ago

Sorry, the edit of my comment from yesterday didn't get saved. As stated above:

I just re-tested after changes to the order of isDirty(), invalid() and valueChanged() and the mouse up event is correctly received. But the value of 1.0 of this event somehow doesn't get properly received in the GUI.

The problem might to be somewhere else, not in this part of the code. The strange thing is that this code works perfectly fine on Mac OS and Windows. We implement a hovering function with CAnimknob with three bitmaps:

clwe commented 1 year ago

After more testing I found that this problem is not linux related at all - but is host specific: It happens in Reaper and Bitwig, but the problem does not show in Ableton Live. My gut feeling says, it could be something related with the automation recording or state saving capabilities of the host(?)

scheffle commented 1 year ago

@clwe , it still is interesting if unmodified CAnimKnobs work in those hosts. Error analysis will be much easier if this is known.

clwe commented 1 year ago

Yes, we use several animation knobs as shipped with the sdk and they work as expected in all the mentioned DAWs (but do not implement hovering by default).

scheffle commented 1 year ago

OK, just reread your stuff. You use the CControl::value member in your subclass to select bitmaps. Don't do this, add your own member variable to do this.

clwe commented 1 year ago

OK. How do I inform the host, that my member variable changed?

scheffle commented 1 year ago

Why does the host need to know that you have switched bitmaps?

scheffle commented 1 year ago

Should your subclass still be used as a knob?

clwe commented 1 year ago

You are right, the host doesn't need to know. But the UI needs to update the bitmap according to my member variable somehow. (I don't know the inner workings of CAnimknob.) It's used as a trigger button actually. It has three states as mentioned above.

scheffle commented 1 year ago

Whenever you want to change the bitmap, you call invalid() and set your own class member to the index depending on the situation. Your draw methods will be called a little bit later where you then draw the bitmap according to the index you previously have set in your class member.

clwe commented 1 year ago

Aha, thanks! So I would have to re-implement this function:

void CAnimKnob::draw (CDrawContext *pContext)
{
    if (getDrawBackground ())
    {
        CPoint where (0, 0);
        float val = getValueNormalized ();
        if (val >= 0.f && heightOfOneImage > 0.)
        {
            CCoord tmp = heightOfOneImage * (getNumSubPixmaps () - 1);
            if (bInverseBitmap)
                where.y = floor ((1. - val) * tmp);
            else
                where.y = floor (val * tmp);
            where.y -= (int32_t)where.y % (int32_t)heightOfOneImage;
        }

        getDrawBackground ()->draw (pContext, getViewSize (), where);
    }
    setDirty (false);
}

What is the index you were referring to? How do I add my class to the index? Also, do I have to implement setDirty() as well?

clwe commented 1 year ago

Hmm. I changed the code slightly and don't call onMouseMoved() anymore. I also call beginEdit() and endEdit() directly in sequence and it works! The graphics get displayed correctly now.

CMouseEventResult CKickButtonHover::onMouseDown (CPoint& where, const CButtonState& buttons)
    {
        if (!(buttons & kLButton))
            return kMouseEventNotHandled;

        value = 1.0;
        if (isDirty ())
        {
            invalid ();
            valueChanged ();
        }
        beginEdit ();
        endEdit ();
        return kMouseEventHandled; // onMouseMoved(where, buttons);
    }

    CMouseEventResult CKickButtonHover::onMouseUp (CPoint& where, const CButtonState& buttons)
    {
        if (!(buttons & kLButton))
            return kMouseEventNotHandled;

        value = 0.5;
        if (isDirty ())
        {
            invalid ();
            valueChanged ();
        }
        return kMouseEventHandled;
    }

Is there any side effect that I don't know of?

clwe commented 1 year ago

 Well, not exactly. If I press down and drag the mouse, then the bitmap gets stuck in the pressed state (value=1) until you press or exit the hover area. So I guess I will need to go the longer way and re-implement the methods.

scheffle commented 1 year ago

Hi, I think it is not a good idea to inherit from a control class which implements a special behavior like the CAnimKnob and implement another behavior into it like a kick button as in your case. I've drafted a version of what you want and how I would implement it:

class CKickButtonHover : public CControl
{
public:
    CKickButtonHover (const CRect& r, IControlListener* l, int32_t tag) : CControl (r, l, tag) { std::fill (bitmaps.begin (), bitmaps.end (), nullptr); }

    void setBitmaps (CBitmap* inactive, CBitmap* hover, CBitmap* clicked)
    {
        bitmaps[UIState::Inactive] = inactive;
        bitmaps[UIState::Hover] = hover;
        bitmaps[UIState::Clicked] = clicked;
    }

    void draw (CDrawContext* context) override
    {
        if (auto bitmap = bitmaps[state])
            bitmap->draw (context, getViewSize ());
        setDirty (false);
    }

    void onMouseDownEvent (MouseDownEvent& event) override
    {
        beginEdit ();
        setValue (1.);
        valueChanged ();

        state = UIState::Clicked;
        invalid ();
        event.consumed = true;
    }
    void onMouseUpEvent (MouseUpEvent& event) override
    {
        setValue (0.);
        valueChanged ();
        endEdit ();
        if (getViewSize().pointInside(event.mousePosition))
            state = UIState::Hover;
        else
            state = UIState::Inactive;
        invalid ();
        event.consumed = true;
    }
    void onMouseCancelEvent (MouseCancelEvent& event) override
    {
        if (isEditing ())
            endEdit ();
        state = UIState::Inactive;
        invalid ();
    }
    void onMouseEnterEvent (MouseEnterEvent& event) override
    {
        if (isEditing ())
            return;
        state = UIState::Hover;
        invalid ();
    }
    void onMouseExitEvent (MouseExitEvent& event) override
    {
        if (isEditing ())
            return;
        state = UIState::Inactive;
        invalid ();
    }

    CLASS_METHODS_NOCOPY (CKickButtonHover, CControl)
private:
    enum UIState
    {
        Inactive,
        Hover,
        Clicked,

        Count
    };

    UIState state {UIState::Inactive};
    std::array<SharedPointer<CBitmap>, UIState::Count> bitmaps;
};
mxa commented 1 year ago

It would be nice to have usable examples for these basic things. My snarky comment from 2 years ago still holds.

scheffle commented 1 year ago

VSTGUI is open source, it lives from the participation of its users. When no one provides "usable examples", then there are no "usable examples". And it's always discussable what "usable" actually means. Can we close this issue now, or are you still have issue with mouse up events?

clwe commented 1 year ago

Thanks a lot for your time and help! I haven't found the time yet to implement it as above, will do ASAP. The issue can be closed - mouse up events are received as advertised!