Search code examples
javafxjavafx-css

JavaFX - Style first and last tab only


Is there a way (CSS or Java) to style first and last tab only in a dynamic TabPane?

Example :
example

Thanks!


Solution

  • You can observe the ObservableList<Tab> returned by TabPane#getTabs() and update the style class of each Tab as appropriate. For example:

    App.java:

    import javafx.application.Application;
    import javafx.collections.ListChangeListener.Change;
    import javafx.collections.ObservableList;
    import javafx.css.Styleable;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.control.Tab;
    import javafx.scene.control.TabPane;
    import javafx.scene.control.TabPane.TabClosingPolicy;
    import javafx.scene.control.TabPane.TabDragPolicy;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    
    public class App extends Application {
    
      @Override
      public void start(Stage primaryStage) {
        TabPane pane = new TabPane();
        pane.setTabClosingPolicy(TabClosingPolicy.ALL_TABS);
        pane.setTabDragPolicy(TabDragPolicy.REORDER); // requires JavaFX 10+
        pane.getTabs().addListener(App::tabsChanged);
        pane.getTabs() // add tabs **after** adding ListChangeListener
            .addAll(
                new Tab("Test Tab #1", new StackPane(new Label("Content #1"))),
                new Tab("Test Tab #2", new StackPane(new Label("Content #2"))),
                new Tab("Test Tab #3", new StackPane(new Label("Content #3"))),
                new Tab("Test Tab #4", new StackPane(new Label("Content #4"))),
                new Tab("Test Tab #5", new StackPane(new Label("Content #5"))));
        Scene scene = new Scene(pane, 600, 400);
        scene.getStylesheets().add(getClass().getResource("/App.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();
      }
    
      private static void tabsChanged(Change<? extends Tab> c) {
        while (c.next()) {
          if (c.wasRemoved()) {
            for (Tab removed : c.getRemoved()) {
              removed.getStyleClass().removeAll("first-tab", "last-tab");
            }
          }
        }
    
        ObservableList<? extends Tab> tabs = c.getList();
        if (tabs.size() == 1) {
          Tab tab = tabs.get(0);
          addStyleClassIfAbsent(tab, "first-tab");
          addStyleClassIfAbsent(tab, "last-tab");
        } else if (!tabs.isEmpty()) {
          Tab first = tabs.get(0);
          addStyleClassIfAbsent(first, "first-tab");
          first.getStyleClass().remove("last-tab");
    
          Tab last = tabs.get(tabs.size() - 1);
          addStyleClassIfAbsent(last, "last-tab");
          last.getStyleClass().remove("first-tab");
    
          for (Tab middle : tabs.subList(1, tabs.size() - 1)) {
            middle.getStyleClass().removeAll("first-tab", "last-tab");
          }
        }
      }
    
      private static void addStyleClassIfAbsent(Styleable styleable, String styleClass) {
        ObservableList<String> styleClasses = styleable.getStyleClass();
        if (!styleClasses.contains(styleClass)) {
          styleClasses.add(styleClass);
        }
      }
    }
    

    App.css:

    .first-tab,
    .last-tab {
      -fx-base: pink;
    }
    

    The -fx-base is a looked-up color added by modena.css (i.e. the default user-agent stylesheet in JavaFX 8+). I set that instead of the -fx-background-color property in order to hook into the "theming" provided by modena.css. In the above example you can see the styles change dynamically by reordering the tabs via mouse-dragging (JavaFX 10+) or by closing tabs.

    Note I would have preferred to use PseudoClass for this. However, from what I can tell, the Tab class does not allow you to (de)activate pseudo-classes directly. The way it handles the :selected pseudo-class is internal to the TabPane's default skin, meaning we can't access that same functionality reliably for our purposes from the outside.