Check if "shortcut" key is pressed on action event

115 Views Asked by At

I'm trying to detect when a button's action is triggered (which could be through clicking or something else like the keyboard) while the "shortcut" key is pressed. I couldn't find a way to get the keys pressed from an ActionEvent, so I put event filters for key press and release on the scene which keep track of which keys are pressed. Then in the button's action I check if the shortcut key is pressed.

    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.input.KeyCode;
    import javafx.scene.input.KeyEvent;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import java.util.HashSet;
    import java.util.Set;
    
    public class App extends Application {
        private final Set<KeyCode> pressedKeys = new HashSet<>();
        
        public static void main(String[] args) {
            launch(args);
        }
        
        @Override
        public void start(Stage stage) {
            Button button = new Button("Click with shortcut key pressed");
            button.setOnAction(e -> {
                if (pressedKeys.contains(KeyCode.SHORTCUT)) {
                    System.out.println("Success!");
                } else {
                    System.out.println("Failure!");
                }
            });
            VBox vBox = new VBox(button);
            vBox.setStyle("-fx-background-color:red;");
            Scene scene = new Scene(vBox);
            scene.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
                System.out.println("Key pressed: " + e.getCode());
                pressedKeys.add(e.getCode());
            });
            scene.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
                System.out.println("Key released: " + e.getCode());
                pressedKeys.remove(e.getCode());
            });
            stage.setScene(scene);
            stage.show();
        }
        
        public static class AppRunner {
            public static void main(String[] args) {
                App.main(args);
            }
        }
    }

However I find there are 2 problems:

  1. When I'm holding down the shortcut key and clicking on the button, the action isn't fired at all (neither success or failure are printed)
  2. If I change the button's action to a click event filter I always get failure. Using IntelliJ's debugger I see the KeyCode.CONTROL key is known to be pressed, but not the SHORTCUT key, even though they are the same key in this case. How do I check if any of the pressed keys are the shortcut key in a platform independent way?
button.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
    if (pressedKeys.contains(KeyCode.SHORTCUT)) {
        System.out.println("Success!");
    } else {
        System.out.println("Failure!");
    }
});

I also need this to work with actions on other controls, such as ComboBoxes and TextFields.

Edit: I found another bug with this approach. If the user holds down a key and switches to another scene or application then releases the key, the scene won't detect the key release and will still think the key is pressed. So I could do with a way to detect what keys are down without tracking every key press/release. How are MouseEvents informed of which keys are down so they can use MouseEvent.isShortcutDown()? Can the same method be used here?

Edit: To answer jewelsea's question for more background, it's a little complicated but imagine I have a form with multiple types of controls that can be used to set values in the form, and the form needs to be filled out many times. Therefore I want a way not only to set a value in the form but also fix it or make it the default so that for the next time it needs filling out it will be pre-populated with that value. The shortcut key is a convenient way to differentiate between setting the value without fixing it and setting it while fixing it. This can apply to shortcut-clicking a button or dropdown item from a combobox or holding the shortcut key while pressing enter in a text field. I also think answers to this question will be more useful to others if they work for action events on any control, but even getting it to work for a button would be good.

3

There are 3 best solutions below

0
Zyxl On BEST ANSWER

I managed to solve all 3 problems without re-implementing the action triggering logic. The solution can be applied to any new control just by calling makeShortcutActionable(control). It's platform-independent and works well for other modifier keys and non-modifier keys. However, the solution is somewhat hacky and for controls that use the same modifier key already (e.g., Ctrl-A/Cmd-A to select all text in a TextField or editable ComboBox) it disables the original use of the modifier key.

