Waiting for parameter to be valued in Java - java

I recently starting to learn JAVA and I tried to create a little shell-like program (I don't rely on system shell for executing command).
I managed to get the basic I/O thing to work but I'm stuck in the following situation :
Let's say I use the command "makeFile path/to/file" the command will check if the exists and ask "File already exists ! Do you want to erase it ? Y/N"
My issue is to wait for the user inputting Y, N or anything else without locking the shell interface (A JTextArea).
import java.util.StringTokenizer;
public abstract class Command {
private final String bin;
protected Shell shell;
public Command(Shell shell, final String bin) {
this.shell = shell;
this.bin = bin;
}
String getBin() {
return this.bin;
}
protected String ask(String question) {
shell.setQuestionAsked(true);
shell.setResponse("");
shell.write(question);
String response = shell.getResponse();
while(response.isEmpty()) {
response = shell.getResponse();
}
shell.setQuestionAsked(false);
return response;
}
public abstract void execute(StringTokenizer stringTokenizer);
}
I tried to find solution in Concurrency / Threading but can't find a solution.
In the light #Holger comment, this is the GUI Code part, as you can see a Listener already exists. My issue lay in the fact that when a command ask an user input with the ask method shown above the execution will not wait for the user input and just go on or in the current the while(response.isEmpty()) cause a deadlock.
So I'm looking for a solution to hold the ask() method's execution until the user press enter in the GUI.
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
public class ShellPanel extends Shell {
private JTextArea shellArea;
private JPanel panel;
private JScrollPane scrollPanel;
private int bufferLength = 0;
private String oldText = "";
ShellPanel() {
shellArea.addKeyListener(new KeyAdapter() {
#Override
public void keyPressed(KeyEvent e) {
super.keyPressed(e);
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
case KeyEvent.VK_DOWN:
e.consume();
break;
case KeyEvent.VK_LEFT:
if (shellArea.getCaretPosition() <= bufferLength) {
e.consume();
}
break;
case KeyEvent.VK_BACK_SPACE:
if (shellArea.getText().length() <= bufferLength) {
e.consume();
}
break;
case KeyEvent.VK_DELETE:
break;
case KeyEvent.VK_ENTER:
read(getNewInput());
updateReferences();
e.consume();
break;
case KeyEvent.VK_HOME:
shellArea.setCaretPosition(bufferLength);
break;
}
}
});
shellArea.addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
super.mouseClicked(e);
switch (e.getButton()) {
case MouseEvent.BUTTON3:
String selected = shellArea.getSelectedText();
StringSelection selection = new StringSelection(selected);
if (!selected.isEmpty()) {
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection);
}
try {
String clip = (String) Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor);
if (!clip.isEmpty()) {
shellArea.append(clip);
}
} catch (UnsupportedFlavorException | IOException e1) {
e1.printStackTrace();
}
break;
}
}
});
init();
updateReferences();
}
JPanel getPanel() {
return panel;
}
private void updateReferences() {
oldText = shellArea.getText();
bufferLength = oldText.length();
shellArea.setCaretPosition(bufferLength);
}
#Override
public void write(String content) {
shellArea.append(content);
updateReferences();
}
private String getNewInput() {
return this.shellArea.getText().replace(this.oldText, "");
}
#Override
public void clear() {
this.shellArea.setText("");
}
}
Thanks

Generally, you should avoid program logic that has to wait for a UI event instead of letting the appropriate event listener trigger the subsequent action.
The wait logic is similar to opening a modal dialog, whereas the opening method will return when the dialog has been closed. In the case of JOptionPane.showInputDialog, it will even return the entered value. This logic has been abstracted in Java 7 to allow arbitrary “wait for an event” scenarios. Here is a self-contained example:
public class WaitForInput {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame=new JFrame("Example");
JTextArea ta=new JTextArea(20,40);
ta.setEditable(false);
frame.setContentPane(new JScrollPane(ta));
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
char result = waitForInput(ta, "Press Y or N",
ch -> Character.toUpperCase(ch)=='Y' || Character.toUpperCase(ch)=='N');
waitForInput(ta,
"You pressed "+result+", press Enter to continue", ch -> ch==10);
ta.append("Ok, I'm happy... Close window to quit");
});
}
static char waitForInput(JTextArea ta, String message, IntPredicate p) {
ta.append(message);
ta.append("\n");
SecondaryLoop loop = ta.getToolkit().getSystemEventQueue().createSecondaryLoop();
final class WaitOp extends KeyAdapter {
char actualChar;
public void keyTyped(KeyEvent e) {
if(p.test(e.getKeyChar())) {
actualChar=e.getKeyChar();
loop.exit();
}
}
}
WaitOp op = new WaitOp();
ta.addKeyListener(op);
loop.enter();
ta.removeKeyListener(op);
return op.actualChar;
}
}
But it must be emphasized that unlike a modal dialog, there is no visual indicator that there is an action that has stopped in the middle, waiting for a particular input (unless you program it yourself). Also, the rest of the UI isn’t blocked and could spawn new actions that could also stop in the middle, waiting for a different event, bringing the program in an unmaintainable state.
That’s why this should be avoided whenever possible. But if you restrict its use to a maintainable minimum, it can be quite useful.

Related

MouseHook Deletes Text on Mac OS X

