Steve Cohen
Steve Cohen

Reputation: 4859

Mouse Clicks being cached?

I have a java swing application with a login screen. The login screen has a submit button for pressing after the user's credentials have been entered. When the button is pressed, the a wait cursor is thrown up over the window using its glass pane. There is also a default mouse adapter that does nothing for any mouse action.

private final static MouseAdapter mouseAdapter =
        new MouseAdapter() {};

/** Sets cursor for specified component to Wait cursor */
public static void startWaitCursor(JComponent component) {
    log.debug("startWaitCursor()");
    RootPaneContainer root =
            ((RootPaneContainer) component.getTopLevelAncestor());
    Component glass = root.getGlassPane();
    glass.setCursor(WAIT_CURSOR);
    glass.addMouseListener(mouseAdapter);
    glass.setVisible(true);
    //force repaint of glass pane in 20ms for more responsive GUI
    glass.repaint(20);
}

public static void stopWaitCursor(JComponent component) {
    log.debug("stopWaitCursor()");
    RootPaneContainer root =
            ((RootPaneContainer) component.getTopLevelAncestor());
    Component glass = root.getGlassPane();

    glass.setCursor(DEFAULT_CURSOR);
    glass.removeMouseListener(mouseAdapter);
    //force repaint of glass pane in 20ms for more responsive GUI
    glass.repaint(20);
    glass.setVisible(false);
}

I had assumed that this setup protected me against multiple clicks/keypresses while the backend methods were taking place. I found out that this was not the case. So in the ButtonListener.actionPerformed, I put some logic like the following:

static boolean waiting = false; 
class ButtonListener implements ActionListener {
      ButtonListener() {
          super();
      }

      public void actionPerformed(ActionEvent e) {      
          log.info("LoginWindow.ButtonListener.actionPerformed()");
          LoginWindow.this.repaint(50);
          if (!waiting) {
              try {
                  waiting = true;
                  verifyLogin();        
              } finally {
                  waiting = false;
              }
          }
      }
}

I found that this protected me against keypresses, but not mouse clicks! If I repeatedly press the submit button while verifyLogin() is executing, the mouse clicks seem to be being cached somewhere, and after verify login finishes, each mouse click is processed!

I am extremely puzzled about what is going on here. Does someone have an idea?

Update:

Hmm, by following the methodology suggested by Cyrille Ka: i.e. executing the verifyLogin() method in a separate thread and disabling the button, I now only get TWO events after multiple mouse clicks but the second one still annoys.

Code is now:

      public void actionPerformed(ActionEvent e) {      
          loginButton.setEnabled(false);
          log.infof("LoginWindow.ButtonListener.actionPerformed(). Event occurred at %1$tb %1$te %1$tY %1$tT.%1$tL",
                  new Date(e.getWhen()));
          LoginWindow.this.repaint(50);
          SwingUtilities.invokeLater( new Runnable() {
            @Override
            public void run() {
                verifyLogin();
                loginButton.setEnabled(true);

            }});
      }

but the second event still gets in. My log shows me that the second event took place about 280 ms after the first, but did not execute until 4 seconds later, in spite of the fact that setEnabled() was the first thing the actionPerformed() event did.

2013-11-13 10:33:57,186 [AWT-EventQueue-0] INFO c.a.r.s.c.g.LoginWindow - LoginWindow.ButtonListener.actionPerformed(). Event occurred at Nov 13 2013 10:33:57.175 2013-11-13 10:34:01,188 [AWT-EventQueue-0] INFO c.a.r.s.c.g.LoginWindow - LoginWindow.ButtonListener.actionPerformed(). Event occurred at Nov 13 2013 10:33:57.453

I suppose I could do a hack and discard events over a second old or something, but that feels ugly. This should not be so difficult, I keep thinking.

Update 2: comment from JComponent.java for setEnabled()

 * <p>Note: Disabling a lightweight component does not prevent it from
 * receiving MouseEvents.

Since all of the Swing components are lightweight, and setEnabled does not prevent the component from receiving mouse events, what does prevent this?

Upvotes: 0

Views: 412

Answers (3)

Steve Cohen
Steve Cohen

Reputation: 4859

This works:

class ButtonListener implements ActionListener {
    long previousEventEnd;
    public void actionPerformed(ActionEvent e) {
        if (e.getWhen() <= previousEventEnd ) {
            log.tracef("discarding stale event, event occurred at  %1$tb %1$te %1$tY %1$tT.%1$tL",
                    new Date(e.getWhen()));
            return;
        }
        log.infof("LoginWindow.ButtonListener.actionPerformed(). Event occurred at %1$tb %1$te %1$tY %1$tT.%1$tL",
                new Date(e.getWhen()));
        LoginWindow.this.repaint(50);
        SwingUtilities.invokeLater( new Runnable() {
            @Override
            public void run() {
                verifyLogin();
                previousEventEnd = System.currentTimeMillis();
            }
        });
    }
}

I have to admit I'm astonished. I usually defend Java to its detractors. Here I have no defense at this point. This should not be necessary.

Upvotes: 0

camickr
camickr

Reputation: 324108

I had assumed that this setup protected me against multiple clicks/keypresses while the backend methods were taking place. I found out that this was not the case.

The section from the Swing tutorial on The Glass Pane gives an example of how you might do this. Don't remember if it only handles MouseEvents or KeyEvents as well.

In any case you can also check out Disabled Glass Pane, which does handle both events.

Upvotes: 1

Cyrille Ka
Cyrille Ka

Reputation: 15523

I presume verifyLogin() is blocking until the login is done. By doing this, you are just blocking the Swing event dispatcher thread. The events from the OS still are queuing to be sent to your GUI when the thread will be available.

There are two ways to prevent your user clicking repeatidly:

  • Just disable the button: button.setEnabled(false); and enable it back when the process is finished.
  • Launch a modal dialog (for example with a wait animation) and remove it when the process is finished.

Edit: In general, you should return quickly from event listeners, since you don't want to block all your GUI, only certain part, and in any case it makes your app feel sluggish (the window won't repaint in the meantime if it is moved or other stuff). Use Thread to launch a task running a verifyLogin() and disable your button in the meantime.

Upvotes: 0

Related Questions