Solving problem 1: The way we get the action to fire when the shortcut (or any modifier key) is pressed is to fool the control into thinking it isn't pressed. For that we consume the event if event.isShortcutDown() is true and then fire a copy of the event which thinks the shortcut key isn't pressed. The way the action event handler knows the shortcut key is pressed is by asking the scene which keeps track of which keys are pressed. Unfortunately the default event handlers of the control don't know the shortcut key is pressed so Ctrl-A/Cmd-A in a TextField won't do anything (but luckily it doesn't type an A either). Therefore for TextFields it's probably better to add an event handler that looks for shortcut-enter instead (re-implementing the action logic is easy for TextField).

Solving problem 2: The scene not only records the KeyCodes that were pressed/released but also checks event.isShortcutDown(). When modifying events for re-firing we have to know which modifier key is the shortcut key so we use Toolkit.getToolkit().getPlatformShortcutKey(), which is what isShortcutDown() uses in the source code.

Solving problem 3: Tracking key states through key presses and releases is insufficient when switching between windows so we use isShortcutDown() instead. The scene tracks all key and mouse events so basically once there is any interaction with the scene again it checks if the shortcut key is pressed (which happens before any action event can be triggered). Non-modifier keys can't be checked in the same way, but when they are held down they continue to fire KeyEvents. Therefore when the scene loses focus we can assume all non-modifier keys were released and when the scene gets focus again we will quickly be informed which keys are still pressed. Finally because we are firing fake events which claim the shortcut key isn't pressed we have to tell the key tracker which events are fake so it can ignore them.

Here's an example with a Button, a ComboBox and a TextField. Each has an action which checks whether the shortcut or S key is pressed. For the TextField I opted to use an event handler for the shortcut-enter combo instead of breaking the shortcut-based keyboard commands.

import com.sun.javafx.tk.Toolkit;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.event.Event;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.input.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.*;

public class App extends Application {
    private final PressedKeys pressedKeys = new PressedKeys();
    private final VBox root = new VBox();
    private final Scene scene = new Scene(root);
    
    public static void main(String[] args) {
        launch(args);
    }
    
    @Override
    public void start(Stage stage) {
        Button button = new Button("Click with shortcut key pressed");
        button.setOnAction(e -> doAction());
        makeShortcutActionable(button);
        ComboBox<String> comboBox = new ComboBox<>(FXCollections.observableArrayList("A", "B", "C"));
        comboBox.setOnAction(e -> doAction());
        makeShortcutActionable(comboBox);
        TextField textField = new TextField();
        textField.setOnAction(e -> doAction());
//      makeShortcutActionable(textField);
        textField.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
            if (new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHORTCUT_DOWN).match(e))
                doAction();
        });
        root.getChildren().addAll(button, comboBox, textField);
        root.setStyle("-fx-background-color:red;");
        scene.addEventFilter(KeyEvent.ANY, e -> {
            pressedKeys.update(e);
        });
        scene.addEventFilter(MouseEvent.ANY, e -> {
            pressedKeys.update(e);
        });
        stage.focusedProperty().addListener(((observable, oldValue, newValue) -> {
            if (newValue == false) pressedKeys.clear(); // Necessary for non-modifier keys to get released
        }));
        stage.setScene(scene);
        stage.show();
    }
    
    private void makeShortcutActionable(Node node) {
        node.addEventFilter(KeyEvent.ANY, e -> removeShortcutKey(node, e));
        node.addEventFilter(MouseEvent.ANY, e -> removeShortcutKey(node, e));
    }
    
    private void doAction() {
        if (pressedKeys.contains(KeyCode.SHORTCUT)) {
            System.out.println("Success!");
        } else if (pressedKeys.contains(KeyCode.S)) {
            System.out.println("Special function!");
        } else {
            System.out.println("Failure!");
        }
    }
    
    private void removeShortcutKey(Node node, KeyEvent e) {
        if (e.isShortcutDown()) {
            e.consume();
            KeyEvent newEvent = new KeyEvent(e.getSource(), e.getTarget(), e.getEventType(), e.getCharacter(), e.getText(), e.getCode(),
                    e.isShiftDown() && !isShortcut(KeyCode.SHIFT),
                    e.isControlDown() && !isShortcut(KeyCode.CONTROL),
                    e.isAltDown() && !isShortcut(KeyCode.ALT),
                    e.isMetaDown() && !isShortcut(KeyCode.META));
            pressedKeys.fireEvent(newEvent, node);
        }
    }
    
    private void removeShortcutKey(Node node, MouseEvent e) {
        if (e.isShortcutDown()) {
            e.consume();
            MouseEvent newEvent = new MouseEvent(e.getSource(), e.getTarget(), e.getEventType(), e.getX(), e.getY(), e.getScreenX(), e.getScreenY(), e.getButton(), e.getClickCount(),
                    e.isShiftDown() && !isShortcut(KeyCode.SHIFT),
                    e.isControlDown() && !isShortcut(KeyCode.CONTROL),
                    e.isAltDown() && !isShortcut(KeyCode.ALT),
                    e.isMetaDown() && !isShortcut(KeyCode.META),
                    e.isPrimaryButtonDown(), e.isMiddleButtonDown(), e.isSecondaryButtonDown(), e.isBackButtonDown(), e.isForwardButtonDown(), e.isSynthesized(), e.isPopupTrigger(), e.isStillSincePress(), e.getPickResult());
            pressedKeys.fireEvent(newEvent, node);
        }
    }
    
    private boolean isShortcut(KeyCode keyCode) {
        return keyCode == Toolkit.getToolkit().getPlatformShortcutKey();
    }
    
    private class PressedKeys {
        private final Set<KeyCode> keys = new HashSet<>();
        private final List<Event> eventsToIgnore = new ArrayList<>();
        
        private synchronized boolean contains(KeyCode keyCode) {
            return keys.contains(keyCode);
        }
        
        private synchronized void update(KeyEvent e) {
            if (!shouldIgnore(e)) {
                if (e.isShortcutDown()) {
                    boolean justPressed = keys.add(KeyCode.SHORTCUT);
                    if (justPressed) System.out.println("Shortcut pressed");
                } else {
                    boolean justReleased = keys.remove(KeyCode.SHORTCUT);
                    if (justReleased) System.out.println("Shortcut released");
                }
                if (e.getEventType() == KeyEvent.KEY_PRESSED) {
                    keys.add(e.getCode());
                } else if (e.getEventType() == KeyEvent.KEY_RELEASED) {
                    keys.remove(e.getCode());
                }
            }
        }
        
        private synchronized void update(MouseEvent e) {
            if (!shouldIgnore(e)) {
                if (e.isShortcutDown()) {
                    boolean justPressed = keys.add(KeyCode.SHORTCUT);
                    if (justPressed) System.out.println("Shortcut pressed");
                } else {
                    boolean justReleased = keys.remove(KeyCode.SHORTCUT);
                    if (justReleased) System.out.println("Shortcut released");
                }
            }
        }
        
        private synchronized void clear() {
            keys.clear();
        }
        
        private synchronized void fireEvent(Event e, Node node) {
            Event modE = e.copyFor(scene, node); // Events use reference equality but event filters receive different objects for what is really the same event. KeyEvent is final so we can't add an ID to the event either. But the same event object will get passed to the scene's event filter if the source and target are already correct.
            eventsToIgnore.add(modE);
            if (eventsToIgnore.size() > 100) { // Prevent a memory leak by keeping the list small
                eventsToIgnore.remove(0);
            }
            node.fireEvent(modE);
        }
        
        private boolean shouldIgnore(Event e) {
            return eventsToIgnore.contains(e);
        }
    }
    
    public static class AppRunner {
        public static void main(String[] args) {
            App.main(args);
        }
    }
}
3
hedfol On

