Ordering of mouseDown() hooks is messing up my popup button


#1

In this example program, a FocusableComponent is a container that intercepts all clicks anywhere, including child components, and hilites itself to indicate that it has received the focus. Everything works fine, except for the case where a popup button (or any other control) goes into a modal state in response to its mouseDown(). In this case, the focus is not changed until after the modal operation completes.

The reason is that Component::internalMouseDown() calls the mouse listeners after it calls Component::mouseDown():

    // Component::internalMouseDown()
    mouseDown (me);

    if (checker.shouldBailOut())
        return;

    desktop.resetTimer();
    desktop.mouseListeners.callChecked (checker, &MouseListener::mouseDown, me);

    MouseListenerList::sendMouseEvent (this, checker, &MouseListener::mouseDown, me);

Here is an example program. Clicking on the button pops a menu, but the focus is not changed until after the popup menu is done with its modal state. Clicking outside the button gives focus to the container as normal.

#include "juce.h"

struct PopupButton : Button
{
  PopupButton() : Button(String::empty)
  {
    setTriggeredOnMouseDown (true);
  }
  void paintButton (Graphics& g, bool over, bool down)
  {
    Colour c;
    if (down)
      c=Colours::red;
    else if (over)
      c=Colours::green;
    else
      c=Colours::blue;
    g.setColour (c);
    g.fillAll();
    g.setColour (Colours::black);
    g.drawRect (getLocalBounds(), 1);
  }
  void clicked ()
  {
    PopupMenu m;
    m.addItem (1, "Juce!");
    m.addSeparator ();
    m.addItem (2, "Test");
    m.showAt (this);
  }
};

Component* focusedComponent=0;

struct FocusableContainer : Component
{
  PopupButton button;
  FocusableContainer ()
  {
    button.setBounds (48, 32, 64, 32);
    addAndMakeVisible (&button);
  }

  void paint (Graphics& g)
  {
    g.setColour (Colours::grey);
    g.fillAll ();
    if (focusedComponent==this)
      g.setColour (Colours::orange);
    else
      g.setColour (Colours::black);
    g.drawRect (getLocalBounds(), 2);
  }
};

struct FocusCatcher : MouseListener
{
  FocusableContainer* owner;
  FocusCatcher (FocusableContainer* owner_) : owner(owner_)
  {
    owner->addMouseListener (this, true);
  }
  void mouseDown (const MouseEvent& e)
  {
    focusedComponent=owner;
    owner->getPeer()->getComponent()->repaint();
  }
};

struct Panel : Component
{
  FocusableContainer container[2];
  FocusCatcher c1, c2;
  Panel() : c1 (&container[0]), c2 (&container[1])
  {
    container[0].setBounds (0, 0, 160, 128);
    addAndMakeVisible (&container[0]);
    container[1].setBounds (160, 0, 160, 128);
    addAndMakeVisible (&container[1]);
  }
};

struct MainWindow
  : DocumentWindow
  , Button::Listener
{
  MainWindow()
  : DocumentWindow (JUCE_T("Test")
  , Colours::black
  , DocumentWindow::allButtons
  , true )
  {
    Panel* p = new Panel;
    p->setSize( 320, 128 );
    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)

Is there something I am missing?


#2

How can you have a “focus catcher” that only listens for mouse-clicks? What if the focus changes by some other means? Try the FocusChangeListener class instead.

OT: I don’t recommend modal loops if you can avoid them - they can get you into awkward situations. Better to use a ModalComponentManager::Callback for things like menus if you can.


#3

The only way for the focus to be changed is through a mouse click. This isn’t the regular focus for keyboard edit this is a specific concept in my interface where hot keys and menus apply to different Component depending on what was clicked.

ModalComponentManager seemed like a good idea but when I derive my Button also from ModalComponentManager::Callback, it ends up deleting my Component when it is done. This is what I’m doing:

class CustomPopupButton
  : public Button
  , public ModalComponentManager::Callback
{
  void clicked ()
  {
    PopupMenu m;
    m.addItem (1, JUCE_T("Menu Item"));
 
    m.showAt (this, 0, 0, 0, 16, this); // modal component callback
  }
};

My CustomPopupButton gets deleted after the callback is finished. Also, this won’t work if the Component was not allocated with operator new (i.e. it is a data member of another class).


#4

Well if you want to write your own focus system, then you’re on your own with that… If it’s fragile enough that the order in which mouse-events get delivered will disrupt it, then you probably need to come up with a better design.

Why would your callback have to know anything about the button that triggered it? It should respond to the menu item, regardless of the component that invoked it. It also seems pretty wacky to delete a button when you click it… Buttons aren’t really intended for use as temporary objects.


#5

The modal callback was exactly what I needed, it worked perfect.

Why would your callback have to know anything about the button that triggered it? It should respond to the menu item, regardless of the component that invoked it. It also seems pretty wacky to delete a button when you click it… Buttons aren’t really intended for use as temporary objects.[/quote]

LOL Jules! I’m not telling you how I want it to work, I’m telling you what happened when I derived my object from both a Callback and a Button. The ModalComponentManager calls operator delete on the Callback when it is done! So I had to make a helper object to get the message to my control. I don’t use model/view/controller with separate classes for everything, I roll the appearance, functionality, and behavior for each control up into one class. Here’s my PopupButton base:

class PopupButton
  : public Button
{
public:
  PopupButton ();

  void setItemHeight (int height);
 
  virtual void doMenuItem (int menuItemId) = 0;

protected:
  virtual void buildMenu (PopupMenu& menu) = 0;

private:
  void clicked ();

private:
  struct Callback : ModalComponentManager::Callback
  {
    PopupButton& m_owner;
    Callback (PopupButton& owner);
    void modalStateFinished (int returnValue);
  };

  int m_itemHeight;
};

.cpp

PopupButton::Callback::Callback (PopupButton& owner)
  : m_owner (owner)
{
}

void PopupButton::Callback::modalStateFinished (int returnValue)
{
  if (returnValue)
    m_owner.doMenuItem (returnValue);
}

PopupButton::PopupButton()
  : Button (String::empty)
  , m_itemHeight (0)
{
  setTriggeredOnMouseDown (true);
}

void PopupButton::setItemHeight (int height)
{
  m_itemHeight = height;
}

void PopupButton::clicked ()
{
  PopupMenu menu;
  buildMenu (menu);

  menu.showAt (this, 0, 0, 0, m_itemHeight, new Callback(*this));
}

As you can see, the call to PopupMenu::showAt() creates a temporary Callback via operator new because thats a requirement of the ModalComponentManager interface (it stores the Callback in an OwnedArray I think?)

I’m just saying, Callback shouldn’t impose a particular style of allocation but to use your own words this is hardly a showstopper.


#6

Sorry, I misunderstood. …but, in this case, Callback definitely should make you allocate the object, because the object has to be certain to exist until the callback is triggered. It’s the same kind of principal as sending a message - you create a new object and dispatch it, and let the handler delete it later on when it’s done.