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 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!
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
.
Creating your own Listeners
When needed, you can create additional MouseListener
s 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 betrue
. - Check to see if
setInterceptsMouseClicks
is set on parents in the hierarchy. If so, the second argument should betrue
. - 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
or by clicking on the element.grabKeyboardFocus
- If a child is also responding to keyboard events, make sure it returns
false
onkeyPressed
so that the event bubbles up to the component in question. - Pop in some
focusLost
andfocusGained
functions in your component so you can log the behavior.
Acknowledgements
Thanks to Eyal and Daniel for their (as always) valuable contributions and advice!
Leave a Reply