If you just need to check whether the shortcut key is pressed during a keyboard or mouse event, then event.isShortcutDown() should be enough. To find out which key is a shortcut e.isControlDown(), e.isShiftDown(), e.isAltDown(), e.isMetaDown() can be used.

And you don't have to worry about switching to another scene - these methods return correct values even in that case.

Specific Event Filters

The following application creates two scenes which can be switched ("Switch Scene" and "Switch Back" buttons). The first one contains a few UI controls (Button, TextField, ComboBox) with KEY_PRESSED and MOUSE_PRESSED event filters that print shortcut detection results. A purpose of the seconds scene is only switching (it does not capture shortcut events).

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class App extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        Button button = new Button("Click with shortcut key pressed");
        TextField text = new TextField("TextField Value");

        ComboBox<String> combo = new ComboBox<>(
                FXCollections.observableArrayList("ComboBox Value 1", "ComboBox Value 2"));
        combo.getSelectionModel().select(0);
        Button switcher = new Button("Switch Scene");

        VBox vBox = new VBox(button, text, combo, switcher);
        vBox.setStyle("-fx-background-color:red;");
        vBox.getChildren().forEach(App::addShortcutCaptureFilters);
        Scene scene = new Scene(vBox);

        Button switcherBack = new Button("Switch Back");
        Scene anotherScene = new Scene(new VBox(new Button("Another Button"), switcherBack));

        switcher.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> stage.setScene(anotherScene));
        switcherBack.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> stage.setScene(scene));

        stage.setScene(scene);
        stage.show();
    }

    private static void addShortcutCaptureFilters(Node control) {

        control.addEventFilter(KeyEvent.KEY_PRESSED, e -> onShortcutCaptureEvent(control, e, e.isShortcutDown(),
                e.isControlDown(), e.isShiftDown(), e.isAltDown(), e.isMetaDown()));

        control.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> onShortcutCaptureEvent(control, e, e.isShortcutDown(),
                e.isControlDown(), e.isShiftDown(), e.isAltDown(), e.isMetaDown()));
    }

    private static void onShortcutCaptureEvent(Node control, InputEvent event, boolean shortcut, boolean ctrl,
            boolean shift, boolean alt, boolean meta) {
        KeyCode specKeyDown = null;
        if (ctrl) {
            specKeyDown = KeyCode.CONTROL;
        } else if (shift) {
            specKeyDown = KeyCode.SHIFT;
        } else if (alt) {
            specKeyDown = KeyCode.ALT;
        } else if (meta) {
            specKeyDown = KeyCode.META;
        }
        String ln = shortcut ? "\n" : "";
        String title = shortcut ? "SHORTCUT DETECTED" : "not a shortcut";

        System.out.println(String.format("%s%s: %s (specKey: %s) %s%s", ln, control.getClass().getSimpleName(), title,
                specKeyDown, event.getEventType(), ln));
    }

    public static class AppRunner {
        public static void main(String[] args) {
            App.main(args);
        }
    }
}

