I talked about adding just a little more functionality to the Button class so I went ahead and did it. This is the function that I added:
/** Sets whether the button will release if the cursor leaves the Component
bounds while the mouse is down. Optionally, if isAlwaysDownDuringDrag
is true, then the cursor can be hidden as long as the button is down.
By default the button will release when the cursor leaves the Component
even while the user has the mouse button pressed.
This is useful for buttons that have a repeat, for buttons that cause
a continuous action as long as they are held, and for the situation where
the user might not have the mouse stable (for example a live performance
environment)
*/
void setAlwaysDownDuringDrag (bool isAlwaysDownDuringDrag,
bool shouldAlsoHideCursor=false) throw();
So, if isAlwaysDownDuringDrag is true, then for as long as the mouse button is held down the button will be considered to be in the “buttonDown” state (i.e. looks pressed), even if the mouse leaves the Component bounds during the drag operation. I tested, and did this in a way that is compatible with setting a repeat on the button (which is kind of the whole point of this feature).
Furthermore, to not confuse the user and also to handle the case where you are pressing this button on a computer located in a crowded venue where someone bumps into you, the mouse is on an unstable surface, or god forbid you are using the crappy trackpad on a laptop, it is also possible to have the cursor hidden.
If isAlwaysDownDuringDrag is true, and shouldAlsoHideCursor is also true, then the cursor is hidden using setUnboundedMouseMovement for as long as the mouse button is down. This serves two functions. First, it makes things less distracting and unintuitive since the user will not see the cursor leave the client area, and second no matter how much the mouse is moved during the button down, the cursor will reappear in the client area of the Button when the mouse is released.
Note that I did this in mouseDrag and not mouseDown, this way if the mouse is not moved the cursor will not disappear. This is more visually pleasing for the cases where the user just taps the button once, or double clicks.
Here’s a .patch with the changes
Index: src/gui/components/buttons/juce_Button.cpp
===================================================================
--- src/gui/components/buttons/juce_Button.cpp (revision 10)
+++ src/gui/components/buttons/juce_Button.cpp (working copy)
@@ -67,6 +67,8 @@
needsRepainting (false),
isKeyDown (false),
triggerOnMouseDown (false),
+ alwaysDownDuringDrag (false),
+ hideCursorDuringDrag (false),
generateTooltip (false)
{
setWantsKeyboardFocus (true);
@@ -240,9 +242,11 @@
if (isEnabled() && isVisible() && ! isCurrentlyBlockedByAnotherModalComponent())
{
- if ((down && (over || (triggerOnMouseDown && buttonState == buttonDown))) || isKeyDown)
+ bool over2 = (alwaysDownDuringDrag && down) ? true : over;
+
+ if ((down && (over2 || (triggerOnMouseDown && buttonState == buttonDown))) || isKeyDown)
newState = buttonDown;
- else if (over)
+ else if (over2)
newState = buttonOver;
}
@@ -292,6 +296,14 @@
triggerOnMouseDown = isTriggeredOnMouseDown;
}
+void Button::setAlwaysDownDuringDrag (bool isAlwaysDownDuringDrag, bool shouldAlsoHideCursor) throw()
+{
+ jassert (!shouldAlsoHideCursor||isAlwaysDownDuringDrag);
+
+ alwaysDownDuringDrag = isAlwaysDownDuringDrag;
+ hideCursorDuringDrag = shouldAlsoHideCursor;
+}
+
//==============================================================================
void Button::clicked()
{
@@ -429,8 +441,11 @@
internalClickCallback (e.mods);
}
-void Button::mouseDrag (const MouseEvent&)
+void Button::mouseDrag (const MouseEvent& e)
{
+ if (hideCursorDuringDrag)
+ e.source.enableUnboundedMouseMovement (true);
+
const ButtonState oldState = buttonState;
updateState (isMouseOver(), true);
Index: src/gui/components/buttons/juce_Button.h
===================================================================
--- src/gui/components/buttons/juce_Button.h (revision 10)
+++ src/gui/components/buttons/juce_Button.h (working copy)
@@ -291,6 +291,21 @@
*/
void setTriggeredOnMouseDown (bool isTriggeredOnMouseDown) throw();
+ /** Sets whether the button will release if the cursor leaves the Component
+ bounds while the mouse is down. Optionally, if isAlwaysDownDuringDrag
+ is true, then the cursor can be hidden as long as the button is down.
+
+ By default the button will release when the cursor leaves the Component
+ even while the user has the mouse button pressed.
+
+ This is useful for buttons that have a repeat, for buttons that cause
+ a continuous action as long as they are held, and for the situation where
+ the user might not have the mouse stable (for example a live performance
+ environment)
+ */
+ void setAlwaysDownDuringDrag (bool isAlwaysDownDuringDrag,
+ bool shouldAlsoHideCursor=false) throw();
+
/** Returns the number of milliseconds since the last time the button
went into the 'down' state.
*/
@@ -486,6 +501,8 @@
bool needsRepainting : 1;
bool isKeyDown : 1;
bool triggerOnMouseDown : 1;
+ bool alwaysDownDuringDrag : 1;
+ bool hideCursorDuringDrag : 1;
bool generateTooltip : 1;
void repeatTimerCallback();
I’m not sure if I implemented it in the best way, according to Juce philosophy, but it seems to work and demonstrates the concept. Modify as you see fit.
Here is a simple test program that demonstrates this new behavior.
#include "juce.h"
struct Panel : Component, Button::Listener
{
Button* b2;
Panel()
{
Button* b = new TextButton (JUCE_T("TEST"));
b->setBounds (224, 64, 64, 40);
b->setRepeatSpeed (500, 125);
b->addButtonListener (this);
b->setTriggeredOnMouseDown (true);
b->setAlwaysDownDuringDrag (true, true);
addAndMakeVisible (b);
b2 = new TextButton(String::empty);
b2->setBounds (224, 128, 64, 40);
addAndMakeVisible (b2);
}
~Panel() { deleteAllChildren(); }
void paint (Graphics& g)
{
Rectangle<int> b = getLocalBounds();
g.setColour( Colours::grey );
g.fillAll();
}
void buttonClicked (Button* button)
{
b2->setToggleState (!b2->getToggleState(), false);
}
};
struct MainWindow
: DocumentWindow
, Button::Listener
{
MainWindow()
: DocumentWindow (JUCE_T("Test")
, Colours::black
, DocumentWindow::allButtons
, true )
{
Panel* p = new Panel;
p->setSize( 512, 384 );
setContentComponent (p, true, true);
centreWithSize (getWidth(), getHeight());
setVisible( true );
}
~MainWindow() {}
void buttonClicked (Button* button)
{
Component* c = getContentComponent()->getChildComponent(1);
c->setVisible (true);
c->setTopLeftPosition (64, 64);
}
void closeButtonPressed() { JUCEApplication::quit(); }
};
struct MainApp : JUCEApplication
{
MainApp() : mainWindow(0) { s_app=this; }
~MainApp() { s_app=0; }
static MainApp& GetInstance() { return *s_app; }
const String getApplicationName() { return JUCE_T("JuceTest"); }
const String getApplicationVersion() { return JUCE_T("0.1.0"); }
bool moreThanOneInstanceAllowed() { return true; }
void anotherInstanceStarted (const String& commandLine) {}
void initialise (const String& commandLine)
{
mainWindow = new MainWindow;
}
void shutdown()
{
delete mainWindow;
}
static MainApp* s_app;
MainWindow* mainWindow;
};
MainApp* MainApp::s_app = 0;
START_JUCE_APPLICATION (MainApp)