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.

kleopatra
  • 51,061
  • 28
  • 99
  • 211
Sonja
  • 505
  • 1
  • 7
  • 15
  • you can request focus for any node in javafx by calling node.requestFocus() method – invariant Mar 06 '13 at 04:52
  • Yes i know, but this is not a very neat solution. – Sonja Mar 06 '13 at 06:12
  • Please expose clearly your problem and provide and [sscce](http://sscce.org/). – gontard Mar 06 '13 at 09:49
  • 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. – Vertex Mar 06 '13 at 10:11
  • 1
    @gontard, question is asked, so that there could be an answer. Sonja is asking, is that possible to customize way of focus traversing. – Alexander Kirov Mar 06 '13 at 14:07

11 Answers11

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" />
Bluehair
  • 211
  • 2
  • 2
  • 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. – Channa Mar 23 '14 at 15:10
  • It would be nice if SB could automatically do this for us. – User Sep 26 '16 at 21:03
  • This is definitely the simplest way. Thanks. – pakpe Jun 27 '19 at 03:09
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

none_
  • 535
  • 4
  • 9
12

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 ;)

Alexander Kirov
  • 3,624
  • 1
  • 19
  • 23
  • 6
    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) – kleopatra May 07 '15 at 09:54
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().

dwagelaar
  • 174
  • 1
  • 6
  • This is the easiest and best. Thanks. – chris Feb 26 '16 at 23:16
  • 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. – aboger Oct 05 '16 at 09:02
  • The Best suitable answer even for Controls inside GirdPane and also for Dynamically Generated Controls using Java Code – GreenROBO Jan 18 '21 at 13:38
7

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):
 * http://stackoverflow.com/q/15238928/203657
 * 
 * New question (closed as duplicate by ... me ..)
 * http://stackoverflow.com/q/30094080/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);
    }
}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • 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. – pdem May 12 '16 at 09:00
  • since Java 9 all these internal APIs is not exported and this back door is available no more. – Sarel Foyerlicht Apr 01 '19 at 12:05
  • they are still available if you are allowed to modify encapsulation - that's about the same level of dirtyness as in previous versions :) – kleopatra Apr 01 '19 at 12:10
  • hmm .. mileage varies but .. adding exports/opens is not a big deal, IMO :) – kleopatra Apr 01 '19 at 12:22
  • 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... – Sarel Foyerlicht Apr 01 '19 at 12:36
  • 1
    not talking about custom fx distribution - just modify the build/runtime path with --add-exports / --add-opens as necessary ... – kleopatra Apr 01 '19 at 12:45
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.

Chris
  • 11
  • 1
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;
            }
        }
    }); 
  • 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 :) – BigMac66 Aug 10 '21 at 05:51
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

Jamith NImantha
  • 1,999
  • 2
  • 20
  • 27
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.

Gerhard
  • 22,678
  • 7
  • 27
  • 43
Aaron
  • 38
  • 5
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.

Don Smith
  • 473
  • 4
  • 10
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 :)

BigMac66
  • 844
  • 7
  • 13