Issue
I'm trying to change the close icon background of the selected tab only, the problem is that it works when there is one tab, but when I add more nothing happens until I close the first one, here is what I did :
tabPane.lookup(".tab:selected:top").lookup(".tab-container").lookup(".tab-close-button").setStyle("-fx-background-color: red");
Now the problem is that when I select another tab and try to change it's color nothing happens, until I close the previous changed one.
Quick example:
I have 2 tabs with white close buttons,
I select tab1 : the color changed,
I select tab2 : nothing happens,
I close tab1 then select tab2 : tab2's close button changed.
The selected tab is correct because if I print those 2 lines for each tab:
System.out.println(tabPane.getSelectionModel().getSelectedItem());
System.out.println(tabPane.lookup(".tab:selected:top").lookup(".tab-container").lookup(".tab-close-button"));
here is the result :
javafx.scene.control.Tab@6d81182e <----tab1
TabPaneSkin$TabHeaderSkin$2@7d620380[styleClass=tab-close-button] <----tab1
//now I switch to tab2
javafx.scene.control.Tab@416ca04a <----tab2
TabPaneSkin$TabHeaderSkin$2@7d620380[styleClass=tab-close-button] <----this address should not be the same as the first one.
PS : I can't do it in CSS because the color should change dynamically, the line is executed inside a listener when I switch tab.
Thanks for the help!
Solution
CSS lookups are inherently reliant on implementation details of the rendering framework, and as such they can be unreliable and you should really avoid using them whenever possible. In particular, lookups will not work if a CSS pass has not been made on the scene graph (typically this happens during rendering), or if changes to the CSS state have occurred since the last CSS pass. I think what is happening with your code is that the listener is invoked before the CSS state has been updated with the new selection state, causing you to retrieve the wrong Tab
from the .tab:selected
lookup (though I'm by no means certain this is what's happening).
Note that you already have most of the dynamic behavior you need, because the "selected"
pseudoclass will be updated dynamically.
The best approach for functionality like this is to define an external CSS file which covers all your possible styles, and then to programmatically modify CSS state using one of a number of mechanisms. A couple of ways to do this are:
- Use "looked-up colors". These act like variable names in CSS.
- Use custom pseudoclasses, which you can use as selectors in the CSS file, and whose state you can update programatically.
Here's an example using the first approach. Here we define a looked-up color in the CSS file called "selected-tab-color"
which initially sets the close button to be blue.
tabpane.css:
.tab-pane {
selected-tab-color: blue ;
}
.tab:selected > .tab-container > .tab-close-button {
-fx-background-color: selected-tab-color ;
}
In Java code, we can update this simply with a call to tabPane.setStyle("selected-tab-color: red;")
, which you can do, e.g., in an event handler (but really anywhere, as long as it's on the JavaFX application thread). In this example, we just list to the selected tab, and make one tab have a red close button, the others blue.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TabPaneTest extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
TabPane tabPane = new TabPane();
Tab tab1 = new Tab("Tab 1", new Label("Tab 1"));
Tab tab2 = new Tab("Tab 2", new Label("This tab has\na custom close button"));
Tab tab3 = new Tab("Tab 3", new Label("Tab 3"));
tabPane.getTabs().addAll(tab1, tab2, tab3);
tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> {
if (newTab == tab2) {
tabPane.setStyle("selected-tab-color: red;");
} else {
tabPane.setStyle("selected-tab-color: blue;");
}
});
BorderPane root = new BorderPane(tabPane);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("tabpane.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setWidth(250);
primaryStage.setHeight(250);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
You can use this technique to turn the effect off entirely, by reverting to the default:
tabPane.setStyle("selected-tab-color: -fx-mark-color;");
however, this relies on knowing the details of the default CSS implementation (i.e. the name of the default color for the tab close button).
A slightly better way to turn the functionality on or off programmatically is to use a custom Pseudoclass. These work like any other CSS pseudoclass, and can have their state modified programmatically.
In this example, the color of the close button is red only if the containing tab pane has the warn-on-close
pseudoclass set:
.tab-pane:warn-on-close .tab:selected > .tab-container > .tab-close-button {
-fx-background-color: red ;
}
And in the Java code here, we switch this on when tab 2 is selected, and off when the others are selected. Again, arbitrary logic is possible here.
import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TabPaneTest extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
TabPane tabPane = new TabPane();
Tab tab1 = new Tab("Tab 1", new Label("Tab 1"));
Tab tab2 = new Tab("Tab 2", new Label("This tab has\na custom close button"));
Tab tab3 = new Tab("Tab 3", new Label("Tab 3"));
tabPane.getTabs().addAll(tab1, tab2, tab3);
PseudoClass warnOnClosePseudoClass = PseudoClass.getPseudoClass("warn-on-close");
tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) ->
tabPane.pseudoClassStateChanged(warnOnClosePseudoClass, newTab == tab2));
BorderPane root = new BorderPane(tabPane);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("tabpane.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setWidth(250);
primaryStage.setHeight(250);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
Answered By - James_D