[FR] Buttons: add option to ignore clicks with right mouse button

What I wish to do is have a Button display a context menu when right-clicked.

A MouseListener attached to it takes care of displaying the PopupMenu just fine.

The problem is that the Button still receives its “click” event when the right mouse button acts upon it, which is something I don’t want in my use case.

Would it be possible to add an option to the base Button class to avoid that?

EDIT: if it is of any help, this is the patch with which I have hardcoded that behavior (left-click only) in my local JUCE repo. It is provided only to show the sole three lines that would require intervention: left-click-only.txt (1.9 KB)

2 Likes

+1!

+1.
I had to write a subclass just for this purpose.

Surely that’s why we have the other clicked() method that provides the modifier keys?

https://docs.juce.com/develop/classButton.html#af57cd4eec484491dc536e05d75e556dd

I’m not sure you understood our use case Jules. here is a example :

you have a large component that contains many children buttons of all kind (textButtons, toggleButtons, etc…)
you want the user to be able to right click anywhere on that parent component to open a contextual menu.
at the moment, if the right click happens over a button, you will get the contextual menu, but the underlying button will also get clicked (and we don’t want that)

so we were more looking for something like a Button::setIgnoreRightClicks() or an additional flag to setInterceptClicks(). for now we have to inherits the buttons class.

I ended up doing something like that :

template <typename ButtonType>
struct ButtonIgnoringRightClick : public ButtonType
{
    using ButtonType::ButtonType;

    void mouseDown (const MouseEvent& e) override
    {
        if (e.mods.isPopupMenu())
            return;

        ButtonType::mouseDown (e);
    }

    void mouseUp (const MouseEvent& e) override
    {
        if (e.mods.isPopupMenu())
            return;

        ButtonType::mouseUp (e);
    }
};

ButtonIgnoringRightClick<TextButton> myTextButton;

1 Like

What about things like HyperlinkButton that open the link with a right click? Or ToggleButtons that change state with a right click? Neither of those make any sense from a UX perspective. Of course it’s not the end of the world to sub-class, but equally would it really be the end of the world to implement something like Button::setIgnoreRightClicks() or similar?
Unless I missed something about those particular examples that can stop them behaving in that odd manner.

I solved this with a custom class derived by ToggleButton that just ignores the right click:

class LeftOnlyToggleButton : public ToggleButton
{
public:
	LeftOnlyToggleButton(String name) : ToggleButton(name)
	{
	}

	// Mouse click action takes place when the mouse button is released
	void mouseUp(const MouseEvent& event) override
	{
		// Ignore if it was a right click
		if (event.mods.isPopupMenu())
			return;

		// Normally (with left click), toggle button state
		setToggleState(!getToggleState(), sendNotification);
	}
};

And just use LeftOnlyToggleButton as if it was a ToggleButton. You can still addMouseListener(yourListener, false) to catch the right click and maybe open a contextual menu.

1 Like

This is not really a universal solution because it would require a derived class for every JUCE class that already is derived for Button: we would then end up with LeftOnlyToggleButton, LeftOnlyTextButton, LeftOnlyDrawableButton, LeftOnlyHyperlinkButton, etc.
That’s plain ugly.

@jules, @ed95 : requests in this direction keep appearing: Button right click confusion . Is it so hard to add a setIgnoreRightClicks() in the base Button class?

Also, this solution still has a problem: when the button is right-clicked, it is still animated exactly AS IF a click has been performed on it, while no click has been triggered in reality.

The behaviour that I want to accomplish is right clicks not having any visible action on the Button whatsoever.

That can easily be accomplished by changing 3 lines in juce_Button.cpp where updateState is called, changing them to:

updateState (whatever, isClickButtonDown())

with isClickButtonDown() being a private method like:

bool Button::isClickButtonDown() 
{
    if (onlyLeftClicks)
        return isLeftButtonDown();
    
    return isMouseButtonDown();
}

the 3 places in question are at the beginning of Button::mouseDown(), Button::mouseDrag() and inside the updateState() with no parameters.

2 Likes

I took the liberty to move this to the Feature requests (it predates the category)

You can create a template, something like:

template <typename ComponentType>
class LeftClickOnly : public ComponentType
{
public:
    using ComponentType::ComponentType;

    void mouseUp(const MouseEvent& e) override
    {
        if(e.mods.isLeftButtonDown())
        {
            ComponentType::mouseUp(e);
        }
    }
    void mouseDown(const MouseEvent& e) override
    {
        if(e.mods.isLeftButtonDown())
        {
            ComponentType::mouseDown(e);
        }
    }
    void mouseDrag(const MouseEvent& e) override
    {
        if(e.mods.isLeftButtonDown())
        {
            ComponentType::mouseDrag(e);
        }
    }
};

