0

I have a JavaFX Button that triggers when the user presses enter. This causes a FileChooser to open up. Some people (like myself) may hit enter inside the FileChooser to save the file. However, this causes the save button to trigger itself again and open the FileChooser again to save a new file. Clicking the button (in the FileChooser) with the mouse does not have this issue.

I thought consuming the event from the button would do something about this issue, but it only consumes the button on the GUI's event, rather than the FileChooser button. I've tried looking for ways to modify the FileChooser's EventHandler to consume an enter keypress, but with no success.
I've also tried taking the focus off the button and moving it to the parent (a Pane) so it can't be clicked again. However, there are buttons that benefit being clicked multiple times without having to regain focus on them again.

A example of my code looks like this (obviously this would be part of a bigger class that extends Application):

EventHandler<KeyEvent> enter = event -> {
    if (event.getCode() == KeyCode.ENTER && event.getSource() instanceof Button) {
        Button src = (Button) event.getSource();
        src.fire();
    }
    event.consume();
};

Button b1 = new Button("Save");

b1.setOnKeyReleased(enter);

/* Called by .fire method */
b1.setOnAction(event -> {
    /* Create the save dialog box */
    FileChooser saveDialog = new FileChooser();
    saveDialog.setTitle("Save");

    /* Get file */
    File f = saveDialog.showSaveDialog(stage);
    /*
     * ... do stuff with file ...
     */
});

Note: This example isn't my exact code. Instead the key released event is a variable used for multiple buttons, rather than just the save button (i.e. b2.setOnKeyReleased(enter); b2.setOnAction(event -> {/* Do something */});).

How could I go about preventing the button from triggering when the user presses enter in the FileChooser? I don't want the user to be stuck in a loop if they don't have a mouse. I'm aware that pressing Alt+S also saves it, but I can't expect all users to be aware of that.

EDIT: As requested in a comment which appears to be deleted now, here's a runnable version of the code:

import java.io.File;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

public class ButtonTest extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        /* EventHandler to be used with multiple buttons */
        EventHandler<KeyEvent> enter = event -> {
            if (event.getCode() == KeyCode.ENTER && event.getSource() instanceof Button) {
                Button src = (Button) event.getSource();
                src.fire();
            }
            event.consume();
        };

        /* Create a new button */
        Button b1 = new Button("Save");
        Button b2 = new Button("Print");
        /* Add event handlers */
        b1.setOnKeyReleased(enter);
        b2.setOnKeyReleased(enter);

        /* Called by .fire method of save button */
        b1.setOnAction(event -> {
            /* Create the save dialog box */
            FileChooser saveDialog = new FileChooser();
            saveDialog.setTitle("Save");

            /* Get file */
            File f = saveDialog.showSaveDialog(stage);
            /* ... do stuff with file ... */
        });
        /* Called by .fire method of print button */
        b2.setOnAction(event -> System.out.println("Pressed"));

        Scene scene = new Scene(new HBox(b1, b2));
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}
JolonB
  • 415
  • 5
  • 25
  • I'm confused as to why you would call `src.fire()` inside the `onKeyReleased` handler? The `onAction` handler is invoked by the enter key already (i.e. the enter key fires the button). – Slaw Jan 21 '19 at 00:30
  • Not for me. Space is the only key that triggers a button. I think the reason for that is because enter is used to trigger the default button which is set using `btn.setDefaultButton(true);` – JolonB Jan 21 '19 at 00:35
  • I just can't reproduce the problem. If the `Button` has focus then pressing enter fires the action event and shows the `FileChooser`. If I removed the `onKeyReleased` handlers the `FileChooser` doesn't reappear when closing it via pressing enter. **Edit:** I can reproduce the problem using Java 8u202 but I cannot reproduce the problem on OpenJFX 11.0.2. I'm looking for a bug report regarding any changes but I can only find one for version 1.3 fixed 2.0. – Slaw Jan 23 '19 at 18:37
  • Can you let me know what OS and version of JavaFX you're using? Because if you're using JavaFX 8 I have a potential answer. – Slaw Jan 23 '19 at 18:38
  • Windows 7 and JavaFX 8. Thanks for putting in so much effort to help. – JolonB Jan 23 '19 at 19:17

2 Answers2

3

The problem is firing the Button from the onKeyReleased handler. By the time you release the ENTER key the FileChooser has been hidden and the Stage has regained focus, meaning the key-release event is given to your Stage/Button. Obviously this will cause a cycle.

One possible solution is to fire the Button from inside a onKeyPressed handler. This will give slightly different behavior relative to other applications, however, which your users might not expect/appreciate.

Another possible solution is to track if the FileChooser had been open before firing the Button, like what Matt does in his answer.

What you seem to be trying to do is allow users to use the ENTER key to fire the Button; this should be default behavior on platforms like Windows.

Not for me. Space is the only key that triggers a button. I think the reason for that is because enter is used to trigger the default button which is set using btn.setDefaultButton(true);

For me, pressing ENTER while the Button has focus fires the action event when using JavaFX 11.0.2 but not JavaFX 8u202, both on Windows 10. It appears the behavior of Button changed since JavaFX 8. Below is the different implementations of com.sun.javafx.scene.control.behavior.ButtonBehavior showing the registered key bindings.

JavaFX 8u202

protected static final List<KeyBinding> BUTTON_BINDINGS = new ArrayList<KeyBinding>();
static {
        BUTTON_BINDINGS.add(new KeyBinding(SPACE, KEY_PRESSED, PRESS_ACTION));
        BUTTON_BINDINGS.add(new KeyBinding(SPACE, KEY_RELEASED, RELEASE_ACTION));
}