shortcut app

Example of an output:

Button: not a shortcut (specKey: null) MOUSE_PRESSED

Button: SHORTCUT DETECTED (specKey: CONTROL) KEY_PRESSED

TextField: not a shortcut (specKey: null) MOUSE_PRESSED
TextField: not a shortcut (specKey: SHIFT) KEY_PRESSED

TextField: SHORTCUT DETECTED (specKey: CONTROL) KEY_PRESSED

ComboBox: not a shortcut (specKey: null) MOUSE_PRESSED
ComboBox: not a shortcut (specKey: ALT) KEY_PRESSED

ComboBox: SHORTCUT DETECTED (specKey: CONTROL) MOUSE_PRESSED

General Event Filter

The next version contains a shortcut detection on a Stage level and a general EventFilter for all controls (excessive mouse events are skipped). The shortcutPressed field is used to decide whether or not to fix the value of an input in the form.


import java.util.Set;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.event.EventType;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class ShortcutApp extends Application {

    private static final Set<EventType<?>> EXCESSIVE_ACTION_EVENTS = Set.of(MouseEvent.MOUSE_ENTERED,
            MouseEvent.MOUSE_EXITED, MouseEvent.MOUSE_ENTERED_TARGET, MouseEvent.MOUSE_EXITED_TARGET,
            MouseEvent.MOUSE_MOVED);

    private boolean shortcutPressed = false;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        Button button = new Button("Click with shortcut key pressed");
        TextField text = new TextField("TextField Value");
        ComboBox<String> combo = new ComboBox<>(
                FXCollections.observableArrayList("ComboBox Value 1", "ComboBox Value 2"));
        combo.getSelectionModel().select(0);
        Button switcher = new Button("Switch Scene");

        VBox vBox = new VBox(button, text, combo, switcher);
        vBox.setStyle("-fx-background-color:red;");

        vBox.getChildren().forEach(c -> c.addEventFilter(InputEvent.ANY, e -> {
            if (!EXCESSIVE_ACTION_EVENTS.contains(e.getEventType())) {
                System.out.println(String.format("%s action: %s", c.getClass().getSimpleName(),
                        shortcutPressed ? "SHORTCUT - FIX THE VALUE" : "no shortcut"));
            }
        }));

        Scene scene = new Scene(vBox);
        Button switcherBack = new Button("Switch Back");
        Scene anotherScene = new Scene(new VBox(new Button("Another Button"), switcherBack));
        switcher.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> stage.setScene(anotherScene));
        switcherBack.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> stage.setScene(scene));

        stage.setScene(scene);
        stage.addEventFilter(KeyEvent.ANY, e -> shortcutPressed = e.isShortcutDown());
        stage.show();
    }

    public static class AppRunner {
        public static void main(String[] args) {
            ShortcutApp.main(args);
        }
    }
}

Output:

Button action: no shortcut
Button action: SHORTCUT - FIX THE VALUE
Button action: no shortcut
ComboBox action: SHORTCUT - FIX THE VALUE
ComboBox action: SHORTCUT - FIX THE VALUE
ComboBox action: SHORTCUT - FIX THE VALUE
ComboBox action: no shortcut
TextField action: no shortcut
TextField action: no shortcut
TextField action: SHORTCUT - FIX THE VALUE
TextField action: no shortcut
4
James_D On

What you are trying to do here is modify the behavior of the button (and other classes). The default behavior of a button is not to fire an action event if the shortcut key is pressed when the mouse is pressed. So, essentially, you are trying to add a new behavior for that (and similarly for key presses).

In general, modifying behaviors of controls is not particularly well supported in JavaFX. There is a little history to this: in earlier versions of the library, neither the skin classes nor the behavior classes were part of the public API. In JavaFX 9, the skin classes were moved to the public API, making skins easier to customize. At that time, it was determined that making the behavior part of the public API was not feasible in the time available before release, so that task was deferred until a later date. At the time of writing, behavior classes are not still part of the public API.

This means that replacing behaviors is not usually too easy in JavaFX. However, in this case, since you are not removing or replacing existing behaviors but you can couch your requirement as simply adding a brand new behavior, it is not too bad. The following outlines an approach which is not well-tested but at least demonstrates a way of getting this working.

The approach here is to subclass Button, adding a new property for shortcutArmed (similar to the existing Button property armed, but indicating that the button is armed with the shortcut modifier) and for onShortcutAction, which is equivalent to onAction.

Behaviors are installed by the skin classes. Here we take the approach that we will inherit all the existing behavior (this works since we are not removing or modifying existing behaviors, but simply adding a new one). So we create a skin class that is a subclass of ButtonSkin, which will install all the default behavior, and then install some additional behaviors for the mouse and space key being pressed and release with the shortcut modifier down.

First, the event class:

package org.jamesd.examples.shortcutaction;

import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.event.EventType;

public class ShortcutActionEvent extends Event {

    public static final EventType<ShortcutActionEvent> SHORTCUT_ACTION = new EventType<>(Event.ANY, "SHORTCUT_ACTION");


    public ShortcutActionEvent(Object source, EventTarget target) {
        super(source, target, SHORTCUT_ACTION);
    }

    @Override
    public EventType<? extends ShortcutActionEvent> getEventType() {
        return SHORTCUT_ACTION;
    }

}

The button subclass:

package org.jamesd.examples.shortcutaction;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Skin;


public class ShortcutButton extends Button {

    private final ReadOnlyBooleanWrapper shortcutArmed = new ReadOnlyBooleanWrapper(false);


    public ReadOnlyBooleanProperty shortcutArmedProperty() {
        return shortcutArmed.getReadOnlyProperty();
    }
    public final boolean isShortcutArmed() {
        return shortcutArmedProperty().get();
    }

    public final void shortcutArm() {
        shortcutArmed.set(true);
    }

    public final void shortcutDisarm() {
        shortcutArmed.set(false);
    }

    public final ObjectProperty<EventHandler<ShortcutActionEvent>> onShortcutAction = new SimpleObjectProperty<>();

    public ObjectProperty<EventHandler<ShortcutActionEvent>> onShortcutActionProperty() {
        return onShortcutAction;
    }

    public final EventHandler<ShortcutActionEvent> getOnShortcutAction() {
        return onShortcutActionProperty().get();
    }

    public final void setOnShortcutAction(EventHandler<ShortcutActionEvent> onShortcutAction) {
        onShortcutActionProperty().set(onShortcutAction);
    }

