JavaFX TitledPane changes the title background is reset on mouse input

JavaFX TitledPane changes the title background is reset on mouse input … here is a solution to the problem.

JavaFX TitledPane changes the title background is reset on mouse input

I need to change the TitledPane title background at runtime based on some incoming value, otherwise reset it.

All my TitledPane are styled on the CSS attached to the scene.

It’s okay to change the background.
The problem is that when the mouse enters the title after the background change, the background is therefore reset to the CSS background.

Test application to change the background of the TitledPane title:

import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class TitledPaneApplication extends Application {

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

@Override
    public void start(Stage primaryStage) throws Exception {
        final StackPane root = new StackPane();

final TitledPane titledPane = new TitledPane();
        titledPane.setText("Title");
        root.getChildren().add(titledPane);

final String titleBackgroundValue = "#00ff11";
        final ToggleButton button = new ToggleButton("Change");
        button.setOnAction(event -> {
            boolean selected = button.isSelected();

final Node node = titledPane.lookup(".title");
            if (selected) {
                final Color color = Color.valueOf(titleBackgroundValue);
                ((Region) node).setBackground(new Background(new BackgroundFill(color, null, null)));
            } else {
                ((Region) node).setBackground(null);
                titledPane.applyCss();
            }
        });

button.setSelected(false);
        titledPane.setContent(button);

final Scene scene = new Scene(root, 400, 400);
        scene.getStylesheets().add(getClass().getResource("light.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.setTitle("TestApplication");
        primaryStage.show();
    }

}

Scene CSS light .css

.root {
    -fx-base: rgb(240, 240, 240);
    -fx-background: rgb(240, 240, 240);
    -fx-border-color: rgb(220, 220, 220);

/* make controls (buttons, thumb, etc.) slightly lighter */
    -fx-color: derive(-fx-base, 10%);

/* text fields and table rows background */
    -fx-control-inner-background: rgb(248, 248, 248);
    /* version of -fx-control-inner-background for alternative rows */
    -fx-control-inner-background-alt: derive(-fx-control-inner-background, -2.5%);

/* text colors depending on background's brightness */
    -fx-light-text-color: rgb(220, 220, 220);
    -fx-mid-text-color: rgb(100, 100, 100);
    -fx-dark-text-color: rgb(20, 20, 20);

/* A bright blue for highlighting/accenting objects.  For example: selected
     * text; selected items in menus, lists, trees, and tables; progress bars */
    -fx-accent: rgb(0, 80, 100);

/* color of non-focused yet selected elements */
    -fx-selection-bar-non-focused: rgb(50, 50, 50);

-fx-font-family: "Roboto"; /* "Segoe UI Semibold", "Roboto", "Monospaced" */
    -fx-font-size: 1em;

-primary-border-color: rgb(220, 220, 220);
}

/* Fix derived prompt color for text fields */
.text-input {
    -fx-prompt-text-fill: derive(-fx-control-inner-background, -50%);
}

/* Keep prompt invisible when focused (above color fix overrides it) */
.text-input:focused {
    -fx-prompt-text-fill: transparent;
}

/* Fix scroll bar buttons arrows colors */
.scroll-bar > .increment-button > .increment-arrow,
.scroll-bar > .decrement-button > .decrement-arrow {
    -fx-background-color: -fx-mark-highlight-color, rgb(220, 220, 220);
}

.scroll-bar > .increment-button:hover > .increment-arrow,
.scroll-bar > .decrement-button:hover > .decrement-arrow {
    -fx-background-color: -fx-mark-highlight-color, rgb(240, 240, 240);
}

.scroll-bar > .increment-button:pressed > .increment-arrow,
.scroll-bar > .decrement-button:pressed > .decrement-arrow {
    -fx-background-color: -fx-mark-highlight-color, rgb(255, 255, 255);
}

.text-field {
    -fx-font-size: 10pt;
}

.combo-box {
    -fx-font-size: 10pt;
}

/* ScrollPane style. */
.scroll-pane {
    -fx-background-color: transparent;
}

.scroll-pane > .viewport {
    -fx-background-color: transparent;
}

/* TabPane style. */
.tab-pane > .tab-header-area {
    -fx-background-color: transparent;
}

/* TitledPane style. */
.titled-pane {
    -fx-border-width: 1;
    -fx-border-color: -primary-border-color;
    -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.15), 5, 0.0, 0, 1);
}

.titled-pane > .content {
    -fx-border-width: 0;
}

.titled-pane .title .arrow-button {
    visibility: false;
}

.titled-pane > .title {
    -fx-background-color: -primary-border-color;
    -fx-background-insets: 0;
    -fx-background-radius: 0 0 0 0;
    -fx-padding: 0.2em 0.2em 0.2em 0.2em;
}

