Keilly: MouseEvents for non-rectangluar components in Swing

Thursday, November 20, 2008

MouseEvents for non-rectangluar components in Swing

Warning: Swing hack ahead!

In Swing component bounds are defined as x,y,width,height, i.e. rectangular.

Also in Swing it's very hard to trap mouse events only for a particular section of a component. Sure we can test in our mouse listener if the location of the event is in a particular section, but this isn't always good enough because the mere act of receiving a mouse event means that the parents of that component never get a crack at processing the event. (See here).

This means that in Swing it is very difficult to simulate non-rectangular components.

Consider the resize grip area in this custom text area:

Although it looks triangular, the grip is actually a (rectangular) component layered in front of the text area. We want the user to be able to grab the grip and drag to resize, but also click in the non-grip area and interact directly with the text field.

This is what we have by default:


This is what we want:


Visually its easy to split into two: make the component non-opaque and paint the corner section with some grip graphics in the paintComponent method - letting the parent show through in the unpainted corner.

However event-wise there's no API to split a component into responsive and non-responsive areas. It would be nice for example if JComponent.processMouseEvent call returned a boolean indicating if the event was actually processed. It doesn't, so we have to hack in a fix.

Luckily Component.getCursor() is continually polled as the mouse cursor moves over a component. Overriding this we can detect where the mouse location is over our grip area and act accordingly.
If it's over the visible grip area we can add a mouse listener and start receiving events, if it's over the transparent area we remove the listener and the events are automatically routed to the parent.

Here's the code for the grip:

public Cursor getCursor(){
PointerInfo info = MouseInfo.getPointerInfo();
Point p = info.getLocation();
SwingUtilities.convertPointFromScreen(p, this);

// Test if in the visible area
if (p.x+p.y > getHeight()){
if (!Arrays.asList(getMouseListeners()).contains(ml))
addMouseListener(ml);

return Cursor.getPredefinedCursor(
Cursor.SE_RESIZE_CURSOR);
}

// In the transparent area, stop listening
if (Arrays.asList(getMouseListeners()).contains(ml))
removeMouseListener(ml);

return super.getCursor();
}

Of course you can make the location test as complex as you like creating components of any shape.

1 comment:

Anonymous said...

It's been a while, but I've handled this situation in the past by overriding JComponent.contains. Correct me if I'm wrong, but I want to say it did actually short circuit the event handling such that the parent got a chance to handle the event...

(btw, came across your blog looking for advice on animated Swing components... very useful, thanks!)