Even better Button? Say it ain't so!


#1

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)

#2

No, I don’t like that idea… If you want the button to fire as soon as the mouse-down happens, then it already has a setting for that. But if you’re waiting until the mouse-up, then you need to offer the user a way to change their mind while they’re dragging. If you make it impossible to do that, then what’s the point in waiting until they release the button? That’d just be frustrating for them if they did change their mind.

Something that I have considered adding would be a setting for a minimum distance beyond the button’s bounds which the mouse needs to drag before the button is released. The iphone has that kind of thing, so that you can be a bit sloppy, but if you drag more than e.g. 50 pixels beyond the button, it does actually release it.


#3

Consider a button that causes something to happen continuously until it is released, and the case where during the mouse down, the user accidentally moves outside of the client area. For example, a synthesizer key. Or a button the plays back a sample until you let it go. Do we really want the sound to start and stop if the user waves the mouse in and out of the button while the mouse button is held down? I don’t think so.

I tried to put this behavior into a subclass but it made it very messy.


#4

Lets say you have a Button that has setTriggeredOnMouseDown(true), and every time it is pressed, the application saves the effect settings into a new preset slot. If the user presses the mouse button on this Button and then moves the cursor in and out of the control do we want it to call clicked() every time? I’m thinking not…


#5

That’s not how it works, it just triggers it once, of course.

I see your point about buttons that operate while held down, but that could still be done the way I suggested. But I think that just having an on/off flag for this wouldn’t be useful enough to justify it.


#6

Well thanks for recognizing this use-case.

How do you suggest that I implement this behavior then if we aren’t going to make it a feature of the Button? I’m all for tips, but just saying “oh we don’t want that behavior anyway” is not really an option.

Also let me point out that I want to have an action when the button goes down, and then take another action when the button goes up. For example, start playback when the button goes down and then stop playback when it goes up. Right now there is no convenient way to do that in a subclass or listener. Or am I missing something? There’s no buttonUp state.


#7

Did you read my first post? My “minimum distance” suggestion would do just what you need, wouldn’t it?


#8

I misunderstood your post. I thought 50 pixels would be hard-coded. Why yes, if the distance can be adjusted by the caller, then that would produce identical results to what I have done! Therefore, I am all for it. Even better that it provides additional functionality. As long as I can pass something suitably large for the minimum distance, then I could get the behavior that I need. I am less concerned about the exact implemntation, as long as I meet the behavioral requirements. It sounds to me like having a minimum distance outside the Component for button up during mouseDrag() would perfectly address my use-case, while satisfying your desire for sensible features. Win-win.

What would be a clean way of getting the unbounded mouse movement behavior during the mouseDown? Subclass and override mouseDrag()?

What do you suggest for being able to perform actions on both the mouse down and the mouse up?


#9

You’ll just have to make do with buttonStateChanged(). The class was designed to provide the basic button functionality that 99% of people need. It’s already way too bloated, with way too many features, so if it still doesn’t do what you need, I think you need to write your own component.


#10

I will try making a subclass and hook buttonStateChanged(), with a state boolean to detect the transition from buttonDown to buttonOver/buttonNormal to know when to send some sort of “released” message.

I really would rather avoid writing my own entire Button, because the existing Juce Button class has a lot of cool stuff. For example the commands, shortcuts, repeat feature, fake clicking, tooltips, connected edges, etc… If I roll my own Button then I won’t get those benefits especially if they are upgraded. If the Button is overloaded, perhaps there could be a way to factor out some of its features into separate classes? I would be a lot more inclined to roll my own button if I could inherit functionality like shortcuts, tooltips, radio group behavior, and so on.

In fact, connected edges seems to me useful beyond buttons, I can already see places in my UI where I would like to have a Label with connected edges (to show dividers between a horizontal row of labels) as well as the ComboBox.


#11

Disregard everything I said in this thread about staying down during a drag…apparently I am not capable of correctly reading code, the Button now magically works exactly as expected. DUH!