8

I have a swing component that has several sub components. What I want to do change some label if the mouse is over any of those components, and then change it to something else if the mouse moves off all of the components. I'm trying to find a more efficient way to do this.

Currently I have mouse listeners over all of the child components that look something like:

class AMouseListener extends MouseAdapter {
    private boolean mouseOver;
    mouseEntered(MouseEvent e) { mouseOver = true; updateLabel(); }
    mouseExited(MouseEvent e) { mouseOver = false; updateLabel(); }

    void updateLabel() {
       String text = "not-over-any-components";
       // listeners are each of the listeners added to the child components
       for ( AMouseListener listener :listeners ) {
          if ( listener.mouseOver ) {
             text = "over-a-component";
             break;
          }
       }
    }
}

This works, but I feel like there should be a better way to handle this by only handling mouseEntered and mouseExited events on the parent container, but since the child components intercept these events, I'm not sure how to go about doing this (I don't necessarily have control over the child components so I Can't forward the mouse events to the parent event if I wanted to).

Jeff Storey
  • 56,312
  • 72
  • 233
  • 406
  • Why not assign the same listener to all the necessary components. That way they all trigger the exact same action. – Morfic Jun 18 '12 at 21:52
  • could you maybe expand your code to show the probelm? as i dont quite understand the probelm and your 'wanted' solution – David Kroukamp Jun 18 '12 at 21:52
  • @Grove, if I assign the same listener to each component, there is a potential race depending on whether mouseEntered on one component occurs before or after mouseExited on another. Let's say I'm over component1 and I move the mouse out to component2. If the mouseEntered Component2 is processed before mouseExited Component1, the text will be wrong. I'm not sure if there is a guaranteed order to these events since the same mouse movement would generate exiting component1 and entering component2. – Jeff Storey Jun 18 '12 at 21:58
  • @DavidKroukamp, the problem is that I seem to need a different listener for each component. I was hoping there was a way I could do this with a single listener for the parent component. See my comment to grove above to see if that clarifies at all. – Jeff Storey Jun 18 '12 at 22:00
  • Technically the event queue mechanism should take care of these problems if I'm not mistaking here, so my expectation is to have `mouseExited component1` treated before `mouseEntered component2`. Alternatively, the solution provided below by David could be adapted to provide you with the component under the mouse and your management logic, I guess. – Morfic Jun 18 '12 at 22:05
  • @Grove, if that is a guaranteed order, this should be easy to do. I'm just not sure if it is. – Jeff Storey Jun 18 '12 at 22:13

4 Answers4

8

For example

enter image description here

enter image description here

import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;

public class TestMouseListener {

    public static void main(String[] args) {
        final JComboBox combo = new JComboBox();
        combo.setEditable(true);
        for (int i = 0; i < 10; i++) {
            combo.addItem(i);
        }
        final JLabel tip = new JLabel();
        tip.setPreferredSize(new Dimension(300, 20));
        JPanel panel = new JPanel();
        panel.add(combo);
        panel.add(tip);
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.add(panel);
        frame.pack();
        frame.setVisible(true);
        panel.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseEntered(MouseEvent e) {
                tip.setText("Outside combobox");
            }

            @Override
            public void mouseExited(MouseEvent e) {
                Component c = SwingUtilities.getDeepestComponentAt(
                   e.getComponent(), e.getX(), e.getY());
                // doesn't work if you move your mouse into the combobox popup
                tip.setText(c != null && SwingUtilities.isDescendingFrom(
                   c, combo) ? "Inside combo box" : "Outside combobox");
            }
        });
    }

    private TestMouseListener() {
    }
}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
  • It looks like this is not completely reliable. If there is no gap between the parent (to which the listener is added) and its children, then neither `mouseEntered` nor `mouseExit` are called. – Marcono1234 Sep 02 '18 at 19:45
2

Check out the docs and examples for the "glass pane".
This should give you what you need: The Glass Pane

Bill the Lizard
  • 398,270
  • 210
  • 566
  • 880
davidfrancis
  • 3,734
  • 2
  • 24
  • 21
  • I could use the glass pane for the entire frame, but the parent component is just a JPanel which doesn't have a glass pane. I was trying to keep the solution localized to just that parent component so it could be reused in other parts of the app (in which there may or may not be a glass pane). – Jeff Storey Jun 18 '12 at 22:00
  • Not sure there's much alternative. I would create a support class to install and interact with a glass pane. The panel can then instantiate and interact with this support class, which will install a glass pane in the parent JFrame to capture events for your panel's bounds within that frame. Then the events could be forwarded on to a mouse listener nominated by the panel. So all the hard work would be done by the support class and the panel would just get the events it requires. – davidfrancis Jun 18 '12 at 22:03
  • @Jeff Storey my question is what do you want to achieve, not clear your question, EDIT do you used some of methods from SwingUtilities – mKorbel Jun 18 '12 at 22:07
  • @mKorbel I'm just trying to find a simpler solution so when I add more sub components it still works. See the thread about with David, and if I can guarantee mouseEntered/mouseExited order, then this should be pretty easy to do. – Jeff Storey Jun 18 '12 at 22:12
  • 3
    getDeepestComponentAt | convertMouseEvent – mKorbel Jun 18 '12 at 22:14
  • Ah yes, mKorbel, I think that using getDeepestComponentAt would be helpful here. I wasn't aware of that method. – Jeff Storey Jun 18 '12 at 22:15