Then use it like:
LeftClickOnly<ToggleButton> myToggle;
it also works for Sliders etc.
There might be other mouse methods you want to override, but this was sufficient for my needs.

1 Like

It works for me because I’m applying this method on custom drawn Toggle Buttons.
Edit_Window

This is the complete calss that I wrote for my custom plain 2D rocker switches:

class GSiRockerSwitch : public ToggleButton
{
public:
	GSiRockerSwitch(String name) : ToggleButton(name)
	{
		setOffOnLabels("OFF", "ON");
	}

	// Set different labels for OFF and ON. Keep short!!! (Max 4 characters)
	void setOffOnLabels(String lblOFF, String lblON)
	{
		txtOFF = lblOFF;
		txtON = lblON;
	}

	// Mouse click action takes place when the mouse button is released
	void mouseUp(const MouseEvent& event) override
	{
		// Ignore if it was a right click (opens ContextMenu)
		if (event.mods.isPopupMenu())
			return;

		// Normally (with left click), toggle button state
		setToggleState(!getToggleState(), sendNotification);
	}

	// Custom look, draw as a rocker switch
	void paintButton(Graphics &g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override
	{
		auto fontSize = jmin(15.0f, getHeight() * 0.75f);
		auto tickWidth = fontSize * 1.1f;

		g.setColour(findColour(ToggleButton::textColourId));
		g.setFont(fontSize);

		if (!isEnabled())
			g.setOpacity(0.5f);

		// Draw switch outline
		g.setColour(Colour(0xFFA0A0A0));
		g.drawRect(10, 2, 80, getHeight() - 4);

		// Off position
		if (!getToggleState())
		{
			g.setColour(Colour(0xFFA0A0A0));
			g.fillRect(10, 2, 40, getHeight() - 4);

			g.setColour(Colour(0xFF000000));
			g.drawSingleLineText(txtOFF, 20, fontSize + 2);

		// On position
		} else {
			g.setColour(Colour(0xFFA0A0A0));
			g.fillRect(50, 2, 40, getHeight() - 4);

			g.setColour(Colour(0xFF000000));
			g.drawSingleLineText(txtON, 60, fontSize + 2);
		}
	}

private:
	String txtOFF, txtON;
};

Thank you, that is surprisingly similar to what I devised myself in the mean time :sweat_smile: , which is:

template <typename ButtonType>
class LeftClick : public ButtonType
{
public:
    using ButtonType::ButtonType;   // inherit all constructors of the wrapped class

    void mouseDown (const MouseEvent& e) override
    {
        if (e.mods.isPopupMenu())
            return;
    }

    void mouseDrag (const MouseEvent& e) override
    {
        if (e.mods.isPopupMenu())
            return;
    }

    void mouseUp (const MouseEvent& e) override
    {
        if (e.mods.isPopupMenu())
            return;
    }
};

Unfortunately, that suffers of the problem that I have mentioned above: upon right click, the “down” state is painted as if a the mouse action reaches the button, even if a click is not actually triggered.

Upon further investigation, that behaviour is observed only the first time the button is right-clicked after the mouse has entered it.

That happens because in those circumstances the Button receives a call to updateState():

Button::ButtonState Button::updateState()
{
    return updateState (isMouseOver (true), isMouseButtonDown());
}

and since isMouseButtonDown() returns true regardless of which mouse button is down, the whole Button enters its “down” state and paints accordingly.

This behaviour cannot be modified because neither Component::isMouseButtonDown() nor Button::updateState() are virtual (the latter also being private).

1 Like

Yes, and since your paint method only cares about the toggle state of the Button, ignoring its “down” state, you don’t see the effect of the spurious paint that I’m talking about.

And this is similar to the suggestion I did in that thread in mai 2018 :wink:

2 Likes

@yfede I found that the wrong painting can be avoided by overriding mouseEnter, mouseExit, focusGained, focusLost and internalClickCallback as well. I think this puts me firmly in “evil” land, so if anyone has a cleaner way I’d be happy to know :slight_smile:
In focusGained and focusLost you don’t get a MouseEvent, but you can check ModifierKeys::getCurrentModifiers().isPopupMenu() instead.

I’d like a comment from the JUCE Team about whether one really should implement the hacky workarounds proposed so far, or if they could take in consideration my initial feature request which only involves changing few lines in the Button class.

The requested change could also be implemented so that it is effective only when the developer calls a setIgnoreRightClicks() method to be added, so that would be a 100% non-breaking change

2 Likes

:roll_eyes: