Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tabbed Pane Shortcuts

I'm trying to create a notebook (in JavaFX) where the tabs have shortcuts much like buttons do. So, in the following example, there's a notebook with "Labor", "Parts" and "Tax".

enter image description here

The user can click on the tabs, of course, but they can also click Alt+L, Alt+P, Alt+T to select the labor, parts and tax panes respectively.

Looking at the docs, it appears the standard shortcut behavior comes from the Labeled class, from which Tab does not descend. How do I get this behavior into my tabbed-pane tabs?

EDIT: To be more specific, I'm looking for the visual effect that matches the existing behavior and control code which does not require the controlling code to know what the hotkeys are.

So, as with buttons, the user presses the meta-key (CTRL, ALT, OPT, whatever) and the appropriate character underlines itself, when they press the key, the parent would probably have to search the text of the tabs to know which one to select.

like image 661
user3810626 Avatar asked Mar 02 '23 14:03

user3810626


2 Answers

You just need to register your own EventFilter on the Scene. This will allow you to listen for your desired keyboard combinations and react accordingly.

In the sample application below, you will pass the desired Tab and KeyCombination to a method which handles the registration of the EventFilter.

In this example, I've configured the shortcuts of CTRL+1, CTRL+2, and CTRL+3 to select Tab 1, 2, or 3, respectively.

Note that you could also use KeyCombination.CONTROL_DOWN instead of KeyCombination.SHORTCUT_DOWN, but using SHORTCUT_DOWN is preferred as it is platform independent.

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class TabPaneShortcuts extends Application {

    public static void main(String[] args) {

        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {

        // **********************************************************************************************
        // Create a basic layout
        // **********************************************************************************************
        VBox root = new VBox(5);
        root.setAlignment(Pos.TOP_CENTER);
        root.setPadding(new Insets(10));

        // **********************************************************************************************
        // Create a TabPane
        // **********************************************************************************************
        TabPane tabPane = new TabPane();

        // **********************************************************************************************
        // Create the Tabs
        // **********************************************************************************************
        Tab tab1 = new Tab("Labor");
        Tab tab2 = new Tab("Parts");
        Tab tab3 = new Tab("Tax");
        tabPane.getTabs().addAll(tab1, tab2, tab3);

        // **********************************************************************************************
        // Add the TabPane to our root layout
        // **********************************************************************************************
        root.getChildren().add(tabPane);

        // **********************************************************************************************
        // Set the Scene for the stage
        // **********************************************************************************************
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);

        // **********************************************************************************************
        // Register keyboard shortcuts to select Tabs with the keyboard. Here, we create the KeyCodeCombination
        // to listen for CTRL + 1,2, or 3.
        // **********************************************************************************************
        registerShortcut(tabPane, tab1, scene,
                         new KeyCodeCombination(KeyCode.DIGIT1,
                                                KeyCombination.SHORTCUT_DOWN));
        registerShortcut(tabPane, tab2, scene,
                         new KeyCodeCombination(KeyCode.DIGIT2,
                                                KeyCombination.SHORTCUT_DOWN));
        registerShortcut(tabPane, tab3, scene,
                         new KeyCodeCombination(KeyCode.DIGIT3,
                                                KeyCombination.SHORTCUT_DOWN));

        // **********************************************************************************************
        // Configure the Stage
        // **********************************************************************************************
        primaryStage.setWidth(300);
        primaryStage.setHeight(200);
        primaryStage.setTitle("Test Application");
        primaryStage.show();
    }

    /**
     * Registers the given KeyCombination to automatically select the given Tab of the TabPane
     *
     * @param tabPane     The TabPane whose selection model we need to manipulate
     * @param tab         The Tab to be selected when the given KeyCombination is detected
     * @param scene       The main Scene on which to register this EventFilter
     * @param combination The KeyCombination to listen for
     */
    private void registerShortcut(TabPane tabPane, Tab tab, Scene scene, KeyCombination combination) {

        // **********************************************************************************************
        // Add an EventFilter to the Scene to listen for the given KeyCombination
        // **********************************************************************************************
        scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
            // **********************************************************************************************
            // If the event matches the KeyCombination passed to this method, select the desired Tab
            // **********************************************************************************************
            if (combination.match(event)) {
                tabPane.getSelectionModel().select(tab);
            }
        });

    }
}

Your question specifically mentioned using ALT+[letter] for the combination and that can be done easily as well:

registerShortcut(tabPane, tab1, scene,
                 new KeyCodeCombination(KeyCode.L,
                                        KeyCombination.ALT_DOWN));

I chose to go with CTRL simply because using ALT causes my Windows 10 system to "ding" every time and I'm too lazy to figure out why. :)

like image 78
Zephyr Avatar answered Mar 12 '23 14:03

Zephyr


Unfortunately (and a bit astonishingly to me), mnemonics on Tabs are not directly supported. Registering eventFilters as suggested in another answer is one way out. Another is to use the available support for mnemonics.

Doing something like:

Label label = new Label("_MyLabel");
label.setMnemonicParsing(true);
label.setLabelFor(otherControl);

will focus otherControl when the system detects a key combination as mnemonic (which basically is OS dependent).

Trying to apply that to Tabs runs into immediate problems:

  • there is no direct access to the label that visualises the tab header
  • hidden nodes (like the content of a not-selected tab) will not get focus

Digging a bit into internals revealed interesting collaborators:

  • there's a class Mnemonic that handles a pair of key combination and node, with a method fire(ActionEvent)
  • Scene has a method addMnemonic(Mnemonic): on detecting the key combination of the given mnemonic, it fires an action event onto its node
  • LabeledSkinBase manages its label's mnemonicParsing by registering a Mnemonic with its scene: usually, it's the pair labelFor/keyCombination - but interestingly the fallback (that is when labelFor is null) pair is label/keyCombination

Combining all, we can use that mechanism to trigger an arbitrary action by mnemonic:

Label label = new Label("_MyLabel");
label.setMnemonicParsing(true);
label.addEventHandler(ActionEvent.ACTION, ac -> {
    // do stuff
});

When applying that approach a TabPane, the label would be in a Tab's header and doStuff would select the Tab it resides in. As we still have no direct access to the label that is created by the skin, we could configure a Tab with graphic only and the graphic being a label as above:

Tab tab = new Tab();
tab.setGraphic(label);
// do stuff: tab.getTabPane().getSelectionModel().select(tab);

Another option would be to lookup the labels created by the skin and configure those in the same way, something like:

// building the tabs/tabPane as usual
Tab tab = new Tab("_MyTabText");

// .. after skin has been created
Set<Node> labels = tabPane.lookupAll(".tab-label");    
labels.forEach(e -> {
    if (e instanceof Label) {
        Label label = (Label) e;
        // beware: internal hierarchy!
        Parent innerContainer = label.getParent();
        Parent tabHeaderSkin = innerContainer.getParent();
        // beware: implementation detail
        Tab tab = (Tab) tabHeaderSkin.getProperties().get(Tab.class);
        
        label.setMnemonicParsing(true);
        label.addEventHandler(ActionEvent.ACTION, c -> {
            tabPane.getSelectionModel().select(tab);
        });

    }
});

Both do work, but each has its drawback/s

  • the first by-passes the tab's text property, requires additional efforts if both text and graphic is needed
  • the second relies on implementation details of the skin and has a visual glitch in not showing the mnenomic underscore (no idea why not)

On the bright side for both: no OS specifics in client code.

like image 41
kleopatra Avatar answered Mar 12 '23 16:03

kleopatra