JUCE Components: Mouse & Keyboard Listening

JUCE Components have lots of mouse and keyboard behavior baked right in. Let’s walk through their differing event models and get clear on how they work.

For more in-depth component spelunking, see my article How JUCE Components Work.

A Component already has what you need

A JUCE Component (including your top level Editor in a plugin) already has most of the keyboard and mouse listening functionality ready to go. There’s a huge grab bag of event related functions exposed.

Component inherits MouseListener (and only MouseListener!)

Component inherits from MouseListener. That provides a slew of mouse related methods that you can override, giving you “out of the box” mouse support:

void Component::mouseEnter (const MouseEvent&)          
void Component::mouseExit  (const MouseEvent&)          
void Component::mouseDown  (const MouseEvent&)          
void Component::mouseUp    (const MouseEvent&)          
void Component::mouseDrag  (const MouseEvent&)          
void Component::mouseMove  (const MouseEvent&)          
void Component::mouseDoubleClick (const MouseEvent&)    

Override mouseDown and you’ll be responding to a click (and so on).

Component also contains methods to get you started responding to the keyboard:

bool Component::keyPressed (const KeyPress&)
bool keyStateChanged (bool isKeyDown)
void modifierKeysChanged (const ModifierKeys& modifiers)

Key and Mouse Listeners have different mechanics

On the web, mouse and keyboard events behave similarly and “bubble up” the DOM.

In JUCE, mouse and keyboard events have different mechanics and gotchas. This starts with their internal implementation:

mouseDown and friends is called from a Component‘s internal implementation. All the logic about how and when to send the event lives in Component itself.

keyPressed is called from the ComponentPeer (the actual OS window, read up on that here).

The way they interact with components differs:

A mouseDown will always be sent to the “child-most” or “front most” component under the cursor.

A keyPressed event will only fire for the component that currently has keyboard focus.

As well as their defaults:

mouseDown will fire out of the box.

keyPressed requires that you setWantsKeyboardFocus(true) on the component or manually grab focus with grabKeyboardFocus.

Mouse Listening

MouseInputSourceInternal contains the glue code responsible for determining who gets the mouse events.

It does so by calling the top level Component‘s getComponentAt method, which recursively digs down the component hierarchy to find the “most specific” or “front most” component at the mouse coordinates.

Left: Red component gets mouse events. Right: Inner gray element gets mouse events.

Note that the mouse coordinates in the mouse event’s struct are translated to be relative to getLocalBounds of the component in question.

Responding to Mouse Events

Ok, so let’s get into some details on how to work with mouse events.

You are passed the coordinates of the mouse event, relative to the local component. This lets you do things like take action when part of a component is clicked:

void mouseDown (const juce::MouseEvent& event) override
{
    auto localPoint = event.getMouseDownPosition();
    auto areaYouWantClicked = getLocalBounds().removeFromTop(200);
    if (areaYouWantClicked.contains (localPoint.getX(), localPoint.getY()))
    {
        // top half was clicked
    }
}

Controlling Mouse Events

There’s a method on Component that lets you completely disable the current and/or child components’ abilities to receive mouse events.

void setInterceptsMouseClicks (bool allowClicksOnThisComponent, 
  bool allowClicksOnChildComponents) 

By default, all (front-most) components are candidates for receiving mouse events when they are under the cursor. So this is a good way to let components “surrender” or “bubble up” control of mouse events to the next higher component in the component hierarchy.

The method is poorly named and the docs are a bit roundabout. This setting applies to all mouse events (not just clicks). Here are the options:

# default
setInterceptsMouseClicks (true, true)

# this component gets mouse events, children do not
setInterceptsMouseClicks (true, false)

# only the children get mouse events
setInterceptsMouseClicks (false, true)

# neither this component nor children get events
setInterceptsMouseClicks (false, false)

Getting specific

By implementing a component’s hitTest function, you can further detail that you only want to receive mouse events in certain areas of the component.

Note that in order for hitTest to be useful, the component must be receiving mouse events overall. In other words, you must pass true as the first argument to setInterceptsMouseClicks.

bool hitTest (int x, int y)

For bounds (relative to the local component) where you want mouse events, return true. For bounds where you don’t want mouse events, return false.

The right half of the red component returns true for hitTest (shown with a dashed border). Mouse events in the left half default to the yellow component underneath.

Getting Fancy

You can create additional MouseListeners and listen to mouse events from any Component, adding them via addMouseListener.

For example, you could have an “interactive help” feature where you add a listener to many UI components and depending on the component, show contextual help.