I'm working on a Java FX application that involves using a moushook to capture text selected with the mouse. This was worked without problems when compiled and run with Java 8, but I'm currently required to use Java 10, which has caused an issue to arise on Mac OS X (there is no problem when run on Windows 10).
The problem is that when text is selected from a document (e.g. text in TexEdit) the selected text is deleted and replaced with 'c', rather than being copied. This doesn't happen every time, but does happen very frequently.
I've stripped the mouseHook class down to a very basic test case that shows the problem:
MouseHook class ---------------------------------------------------------
package problem;
import org.jnativehook.GlobalScreen;
import org.jnativehook.mouse.NativeMouseEvent;
import org.jnativehook.mouse.NativeMouseInputListener;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.KeyEvent;
import java.util.Timer;
import java.util.TimerTask;
public class MouseHook implements NativeMouseInputListener
{
private static MouseHook mouseHook = null;
private Robot mouseRobot = null;
private boolean mouseLeftButtonDown = false;
private boolean dragged = false;
private MouseHook()
{
//disable console output from JNativeHook's internal logger (mouse-tracking, etc)
java.util.logging.Logger logger = java.util.logging.Logger.getLogger(GlobalScreen.class.getPackage().getName());
logger.setLevel(java.util.logging.Level.OFF);
}
public static synchronized MouseHook getInstance()
{
if(mouseHook == null)
{
mouseHook = new MouseHook();
GlobalScreen.addNativeMouseListener(mouseHook);
GlobalScreen.addNativeMouseMotionListener(mouseHook);
}
return mouseHook;
}
#Override
public void nativeMousePressed(NativeMouseEvent e)
{
//If left button pressed, set mouseLeftButtonDown and look-out for dragging
if(e.getButton() == NativeMouseEvent.BUTTON1)
{
mouseLeftButtonDown = true;
}
}
#Override
public void nativeMouseDragged(NativeMouseEvent e)
{
//only record a drag if left mouse button is held down.
//NativeMouseEvent doesn't say which button was the source, hence test the variable.
if(mouseLeftButtonDown)
{
dragged = true;
}
}
#Override
public void nativeMouseReleased(NativeMouseEvent e)
{
//copy text if the mouse was dragged while the left mouse button was held down.
if(e.getButton() == NativeMouseEvent.BUTTON1)
{
final Timer timer = new Timer("ReleaseDetect");
timer.schedule(new TimerTask()
{
#Override
public void run()
{
if(dragged)
{
copy();
dragged = false;
}
mouseLeftButtonDown = false;
timer.cancel();
}
}, 500);
}
}
#Override
public void nativeMouseClicked(NativeMouseEvent e)
{}
#Override
public void nativeMouseMoved(NativeMouseEvent e)
{}
private synchronized void copy()
{
try
{
if(mouseRobot == null)
{
mouseRobot = new Robot();
mouseRobot.setAutoDelay(100);
mouseRobot.setAutoWaitForIdle(true);
}
if(System.getProperty("os.name").toLowerCase().contains("mac"))
{
mouseRobot.keyPress(KeyEvent.VK_META);
mouseRobot.keyPress(KeyEvent.VK_C);
mouseRobot.keyRelease(KeyEvent.VK_C);
mouseRobot.keyRelease(KeyEvent.VK_META);
}
else
{
mouseRobot.keyPress(KeyEvent.VK_CONTROL);
mouseRobot.keyPress(KeyEvent.VK_C);
mouseRobot.keyRelease(KeyEvent.VK_C);
mouseRobot.keyRelease(KeyEvent.VK_CONTROL);
}
Thread.sleep(250);
Transferable t=Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
if(t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor))
{
String activeContent = (String) t.getTransferData(DataFlavor.stringFlavor);
System.out.println(activeContent);
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
Class to run the example--------------------------------------------------------
package problem;
import org.jnativehook.GlobalScreen;
import org.jnativehook.NativeHookException;
public class Runnit
{
private static MouseHook mouseHook;
public static void main(String[] args)
{
try
{
GlobalScreen.registerNativeHook();
}
catch(NativeHookException e)
{
System.out.println("Cannot set up the NativeHook listener on the GlobalScreen");
}
mouseHook = MouseHook.getInstance();
}
}
I'm using Oracle's Java 10.0.2 and jNativeHook 2.1.0, with Mac OS High Sierra (10.13.6) on a Mac mini.
The problem seems to be to do this java.awt.Robot, rather than jNativeHook.
e.g. If I replace the VK_C keyPress/Release with VK_B the mousehook deletes selected text and replaces it with 'b'.
My gut feeling was that the VK_META keypress was being ignored. I tried putting in a delay before that keypress, but it makes no difference.
I've also tried using a different mouse, and someone else's Macintosh, but the problem still happens. As I said, it only happens on Mac OS X, not Windows 10, when the code is compiled and run with Java 10.
I would be grateful if anyone has any ideas as to whether this is a lost cause, or if there is a work-around. It may be that I've missed something obvious, as I normally work on Windows and have lsss familiarity with Mac OS X.

Using multi-threading to pause my application does not behave as expected

I am designing a matching memory game, I am almost done with it and everything is working as it should be, however, when the user has opened two different cards the program won't pause(wait) few seconds so the user can see what the second card was.
I have tried using a long for loop operation but encountered the same problem. I have tried Thread.sleep, TimeUnit.SECONDS.sleep, Task and Platform.runLater.
The program opens the card and closes it instantly THEN it waits for the specified duration, keeping in mind that I am calling pauseThread after open and before close functions.
I have tried the above suggestions but they are leading me no where and I can't seem to find where the problem is with my code or where should I place the pauseThread. Thanks in advance.
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class MemoryMatchingGame extends Application{
private static Card selectedCard=null; // This is to save a reference for the first card to use in comparison
private static int numOfCorrectPairs = 0; // Keeping track of how many cards the user got correct
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
String[] images = {"C:\\Users\\userName\\Desktop\\Project#4\\1.png", // This is a string array to store images locations
"C:\\Users\\userName\\Desktop\\Project#4\\2.png",
"C:\\Users\\userName\\Desktop\\Project#4\\3.jpg",
"C:\\Users\\userName\\Desktop\\Project#4\\4.jpg",
"C:\\Users\\userName\\Desktop\\Project#4\\5.jpg",
"C:\\Users\\userName\\Desktop\\Project#4\\6.png",
"C:\\Users\\userName\\Desktop\\Project#4\\7.jpg",
"C:\\Users\\userName\\Desktop\\Project#4\\8.jpg"};
ArrayList<Card> listOfCards = new ArrayList<Card>();
for(int i=0; i<images.length; i++) { // This for loop will add each image twice to the array list
listOfCards.add(new Card(images[i]));
listOfCards.add(new Card(images[i]));
}
Collections.shuffle(listOfCards); // Shuffling the deck of cards
primaryStage.setTitle("Memory Matching Game");
HBox hb = new HBox();
VBox firstColoumn = new VBox();
for(int i=0; i<4; i++)
firstColoumn.getChildren().add(listOfCards.get(i));
VBox secondColoumn = new VBox();
for(int i=4; i<8; i++)
secondColoumn.getChildren().add(listOfCards.get(i));
VBox thirdColoumn = new VBox();
for(int i=8; i<12; i++)
thirdColoumn.getChildren().add(listOfCards.get(i));
VBox fourthColoumn = new VBox();
for(int i=12; i<16; i++)
fourthColoumn.getChildren().add(listOfCards.get(i));
hb.getChildren().addAll(firstColoumn, secondColoumn, thirdColoumn, fourthColoumn);
Scene scene = new Scene(hb, 460, 450);
primaryStage.setScene(scene);
primaryStage.show();
}
private class Card extends Button {
private String imageLocation; // To store the destination of the image
private Image img; // To store a reference of the image to be used when setting graphic on a button
public Card(String imageLocation) throws FileNotFoundException {
this.imageLocation = imageLocation;
FileInputStream fis = new FileInputStream(imageLocation);
img = new Image(fis);
setPrefSize(150, 150);
setOnMouseClicked(e -> {
if(isCardOpen()==true)
return; // To ensure no action is made once an image is already opened and the user clicked on it again
if(selectedCard==null) {// This will test if the user has a card open already for comparison or not, if not it will store a reference to the card to use to compare once another card is opened
selectedCard = this;
open();
}
else { // If we enter this statement, this means the user has a card open already and we are ready to perform comparison
open(); // First action taken is to reveal the second card then perform comparison
if(this.isEqual(selectedCard)) {
numOfCorrectPairs++;
System.out.println("Got one");
}
else {
//Get program to pause here
Hold pauseThread = new Hold();
pauseThread.run();
System.out.println("After pausing");
this.close();
selectedCard.close();
}
selectedCard=null; // This will nullify the variable so that we are able to perform comparison again for two other cards
} // End of else statement
}); // End of actionHandler
close(); // This will ensure whenever a card is created it is set face-down
}
private void close() {
setGraphic(null);
}
public void open() {
setGraphic(new ImageView(img));
System.out.println("Open");
}
private boolean isCardOpen() {
return this.getGraphic()!=null;
}
private boolean isEqual(Card selectedCard) {
return this.imageLocation.equals(selectedCard.imageLocation);
}
}
private class Hold extends Thread{
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Everything in your code is running in the JavaFX Application Thread. You don't want to pause this thread because it will lock your GUI. As has already been mentioned, you are starting another thread and putting it to sleep, but this doesn't add delay to your GUI that is running in the JavaFX Thread.
An alternative approach would be to use Platform.runLater(). The Hold thread can invoke a method in the JavaFX thread that implements a Platform.runLater() runnable. The runnable is a short lambda that holds the code to close the selected card. The timing may vary slightly from 3000 ms, but you don't have much going on in the JavaFX thread and it doesn't seem critical for this application.
Here are the modifications to try.
First modify the Hold class to include a constructor to pass in the Card object. Then call the closeAfterPause() method on card.
private class Hold extends Thread {
private Card card;
public Hold(Card card) {
this.card = card;
}
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
card.closeAfterPause();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Then create the closeAfterPause() method in the MemoryMatchingGame class.
private void closeAfterPause() {
Platform.runLater(() -> {
System.out.println("After Pausing");
close();
selectedCard.close();
});
}
Then modify the else part of your if-else statement as follows
else {
//Get program to pause here
Hold pauseThread = new Hold(this);
new Thread(pauseThread).start();
}
FX comes with a rich set of Animation/Timeline support - there's no need for falling back onto bare Threads. The most simple form of getting wait-for-xx is to use a Timeline configured with xx and an actionHandler that does something when ready:
Timeline holdTimer = new Timeline(new KeyFrame(
Duration.seconds(2), e -> closeCards()));
Also it's a good idea to centralize all logic outside of a Control. Actually, you should never-ever extend a view for the purpose to include view-unrelated logic. So your long-time goal should be to
extract all single card logic from Card into a CardModel which exposes properties like f.i. image, id, open, disposed
use a plain Button, configure and bind its properties to model properties as appropriate
centralize all game logic like timing, selecting, when opening is allowed, when succeed into a game controller
As I don't want to spoil your fun in doing that - I'll just post a little outline in the direction of the last bullet. Its responsibilities so far
provide api to open a single card
provide api to end a turn: either match or close cards
internals to keep track of opened cards and timing
The snippets just re-mixes your code a bit, moving game logic from the button into the controller (aka: here simply the outer class) and setting the button's action handler to access the controller.
private Card firstCard;
private Card secondCard;
private Timeline holdTimer = new Timeline(new KeyFrame(
Duration.millis(2000), e -> closeCards()));
public void closeCards() {
if (firstCard == null || secondCard == null) {
System.out.println("error!!");
return;
}
if (firstCard.isEqual(secondCard)) {
System.out.println("success");
firstCard.setDisable(true);
secondCard.setDisable(true);
firstCard = null;
secondCard = null;
} else {
firstCard.close();
secondCard.close();
firstCard = null;
secondCard = null;
}
}
public void openCard(Card card) {
if (card.isCardOpen()) return;
if (holdTimer.getStatus() == Status.RUNNING) return;
if (firstCard == null) {
firstCard = card;
firstCard.open();
} else if (secondCard == null) {
secondCard = card;
secondCard.open();
holdTimer.playFromStart();
}
}
// Dont! dont, dont!!! ever extend a Control
//**TBD**: Move open/close state logic into a CardModel
// then configure a plain Button with the properies of that model
private class Card extends Button {
private String imageLocation; // To store the destination of the image
// private Image img; // To store a reference of the image to be used when setting graphic on a button
public Card(String imageLocation) throws FileNotFoundException {
this.imageLocation = imageLocation;
setPrefSize(150, 150);
setOnAction(e -> openCard(this));
}
public void close() {
setText("");
}
public void open() {
setText(imageLocation);
System.out.println("Open");
}
public boolean isCardOpen() {
return getText() != null && getText().length() > 0;//this.getGraphic()!=null;
}
private boolean isEqual(Card selectedCard) {
if (selectedCard == null) return false;
return this.imageLocation.equals(selectedCard.imageLocation);
}
}

Java Swing Dialog, how to call wait method when capturing WindowClosing event

I have a JRuby script that opens a Java dialog to report on the progress of the script.
I am capturing a windowclosing event, and want the dialog to wait until some cleanup in the JRuby script has occurred, and then dispose. Instead, the dialog just hangs when the user presses the top right red x button.
How do I correctly call the wait method to wait on that flag change? Am I using the lock object correctly?
A jruby script calls this dialog.
If a user presses the top right red X, the dialog captures the windowclosing event and sets a 'cancelled' flag.
The script keeps an eye on that flag, then starts shutting down some long running tasks and what not . When done, it updates a flag on the dialog to say cleanup has occurred.
Meanwhile, the dialog is looping, waiting on that flag to change. Then it calls dispose().
I've tried using sleep. For some reason, that upsets something between my JRuby and dialog, cleanup occurs okay, but the dialog does not dispose.
Using wait, with synchronize(this) generates a IllegalMonitorException, but the script cleans up and the dialog does dispose correctly apart from the exception.
Have looked at a bunch of other posts on how to synchorize the wait method, would very much like to understand this.
Thanks very much for any assistance.
Dialog class as follows:
import javax.swing.*;
import java.awt.*;
public class MyDialog extends JDialog {
private boolean userCancelled;
private boolean scriptCleanedUp;
//private static Object lock = new Object();
public MyDialog(lock) {
userCancelled = false;
scriptCleanedUp = false;
setDefaultCloseOperation(2);
//[..] add various controls to dialog
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent we) {
// the jruby script keeps an eye on this flag to see if the user has cancelled the dialog
userCancelled = true;
/* once cancelled, wait for script to flag that it has performed its cleanup
*/
/* here is the problem area, what do I need to synchronize to use the wait method?
*/
while (!scriptCleanedUp) {
try {
synchronized (lock) {
lock.wait(1000000000);
}
// Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
dispose();
}
});
super.paint(super.getGraphics());
}
public boolean user_cancelled() { return userCancelled; }
public void setScriptCleanedUpToTrue() { this.scriptCleanedUp = true; }
public static void forBlock(MyDialogBlockInterface block)
{
MyDialog dialog = new MyDialog(new Object());
dialog.setVisible(true);
block.DoWork(dialog);
dialog.dispose();
}
}
And if it helps, this is how the JRuby script calls the dialog
MyDialog.forBlock do |dialog|
#do long running jruby task here
end
You've got a lot of problems with that code including:
You're making long-running calls on the Swing event thread, something that will tie up this critical thread and is thus guaranteed to freeze your GUI.
Your calling paint(...) directly, something that should almost never be done.
I'll bet that most of your problem could be called by just making sure that your dialog is a modal JDialog, if you make long-running calls in a background thread such as with a SwingWorker, and rather than trying to wait for a lock to be released, use a call-back mechanism to notify the dialog to shut itself down.
For example:
import java.awt.Dialog.ModalityType;
import java.awt.Dimension;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.*;
/**
* http://stackoverflow.com/a/29933423/522444
* #author Pete
*
*/
#SuppressWarnings("serial")
public class TestMyDialog2 extends JPanel {
private static final int PREF_W = 400;
private static final int PREF_H = PREF_W;
public TestMyDialog2() {
add(new JButton(new MyDialogAction("Please press this button!", this)));
}
#Override
public Dimension getPreferredSize() {
if (isPreferredSizeSet()) {
return super.getPreferredSize();
}
// let's make this reasonably big
return new Dimension(PREF_W, PREF_H);
}
private static void createAndShowGui() {
TestMyDialog2 mainPanel = new TestMyDialog2();
JFrame frame = new JFrame("TestMyDialog2");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
#SuppressWarnings("serial")
class MyDialogAction extends AbstractAction {
private JDialog dialog;
private MyWorker myWorker;
private TestMyDialog2 testMyDialog2;
public MyDialogAction(String name, TestMyDialog2 testMyDialog2) {
super(name);
int mnemonic = (int) name.charAt(0);
putValue(MNEMONIC_KEY, mnemonic);
this.testMyDialog2 = testMyDialog2;
}
public void dialogIsClosing(WindowEvent e) {
if (myWorker != null && !myWorker.isDone()) {
myWorker.setKeepRunning(false);
} else {
if (dialog != null && dialog.isVisible()) {
dialog.dispose();
}
}
}
#Override
public void actionPerformed(ActionEvent e) {
Window mainGui = SwingUtilities.getWindowAncestor(testMyDialog2);
dialog = new JDialog(mainGui, "My Dialog", ModalityType.APPLICATION_MODAL);
dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
dialog.add(Box.createRigidArea(new Dimension(200, 100)));
dialog.addWindowListener(new DialogWindowListener(this));
dialog.pack();
myWorker = new MyWorker();
myWorker.addPropertyChangeListener(new MyWorkerListener(dialog));
myWorker.execute();
dialog.setLocationRelativeTo(mainGui);
dialog.setVisible(true);
}
}
class MyWorker extends SwingWorker<Void, Void> {
private volatile AtomicBoolean keepRunning = new AtomicBoolean(true);
#Override
protected Void doInBackground() throws Exception {
// to emulate long-running code
while (keepRunning.get()) {
Thread.sleep(200);
System.out.println("Long running background code is running");
}
System.out.println("Doing shut-down process. Will close in 10 seconds");
for (int i = 0; i < 10; i++) {
System.out.println("Countdown: " + (10 - i));
Thread.sleep(1000); // emulate a long running shut-down process
}
return null;
}
public void setKeepRunning(boolean newValue) {
this.keepRunning.getAndSet(newValue);
}
}
class MyWorkerListener implements PropertyChangeListener {
private JDialog dialog;
public MyWorkerListener(JDialog dialog) {
this.dialog = dialog;
}
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
dialog.dispose();
try {
((MyWorker) evt.getSource()).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
class DialogWindowListener extends WindowAdapter {
private MyDialogAction myDialogAction;
public DialogWindowListener(MyDialogAction myDialogAction) {
this.myDialogAction = myDialogAction;
}
#Override
public void windowClosing(WindowEvent e) {
myDialogAction.dialogIsClosing(e);
}
}

JButton stays pressed when focus stolen by JOptionPane

I am new to Swing and I have a situation. I am designing an application that renders the GUI components dynamically based on an xml file input(meta-data) . Now most of my JTextFields have InputVerifier set to them, for validation purpose. The input verifier pops up JOptionPane whenever there is an invalid input.
Now, if a user enter an invalid data and moves ahead and clicks a button on the Panel, then a dialog pops up and the user have to respond to it. but after that also the button does not paint to release state. It still looked like it is pressed but actually it is not. As the whole code is pretty messy, I am putting the problem scenario in the code below:-
What should I do so that the JButton looks unpressed? I would appreciate if the logic is also explained.
Thanks in advance.
package test;
import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
public class VerifierTest extends JFrame {
private static final long serialVersionUID = 1L;
public VerifierTest() {
JTextField tf;
tf = new JTextField("TextField1");
getContentPane().add(tf, BorderLayout.NORTH);
tf.setInputVerifier(new PassVerifier());
final JButton b = new JButton("Button");
b.setVerifyInputWhenFocusTarget(true);
getContentPane().add(b, BorderLayout.EAST);
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (b.hasFocus())
System.out.println("Button clicked");
}
});
addWindowListener(new MyWAdapter());
}
public static void main(String[] args) {
Frame frame = new VerifierTest();
frame.setSize(400, 200);
frame.setVisible(true);
//frame.pack();
}
class MyWAdapter extends WindowAdapter {
public void windowClosing(WindowEvent event) {
System.exit(0);
}
}
class PassVerifier extends InputVerifier {
public boolean verify(JComponent input) {
JTextField tf = (JTextField) input;
String pass = tf.getText();
if (pass.equals("Manish"))
return true;
else {
String message = "illegal value: " + tf.getText();
JOptionPane.showMessageDialog(tf.getParent(), message,
"Illegal Value", JOptionPane.ERROR_MESSAGE);
return false;
}
}
}
}
The method verify is actually not a good place to open a JOptionPane.
There are several approaches you could consider to solve your problem:
You want this JOptionPane to appear everytime the textfield looses the focus and the input is incorrect: use a FocusListener on the JTextField and act upon appropriate events
You want this JOptionPane to appear everytime the buttons is pressed: use your ActionListener to do it if the input is incorrect.
Here is a small snippet of the latter option:
import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
public class VerifierTest extends JFrame {
private static final long serialVersionUID = 1L;
public VerifierTest() {
final JTextField tf = new JTextField("TextField1");
getContentPane().add(tf, BorderLayout.NORTH);
tf.setInputVerifier(new PassVerifier());
final JButton b = new JButton("Button");
b.setVerifyInputWhenFocusTarget(true);
getContentPane().add(b, BorderLayout.EAST);
b.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (!tf.getInputVerifier().verify(tf)) {
JOptionPane.showMessageDialog(tf.getParent(), "illegal value: " + tf.getText(), "Illegal Value",
JOptionPane.ERROR_MESSAGE);
}
if (b.hasFocus()) {
System.out.println("Button clicked");
}
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
}
public static void main(String[] args) {
Frame frame = new VerifierTest();
frame.setSize(400, 200);
frame.setVisible(true);
}
class PassVerifier extends InputVerifier {
#Override
public boolean verify(JComponent input) {
final JTextField tf = (JTextField) input;
String pass = tf.getText();
return pass.equals("Manish");
}
}
}
Also consider setting the default close operation of the JFrame instead of adding a window listener (but it is a good approach to use a WindowListener if you want to pop up a dialog asking the user if he is sure he wants to exit your application).
I added a call to SwingUtilities to ensure that the GUI is on the event thread, and I removed your reference to Frame.
The GUI works for me on Windows XP.
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
public class VerifierTest implements Runnable {
private static final long serialVersionUID = 1L;
public VerifierTest() {
}
#Override
public void run() {
JFrame frame = new JFrame();
frame.setSize(400, 200);
JTextField tf;
tf = new JTextField("TextField1");
tf.setInputVerifier(new PassVerifier());
frame.getContentPane().add(tf, BorderLayout.NORTH);
final JButton b = new JButton("Button");
b.setVerifyInputWhenFocusTarget(true);
frame.getContentPane().add(b, BorderLayout.EAST);
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (b.hasFocus())
System.out.println("Button clicked");
}
});
frame.addWindowListener(new MyWAdapter());
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new VerifierTest());
}
class MyWAdapter extends WindowAdapter {
#Override
public void windowClosing(WindowEvent event) {
System.exit(0);
}
}
class PassVerifier extends InputVerifier {
#Override
public boolean verify(JComponent input) {
JTextField tf = (JTextField) input;
String pass = tf.getText();
if (pass.equals("Manish"))
return true;
else {
String message = "illegal value: " + tf.getText();
JOptionPane.showMessageDialog(tf.getParent(), message,
"Illegal Value", JOptionPane.ERROR_MESSAGE);
return false;
}
}
}
}
I have added a new mouse listener to the button as below and its seems to be working fine for me now, but I am not sure if it is a good way of rectifying the buttons selection state.
package test;
import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.plaf.basic.BasicButtonListener;
public class VerifierTest extends JFrame {
private static final long serialVersionUID = 1L;
public VerifierTest() {
JTextField tf;
tf = new JTextField("TextField1");
getContentPane().add(tf, BorderLayout.NORTH);
tf.setInputVerifier(new PassVerifier());
final JButton b = new JButton("Button");
b.setVerifyInputWhenFocusTarget(true);
getContentPane().add(b, BorderLayout.EAST);
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (b.hasFocus())
System.out.println("Button clicked");
}
});
b.addMouseListener(new BasicButtonListener(b) {
#Override
public void mouseExited(MouseEvent e) {
((JButton)e.getSource()).getModel().setArmed(false);
((JButton)e.getSource()).getModel().setPressed(false);
}
});
addWindowListener(new MyWAdapter());
}
public static void main(String[] args) {
Frame frame = new VerifierTest();
frame.setSize(400, 200);
frame.setVisible(true);
// frame.pack();
}
class MyWAdapter extends WindowAdapter {
public void windowClosing(WindowEvent event) {
System.exit(0);
}
}
class PassVerifier extends InputVerifier {
public boolean verify(JComponent input) {
JTextField tf = (JTextField) input;
String pass = tf.getText();
if (pass.equals("Manish"))
return true;
else {
final String message = "illegal value: " + tf.getText();
JOptionPane.showMessageDialog(null, message,
"Illegal Value", JOptionPane.ERROR_MESSAGE);
return false;
}
}
}
}
First: all implementations of InputVerifier which open the dialog in verify() are invalid. They violated their contract, API doc:
This method should have no side effects.
with the "should" really meaning "must not". The correct place for side-effects is shouldYieldFocus.
Second: moving the side-effect (showing the message dialog) correctly into the shouldYieldFocus doesn't work as well ... due to a bug (THEY call it feature request ;-), that's older than a decade and in the top 10 RFEs
Being a hack-around a bug, #dareurdrem's mouseListener is as good as any workable hack can get :-)
Update
After playing a bit with different options to hack around the bug, here's another hack - it's as brittle as all hacks are (and doesn't survive a LAF toggle, has to be re-installed if dynamic toggling is required)
For hacking the mouse behaviour the basic approach is to hook into the listener installed by the ui:
find the original
implement a custom listener which delegates most events directly to the original
for pressed events request focus first: if yielded delegate to original, if not do nothing
The last bullet is slightly more involved because focus events can be asynchronous, so we have to invoke the check for being focused. Invoking, in turn, requires to send a release in case nobody objected.
Another quirk is the rootPane's pressed action (for its defaultButton): it's done without respecting any inputVerifiers by unconditionally calling doClick. That can be hacked by hooking into the action, following the same pattern as hooking into the mouseListener:
find the rootPane's pressed action
implement a custom action which checks for a potentially vetoing inputVerifier: delegate to the original if not, do nothing otherwise
The example modified along those lines:
public class VerifierTest implements Runnable {
private static final long serialVersionUID = 1L;
#Override
public void run() {
InteractiveTestCase.setLAF("Win");
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 200);
JTextField tf = new JTextField("TextField1");
tf.setInputVerifier(new PassVerifier());
frame.add(tf, BorderLayout.NORTH);
final JButton b = new JButton("Button");
frame.add(b);
b.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked");
}
});
// hook into the mouse listener
replaceBasicButtonListener(b);
frame.add(new JTextField("not validating, something else to focus"),
BorderLayout.SOUTH);
frame.getRootPane().setDefaultButton(b);
// hook into the default button action
Action pressDefault = frame.getRootPane().getActionMap().get("press");
frame.getRootPane().getActionMap().put("press", new DefaultButtonAction(pressDefault));
frame.setVisible(true);
}
protected void replaceBasicButtonListener(AbstractButton b) {
final BasicButtonListener original = getButtonListener(b);
if (original == null) return;
Hacker l = new Hacker(original);
b.removeMouseListener(original);
b.addMouseListener(l);
}
public static class Hacker implements MouseListener {
private BasicButtonListener original;
/**
* #param original the listener to delegate to.
*/
public Hacker(BasicButtonListener original) {
this.original = original;
}
/**
* Hook into the mousePressed: first request focus and
* check its success before handling it.
*/
#Override
public void mousePressed(final MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
if(e.getComponent().contains(e.getX(), e.getY())) {
// check if we can get the focus
e.getComponent().requestFocus();
invokeHandleEvent(e);
return;
}
}
original.mousePressed(e);
}
/**
* Handle the pressed only if we are focusOwner.
*/
protected void handlePressed(final MouseEvent e) {
if (!e.getComponent().hasFocus()) {
// something vetoed the focus transfer
// do nothing
return;
} else {
original.mousePressed(e);
// need a fake released now: the one from the
// original cycle might never has reached us
MouseEvent released = new MouseEvent(e.getComponent(), MouseEvent.MOUSE_RELEASED,
e.getWhen(), e.getModifiers(),
e.getX(), e.getY(), e.getClickCount(), e.isPopupTrigger()
);
original.mouseReleased(released);
}
}
/**
* focus requests might be handled
* asynchronously. So wrap the check
* wrap the block into an invokeLater.
*/
protected void invokeHandleEvent(final MouseEvent e) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
handlePressed(e);
}
});
}
#Override
public void mouseClicked(MouseEvent e) {
original.mouseClicked(e);
}
#Override
public void mouseReleased(MouseEvent e) {
original.mouseReleased(e);
}
#Override
public void mouseEntered(MouseEvent e) {
original.mouseEntered(e);
}
#Override
public void mouseExited(MouseEvent e) {
original.mouseExited(e);
}
}
public static class DefaultButtonAction extends AbstractAction {
private Action original;
/**
* #param original
*/
public DefaultButtonAction(Action original) {
this.original = original;
}
#Override
public void actionPerformed(ActionEvent e) {
JRootPane root = (JRootPane) e.getSource();
JButton owner = root.getDefaultButton();
if (owner != null && owner.getVerifyInputWhenFocusTarget()) {
Component c = KeyboardFocusManager
.getCurrentKeyboardFocusManager()
.getFocusOwner();
if (c instanceof JComponent && ((JComponent) c).getInputVerifier() != null) {
if (!((JComponent) c).getInputVerifier().shouldYieldFocus((JComponent) c)) return;
}
}
original.actionPerformed(e);
}
}
/**
* Returns the ButtonListener for the passed in Button, or null if one
* could not be found.
*/
private BasicButtonListener getButtonListener(AbstractButton b) {
MouseMotionListener[] listeners = b.getMouseMotionListeners();
if (listeners != null) {
for (MouseMotionListener listener : listeners) {
if (listener instanceof BasicButtonListener) {
return (BasicButtonListener) listener;
}
}
}
return null;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new VerifierTest());
}
public static class PassVerifier extends InputVerifier {
/**
* Decide whether or not the input is valid without
* side-effects.
*/
#Override
public boolean verify(JComponent input) {
final JTextField tf = (JTextField) input;
String pass = tf.getText();
if (pass.equals("Manish"))
return true;
return false;
}
/**
* Implemented to ask the user what to do if the input isn't valid.
* Note: not necessarily the best usability, it's mainly to
* demonstrate the different effects on not/agreeing with
* yielding focus transfer.
*/
#Override
public boolean shouldYieldFocus(final JComponent input) {
boolean valid = super.shouldYieldFocus(input);
if (!valid) {
String message = "illegal value: " + ((JTextField) input).getText();
int goAnyWay = JOptionPane.showConfirmDialog(input, "invalid value: " +
message + " - go ahead anyway?");
valid = goAnyWay == JOptionPane.OK_OPTION;
}
return valid;
}
}
}
Actually the real problem is in how the focus system and awt listeners interact. There are a few bugs declared in Java that the developers are going back and forth on who is responsible.
The mouse listener does : processMouseEvent and within that logic, the current FocusOwner is asked to yield Focus. it fails. But because half the event is processed already, the button becomes armed and the focus remains with the field.
I finally saw one developer comment: Don't let the listener proceed if the field is not allowed to lose focus.
For example:
Define a JTextfield with edits to only allow values < 100.
A message pops up when you lose focus.
I overrode my base JButton classes' processMouseEvent(MouseEvent e)
with code:
protected void processMouseEvent(MouseEvent e) {
if ( e.getComponent() != null && e.getComponent().isEnabled() ) { //should not be processing mouse events if it's disabled.
if (e.getID() == MouseEvent.MOUSE_RELEASED && e.getClickCount() == 1) {
// The mouse button is being released as per normal, and it's the first click. Process it as per normal.
super.processMouseEvent(e);
// If the release occured within the bounds of this component, we want to simulate a click as well
if (this.contains(e.getX(), e.getY())) {
super.processMouseEvent(new MouseEvent(e.getComponent(),
MouseEvent.MOUSE_CLICKED,
e.getWhen(),
e.getModifiers(),
e.getX(),
e.getY(),
e.getClickCount(),
e.isPopupTrigger(),
e.getButton()));
}
}
else if (e.getID() == MouseEvent.MOUSE_CLICKED && e.getClickCount() == 1) {
// Normal clicks are ignored to prevent duplicate events from normal, non-moved events
}
else if (e.getID() == MouseEvent.MOUSE_PRESSED && e.getComponent() != null && (e.getComponent().isFocusOwner() || e.getComponent().requestFocusInWindow())) {// if already focus owner process mouse event
super.processMouseEvent(e);
}
else {
// Otherwise, just process as per normal.
if (e.getID() != MouseEvent.MOUSE_PRESSED) {
super.processMouseEvent(e);
}
}
}
}
in the guts of this logic is the simple questions.
Button: Are you already focus owner.
if not: can you(Button) possibly GAIN focus ( remember - shouldYieldFocus() is called on the current focus holder inside the requestFocusInWindow() call and will return false ALWAYS if not valid )
This Also has the side affect of popping up your error dialog!
This logic Stops the Java libraries processMouseEvent logic from processing half an event while the Focus System stops it from completing.
Obviously you'll need this type of logic on all your different JComponents that perform an action on a click.

