Rare case of Button::mouseEnter() state problem


#1

if an invisible Button is placed under a visible Component, the mouse is pressed in the visible Component, and within the mouseDown() handler of the visible component, the invisible button is moved so its bounds lies outside the original click, and then the invisible button is made visible, it will draw with isMouseOverButton=true. In fact, the ButtonState for this button will be erroneously stuck in “buttonOver”, until some user activity causes it to get updated.

I believe that the fix for this is to add moved() and resized() members to Button, and call updateState() from these routines. This handles the case when a Button’s bounds are modified during a synchronous mouse operation related to another control,

A workaround in the client code would be to change the order of operations during the click - first move the button then make it visible.


#2

Good grief, I’m going to change your login name to “Captain Edge-Case”!

Whenever a component gets moved, it gets sent a fake mouse-move event to make sure that things don’t get stuck like this, so adding code to moved() or resized() would probably be redundant. I’m guessing that the problem in your case is because the updateState() call isn’t taking into account the button’s visibility? Does it sort it out if you change the function like this…

Button::ButtonState Button::updateState() { return updateState (isShowing() && reallyContains (getMouseXYRelative(), true), isMouseButtonDown()); }


#3

This doesn’t fix it. The problem is not with the visibility, the problem is that the button becomes visible under the cursor during a mouseDown, and then gets moved. I will try to trace it with the debugger although its really tough to do because its during a mouse operation.


#4

Okay I am having a really tough time trying to debug this, so I wrote a small test program that demonstrates the problem. The blue button in the application draws red for mouse over, dark for mouse down. Clicking the blue button will cause the other invisible button to become visible and have its bounds changed. When this happens, you can see that the new button is in the “mouse over” state (i.e. it is red when it should be blue). Waving the mouse over it for real, will clear the state and the color will go back to normal.

#include "juce.h"

struct MyButton : Button
{
  MyButton() : Button( String::empty ) {}
  void paintButton (Graphics& g,
                    bool isMouseOverButton,
                    bool isButtonDown)
  {
    Colour cFace( 128, 200, 245 );
    if( isButtonDown )
      cFace = cFace.darker();
    else if( isMouseOverButton )
      cFace = Colour( 245, 64, 32 );
    g.setColour( cFace );
    g.fillAll();
    g.setColour( Colours::black );
    g.drawRect( getLocalBounds(), 1 );
  }
};

struct Panel : Component
{
  ~Panel() { deleteAllChildren(); }
  void paint (Graphics& g)
  {
    g.setColour( Colours::grey );
    g.fillAll();
    g.setColour( Colours::black );
    g.drawRect( getLocalBounds(), 1 );
  }
};

struct MainWindow  : DocumentWindow, Button::Listener
{
  MainWindow()
  : DocumentWindow (JUCE_T("Test")
  , Colours::black
  , DocumentWindow::allButtons
  , true )
  {
    Panel* p = new Panel;
    p->setSize( 512, 384 );
    MyButton* b;
    b = new MyButton;
    b->setBounds (256, 160, 40, 32);
    b->addButtonListener (this);
    p->addAndMakeVisible (b);
    b = new MyButton;
    b->setBounds (256, 160, 40, 32);
    p->addChildComponent (b);
    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; }

  void initialise (const String& commandLine)
  {
    mainWindow = new MainWindow;
  }

  void shutdown()
  {
    delete mainWindow;
  }

  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) {}

  static MainApp* s_app;
  MainWindow* mainWindow;
};

MainApp* MainApp::s_app = 0;

START_JUCE_APPLICATION (MainApp)

#5

Haha well…I’m a nut I guess. I am porting a pretty hefty desktop application over to Juce and it is quite UI-intensive so as you can imagine, I am giving this framework a serious workout. So far Juce is pretty awesome! Don’t let my complaints get to you, its just business. I am extremely obsessive when it comes to user interfaces, no detail escapes my notice, as you noticed elsewhere I go over screenshots of my application in Photohop and I zoom in and measure every pixel to make sure its right. I will fix even a single out of place pixel. That’s why I want the font hinting.

In the case of this issue, I have a grid of buttons that can be scrolled. When they scroll (in response to a scroll button click), some buttons are hidden, others are made visible, and all of their bounds changed. This is how I discovered the state problem with mouseOver.


#6

Your attention to detail is appreciated! Thanks for the example code, I’ll see if I can reproduce it…


#7

Hmm - quite a subtle one, that. The updateState() method was checking the mouse’s physical position, but wasn’t taking into account the fact that the mouse could still be in mid-drag on another component… It’s an easy fix, I’ll check something in later. Thanks!


#8

Cool, I prefer that over mucking with moved() and resized() because then it complicates subclasses of Button.


#9

Looks like it is fixed in the latest tip - thanks.