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:
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:
This is not what I expect, because only one TabPane
can have a Tab
with red border - that one which has a focused table.
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 arefocused
, 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 forfocus-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:
- focused
- focus-within
- 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-withinNote that
focus-visible
only matches nodes that are also focus owners, so there's no need to have afocus-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.