Search code examples
javajavafxjavafx-css

Pseudo-class focus-within doesn't work for tab in JavaFX


According to https://openjfx.io/javadoc/22/javafx.graphics/javafx/scene/doc-files/cssref.html JavaFX 22 supports pseudo-class focus-within. I use JavaFX 23-ea+3 and this is my code:

public class JavaFxTest7 extends Application {

    private static class Student {

        private int id;

        private int mark;

        public Student(int id, int mark) {
            this.id = id;
            this.mark = mark;
        }

        public int getId() {
            return id;
        }

        public int getMark() {
            return mark;
        }
    }

    private static class MyTab extends Tab {

        private final TableView<Student> table = new TableView<>(FXCollections.observableList(
            List.of(new Student(1, 3), new Student(2, 4), new Student(3, 5))));

        private final MenuItem menuItem = new MenuItem("New Tab");

        private final ContextMenu contextMenu = new ContextMenu(menuItem);

        public MyTab() {
            var idColumn = new TableColumn<Student, Integer>();
            idColumn.setCellValueFactory((data) ->  new ReadOnlyObjectWrapper<>(data.getValue().getId()));
            var markColumn = new TableColumn<Student, Integer>();
            markColumn.setCellValueFactory((data) ->  new ReadOnlyObjectWrapper<>(data.getValue().getMark()));
            table.getColumns().addAll(idColumn, markColumn);
            menuItem.setOnAction((e) -> {
                var newTab = new MyTab();
                this.getTabPane().getTabs().add(newTab);
            });
            table.setContextMenu(contextMenu);
            this.setContent(table);
        }

    }

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

    @Override
    public void start(Stage stage) {
        TabPane tabPane = new TabPane();
        tabPane.getTabs().add(new MyTab());
        var scene = new Scene(tabPane, 400, 300);
        var css= this.getClass().getResource("test7.css").toExternalForm();
        scene.getStylesheets().add(css);
        stage.setScene(scene);
        stage.show();
    }
}

and test7.css:

.tab-pane > .tab-header-area .tab .focus-indicator {
    -fx-border-width: 2 0 0 0;
    -fx-border-color: green;
    -fx-border-insets: -3 -5 0 -5;
    -fx-border-radius: 2 2 0 0;
}

.tab-pane > .tab-header-area .tab:focus-within .focus-indicator {
    -fx-border-width: 2 0 0 0;
    -fx-border-color: red;
    -fx-border-insets: -3 -5 0 -5;
    -fx-border-radius: 2 2 0 0;
}

And this is the result:

enter image description here

As you see the table in the first tab has focus, but the tab has green border, not the red one. Could anyone say how to fix it? Or do I misunderstand the meaning of this pseudo class?

Edit:

@James_D explained the proper use of focus-widthin with TabPane. However, when I changed my code to :

...
@Override
public void start(Stage stage) {
    TabPane tabPane0 = new TabPane();
    tabPane0.getTabs().add(new MyTab());
    TabPane tabPane1 = new TabPane();
    tabPane1.getTabs().add(new MyTab());
    var scene = new Scene(new VBox(tabPane0, tabPane1), 400, 300);
    var css= this.getClass().getResource("test7.css").toExternalForm();
    scene.getStylesheets().add(css);
    stage.setScene(scene);
    stage.show();
}
...

with these CSS settings:

.tab-pane > .tab-header-area .tab .focus-indicator {
    -fx-border-width: 2 0 0 0;
    -my-tab-border-color: green;
    -fx-border-color: -my-tab-border-color;
    -fx-border-insets: -3 -5 0 -5;
    -fx-border-radius: 2 2 0 0;
}

.tab-pane:focus-within > .tab-header-area .tab:selected .focus-indicator {
    -my-tab-border-color: red;
}

I got this result:

enter image description here

This is not what I expect, because only one TabPane can have a Tab with red border - that one which has a focused table.


Solution

  • It turns out that @James_D's approach is sometimes correct, but not when TableView or ListView are in the Tab contents. The underlying assumption in the design of this approach - that only one Node can have focus at any given time - is not always true. From the https://bugs.openjdk.org/browse/JDK-8317426?jql=text%20~%20%22focusWithin%22 JDK Issue:

    Michael Strauß added a comment - 2023-10-04 05:58 This is not a bug. focusWithin is set when any of the child nodes are focused, which is always true for a TableView with selected cells.

    Here's the source of confusion: "focus-within should always immediately become false whenever the Scene's focus moves elsewhere." (from submitter report) That's not how focus-within is specified. The submitter is looking for focus-owner-within, which doesn't exist.

    A potential enhancement is described here: https://mail.openjdk.org/pipermail/openjfx-dev/2023-August/042005.html

    Further explanation is supplied in the linked comment:

    Scene graph nodes have three focus-related properties:

    1. focused
    2. focus-within
    3. focus-visible

    What might be a bit surprising is that multiple nodes can be focused at the same time, but only one of those focused nodes can be the scene's focus owner. The Scene.focusOwner property indicates which of the focused nodes is the current focus owner.

    Even more surprisingly, a node can be unfocused and be the focus owner at the same time (this can happen when the window is deactivated).

    From the perspective of Node, there's currently no API to easily determine whether it is the focus owner, and there's no CSS pseudo-class that matches a node that is the focus owner.

    We could add two more properties to the Node class to support this use case: 4. focus-owner 5. focus-owner-within

    Note that focus-visible only matches nodes that are also focus owners, so there's no need to have a focus-owner-visible property.

    The two additional properties would suffice to match any possible focus state, but it might also be not useful enough to warrant the additional API surface. What do you think?

    The bottom line: More than one Node in a Scene can have focus at any given time, and this is typical with TableView and ListView that have rows selected. Any design based on the assumption that only one Node can have focus at a time is faulty (at least when TableView or ListView are involved).

    At present, there are no Properties that can be used to work around this. In this example, both TableViews have focus simply because they have selected rows, and the focusWithinProperty() of both Tabs will be true, and this is not a bug.

    Presumably, you could use Scene.focusOwnerProperty() to recursively check through all of the children of the Tab contents to find a match, and then bind that to a StyleableProperty of the Tab, but that seems out of scope of the original question.