19

can anyone think of a good way to ignore the single click that comes with a double-click in Java ?

I'm looking to have different behaviors for each such that:

  • single-click paints crosshairs on the click point
  • double-click selects an object on the screen, but should not paint crosshairs on the click point

... can anyone think of a way to do this ? Some sort of timer set-up maybe ? An ideas appreciated :-)

<disclaimer> ...and yes, I know I'm committing a most heinous usability / UI faux pas. </disclaimer>

EDIT #2:

Even though this works the delay due to the timer is maddening - I'm abandoning this solution, and using middle-click for selection instead of double-click...

EDIT:

Thanks cgull - this is what I was able to come up with given your confirmation that there's no easy way to do this (note that if I set the timer < 200 odd racing is seen between the click & the timer, but as long as I set this to a value > 200 things work just peachy) :

public void mouseClicked(MouseEvent e) {
    System.out.println( "Click at (" + e.getX() + ":" + e.getY() + ")" );
    if (e.getClickCount() == 2) {  
        System.out.println( "  and it's a double click!");
        wasDoubleClick = true;
    }else{
        Integer timerinterval = (Integer) 
          Toolkit.getDefaultToolkit().getDesktopProperty(
                      "awt.multiClickInterval");
        timer = new Timer(timerinterval.intValue(), new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                if (wasDoubleClick) {
                    wasDoubleClick = false; // reset flag
                } else {
                    System.out.println( "  and it's a simple click!");
                }
            }    
        });
        timer.setRepeats(false);
        timer.start();
    }
}
1
  • how can this work? wasDoubleClick can't be final, and it's used inside another class (the anonymous ActionListener one) Commented Sep 15, 2012 at 10:20

6 Answers 6

16

Indeed you'll need to set up a Timer in your overridden mouseClicked() method of your MouseAdapter to detect the time interval between the two clicks. The default interval in ms can be found by querying Toolkit.getDefaultToolkit().getDesktopProperty("awt.multiClickInterval"). If another mouse click is detected before the timer expires, then you have a double-click, else once the timer expires, you can process the single-click.

Sign up to request clarification or add additional context in comments.

Comments

2

Actually I think there is a simpler solution (use InputEvent's getWhen() method):

class DCListener extends MouseAdapter{

    private long maxTimeBetweenClicks; // in millis
    private long firstClickTime=0;
    private Runnable eventHandler;

    public DCListener(long maxTimeBetweenClicks,Runnable eventHandler){
        this.maxTimeBetweenClicks=maxTimeBetweenClicks;
        this.eventHandler=eventHandler;
    }

    public void mouseClicked(MouseEvent e){

        if((e.getWhen()-firstClickTime)<=maxTimeBetweenClicks){
            firstClickTime=0; // 3 clicks are not 2 double clicks
            eventHandler.run();
        }else{
            firstClickTime=e.getWhen();
        }

    }
}

Comments

1

An alternative solution:

I figured out this before I found the solution in this question. The idea is the same, use a timer, although more complicated :).

Use SwingWorker:

class BackgroundClickHandler extends SwingWorker<Integer, Integer> {
  @Override
  protected Integer doInBackground() throws Exception {
    Thread.sleep(200);
    // Do what you want with single click
    return 0;
  }

}

In mouseClicked() method you can do something like this:

 if (event.getClickCount() == 1) {
  // We create a new instance everytime since
  // the execute() method is designed to be executed once
  clickHandler = new BackgroundClickHandler();

  try {
    clickHandler.execute();
  } catch(Exception e) {
    writer.println("Here!");
  }
}

if (event.getClickCount() == 2) {
  clickHandler.cancel(true);

  //Do what you want with double click
}

I hope it be useful.

Comments

1

Looking up the answer to this question I was thinking about a different solution. Couldn't find it so I share my thoughts:

The delay the other solutions introduce in the single click processing are a little painful because you get this delayed response. However those solutions are the simplest to implement and don't bother the rest of the code.

The other option is:

All events that a single click can produce are Undoable and are undone once an asociated double click event arrives.

Implementing Undo is an interesting topic, but for simplicity I will give a very basic example instead:

import javafx.fxml.FXML;
import javafx.scene.input.MouseEvent;

class SomeController {

