JavaFX: How to change the focus traversal policy?
Asked Answered
S

11

28

Is it possible in JavaFX to change the focus traversal policy, like in AWT?

Because the traversal order for two of my HBoxes is wrong.

Sawtelle answered 6/3, 2013 at 4:21 Comment(5)
you can request focus for any node in javafx by calling node.requestFocus() methodDiary
Yes i know, but this is not a very neat solution.Sawtelle
Please expose clearly your problem and provide and sscce.Cockatiel
It would be nice to group controls in in a "traversal group" and set a tab index. If one of a control in this traversal group has focus, a pressing of [Tab]-Key forces the control with the next higher index to become focus. Analogous for pressing [Tab] + [Shift] with the next lower index.Demarcation
@gontard, question is asked, so that there could be an answer. Sonja is asking, is that possible to customize way of focus traversing.Ayeaye
A
13

In common case the navigation is done in a container order, in order of children, or according to arrow keys pressing. You can change order of nodes - it will be the optimal solution for you in this situation.

There is a back door in JFX about traversal engine strategy substitution :

you can subclass the internal class com.sun.javafx.scene.traversal.TraversalEngine

engine = new TraversalEngine(this, false) {
            @Override public void trav(Node owner, Direction dir) {
                // do whatever you want
            }
        };

And use

setImpl_traversalEngine(engine); 

call to apply that engine.

You can observe the code of OpenJFX, to understand, how it works, and what you can do.

Be very careful : it is an internal API, and it is likely to change, possibly, in the nearest future. So don't rely on this (you cannot rely on this officialy, anyway).

Sample implementation :

public void start(Stage stage) throws Exception {
    final VBox vb = new VBox();

    final Button button1 = new Button("Button 1");
    final Button button2 = new Button("Button 2");
    final Button button3 = new Button("Button 3");

    TraversalEngine engine = new TraversalEngine(vb, false) {
        @Override
        public void trav(Node node, Direction drctn) {
            int index = vb.getChildren().indexOf(node);

            switch (drctn) {
                case DOWN:
                case RIGHT:
                case NEXT:
                    index++;
                    break;
                case LEFT:
                case PREVIOUS:
                case UP:
                    index--;
            }

            if (index < 0) {
                index = vb.getChildren().size() - 1;
            }
            index %= vb.getChildren().size();

            System.out.println("Select <" + index + ">");

            vb.getChildren().get(index).requestFocus();
        }
    };

    vb.setImpl_traversalEngine(engine);

    vb.getChildren().addAll(button1, button2, button3);
    Scene scene = new Scene(vb);
    stage.setScene(scene);
    stage.show();
}

It will require strong analitical skills for common case ;)

Ayeaye answered 6/3, 2013 at 14:37 Comment(1)
the internal api seems to have changed considerably in fx8, no longer possible (probably need to adjust by implementing a custom Algorithm, configure a ParentFocusTraversalEngine with it)Tutankhamen
D
21

The simplest solution is to edit the FXML file and reorder the containers appropriately. As an example, my current application has a registration dialog in which a serial number can be entered. There are 5 text fields for this purpose. For the focus to pass from one text field to the other correctly, I had to list them in this way:

<TextField fx:id="tfSerial1" layoutX="180.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial2" layoutX="257.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial3" layoutX="335.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial4" layoutX="412.0" layoutY="166.0" prefWidth="55.0" />
<TextField fx:id="tfSerial5" layoutX="488.0" layoutY="166.0" prefWidth="55.0" />
Dromous answered 27/5, 2013 at 15:50 Comment(3)
Yes this is the simplest way. I had the same issue. That mean while we are design the UI Scene Builder regenerates the relevant FXML tags in add-hock order. Once we rearrange them correct order it works fine.Goer
It would be nice if SB could automatically do this for us.Biannual
This is definitely the simplest way. Thanks.Mission
E
16

Bluehair's answer is right, but you can do this even in JavaFX Scene Builder.

You have Hierarchy panel in left column. There are all your components from scene. Their order represents focus traversal order and it responds to their order in FXML file.

I found this tip on this webpage:www.wobblycogs.co.uk

Excitable answered 9/12, 2013 at 18:3 Comment(0)
A
13

In common case the navigation is done in a container order, in order of children, or according to arrow keys pressing. You can change order of nodes - it will be the optimal solution for you in this situation.

There is a back door in JFX about traversal engine strategy substitution :

you can subclass the internal class com.sun.javafx.scene.traversal.TraversalEngine

engine = new TraversalEngine(this, false) {
            @Override public void trav(Node owner, Direction dir) {
                // do whatever you want
            }
        };

And use

setImpl_traversalEngine(engine); 

call to apply that engine.

You can observe the code of OpenJFX, to understand, how it works, and what you can do.

