 |
Swing Hack 4: The universal right click
Posted by joshy on October 03, 2003 at 09:13 AM | Comments (10)
I received an email today asking about my use of the glass
pane. It seems this fellow wants to handle right clicks on
any component in each screen. A logical request. In most cases
your right clicks are not limited to a single component, yet to
receive the events required to show popups you have to add a
listener to each component! Not enjoyable.
To get around this we can use a glass pane. Remember from last time
that a glass pane is an invisible component covering the entire frame.
We could catch the events there and handle right clicks on any component. The problem, of course, is that now none of your components
can get any of the events.
To get around this issue we can test for the right click and then
redispatch the event to make sure the correct component gets it. Not
trivial but not too hard either. The tricky part is rebroadcasting
the event. First you have to figure out which component was supposed
to be hit. To find this we must convert the click point from
being relative to the glass pane to being relative to the content
pane:
// get the mouse click point relative to the content pane
Point containerPoint = SwingUtilities.convertPoint(this, e.getPoint(),contentPane);
Then we can search for the child component which was hit
// find the component that under this point
Component component = SwingUtilities.getDeepestComponentAt(
contentPane,
containerPoint.x,
containerPoint.y);
Once that's done we convert the point to be relative to the target
component:
// convert point relative to the target component
Point componentPoint = SwingUtilities.convertPoint(
this,
e.getPoint(),
component);
And now we redispatch the event:
// redispatch the event
component.dispatchEvent(new MouseEvent(component,
e.getID(),
e.getWhen(),
e.getModifiers(),
componentPoint.x,
componentPoint.y,
e.getClickCount(),
e.isPopupTrigger()));
Here's the complete code. Parts of it were
adapted from the Java Tutorial sections on
glasspanes and popup menus.
The code makes a frame with a button and a textfield.
Then it creates a right click handler to grab the
events, check for right clicks, and redispatch the
others.
for more on glass panes and popup menus you can read
here
and
here
.
Enjoy!
- joshua
: (hmm. maybe I should make this a link).
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
/**
Written by Joshua Marinacci (joshy@joshy.org)
Adapted from Sun tutorial code at
http://java.sun.com/docs/books/tutorial/uiswing/components/rootpane.html
*/
public class RightClick extends JComponent implements MouseListener, MouseMotionListener {
JPopupMenu popup;
Container contentPane;
public RightClick(Container contentPane) {
addMouseListener(this);
addMouseMotionListener(this);
this.contentPane = contentPane;
popup = new JPopupMenu();
popup.add(new JMenuItem("Dogs"));
popup.add(new JMenuItem("Cats"));
popup.add(new JMenuItem("Mass Hysteria"));
}
// draw some text just so we know the glass pane
// is installed and visible
public void paint(Graphics g) {
g.drawString("I'm a glass pane",50,50);
}
// catch all mouse events and redispatch them
public void mouseMoved(MouseEvent e) {
redispatchMouseEvent(e, false);
}
public void mouseDragged(MouseEvent e) {
redispatchMouseEvent(e, false);
}
public void mouseClicked(MouseEvent e) {
p("mouse clicked");
redispatchMouseEvent(e, false);
}
public void mouseEntered(MouseEvent e) {
redispatchMouseEvent(e, false);
}
public void mouseExited(MouseEvent e) {
redispatchMouseEvent(e, false);
}
public void mousePressed(MouseEvent e) {
redispatchMouseEvent(e, false);
}
public void mouseReleased(MouseEvent e) {
redispatchMouseEvent(e, false);
}
private void redispatchMouseEvent(MouseEvent e,
boolean repaint) {
// if it's a popup
if(e.isPopupTrigger()) {
p("it's a popup");
// show the popup and return
popup.show(e.getComponent(), e.getX(), e.getY());
} else {
// since it's not a popup we need to redispatch it.
// get the mouse click point relative to the content pane
Point containerPoint = SwingUtilities.convertPoint(this,
e.getPoint(),contentPane);
// find the component that under this point
Component component = SwingUtilities.getDeepestComponentAt(
contentPane,
containerPoint.x,
containerPoint.y);
// return if nothing was found
if (component == null) {
return;
}
// convert point relative to the target component
Point componentPoint = SwingUtilities.convertPoint(
this,
e.getPoint(),
component);
// redispatch the event
component.dispatchEvent(new MouseEvent(component,
e.getID(),
e.getWhen(),
e.getModifiers(),
componentPoint.x,
componentPoint.y,
e.getClickCount(),
e.isPopupTrigger()));
}
}
public static void main(String[] args) {
// create a frame with some components in it
JFrame frame = new JFrame("Right Click Test");
JButton button = new JButton("this is a button");
JTextField tf = new JTextField("this is a textfield");
JPanel panel = new JPanel();
panel.add(button);
panel.add(tf);
frame.getContentPane().add(panel);
// create the right click glass pane.
RightClick rc = new RightClick(frame.getContentPane());
// set as glasspane and make it visible
frame.setGlassPane(rc);
rc.setVisible(true);
// pack and show the frame
frame.pack();
frame.setSize(400,200);
frame.show();
}
// utiltity function
public static void p(String str) {
System.out.println(str);
}
}
Bookmark blog post: del.icio.us Digg DZone Furl Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment
-
With Drag & Drop?
Using a glass pane is all very well, but it's not without consequences. Amongst other thing, all drag & drop will cease to work. Apparently that's not a bug.
The redispatch code isn't entirely correct. Trivially you miss 1.4's MouseEvent modifiersEx, but then so does most of Swing. More seriously there are cases to do with mouse drags moving outside of component's visible area.
Posted by: tackline on October 06, 2003 at 11:57 AM
-
With Drag & Drop?
This is quite true. I could fix the redispatch code to be 1.4 compliant and probably fix the mouse drag issues but there's probably nothing I can do about drag and drop. Maybe I could make the entire glasspane be registered as a drag target and then forward the calls onto a recursive search of the component tree underneath. That strikes me as rather slow though.
However, I think for most people this will do the job, which is to have a universal right click. Still, it would be nice if Swing's event model had some sort of an interceptor pattern.
Posted by: joshy on October 07, 2003 at 10:04 AM
-
With Drag & Drop?
Drag & drop should be used more often. Dnd text editing, file management, drawing - all good stuff.
I decided to have a closer look at AWT to see if there wasn't anyway around the problem of intercepting events. EventQueue seems like a good place to start. After much fiddling I came up with a queue that rerouted popup events. Unfortunately, it probably wont work in WebStart for security reasons. Similarly a version AWTEventListeners will suffer the same problem.
The only other way that springs to mind is to put an event listener on absolutely everything. It's probably not too fast, but it should work.
The code is a bit long, but I'm going to include it anyway...
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
/*
Adapted by Thomas Hawtin (tackline@tackline.demon.co.uk)
Written by Joshua Marinacci (joshy@joshy.org)
Adapted from Sun tutorial code at
http://java.sun.com/docs/books/tutorial/uiswing/components/rootpane.html
*/
/**
* Components that wish to intercept popup events
* should implement this class.
*/
interface PopupIntercepter {
void popupEvent(MouseEvent event);
}
class Utils {
/** Dispatches popup events to ancestor implementing PopupInterceptor. */
public static boolean dispatchPopup(AWTEvent event) {
if (!(event instanceof MouseEvent) || event instanceof ActiveEvent) {
return false;
}
MouseEvent mouseEvent = (MouseEvent)event;
if (!mouseEvent.isPopupTrigger()) {
return false;
}
Object src = event.getSource();
if (!(src instanceof Component)) {
return false;
}
Component component = (Component)src;
while (component != null) {
if (component instanceof PopupIntercepter) {
PopupIntercepter intercepter = (PopupIntercepter)component;
intercepter.popupEvent(mouseEvent);
mouseEvent.consume();
return true;
}
component = component.getParent();
}
return false;
}
}
class JPopupablePane extends JFrame implements PopupIntercepter {
private final JPopupMenu popup;
public JPopupablePane(String title) {
super(title);
popup = new JPopupMenu();
popup.add(new JMenuItem("Dogs"));
popup.add(new JMenuItem("Cats"));
popup.add(new JMenuItem("Mass Hysteria"));
}
public void popupEvent(MouseEvent event) {
System.out.println("it's a popup");
popup.show(event.getComponent(), event.getX(), event.getY());
}
public static JFrame create() {
final JFrame frame = new JPopupablePane("Right Click Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton button = new JButton("this is a button");
JTextField tf = new JTextField("this is a textfield");
JPanel panel = new JPanel();
panel.add(button);
panel.add(tf);
frame.getContentPane().add(panel);
// pack and show the frame
frame.pack();
frame.setSize(400,200);
frame.show();
return frame;
}
}
class PopupablePane extends Frame implements PopupIntercepter {
private final PopupMenu popup;
public PopupablePane(String title) {
super(title);
popup = new PopupMenu();
popup.add(new MenuItem("Dogs"));
popup.add(new MenuItem("Cats"));
popup.add(new MenuItem("Mass Hysteria"));
add(popup);
}
public void popupEvent(MouseEvent event) {
System.out.println("it's a popup");
popup.show(event.getComponent(), event.getX(), event.getY());
}
public static Frame create() {
final Frame frame = new PopupablePane("Right Click Test");
Button button = new Button("this is a button");
TextField tf = new TextField("this is a textfield");
Panel panel = new Panel();
panel.add(button);
panel.add(tf);
frame.add(panel);
// pack and show the frame
frame.pack();
frame.setSize(400,200);
frame.show();
return frame;
}
}
class NoRightClick {
public static void main(String[] args) {
final JFrame light = JPopupablePane.create();
final Frame heavy = PopupablePane.create();
}
}
public class MoreRightClick {
public static void main(String[] args) {
Toolkit.getDefaultToolkit().getSystemEventQueue().push(
new EventQueue() {
protected void dispatchEvent(AWTEvent event) {
if (!Utils.dispatchPopup(event)) {
super.dispatchEvent(event);
}
}
}
);
final JFrame light = JPopupablePane.create();
final Frame heavy = PopupablePane.create();
}
}
class DispatchRightClick {
public static void main(String[] args) {
Toolkit.getDefaultToolkit().addAWTEventListener(
new AWTEventListener() {
public void eventDispatched(AWTEvent event) {
Utils.dispatchPopup(event);
}
}, AWTEvent.MOUSE_EVENT_MASK
);
final JFrame light = JPopupablePane.create();
final Frame heavy = PopupablePane.create();
}
}
class QuietRightClick {
private MouseListener mouseListener = new MouseListener() {
public void mouseClicked(MouseEvent event) {
mouseEvent(event);
}
public void mouseEntered(MouseEvent event) {
mouseEvent(event);
}
public void mouseExited(MouseEvent event) {
mouseEvent(event);
}
public void mousePressed(MouseEvent event) {
mouseEvent(event);
}
public void mouseReleased(MouseEvent event) {
mouseEvent(event);
}
private void mouseEvent(MouseEvent event) {
Utils.dispatchPopup(event);
}
};
private ContainerListener containerListener = new ContainerListener() {
public void componentAdded(ContainerEvent event) {
setListen(event.getChild(), true);
}
public void componentRemoved(ContainerEvent event) {
setListen(event.getChild(), false);
}
};
public static void main(String[] args) {
final JFrame light = JPopupablePane.create();
final Frame heavy = PopupablePane.create();
QuietRightClick quiet = new QuietRightClick();
quiet.setListen(light, true);
quiet.setListen(heavy, true);
}
private void setListen(Component c, boolean enable) {
if (c == null) {
return;
}
if (enable) {
c.addMouseListener(mouseListener);
} else {
c.removeMouseListener(mouseListener);
}
if (!(c instanceof Container)) {
return;
}
Container container = (Container)c;
// Checks for components added or removed.
if (enable) {
container.addContainerListener(containerListener);
} else {
container.removeContainerListener(containerListener);
}
// Listen to existing children!
Component[] children = container.getComponents();
final int num = children.length;
for (int ct=0; ct<num; ++ct) {
setListen(children[ct], enable);
}
}
}
As a minor point, if the popup is open when another popup event occurs, then the popup should open (on Metal) which doesn't happen with the original code.
Posted by: tackline on October 07, 2003 at 05:53 PM
-
Hi Josh,
I think your second attempt is actually much faster. You don't have to go through the overhead of a full redispatching of the event. At least, in my application the GUI doesn't seem as sluggish, and things aren't wonky (for example, with a glasspane, JMenuBar and JPopupMenu mouse tracking stops working).
Overall, glasspane seems like a good idea but an extremely poor implemention on Sun's behalf. What would have been more useful was a more programmer-friendly way to track system-wide events other than making a new EventQueue which seems a little "low level" to me... kind of a last resort if you get my drift.
Thanks for the great article,
Rob
Posted by: rslifka on January 11, 2005 at 12:02 PM
-
The following glass pane appears to work well with dragging JSplitPane dividers. It also works with other drag and drop. It doesn't fix the modifiersEx problem -- is there a solution for this?
Sample glass pane implementation:
private class PartManagerGlassPane extends JPanel implements MouseMotionListener,MouseListener,MouseWheelListener {
private Component previousMoveComponent;
private Component mouseEventTarget;
public PartManagerGlassPane() {
// the default Swing glass pane is not visible.
// we make this pane visible so that we can paint
// into it.
setVisible(true);
// make the pane transparent, so that we can see
// what's underneath
setOpaque(false);
addMouseMotionListener(this);
addMouseListener(this);
addMouseWheelListener(this);
}
public void mouseDragged(MouseEvent e) {
redispatchMouseEvent(e,true);
}
public void mouseMoved(MouseEvent e) {
redispatchMouseEvent(e,true);
}
private void redispatchMouseEvent(MouseEvent e,boolean updateCursor) {
Point glassPanePoint = e.getPoint();
Component component = null;
Container container = contentPane;
Point containerPoint = SwingUtilities.convertPoint(
glassPane,
glassPanePoint,
contentPane);
component = SwingUtilities.getDeepestComponentAt(
container,
containerPoint.x,
containerPoint.y);
if (previousMoveComponent != null && component != previousMoveComponent) {
MouseEvent me = new MouseEvent(previousMoveComponent,
MouseEvent.MOUSE_EXITED,
e.getWhen(),
0,
0,
0,
0,
false);
previousMoveComponent.dispatchEvent(me);
}
if (component != null) {
Cursor cursor2 = component.getCursor();
if (cursor2 != getCursor()) {
setCursor(cursor2);
}
if (component != previousMoveComponent) {
MouseEvent me = new MouseEvent(component,
MouseEvent.MOUSE_ENTERED,
e.getWhen(),
0,
0,
0,
0,
false);
component.dispatchEvent(me);
}
previousMoveComponent = component;
int eventId = e.getID();
if (eventId != MouseEvent.MOUSE_ENTERED && eventId != MouseEvent.MOUSE_EXITED) {
// 4508327 : MOUSE_CLICKED should only go to the recipient of
// the accompanying MOUSE_PRESSED, so don't reset mouseEventTarget on a
// MOUSE_CLICKED.
if (!isMouseGrab(e) && eventId != MouseEvent.MOUSE_CLICKED) {
mouseEventTarget = component;
}
if (mouseEventTarget != null) {
Point mouseEventTargetPoint = SwingUtilities.convertPoint(
glassPane,
glassPanePoint,
mouseEventTarget);
mouseEventTarget.dispatchEvent(new MouseEvent(mouseEventTarget,
e.getID(),
e.getWhen(),
e.getModifiers(),
mouseEventTargetPoint.x,
mouseEventTargetPoint.y,
e.getClickCount(),
e.isPopupTrigger()));
}
}
}
}
public void mouseClicked(MouseEvent e) {
redispatchMouseEvent(e,false);
}
public void mouseEntered(MouseEvent e) {
redispatchMouseEvent(e,false);
}
public void mouseExited(MouseEvent e) {
redispatchMouseEvent(e,false);
}
public void mousePressed(MouseEvent e) {
redispatchMouseEvent(e,false);
}
public void mouseReleased(MouseEvent e) {
redispatchMouseEvent(e,false);
}
public void mouseWheelMoved(MouseWheelEvent e) {
redispatchMouseEvent(e,false);
}
/* This method effectively returns whether or not a mouse button was down
* just BEFORE the event happened. A better method name might be
* wasAMouseButtonDownBeforeThisEvent().
*/
private boolean isMouseGrab(MouseEvent e) {
int modifiers = e.getModifiersEx();
if(e.getID() == MouseEvent.MOUSE_PRESSED
|| e.getID() == MouseEvent.MOUSE_RELEASED)
{
switch (e.getButton()) {
case MouseEvent.BUTTON1:
modifiers ^= InputEvent.BUTTON1_DOWN_MASK;
break;
case MouseEvent.BUTTON2:
modifiers ^= InputEvent.BUTTON2_DOWN_MASK;
break;
case MouseEvent.BUTTON3:
modifiers ^= InputEvent.BUTTON3_DOWN_MASK;
break;
}
}
/* modifiers now as just before event */
return ((modifiers & (InputEvent.BUTTON1_DOWN_MASK
| InputEvent.BUTTON2_DOWN_MASK
| InputEvent.BUTTON3_DOWN_MASK)) != 0);
}
}
Posted by: dgreen on July 06, 2005 at 08:40 AM
-
Hmm, I have a similar Glasspane-class, that do the job of redispatching mouse-events. However there are two problems I got:
1) When redispatching you use getDeepestComponent... That is not really correct, if the deepest component is for example a JLabel. JLabel is not interested in mouse-events. So the event will get lost. When the container of the JLabel has a MouseListener it will not be notified. When there is no active glasspane this would work correctly.
In the deep of the jdk-sources I have found the method Container.getMouseEventTarget(int x, int y, boolean includeSelf). This would be exact what we need, but ... it is package-private... :-(
2) When a PopupFactory is used (JDK1.4.2) and there will be a mediumweight popup, then there seems to be a little disfunction: the popup doesn't repaint correctly and the mouse-events are not dispatched to the glasspane but the popup gets them directly!
Anybody there who nows how to get around this.
And after all: thanks to all, who publish here their thoughts and ideas to the theme "glasspane". It appears to me that there aren't very many people who are interested in this theme.
Posted by: mermi on July 11, 2005 at 05:58 AM
-
This is a concise and efficient code snippet, Josh. Very impressive.
Unfortunately, this code doesn't work for RootPanes that include JMenuBars, as JMenuBars exist outside of the contentPane.
I've scoured the web for Sun's recommended way of forwarding these events (they conveniently leave it out in their Swing GlassPaneDemo... "//Could handle specially.").
Any advice you might be willing to lend? Does your book include an example of this?
Thanks!
Joel
Posted by: wiegmajd on November 08, 2005 at 12:30 PM
-
P.S. - I dug a little deeper, and the reason this Swing Hack doesn't work for JMenuBars is because the GlassPane only has knowledge of MOUSE_ENTERED and MOUSE_EXITED events on itself (which is basically the boundaries of the contentPane). So any component underneath the GlassPane that needs MOUSE_ENTERED and/or MOUSE_EXITED events will no longer receive these events (because the GlassPane has no idea if it has entered or exited anything underneath it).
Posted by: wiegmajd on November 09, 2005 at 05:55 AM
-
There might be another way to get the same result... I've installed a enhanced event queue which does the popup check. Once installed it works on all components.
Toolkit.getDefaultToolkit().getSystemEventQueue().push(new StandardComponentPopupMenu());
In the protected void dispatchEvent(AWTEvent event) the event can be processed.
Posted by: tbee on February 14, 2006 at 01:16 AM
-
wow power leveling
wow powerleveling
wow power leveling
wow gold
wow items
feelingame.com
wow tips
Most Valuable WOW Power Leveling Service
wow power leveling faq
cheap wow power leveling
wow power leveling
wow powerleveling
wow power lvl
Posted by: wowleveling3 on December 13, 2007 at 06:12 PM
|