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. This gives 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 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. They “bubble up” the DOM.

In JUCE, mouse and keyboard events have different mechanics. 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 is always sent to the “child-most” or “front most” component under the cursor.

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

As well as their defaults:

mouseDown just works with the defaults out of the box.

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

Mouse Listening

MouseInputSource 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.

The component has to be visible and pass the hitTest to be a candidate!

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

You can completely disable the current and 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. 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.

setInterceptsMouseClicks is poorly named and the docs are a bit roundabout. The 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 with hitTest

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.

In order for hitTest to be useful, the component must be receiving mouse events. 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.

Creating your own Listeners

When needed, you can create additional MouseListeners to listen to mouse events from any Component.

For example, you could have an “interactive help” feature. Depending on what the user is hovering over, you will show some contextual help.

One way of doing this is to create a fresh class called something like InteractiveHelp that derives from MouseListener. On each of your components where you have interactive help, you call addMouseListener(interactiveHelp, true). That second argument is whether or not you’ll get events from all the nested children.

Keep in mind the bounds in the mouse event will be relative to it’s source Component it is listening to.

Daniel’s Forum Gotcha™️: Every component is already a MouseListener. What does this mean? If you add another listener, avoid having that listener be derived from Component, or things get hairy. You’ll have to somehow disentangle mouse events coming in for component itself vs. the listener. Basically, it’s best to create a fresh class that inherits from MouseListener and doesn’t inherit from Component.

Gotcha #2: As with other listeners across JUCE, whatever listeners you add in the constructor, be sure to remove in the destructor by calling removeMouseListener.

Getting Fancy with MouseEvent

In a mouseDown or similar method, you’ll have access to the MouseEvent.

There are a number of convenience helpers that come in handy when rolling your own listeners:

event.originalComponent will give you the first component the event fired on. Remember, behind the scenes, JUCE digs down through the component hierarchy to fire the event on the “most specific” or “front most” component. If you have the mouse down and are dragging over multiple components, this will continue to report that first, original component.

event.withNewPosition(someNewPoint) is good for when you need to fudge the position a bit for some reason.

event.getEventRelativeTo (someOtherComponent) is perfect for translating the mouse position between bounds. It also updates event.eventComponent with someOtherComponent.

Keyboard Listening

It’s nice and easy to have a component respond to key presses.

From any component, you can test against the key you are looking for by plopping in a method like so:

bool keyPressed (const juce::KeyPress& key) override
{
    if (key == juce::KeyPress::escapeKey)
    {
        // do something
    }
}

You can test against key.getTextCharacter() for single characters:

bool keyPressed (const juce::KeyPress& key) override
{
    if (key.getTextCharacter() == 'i')
    {
       // do something
    }
}

Remember, keyboard events get routed to the component that is focused!

Telling JUCE that your component wants focus

Unlike mouse listening, you need to take an explicit step to even 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 still doesn’t actually focus the component, though.

These are just the prerequisites. 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 click to focus by setting setMouseClickGrabsKeyboardFocus(false) on a component.

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

Manually grabbing 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™️: Unfortunately you can’t really call grabKeyboardFocus in a component’s constructor. It only works after the component has been placed in the component hierarchy and is visible.

If you want keyboard focus immediately grabbed by a component when the editor opens, rather than doing something sketchy like conditionally dealing with it within paint, you can use an instance of ComponentMovementWatcher:

void componentVisibilityChanged() override
{
    if (getComponent()->isShowing())
        getComponent()->grabKeyboardFocus();
}

Remember, you can only focus components where isShowing is true.

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 often want 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 will consume the keypress.

bool keyPressed (const juce::KeyPress& key, Component* /*originatingComponent*/) override
{
    if (key == juce::KeyPress::escapeKey)
    {
        this->closeWindow();
        return true; // consumes the event
    }
    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?

Months after originally writing this article, I was still running into issues — so it’s not just you 🙂 Here’s a list of things to check:

  • Ensure setWantsKeyboardFocus (true) is present on the component in question.
  • Make sure setMouseClickGrabsKeyboardFocus isn’t set to false on the component if you are expecting to click to grab focus.
  • Check that you have actually grabbed focus, either with grabKeyboardFocus or by clicking on the element.
  • 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.
  • Pop in some focusLost and focusGained functions in your component so you can log the behavior.

Acknowledgements

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


Responses

  1. Islam Berkemajuan Avatar
    Islam Berkemajuan

    I cannot thank you enough for the wealth of information and resources you’ve provided on this website. Your dedication to sharing knowledge is making a positive impact, and I’m truly thankful for your contributions. I appreciate the well-structured articles that offer a comprehensive understanding of various subjects. Thank you for sharing such valuable information and contributing to our growth. Sankyu tsym!

  2. Diego Avatar

    Hey Sudara! Great blog 🙂
    I really appreciate your work.
    Cheers
    Diego.

Leave a Reply

Your email address will not be published. Required fields are marked *