Daniel’s Forum Gotcha™️: Every component is already a MouseListener, so if you add a listener, avoid having the added listener be another Component. This gets hairy. You’ll have to disentangle the events coming in for the component itself from the ones added.

Gotcha #2: As with other listeners across JUCE, whatever listeners you add in the constructor, be sure to remove in the destructor. In this case, remember to call removeMouseListener.

Gotcha #3: The bounds in the mouse event will be relative to the Component it was added to — if you make another component the listener, you’ll have the confusing situation of mouse events firing with bounds relative to 2 different components.

Keyboard Listening

It’s nice and easy to have a component respond to key presses. Just test against the key you are looking for:

bool keyPressed (const juce::KeyPress& key) override
{
    if (key == juce::KeyPress::escapeKey)
    {
        this->close();
        return true;
    }
    return false;
}

As stated, keyboard events only care about “what component is focused.” But how do we know what’s focused?

Tell JUCE your component wants focus

Unlike mouse listening, you need to take an explicit step to ever receive keyboard events. For a component to get key events, it needs to fit these criteria:

  • Component is enabled
  • Component is visible
  • You’ve called setWantsKeyboardFocus(true) on the component.

This doesn’t actually focus the component, though. These are just the pre-requisites. This is you telling JUCE “yeah, this component might grab focus at some point.”

Click to focus by default

By default, clicking a visible component focuses it. You can disable this by setting setMouseClickGrabsKeyboardFocus(false) on a component.

So, out of the box, you’ll have to click on a component to give it focus.

Manually grab focus

Sometimes you want to dictate which component gets focus.

For example, if you click a button and an overlay appears, you might want to manually give that overlay keyboard focus vs. wait for the user to click somewhere on it.

For this, you can call grabKeyboardFocus on the component in question.

Daniel’s Forum Gotcha™️: Don’t call grabKeyboardFocus in a component’s constructor. It only works once the component has been placed in the component hierarchy.

Respond to focus

Each Component has a focusGained and a focusLost callback. So you can do things like highlight a control when it has focus.

There’s also focusOfChildComponent.

Which component exactly gets focused?

The docs are nice and specific on this:

- if the component that was clicked on actually wants focus (as indicated
  by calling getWantsKeyboardFocus), it gets it.
- if the component itself doesn't want focus, it will try to pass it
  on to whichever of its children is the default component, as determined by
  the getDefaultComponent() implementation of the ComponentTraverser returned
  by createKeyboardFocusTraverser().
- if none of its children want focus at all, it will pass it up to its
  parent instead, unless it's a top-level component without a parent,
  in which case it just takes the focus itself.

Bubbling up key events

Unlike mouse events, you can respond to keyboard events AND let them bubble up to other components.

If you are looking to capture just the ESC key, for example, don’t always return true — this consumes the keypress.

bool keyPressed (const juce::KeyPress& key, Component* /*originatingComponent*/) override
{
    if (key == juce::KeyPress::escapeKey)
    {
        this->closeWindow();
        return true;
    }
    return false; // let other components handle the event too
}

Advanced keyboarding

In addition to grabbing focus you can giveAwayKeyboardFocus().

You can specify the order of keyboard focus with Component methods such as setExplicitFocusOrder.

You can also ask if a component has focus with hasKeyboardFocus() and ask if a child has focus via hasKeyboardFocus(true).

You can also explicitly pass focus to the previous or next sibling component with moveKeyboardFocusToSibling. This depends on containers and traversers, oh my.

There are more manual ways to control focus such as scoping focus with setFocusContainerType or creating a custom ComponentTransverser.

Troubleshooting Mouse Listening

Not getting the mouseDown you expected?

  • Check to see if setInterceptsMouseClicks is set on the component. If so, the first argument should be true.
  • Check to see if setInterceptsMouseClicks is set on parents in the hierarchy. If so, the second argument should be true.
  • Verify that a child component isn’t responding to the mouse events. setInterceptsMouseClicks must be false on children for a parent to receive the events.

Troubleshooting Keyboard Listening

Not getting the keyPressed you expected?

  • Check that you have grabbed focus, either with grabFocus or by clicking on the element.
  • Check to make sure setMouseClickGrabsKeyboardFocus wasn’t set to false on the component.
  • If a child is also responding to keyboard events, make sure it returns false on keyPressed so that the event bubbles up to the component in question.

Acknowledgements

Thanks to Eyal and Daniel for their (as always) valuable contributions and advice!

Leave a comment

Your email address will not be published.