Javafx 2 click and double click

I would like to know if it was possible to detect the double-click in JavaFX 2 ? and how ?
I would like to make different event between a click and a double click.
Thanks
Yes you can detect single, double even multiple clicks:
myNode.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2){
System.out.println("Double clicked");
}
}
}
});
MouseButton.PRIMARY is used to determine if the left (commonly) mouse button is triggered the event. Read the api of getClickCount() to conclude that there maybe multiple click counts other than single or double. However I find it hard to distinguish between single and double click events. Because the first click count of the double click will rise a single event as well.
Here is another piece of code which can be used if you have to distinguish between a single- and a double-click and have to take a specific action in either case.
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class DoubleClickDetectionTest extends Application {
boolean dragFlag = false;
int clickCounter = 0;
ScheduledThreadPoolExecutor executor;
ScheduledFuture<?> scheduledFuture;
public DoubleClickDetectionTest() {
executor = new ScheduledThreadPoolExecutor(1);
executor.setRemoveOnCancelPolicy(true);
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
StackPane root = new StackPane();
primaryStage.setScene(new Scene(root, 400, 400));
primaryStage.show();
root.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (e.getButton().equals(MouseButton.PRIMARY)) {
dragFlag = true;
}
}
});
root.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (e.getButton().equals(MouseButton.PRIMARY)) {
if (!dragFlag) {
System.out.println(++clickCounter + " " + e.getClickCount());
if (e.getClickCount() == 1) {
scheduledFuture = executor.schedule(() -> singleClickAction(), 500, TimeUnit.MILLISECONDS);
} else if (e.getClickCount() > 1) {
if (scheduledFuture != null && !scheduledFuture.isCancelled() && !scheduledFuture.isDone()) {
scheduledFuture.cancel(false);
doubleClickAction();
}
}
}
dragFlag = false;
}
}
});
}
#Override
public void stop() {
executor.shutdown();
}
private void singleClickAction() {
System.out.println("Single-click action executed.");
}
private void doubleClickAction() {
System.out.println("Double-click action executed.");
}
}
Adhering to Java SE 8 lambda expressions would look something like this:
node.setOnMouseClicked(event -> {
if(event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() == 2) {
handleSomeAction();
}
});
Once you get used to lambda expressions - they end up being more understandable than the original class instantiation and overriding (x) method. -In my opinion-
The response by P. Pandey is the simplest approach which actually distinguishes between single and double click, but it did not work for me. For one, the function "currentTimeMillis" already returns milliseconds, so dividing it by 1000 does not seem to be necessary. The version below worked for me in a more consistent fashion.
#Override
public void handle(MouseEvent t) {
long diff = 0;
currentTime=System.currentTimeMillis();
if(lastTime!=0 && currentTime!=0){
diff=currentTime-lastTime;
if( diff<=215)
isdblClicked=true;
else
isdblClicked=false;
}
lastTime=currentTime;
System.out.println("IsDblClicked()"+isdblClicked);
//use the isdblClicked flag...
}
Not sure if someone still follows this OP or refer it, but below is my version of differentiating single click to double click. While most of the answers are quite acceptable, it would be really useful if it can be done in a proper resuable way.
One of the challenge I encountered is the need to have the single-double click differentiation on multiple nodes at multiple places. I cannot do the same repetitive cumbersome logic on each and every node. It should be done in a generic way.
So I opted to implement a custom EventDispatcher and use this dispatcher on node level or I can apply it directly to Scene to make it applicable for all child nodes.
For this I created a new MouseEvent namely 'MOUSE_DOUBLE_CLICKED", so tthat I am still sticking with the standard JavaFX practises. Now I can include the double_clicked event filters/handlers just like other mouse event types.
node.addEventFilter(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e->{..<code to handle double_click>..});
node.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e->{..<code to handle double_click>..});
Below is the implementation and complete working demo of this custom event dispatcher.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.*;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class DoubleClickEventDispatcherDemo extends Application {
#Override
public void start(Stage stage) throws Exception {
Rectangle box1 = new Rectangle(150, 150);
box1.setStyle("-fx-fill:red;-fx-stroke-width:2px;-fx-stroke:black;");
addEventHandlers(box1, "Red Box");
Rectangle box2 = new Rectangle(150, 150);
box2.setStyle("-fx-fill:yellow;-fx-stroke-width:2px;-fx-stroke:black;");
addEventHandlers(box2, "Yellow Box");
HBox pane = new HBox(box1, box2);
pane.setSpacing(10);
pane.setAlignment(Pos.CENTER);
addEventHandlers(pane, "HBox");
Scene scene = new Scene(new StackPane(pane), 450, 300);
stage.setScene(scene);
stage.show();
// SETTING CUSTOM EVENT DISPATCHER TO SCENE
scene.setEventDispatcher(new DoubleClickEventDispatcher(scene.getEventDispatcher()));
}
private void addEventHandlers(Node node, String nodeId) {
node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> System.out.println("" + nodeId + " mouse clicked filter"));
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("" + nodeId + " mouse clicked handler"));
node.addEventFilter(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e -> System.out.println("" + nodeId + " mouse double clicked filter"));
node.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e -> System.out.println(nodeId + " mouse double clicked handler"));
}
/**
* Custom MouseEvent
*/
interface CustomMouseEvent {
EventType<MouseEvent> MOUSE_DOUBLE_CLICKED = new EventType<>(MouseEvent.ANY, "MOUSE_DBL_CLICKED");
}
/**
* Custom EventDispatcher to differentiate from single click with double click.
*/
class DoubleClickEventDispatcher implements EventDispatcher {
/**
* Default delay to fire a double click event in milliseconds.
*/
private static final long DEFAULT_DOUBLE_CLICK_DELAY = 215;
/**
* Default event dispatcher of a node.
*/
private final EventDispatcher defaultEventDispatcher;
/**
* Timeline for dispatching mouse clicked event.
*/
private Timeline clickedTimeline;
/**
* Constructor.
*
* #param initial Default event dispatcher of a node
*/
public DoubleClickEventDispatcher(final EventDispatcher initial) {
defaultEventDispatcher = initial;
}
#Override
public Event dispatchEvent(final Event event, final EventDispatchChain tail) {
final EventType<? extends Event> type = event.getEventType();
if (type == MouseEvent.MOUSE_CLICKED) {
final MouseEvent mouseEvent = (MouseEvent) event;
final EventTarget eventTarget = event.getTarget();
if (mouseEvent.getClickCount() > 1) {
if (clickedTimeline != null) {
clickedTimeline.stop();
clickedTimeline = null;
final MouseEvent dblClickedEvent = copy(mouseEvent, CustomMouseEvent.MOUSE_DOUBLE_CLICKED);
Event.fireEvent(eventTarget, dblClickedEvent);
}
return mouseEvent;
}
if (clickedTimeline == null) {
final MouseEvent clickedEvent = copy(mouseEvent, mouseEvent.getEventType());
clickedTimeline = new Timeline(new KeyFrame(Duration.millis(DEFAULT_DOUBLE_CLICK_DELAY), e -> {
Event.fireEvent(eventTarget, clickedEvent);
clickedTimeline = null;
}));
clickedTimeline.play();
return mouseEvent;
}
}
return defaultEventDispatcher.dispatchEvent(event, tail);
}
/**
* Creates a copy of the provided mouse event type with the mouse event.
*
* #param e MouseEvent
* #param eventType Event type that need to be created
* #return New mouse event instance
*/
private MouseEvent copy(final MouseEvent e, final EventType<? extends MouseEvent> eventType) {
return new MouseEvent(eventType, e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(),
e.getButton(), e.getClickCount(), e.isShiftDown(), e.isControlDown(), e.isAltDown(),
e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(),
e.isSecondaryButtonDown(), e.isSynthesized(), e.isPopupTrigger(),
e.isStillSincePress(), e.getPickResult());
}
}
}
Here is how I have implemented double click
if (e.getEventType().equals(MouseEvent.MOUSE_CLICKED) && !drag_Flag) {
long diff = 0;
if(time1==0)
time1=System.currentTimeMillis();
else
time2=System.currentTimeMillis();
if(time1!=0 && time2!=0)
diff=time2-time1;
if((diff/1000)<=215 && diff>0)
{
isdblClicked=true;
}
else
{
isdblClicked=false;
}
System.out.println("IsDblClicked()"+isdblClicked);
}
Since it is not possible to distinguish between single-click and double-click by default, we use the following approach:
On single-click, we wrap the single-click operation in an abortable runnable. This runnable waits a certain amount of time (i.e., SINGLE_CLICK_DELAY) before being executed.
In the meantime, if a second click, i.e., a double-click, occurs, the single-click operation gets aborted and only the double-click operation is performed.
This way, either the single-click or the double-click operation is performed, but never both.
Following is the full code. To use it, only the three TODO lines have to be replaced by the wanted handlers.
private static final int SINGLE_CLICK_DELAY = 250;
private ClickRunner latestClickRunner = null;
private class ClickRunner implements Runnable {
private final Runnable onSingleClick;
private boolean aborted = false;
public ClickRunner(Runnable onSingleClick) {
this.onSingleClick = onSingleClick;
}
public void abort() {
this.aborted = true;
}
#Override
public void run() {
try {
Thread.sleep(SINGLE_CLICK_DELAY);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!aborted) {
System.out.println("Execute Single Click");
Platform.runLater(() -> onSingleClick.run());
}
}
}
private void init() {
container.setOnMouseClicked(me -> {
switch (me.getButton()) {
case PRIMARY:
if (me.getClickCount() == 1) {
System.out.println("Single Click");
latestClickRunner = new ClickRunner(() -> {
// TODO: Single-left-click operation
});
CompletableFuture.runAsync(latestClickRunner);
}
if (me.getClickCount() == 2) {
System.out.println("Double Click");
if (latestClickRunner != null) {
System.out.println("-> Abort Single Click");
latestClickRunner.abort();
}
// TODO: Double-left-click operation
}
break;
case SECONDARY:
// TODO: Right-click operation
break;
default:
break;
}
});
}
A solution using PauseTransition:
PauseTransition singlePressPause = new PauseTransition(Duration.millis(500));
singlePressPause.setOnFinished(e -> {
// single press
});
node.setOnMousePressed(e -> {
if (e.isPrimaryButtonDown() && e.getClickCount() == 1) {
singlePressPause.play();
}
if (e.isPrimaryButtonDown() && e.getClickCount() == 2) {
singlePressPause.stop();
// double press
}
});
An alternative to single click vs. double click that I'm using is single click vs. press-and-hold (for about a quarter to a half second or so), then release the button. The technique can use a threaded abortable timer as in some of the code snippets above to distinguish between the two. Assuming that the actual event handling happens on the button release, this alternative has the advantage that single click works normally (i.e., without any delay), and for press-and-hold you can give the user some visual feedback when the button has been held long enough to be released (so there's never any ambiguity about which action was performed).
If you are testing how many mouse buttons (==2) are pressed, do not code it in sub-method! The next is working:
listView.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if( mouseEvent.getButton().equals(MouseButton.SECONDARY)) {
System.out.println("isSecondaryButtonDown");
mouseEvent.consume();
// ....
}
else
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2){
System.out.println("Double clicked");
// mousePressedInListViewDC(mouseEvent);
}
else
if(mouseEvent.getClickCount() == 1){
System.out.println("1 clicked");
mousePressedInListView1C(mouseEvent);
}
}
}
})
;
I ran in the same problem, and what I noticed is that single and double click ARE distinguished with basic :
Button btn = new Button("Double click me too");
btn.setOnMousePressed(mouseEvent -> {
// CLICK catches
if (mouseEvent.getClickCount() == 1) {
System.out.println("Button clicked");
} else if (mouseEvent.getClickCount() == 2)
System.out.println("Button double clicked");
});
But a 'single' click is catched as part of the double click. So you will see on the console :
Using mainly the answer of #markus-weninger, I built up a Class extending Button to expose 2 new EventHandlers :
setOnMouseSingleClicked(EventHandler<MouseEvent> eventHandler)
setOnMouseDoubleClicked(EventHandler<MouseEvent> eventHandler)
So with the full example code bellow, when double clicking on last button, we get :
Keep in mind :
The obvious drawback is that even a single click caught with setOnMouseSingleClicked will be delayed with the singleClickDelayMillis (exposed variable which should be set accordingly to the OS, as mentioned by Kleopatra).
Another noticeable fact, is that I extended Button, and not Node where it should be : The Class where the onMouseClicked(...) is implemented.
As a last comment, I decided to add a new EventHandler rather than using the existing setOnMousePressed, setOnMouseReleased or setOnMouseClicked so that the developer can still fully implement these convenience EventHandlers. For example in order to have immediate response from a click on the button without waiting for the singleClickDelayMillis. But this means that if you implement both, the setOnMouseClicked will be fired even on a double click... beware.
Here comes the code :
import java.util.concurrent.CompletableFuture;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.beans.property.ObjectProperty;
import javafx.event.EventHandler;
import javafx.beans.property.SimpleObjectProperty;
public class DblClickCatchedWithoutSingleClick extends Application {
public class ButtonWithDblClick extends Button {
private long singleClickDelayMillis = 250;
private ClickRunner latestClickRunner = null;
private ObjectProperty<EventHandler<MouseEvent>> onMouseSingleClickedProperty = new SimpleObjectProperty<>();
private ObjectProperty<EventHandler<MouseEvent>> onMouseDoubleClickedProperty = new SimpleObjectProperty<>();
// CONSTRUCTORS
public ButtonWithDblClick() {
super();
addClickedEventHandler();
}
public ButtonWithDblClick(String text) {
super(text);
addClickedEventHandler();
}
public ButtonWithDblClick(String text, Node graphic) {
super(text, graphic);
addClickedEventHandler();
}
private class ClickRunner implements Runnable {
private final Runnable onClick;
private boolean aborted = false;
public ClickRunner(Runnable onClick) {
this.onClick = onClick;
}
public void abort() {
this.aborted = true;
}
#Override
public void run() {
try {
Thread.sleep(singleClickDelayMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!aborted) {
Platform.runLater(onClick::run);
}
}
}
private void addClickedEventHandler() {
//Handling the mouse clicked event (not using 'onMouseClicked' so it can still be used by developer).
EventHandler<MouseEvent> eventHandler = me -> {
switch (me.getButton()) {
case PRIMARY:
if (me.getClickCount() == 1) {
latestClickRunner = new ClickRunner(() -> {
System.out.println("ButtonWithDblClick : SINGLE Click fired");
onMouseSingleClickedProperty.get().handle(me);
});
CompletableFuture.runAsync(latestClickRunner);
}
if (me.getClickCount() == 2) {
if (latestClickRunner != null) {
latestClickRunner.abort();
}
System.out.println("ButtonWithDblClick : DOUBLE Click fired");
onMouseDoubleClickedProperty.get().handle(me);
}
break;
case SECONDARY:
// Right-click operation. Not implemented since usually no double RIGHT click needs to be caught.
break;
default:
break;
}
};
//Adding the event handler
addEventHandler(MouseEvent.MOUSE_CLICKED, eventHandler);
}
public void setOnMouseSingleClicked(EventHandler<MouseEvent> eventHandler) {
this.onMouseSingleClickedProperty.set(eventHandler);
}
public void setOnMouseDoubleClicked(EventHandler<MouseEvent> eventHandler) {
this.onMouseDoubleClickedProperty.set(eventHandler);
}
public long getSingleClickDelayMillis() {
return singleClickDelayMillis;
}
public void setSingleClickDelayMillis(long singleClickDelayMillis) {
this.singleClickDelayMillis = singleClickDelayMillis;
}
}
public void start(Stage stage) {
VBox root = new VBox();
Label lbl = new Label("Double click me");
lbl.setOnMouseClicked(mouseEvent -> {
// CLICK catches
if (mouseEvent.getClickCount() == 2) {
System.out.println("Label double clicked");
} else if (mouseEvent.getClickCount() == 1)
System.out.println("Label clicked");
});
Button btn = new Button("Double click me too");
btn.setOnMousePressed(mouseEvent -> {
// CLICK catches
if (mouseEvent.getClickCount() == 1) {
System.out.println("Button clicked");
} else if (mouseEvent.getClickCount() == 2)
System.out.println("Button double clicked");
});
ButtonWithDblClick btn2 = new ButtonWithDblClick("Double click me three ;-)");
btn2.setOnMouseSingleClicked(me -> {
System.out.println("BUTTON_2 : Fire SINGLE Click");
});
btn2.setOnMouseDoubleClicked(me -> {
System.out.println("BUTTON_2 : Fire DOUBLE Click");
});
root.getChildren().add(lbl);
root.getChildren().add(btn);
root.getChildren().add(btn2);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}

Categories

Resources