    public ShortcutButton() {
        this(null);
    }

    public ShortcutButton(String text) {
        super(text);
        onShortcutAction.addListener((obs, oldHandler, newHandler) -> {
            if (oldHandler != null) {
                removeEventHandler(ShortcutActionEvent.SHORTCUT_ACTION, oldHandler);
            }
            if (newHandler != null) {
                addEventHandler(ShortcutActionEvent.SHORTCUT_ACTION, newHandler);
            }
        });
    }

    public ShortcutButton(String text, Node graphic) {
        super(text, graphic);
    }

    @Override
    public Skin<?> createDefaultSkin() {
        return new ShortcutButtonSkin(this);
    }
}

The skin subclass:

package org.jamesd.examples.shortcutaction;

import javafx.scene.control.skin.ButtonSkin;

public class ShortcutButtonSkin extends ButtonSkin {

    private ShortcutButtonBehavior behavior ;
    public ShortcutButtonSkin(ShortcutButton control) {
        super(control);
        behavior = new ShortcutButtonBehavior(control);
        // Ensure style changes correctly when "shortcut armed":
        control.shortcutArmedProperty().addListener((obs, wasArmed, isNowArmed) ->
                pseudoClassStateChanged(PseudoClass.getPseudoClass("armed"), isNowArmed));
    }

    @Override
    public void dispose() {
        super.dispose();
        behavior.dispose();
    }
}

and finally the behavior class:

package org.jamesd.examples.shortcutaction;

import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;

public class ShortcutButtonBehavior {

    private static final KeyCode FIRE_KEY =
            System.getProperty("os.name").startsWith("Mac") ? KeyCode.SPACE : KeyCode.ENTER;


    private ShortcutButton control;

    private boolean shortcutKeyDown = false ;
    public ShortcutButtonBehavior(ShortcutButton control) {
        this.control = control;
        addKeyHandling();
        addMouseHandling();
    }

    private void addKeyHandling() {
        control.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
            if (e.getCode() == FIRE_KEY && e.isShortcutDown()) {
                if (! control.isPressed() && ! control.isShortcutArmed()) {
                    shortcutKeyDown = true;
                    control.shortcutArm();
                }
            }
        });
        control.addEventHandler(KeyEvent.KEY_RELEASED, e -> {
            if (e.getCode() == FIRE_KEY && e.isShortcutDown()) {
                if (shortcutKeyDown) {
                    shortcutKeyDown = false;
                    if (control.isShortcutArmed()) {
                        control.shortcutDisarm();
                        ShortcutActionEvent event = new ShortcutActionEvent(control, e.getTarget());
                        control.fireEvent(event);
                    }
                }
            }
        });
    }

    private void addMouseHandling() {
        control.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
            if (e.getButton() == MouseButton.PRIMARY && e.isShortcutDown()) {
                if (! control.isShortcutArmed()) {
                    control.shortcutArm();
                }
            }
        });
        control.addEventHandler(MouseEvent.MOUSE_RELEASED, e-> {
                if (e.isShortcutDown() && ! shortcutKeyDown && control.isShortcutArmed()) {
                    ShortcutActionEvent event = new ShortcutActionEvent(control, e.getTarget());
                    control.fireEvent(event);
                    control.shortcutDisarm();
                }
        });
    }

    // Just added this for consistency with the existing behavior classes, though there is
    // nothing to do here.
    public void dispose() {

    }
}

Finally, a demo:

package org.jamesd.examples.shortcutaction;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class App extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        ShortcutButton shortcutButton = new ShortcutButton("Click with shortcut key pressed");

        shortcutButton.setOnAction(e -> System.out.println("Failure"));
        shortcutButton.setOnShortcutAction(e -> System.out.println("Success"));

        Button button = new Button("Regular Button");
        button.setOnAction(e -> System.out.println("Button action"));

        VBox vBox = new VBox(5, shortcutButton, button);
        vBox.setPadding(new Insets(5));
        vBox.setAlignment(Pos.CENTER);
        Scene scene = new Scene(vBox);

        stage.setScene(scene);
        stage.show();
    }

    public static class AppRunner {
        public static void main(String[] args) {
            App.main(args);
        }
    }
}