    private static final long MAX_MULTICLICK_INTERVAL = 200; //the milliseconds delay
    private long lastSingleClickTime = 0;

    private int singleClickCounter = 0, doubleClickCounter = 0;

    private void singleClick(MouseEvent event) {
        singleClickCounter++;
    }

    private void undoSingleClick(MouseEvent event) {
        singleClickCounter--;
    }

    private void doubleClick(MouseEvent event) {
        doubleClickCounter++;
    }

    @FXML
    private void onMouseClicked(MouseEvent event) {
        if (event.getClickCount() == 2) {
            if (System.currentTimeMillis() <= lastSingleClickTime + MAX_MULTICLICK_INTERVAL) {
                undoSingleClick(event);
                lastSingleClickTime = 0; //if another double-click arrived we don't want to undo anything
            }
            doubleClick(event);
        } else if (event.getClickCount() == 1) {
            lastSingleClickTime = System.currentTimeMillis(); //in AWT use the event.getWhen() method instead
            singleClick(event);
        } // else: might swallow button clicks with clickCount outside 1 and 2 which is ok for me
        System.out.printf("Single clicks: %d, double clicks: %d\n", singleClickCounter, doubleClickCounter);
    }
}

Note that primary, secondary and middle button events are mixed for simplicity here. You should track a lastSingleClickTime for each of the buttons or limit the processing to the primary button only, since that is usually the only button using double-clicks anyway.

With a data model implementing Undo one would remember the Action the single click fired (let that be a class having a run() and undo() method doing opposite things) and call .undo() on that Action instance when the double click event arrived. There are pitfalls of course. But that would be a long chapter.

Comments

1

Thanks to this thread I made this simillar solution, that is the way I would imagine it. It keeps a Timer thread (where single click events are scheduled with a 200ms delay) alive all the time - this could upset someone, but afterall starting a thread is the expensive stuff, isn't it?

Track of the last TimerTask is kept and it's cancel() method is called if a double click arrives. A double click cancels its single click if it arrives within 200ms, or has no effect if late.

I use JavaFX, but that should not matter. The core stuff is the Timer and TimerTask.

import java.util.Timer;
import java.util.TimerTask;
import javafx.fxml.FXML;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.MouseButton;

class SomeController {
    private static final long MAX_MULTICLICK_INTERVAL = 200; //the milliseconds delay
    private final Timer singleLeftClickTimer = new Timer(true); //isDeamon = true, will be terminated with the rest of the app
    private TimerTask lastSingleClickTask;

    private void onMouseClickedFiltered(MouseEvent event) {
        //TODO process clicks
        //For the primary button you get only single clicks that were not followed by double clicks within 200ms
        //Double clicks and all clicks of other buttons go through unchanged
    }

    @FXML
    private void onMouseClicked(MouseEvent event) {
        if (event.getButton() != MouseButton.PRIMARY) {
            onMouseClickedFiltered(event);
        } else if (event.getClickCount() == 2) {
            lastSingleClickTask.cancel(); //if 200ms passed from associated single click, this will have no effect
            onMouseClickedFiltered(event);
        } else if (event.getClickCount() == 1) {
            lastSingleClickTask = new TimerTask() {
                @Override
                public void run() {
                    onMouseClickedFiltered(event);
                }
            };
            singleLeftClickTimer.schedule(lastSingleClickTask, MAX_MULTICLICK_INTERVAL);
        } // else: might swallow primary button clicks with clickCount outside 1 and 2 which is ok for me
    }
}

EDIT: I use this myself and every now and then I get an IllegalStateException("Timer already cancelled.") from the Timer class, though I only cancel the TimerTask, not the Timer. A workaroud is to use a new Timer for each TimerTask, which I don't like.

Comments

0

Have you tried implementing the MouseListener interface already?

I think MouseEvent has a click count method ( or property ) to know that.

I bet you have gone through that already, so what is the problem you're facing there?

Probably what you can do is to code the time interval elapsed between a single and a double click with a thread or something.

So a single click will only be valid if another click is not issued in let's say 300 ms. ( something configurable )

The idea is:

public void listen for the single click() 
    if (  in x time there has not been another click  ) 
    then 
        we have a single click
        proceed with your single click handling
    else 
        we have a double click
       proceed with your double click handling

Or something like that.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.