JavaFX 11.0.2

public ButtonBehavior(C control) {
    super(control);

    /* SOME CODE OMITTED FOR BREVITY */

    // then button-specific mappings for key and mouse input
    addDefaultMapping(buttonInputMap,
        new KeyMapping(SPACE, KeyEvent.KEY_PRESSED, this::keyPressed),
        new KeyMapping(SPACE, KeyEvent.KEY_RELEASED, this::keyReleased),
        new MouseMapping(MouseEvent.MOUSE_PRESSED, this::mousePressed),
        new MouseMapping(MouseEvent.MOUSE_RELEASED, this::mouseReleased),
        new MouseMapping(MouseEvent.MOUSE_ENTERED, this::mouseEntered),
        new MouseMapping(MouseEvent.MOUSE_EXITED, this::mouseExited),

        // on non-Mac OS platforms, we support pressing the ENTER key to activate the button
        new KeyMapping(new KeyBinding(ENTER, KeyEvent.KEY_PRESSED), this::keyPressed, event -> PlatformUtil.isMac()),
        new KeyMapping(new KeyBinding(ENTER, KeyEvent.KEY_RELEASED), this::keyReleased, event -> PlatformUtil.isMac())
    );

    /* SOME CODE OMITTED FOR BREVITY */

}

As you can see, both register SPACE to fire the Button when it has focus. However, the JavaFX 11.0.2 implementation also registers ENTER for the same—but only for non-Mac OS platforms. I couldn't find any documentation about this change in behavior.

If you want the same behavior in JavaFX 8, and you don't mind hacking into the internals of JavaFX, then you can use reflection to alter the behavior of all button-like controls in your application. Here's a utility method example:

import com.sun.javafx.PlatformUtil;
import com.sun.javafx.scene.control.behavior.ButtonBehavior;
import com.sun.javafx.scene.control.behavior.KeyBinding;
import java.lang.reflect.Field;
import java.util.List;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

public final class ButtonUtils {

  public static void installEnterFiresButtonFix() throws ReflectiveOperationException {
    if (PlatformUtil.isMac()) {
      return;
    }

    Field bindingsField = ButtonBehavior.class.getDeclaredField("BUTTON_BINDINGS");
    Field pressedActionField = ButtonBehavior.class.getDeclaredField("PRESS_ACTION");
    Field releasedActionField = ButtonBehavior.class.getDeclaredField("RELEASE_ACTION");

    bindingsField.setAccessible(true);
    pressedActionField.setAccessible(true);
    releasedActionField.setAccessible(true);

    @SuppressWarnings("unchecked")
    List<KeyBinding> bindings = (List<KeyBinding>) bindingsField.get(null);
    String pressedAction = (String) pressedActionField.get(null);
    String releasedAction = (String) releasedActionField.get(null);

    bindings.add(new KeyBinding(KeyCode.ENTER, KeyEvent.KEY_PRESSED, pressedAction));
    bindings.add(new KeyBinding(KeyCode.ENTER, KeyEvent.KEY_RELEASED, releasedAction));
  }

  private ButtonUtils() {}

}

You would call this utility method early in the startup of your application, before any Buttons are created. Here's an example using it:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

public class Main extends Application {

  @Override
  public void start(Stage primaryStage) {
    try {
      ButtonUtils.installEnterFiresButtonFix();
    } catch (ReflectiveOperationException ex) {
      ex.printStackTrace();
    }
    Button button = new Button("Save");
    button.setOnAction(event -> {
      event.consume();
      System.out.println(new FileChooser().showSaveDialog(primaryStage));
    });
    Scene scene = new Scene(new StackPane(button), 300, 150);
    primaryStage.setScene(scene);
    primaryStage.setTitle("Workshop");
    primaryStage.show();
  }

}

Reminder: This fix is implementation dependent.

Slaw
  • 37,820
  • 8
  • 53
  • 80
1

I added a boolean for the fileChooser being open and it seems to be working for me but I had to split the events up otherwise it will only fire the print button every other

public class Main extends Application {

    private boolean fileChooserOpen = false;

    @Override
    public void start(Stage stage) throws Exception{
        /* EventHandler to be used with multiple buttons */
        EventHandler<KeyEvent> enterWithFileChooser = event -> {
            if (!fileChooserOpen && event.getCode() == KeyCode.ENTER && event.getSource() instanceof Button) {
                Button src = (Button) event.getSource();
                src.fire();
                fileChooserOpen = true;
            }else {
                fileChooserOpen = false;
            }
            event.consume();
        };

        EventHandler<KeyEvent> enter = event -> {
            if (event.getCode() == KeyCode.ENTER && event.getSource() instanceof Button) {
                Button src = (Button) event.getSource();
                src.fire();
            }
            event.consume();
        };

        /* Create a new button */
        Button b1 = new Button("Save");
        Button b2 = new Button("Print");
        /* Add event handlers */
        b1.setOnKeyReleased(enterWithFileChooser);
        b2.setOnKeyReleased(enter);

        /* Called by .fire method of save button */
        b1.setOnAction(event -> {
            /* Create the save dialog box */
            FileChooser saveDialog = new FileChooser();
            saveDialog.setTitle("Save");

            /* Get file */
            File f = saveDialog.showSaveDialog(stage);
            /* ... do stuff with file ... */
        });
        /* Called by .fire method of print button */
        b2.setOnAction(event -> System.out.println("Pressed"));

        Scene scene = new Scene(new HBox(b1, b2));
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) { launch(args); }
}
Matt
  • 3,052
  • 1
  • 17
  • 30