This has the effect you need, in that you can distinguish between the different actions, though it is done using different event handlers rather than a switch in a single event handler. You can make the latter work by making ShortcutActionEvent a subclass of ActionEvent, using a single onAction handler, and testing if (event instanceof ShortcutActionEvent) {...} in the handler.

If you want a more minimal approach, you can bypass the button subclass and skin implementation, and use an event class which subclasses ActionEvent as described in the previous paragraph:

Event class:

package org.jamesd.examples.shortcutaction;

import javafx.event.ActionEvent;
import javafx.event.EventTarget;
import javafx.event.EventType;

public class ShortcutActionEvent extends ActionEvent {

    public static final EventType<ShortcutActionEvent> SHORTCUT_ACTION = new EventType<>(ActionEvent.ACTION, "SHORTCUT_ACTION");


    public ShortcutActionEvent(Object source, EventTarget target) {
        super(source, target);
    }

    @Override
    public EventType<? extends ShortcutActionEvent> getEventType() {
        return SHORTCUT_ACTION;
    }

}

Modified behavior class (just referencing a Button directly, not a subclass):

package org.jamesd.examples.shortcutaction;

import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;

public class ShortcutButtonBehavior {

    private static final KeyCode FIRE_KEY =
            System.getProperty("os.name").startsWith("Mac") ? KeyCode.SPACE : KeyCode.ENTER;

    private Button control;

    private boolean shortcutKeyDown = false ;
    public ShortcutButtonBehavior(Button control) {
        this.control = control;
        addKeyHandling();
        addMouseHandling();
    }

    private void addKeyHandling() {
        control.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
            if (e.getCode() == FIRE_KEY && e.isShortcutDown()) {
                if (! control.isPressed() && ! control.isArmed()) {
                    shortcutKeyDown = true;
                    control.arm();
                }
            }
        });
        control.addEventHandler(KeyEvent.KEY_RELEASED, e -> {
            if (e.getCode() == FIRE_KEY && e.isShortcutDown()) {
                if (shortcutKeyDown) {
                    shortcutKeyDown = false;
                    if (control.isArmed()) {
                        control.disarm();
                        ShortcutActionEvent event = new ShortcutActionEvent(control, e.getTarget());
                        control.fireEvent(event);
                    }
                }
            }
        });
    }

    private void addMouseHandling() {
        control.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
            if (e.getButton() == MouseButton.PRIMARY && e.isShortcutDown()) {
                if (! control.isArmed()) {
                    control.arm();
                }
            }
        });
        control.addEventHandler(MouseEvent.MOUSE_RELEASED, e-> {
                if (e.isShortcutDown() && ! shortcutKeyDown && control.isArmed()) {
                    ShortcutActionEvent event = new ShortcutActionEvent(control, e.getTarget());
                    control.fireEvent(event);
                    control.disarm();
                }
        });
    }

}

And the demo app using this approach:

package org.jamesd.examples.shortcutaction;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class App extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        Button shortcutButton = new Button("Click with shortcut key pressed");
        new ShortcutButtonBehavior(shortcutButton);

        shortcutButton.setOnAction(e -> {
            if (e instanceof ShortcutActionEvent) {
                System.out.println("Success");
            } else {
                System.out.println("Failure");
            }
        });

        Button button = new Button("Regular Button");
        button.setOnAction(e -> System.out.println("Button action"));

        VBox vBox = new VBox(5, shortcutButton, button);
        vBox.setPadding(new Insets(5));
        vBox.setAlignment(Pos.CENTER);
        Scene scene = new Scene(vBox);

        stage.setScene(scene);
        stage.show();
    }

    public static class AppRunner {
        public static void main(String[] args) {
            App.main(args);
        }
    }
}

Note I am on a Mac, on which shortcut+space is intercepted by the OS and opens the Spotlight Search bar, so I have not tested the keyboard handling here. I also have not implemented default and cancel button behavior, which a complete and robust solution would probably need.

You can implement this for combo boxes and text fields in a similar way; you will probably have to dig into the source code for the behavior classes to get hints on how to make it work.