Be very careful : it is an internal API, and it is likely to change, possibly, in the nearest future. So don't rely on this (you cannot rely on this officialy, anyway).

Sample implementation :

public void start(Stage stage) throws Exception {
    final VBox vb = new VBox();

    final Button button1 = new Button("Button 1");
    final Button button2 = new Button("Button 2");
    final Button button3 = new Button("Button 3");

    TraversalEngine engine = new TraversalEngine(vb, false) {
        @Override
        public void trav(Node node, Direction drctn) {
            int index = vb.getChildren().indexOf(node);

            switch (drctn) {
                case DOWN:
                case RIGHT:
                case NEXT:
                    index++;
                    break;
                case LEFT:
                case PREVIOUS:
                case UP:
                    index--;
            }

            if (index < 0) {
                index = vb.getChildren().size() - 1;
            }
            index %= vb.getChildren().size();

            System.out.println("Select <" + index + ">");

            vb.getChildren().get(index).requestFocus();
        }
    };

    vb.setImpl_traversalEngine(engine);

    vb.getChildren().addAll(button1, button2, button3);
    Scene scene = new Scene(vb);
    stage.setScene(scene);
    stage.show();
}

It will require strong analitical skills for common case ;)

Ayeaye answered 6/3, 2013 at 14:37 Comment(1)
the internal api seems to have changed considerably in fx8, no longer possible (probably need to adjust by implementing a custom Algorithm, configure a ParentFocusTraversalEngine with it)Tutankhamen
E
9

We're using JavaFX event filters for this, e.g.:

cancelButton.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
    @Override
    public void handle(KeyEvent event) {
        if (event.getCode() == KeyCode.TAB && event.isShiftDown()) {
            event.consume();
            getDetailsPane().requestFocus();
        }
    }
});

The event.consume() suppresses the default focus traversal, which otherwise causes trouble when calling requestFocus().

Eerie answered 2/7, 2014 at 10:20 Comment(2)
In my optinion, this is the only sustainable answer. We don't call internal API and rearranging Nodes in FXML isn't always possible, e.g. if we want to add Nodes dynamically using Java.Kempf
The Best suitable answer even for Controls inside GirdPane and also for Dynamically Generated Controls using Java CodeSolley
T
8

This is the accepted answer adapted to change of internal api (happened at some point of fx-8, my current version is 8u60b5). Obviously the original disclaimer still applies: it's internal api, open to change without notice at any time!

The changes (compared to the accepted answer)

  • Parent needs a TraversalEngine of type ParentTraversalEngine
  • nav is no longer a method of TraversalEngine (nor ParentTE) but only of TopLevelTraversalEngine
  • the navigation implementation is delegated to strategy called Algorithm
  • actual focus transfer is (seems to be?) handled by TopLevelTE, Algorithm only finds and returns the new target

The plain translation of the example code:

/**
 * Requirement: configure focus traversal
 * old question with old hack (using internal api):
 * https://mcmap.net/q/487946/-javafx-how-to-change-the-focus-traversal-policy/203657
 * 
 * New question (closed as duplicate by ... me ..)
 * https://mcmap.net/q/503266/-change-javafx-tab-button-orderring-of-textfields-from-quot-left-to-right-quot-to-quot-right-to-left-quot-by-fxml-file-duplicate/203657
 * Old hack doesn't work, change of internal api
 * rewritten to new internal (sic!) api
 * 
 */
public class FocusTraversal extends Application {

    private Parent getContent() {
        final VBox vb = new VBox();

        final Button button1 = new Button("Button 1");
        final Button button2 = new Button("Button 2");
        final Button button3 = new Button("Button 3");

        Algorithm algo = new Algorithm() {

            @Override
            public Node select(Node node, Direction dir,
                    TraversalContext context) {
                Node next = trav(node, dir);
                return next;
            }

            /**
             * Just for fun: implemented to invers reaction
             */
            private Node trav(Node node, Direction drctn) {
                int index = vb.getChildren().indexOf(node);

                switch (drctn) {
                    case DOWN:
                    case RIGHT:
                    case NEXT:
                    case NEXT_IN_LINE:    
                        index--;
                        break;
                    case LEFT:
                    case PREVIOUS:
                    case UP:
                        index++;
                }

                if (index < 0) {
                    index = vb.getChildren().size() - 1;
                }
                index %= vb.getChildren().size();

                System.out.println("Select <" + index + ">");

                return vb.getChildren().get(index);
            }

            @Override
            public Node selectFirst(TraversalContext context) {
                return vb.getChildren().get(0);
            }

            @Override
            public Node selectLast(TraversalContext context) {
                return vb.getChildren().get(vb.getChildren().size() - 1);
            }

        };
        ParentTraversalEngine engine = new ParentTraversalEngine(vb, algo);
        // internal api in fx8
        // vb.setImpl_traversalEngine(engine);
        // internal api since fx9
        ParentHelper.setTraversalEngine(vb, engine);
        vb.getChildren().addAll(button1, button2, button3);
        return vb;
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setScene(new Scene(getContent()));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
Tutankhamen answered 7/5, 2015 at 10:34 Comment(6)
See ContainerTabOrder for an example of a full algorithm (need to recurse in children). But it was too complex to me, I have used a workaround to ignore buttons in tab policy, in FXML without java code: set focusTraversable="false". It will continue to the next focusable element.Salesgirl
since Java 9 all these internal APIs is not exported and this back door is available no more.Yuan
they are still available if you are allowed to modify encapsulation - that's about the same level of dirtyness as in previous versions :)Tutankhamen
hmm .. mileage varies but .. adding exports/opens is not a big deal, IMO :)Tutankhamen
if you bring JavaFX as Jar(Java 11) just editing the jar module file I guess, but since in Java 9 is still in Java core, better use reflection in run time than create your own Java distribution, but I am really not expert on that...Yuan
not talking about custom fx distribution - just modify the build/runtime path with --add-exports / --add-opens as necessary ...Tutankhamen
L
1

In scene builder go to the view menu and select show document. on the left will be all objects in your current fxml document. drag controls up or down in the list to reorder for tab index. Select hide document to use the other tools since the document pane hogs the space.

Laspisa answered 6/5, 2014 at 5:35 Comment(0)
R
1

I used Eventfilter as solution in combination with referencing the field ID's, so all you have to do is name the fields like (field1,field2,field3,field4) so you can place the fields where u want:

mainScene.addEventFilter(KeyEvent.KEY_PRESSED, (event) -> {
        if(event.getCode().equals(KeyCode.TAB)){
            event.consume();
            final Node node =  mainScene.lookup("#field"+focusNumber);
            if(node!=null){
                node.requestFocus();
            }
            focusNumber ++;
            if(focusNumber>11){
              focusNumber=1;
            }
        }
    }); 
Reformatory answered 22/2, 2016 at 13:23 Comment(1)
This was the core to my general solution. All Node objects have an id, whenever I created a focusable element I also set the id. I then created a simple List of the ids of elements I wanted include in the focus cycle and a Map of the Node objects. I then would take the target of the key event (ke.getTarget()) get it's id, find the index of that id in the id List, get the prev/next id as desired and look that up in the Map and call requestFocus() on that. There might be better ways to optimize this bit it is simple to understand - I think :)Lanate
A
1

You can do this easily with SceneBuilder. Open fxml file with scenebuilder and go to hierarchy under Document tab. Place input controls into a order you want by dragging input controls. Remember to check Focus Traversal in properties. Then focus traversal policy will work nicely when you press tab bar.

chnage focus traversal policy with SB

Aarau answered 9/4, 2019 at 5:50 Comment(0)
A
0

You can use NodeName.requestFocus() as said above; furthermore, make sure you request this focus after instantiating and adding all of your nodes for the root layout, because as you do so, the focus will be changing.

Architectonic answered 3/5, 2018 at 22:6 Comment(0)
G
0

I had a problem in which a JavaFX Slider was capturing right and left arrow keystrokes that I wanted to be handled by my method keyEventHandler (which handled key events for the Scene). What worked for me was to add the following line to the code that initialized the Slider:

slider.setOnKeyPressed(keyEventHandler);

and to add

keyEvent.consume();

at the end of keyEventHandler.

Gardel answered 29/5, 2020 at 16:18 Comment(0)
L
0

General solution inspired by Patrick Eckert's answer.

When I am creating the UI, say for example adding a TextField, I set things up like so:

List<String> displayOrder;
Map<String, Node> cycle;

TextField tf = new TextField();
tf.setId("meTF");
cycle.put("meTF", tf);
displayOrder.add("meTF");
getChildren().add(tf);

Then on the UI element (layout usually) you add this:

ui.addEventFilter(KeyEvent.KEY_PRESSED, (ke) ->
{
    if(!ke.getCode().equals(KeyCode.TAB) || !(ke.getTarget() instanceof Node))
        return;
            
    int i = displayOrder.indexOf(((Node)ke.getTarget()).getId());
            
    if(i < 0) // can't find it
        return;
            
    if(ke.isShiftDown())
        i = (i == 0 ? displayOrder.size() - 1 : i - 1);
    else
        i = ++i % displayOrder.size();
            
    cycle.get(displayOrder.get(i)).requestFocus();
            
    ke.consume();
});

FYI I think it is important to only consume the event if you are actually going to call request focus on something. Less likely to break something unintentionally that way...

If anyone can think of ways to optimize this further I'd appreciate knowing :)

Lanate answered 10/8, 2021 at 6:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.