.titled-pane > .title .text {
    -fx-font-size: 10pt;
}

Solution

Setting inline CSS styles takes precedence over styles in CSS files. So applying backgrounds via setStyle is fine.

button.setOnAction(event -> {
            final Node node = titledPane.lookup(".title");
            if (button.isSelected()) {
                node.setStyle("-fx-background-color:#00ff11;" );
            } else {
                node.setStyle(null);
            }
        });

Update:
However, more details on the actual issue. To understand this, you first need to know how the background of .title is defined internally and how the hover style is set.

Inside the Modena.css below is the style of the .title background:

.titled-pane > .title {
    -fx-background-color:
        linear-gradient(to bottom,
            derive(-fx-color,-15%) 95%,
            derive(-fx-color,-25%) 100%
        ),
        -fx-inner-border, -fx-body-color;
    -fx-background-insets: 0, 1, 2;
    -fx-background-radius: 3 3 0 0, 2 2 0 0, 1 1 0 0;
    -fx-padding: 0.3333em 0.75em 0.3333em 0.75em; /* 4 9 4 9 */
}

.titled-pane > .title:hover {
    -fx-color: -fx-hover-base;
}

If you notice that the actual background comes from -fx-color, -fx-inner-border, and -fx-body-color. However, -fx-inner-border and -fx-body-color do again derive from -fx-color only.

-fx-inner-border: linear-gradient(to bottom,
                ladder(
                    -fx-color,
                    derive(-fx-color,30%) 0%,
                    derive(-fx-color,20%) 40%,
                    derive(-fx-color,25%) 60%,
                    derive(-fx-color,55%) 80%,
                    derive(-fx-color,55%) 90%,
                    derive(-fx-color,75%) 100%
                ),
                ladder(
                    -fx-color,
                    derive(-fx-color,20%) 0%,
                    derive(-fx-color,10%) 20%,
                    derive(-fx-color,5%) 40%,
                    derive(-fx-color,-2%) 60%,
                    derive(-fx-color,-5%) 100%
                ));
                
-fx-body-color: linear-gradient(to bottom,
            ladder(
                -fx-color,
                derive(-fx-color,8%) 75%,
                derive(-fx-color,10%) 80%
            ),
            derive(-fx-color,-8%));
    

In the :hover

pseudostate, -fx-color changes to -fx-hover-base and the background updates accordingly. That’s your problem. You only set the default background programmatically. When you hover over .title, it still selects the internal CSS file style (because you haven’t defined a custom style for hover).

If we manage to update the -fx-color property, then it will handle the corresponding css update for different pseudo-states.

Here’s what is more correct for your requirements: this way you still get beautiful gradient features of internally defined titles.

button.setOnAction(event -> {
            final Node node = titledPane.lookup(".title");
            if (button.isSelected()) {
                node.setStyle("-fx-color:#00ff11;" );
            } else {
                node.setStyle(null);
            }
        });
    
 In css file  
.titled-pane > .title {
    -fx-color: -primary-border-color;
    -fx-background-insets: 0;
    -fx-background-radius: 0 0 0 0;
    -fx-padding: 0.2em 0.2em 0.2em 0.2em;
}

Update 2:

Find another TitledPane example in TitledPane below.

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.Set;

public class TitledPaneApplication extends Application {

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

@Override
    public void start(Stage primaryStage) throws Exception {
        final VBox root = new VBox();
        root.setSpacing(10);

final TitledPane titledPane = new TitledPane();
        titledPane.setText("Title");

final String titleBackgroundValue = "#00ff11";
        final ToggleButton button = new ToggleButton("Change");
        button.setOnAction(event -> {
            final Set<Node> node = titledPane.lookupAll(".title");
            if (button.isSelected()) {
                node.forEach(n->n.setStyle("-fx-color:#00ff11;" ));
            } else {
                node.forEach(n->n.setStyle(null));
            }
        });
        button.setSelected(false);

VBox inner = new VBox();
        inner.setSpacing(10);
        inner.setPadding(new Insets(10));
        final TitledPane innerTP = new TitledPane();
        innerTP.setText("Inner Title");
        inner.getChildren().addAll(new Label("Inner"),innerTP);
        innerTP.setContent(new Button("Dummy Button"));

titledPane.setContent(inner);

root.getChildren().addAll(button,titledPane);
        final Scene scene = new Scene(root, 400, 400);
        scene.getStylesheets().add(getClass().getResource("light.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.setTitle("TestApplication");
        primaryStage.show();
    }
}

Related Problems and Solutions