Keep arrow of TitledPane visible, even when set to not collapsible

84 Views Asked by At

I'm trying to create an TitledPane, that can't be collapsed, once it has been opened and the TextField inside of it does not have valid input.

This behaviour already works in my code, but as soon as I set the collapsibleProperty to false, the arrow-button disappears.

I'd rather have it stay and be shown in a disabled-state. I know this could be achieved with pseudoclass-states, but the following code, in the update() function in the TitledPaneSkin class, prevents me from using this approach, as the arrow won't even be added into the gui:

private void update() {
    getChildren().clear();
    final TitledPane titledPane = getSkinnable();

    if (titledPane.isCollapsible()) {
        getChildren().add(arrowRegion);
    }

...

I tried to write a custom Skin class that extends the TitledPaneSkin, but the TitleRegion, where the arrowRegion is defined, and its update function, are not accessible.

Disabling the TitledPane itself also didn't work for me, as I need the TextField inside of it to be accessible/editable.

I also tried to set the -fx-graphic property of the TitledPane title to a custom image, but it didn't work out for me.

.titled-pane .title {
    -fx-graphic: url("arrow.png");
}
2

There are 2 best solutions below

1
James_D On BEST ANSWER

This is a bit of a hack, but you can lookup and disable/enable the title in the titled pane. That will not affect the disabled state of the titled pane's content.


import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        VBox content = new VBox();
        TitledPane titledPane = new TitledPane("Test", content);
        titledPane.setExpanded(true);

        content.setAlignment(Pos.CENTER);
        content.setPadding(new Insets(20));
        TextField textField = new TextField();
        content.getChildren().addAll(new Label("Enter at least three characters:"),textField);
        textField.textProperty().addListener((obs, oldText, newText) -> updateTitledPaneState(titledPane, newText.length() >= 3));

        Scene scene = new Scene(titledPane);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        stage.setScene(scene);
        stage.show();

        updateTitledPaneState(titledPane, false);
    }

    private void updateTitledPaneState(TitledPane titledPane, boolean shouldBeCollapsible) {
        Node title = titledPane.lookup(".title");
        title.setDisable(! shouldBeCollapsible);
    }

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

By default, the title doesn't know to change its appearance when disabled, so you can add that using an external style sheet:

style.css:

.titled-pane > .title:disabled {
    -fx-opacity: 0.4;
}
1
Sai Dandem On

+1 for the @James_D answer.

For the sake of different approach or alternate solution , using his demo code, I tried the below solution. You can have look at it.

In this approach, I created a custom TitledPane which has an invalid property. Instead of disabling, I blocked the mouse event to not collapse if the invalid property is true and set the psuedo state for some custom styling.

This may add some extra bit of code, but you can consider it for reusability and custom styling.

enter image description here

import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TitledPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;
import java.util.stream.Stream;

public class TitledPaneValidationDemo extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        VBox content = new VBox(10);
        content.setAlignment(Pos.CENTER);
        content.setPadding(new Insets(20));

        CustomTitledPane titledPane = new CustomTitledPane("Test", content);
        titledPane.setExpanded(true);

        TextField textField1 = new TextField();
        TextField textField2 = new TextField();
        TextField textField3 = new TextField();
        content.getChildren().addAll(new Label("Enter at least three characters in each box:"), textField1, textField2, textField3);
        Stream.of(textField1, textField2, textField3).forEach(tf -> {
            tf.textProperty().addListener(p -> invalidate(titledPane, textField1, textField2, textField3));
        });

        Scene scene = new Scene(titledPane);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        stage.setScene(scene);
        stage.setTitle("Titled Pane Demo");
        stage.show();
        invalidate(titledPane, textField1, textField2, textField3);
    }

    private void invalidate(CustomTitledPane titledPane, TextField textField1, TextField textField2, TextField textField3) {
        titledPane.setInvalid(textField1.getText().length() < 3 || textField2.getText().length() < 3 || textField3.getText().length() < 3);
    }

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

    class CustomTitledPane extends TitledPane {
        private BooleanProperty invalid = new SimpleBooleanProperty(false);

        private final PseudoClass PSEUDO_CLASS_INVALID = PseudoClass.getPseudoClass("invalid");
        private Node title;

        public CustomTitledPane(String text, Node node) {
            super(text, node);
            skinProperty().addListener((obs, old, val) -> title = null);
            invalidProperty().addListener((obs, old, val) -> pseudoClassStateChanged(PSEUDO_CLASS_INVALID, val));
        }

        @Override
        protected void layoutChildren() {
            super.layoutChildren();
            if (title == null) {
                title = lookup(".title");
                title.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
                    if (isInvalid()) {
                        setExpanded(true);
                        e.consume();
                    }
                });
            }
        }

        public boolean isInvalid() {
            return invalid.get();
        }

        public BooleanProperty invalidProperty() {
            return invalid;
        }

        public void setInvalid(final boolean invalid) {
            this.invalid.set(invalid);
        }
    }
}

CSS:

.titled-pane:invalid > .title {
    -fx-color: #FF000050;
}