0

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!

adxl
  • 829
  • 1
  • 9
  • 19

1 Answers1

4

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

}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • I'm going to assume you read the question right. The wording does seem to match your interpretation better than mine. – Slaw Feb 22 '20 at 14:11
  • Thanks for your answer, it works fine when switching tabs, but unfortunately as I said, I can't use an external CSS because I have to check some conditions inside my listener and then decide whether or not the color will change. That's why I think a programmatically solution will be better. but thanks anyway. – adxl Feb 22 '20 at 17:46
  • 2
    @adxl You can use the technique I showed at the end of the post to decide whether or not to use a custom color; to turn it off entirely just set the looked-up color to the default: ‘-fx-mark-color’ – James_D Feb 22 '20 at 19:15
  • 1
    @adxl I updated the answer to give a couple of "programmatic" ways to hook into the external CSS file, and added some explanation of why I think your approach isn't working. – James_D Feb 22 '20 at 21:13
  • @James_D I tried to implement the second technique (looked-up colors) and it works fine, so I guess I was wrong saying that external CSS won't help. Also thank you for your clear explanation and efforts! – adxl Feb 22 '20 at 21:51