2

I know this is very old, but here's a simple solution with which you can create a mouse listener for a component and all components inside it's bounds (without adding the listener to all components individually):

/**
 * Creates an {@link AWTEventListener} that will call the given listener if
 * the {@link MouseEvent} occurred inside the given component, one of its
 * children or the children's children etc. (recursive).
 * 
 * @param component
 *            the component the {@link MouseEvent} has to occur inside
 * @param listener
 *            the listener to be called if that is the case
 */
public static void addRecursiveMouseListener(final Component component, final MouseListener listener) {
    Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {

        @Override
        public void eventDispatched(AWTEvent event) {
            if(event instanceof MouseEvent) {
                MouseEvent mouseEvent = (MouseEvent) event;
                if(mouseEvent.getComponent().isShowing() && component.isShowing()){
                    if (containsScreenLocation(component, mouseEvent.getLocationOnScreen())) {
                        if(event.getID() == MouseEvent.MOUSE_PRESSED) {
                            listener.mousePressed(mouseEvent);
                        }
                        if(event.getID() == MouseEvent.MOUSE_RELEASED) {
                            listener.mouseReleased(mouseEvent);
                        }
                        if(event.getID() == MouseEvent.MOUSE_ENTERED) {
                            listener.mouseEntered(mouseEvent);
                        }
                        if(event.getID() == MouseEvent.MOUSE_EXITED) {
                            listener.mouseExited(mouseEvent);
                        }
                        if(event.getID() == MouseEvent.MOUSE_CLICKED){
                            listener.mouseClicked(mouseEvent);
                        }
                    }
                }
            }
        }
    }, AWTEvent.MOUSE_EVENT_MASK);
}

/**
 * Checks if the given location (relative to the screen) is inside the given component
 * @param component the component to check with
 * @param screenLocation the location, relative to the screen
 * @return true if it is inside the component, false otherwise
 */
public static boolean containsScreenLocation(Component component, Point screenLocation){
    Point compLocation = component.getLocationOnScreen();
    Dimension compSize = component.getSize();
    int relativeX = screenLocation.x - compLocation.x;
    int relativeY = screenLocation.y - compLocation.y;
    return (relativeX >= 0 && relativeX < compSize.width && relativeY >= 0 && relativeY < compSize.height);
}

Note: Once the mouse exits the root component of this listener the mouseExited(mouseEvent) will probably not fire, however you can just add the mouse listener to the root component itself and it should fire.
mouseExited(mouseEvent) is unreliable in general though.

Lahzey
  • 373
  • 5
  • 17
1

You could initiate a single instance of the listener and add that instance to each component. Like this:

AMouseListener aMouseListener=new  AMouseListener();

for each(Component c:components) {
caddMouseListener(aMouseListener);
}
David Kroukamp
  • 36,155
  • 13
  • 81
  • 138
  • David, yes, I could do that. My concern was with the ordering of events. If the mouseExited/mouseEntered behavior can be guaranteed, then this is a simple problem. – Jeff Storey Jun 18 '12 at 22:10
  • Have you tested it? I know its obvious but try it out go over the components and see what it produces. – David Kroukamp Jun 18 '12 at 22:12
  • Sure, but even if it does work, I'm not sure what (if any) guarantees are made about the ordering. – Jeff Storey Jun 18 '12 at 22:12
  • the most you could do then is make the boolean value which says if the mouse is over any of the components a synchronized variable: http://www.javamex.com/tutorials/synchronization_volatile.shtml – David Kroukamp Jun 18 '12 at 22:14
  • Right now I do have a variable to see if it's over any of them (thought it doesn't need to be volatile since they're all accessed on on the EDT) – Jeff Storey Jun 18 '12 at 22:16
  • There should be no for seeable problems as they are on the same thread?.. however Test it using some println()'s and find out whats going on under the hood – David Kroukamp Jun 18 '12 at 22:17
  • I can test for this JVM, but if it's not in the java spec, then I can't depend on it because it may be used with different JVM implementations, and this particular piece of functionality is rather important. – Jeff Storey Jun 18 '12 at 22:19
  • how would the mouseExited method not execute before the mouseEntered method if they are on the same thread and you moved the mouse off the one component to the other? then each instruction because we are using the same instance is proccessed in a procedural way...unless you can move your mouse as fast as superman – David Kroukamp Jun 18 '12 at 22:21
  • I guess that's true. I was thinking that as you are on the border of multiple components that it may generate both the mouse exited and mouse entered (in whichever order it chooses). – Jeff Storey Jun 18 '12 at 22:22
  • Yes if they had their own instances of the listeners(than maybe), not if you use the same initiated listener for all as this is a single listener object. I may be proved wrong though... – David Kroukamp Jun 18 '12 at 22:23
  • 1
    I'll have to do some research on this to find out if there are any guarantees. Otherwise though it looks like using getDeepestComponentAt would be better. Then I can check if I'm over the parent container. While the way you suggested will likely work, I need to be sure it does in all cases. – Jeff Storey Jun 18 '12 at 22:27