mkpaz / atlantafx

Modern JavaFX CSS theme collection with additional controls.
https://mkpaz.github.io/atlantafx
MIT License
829 stars 66 forks source link

HBoxes, ListViews, FlowPanes Issue: Not scrolling or resizing to fit content #68

Closed alexanderjalexander closed 1 year ago

alexanderjalexander commented 1 year ago

Heyo!

Currently developing a JavaFX application around propositional logic. At the moment, any propositions entered by the user is either put into a FlowPane with all the necessary elements, or a HBox containing a VBox. However, utilizing Cupertino Dark with this scheme has resulted in some interesting issues.

Image below is how it is with default JavaFX Modena Styling. H = HBox, V = VBox, F = FlowPane

image

Image below is with AtlantaFX Cupertino Dark. Notice how the buttons start laying on top of one another, and scrollability/wrapping goes away.

image

The code below are the methods from my program that generate said elements. new_simple() creates a FlowPane, while new_complex() creates an HBox with a VBox inside of it. Code is consistent, only thing that is changed is the addition of the Cupertino theme.

@Override
    public void start(Stage stage) throws IOException {
        stage.setTitle("Propositions LIVE");
        stage.setMinHeight(480);
        stage.setMinWidth(480);
        Parent root = FXMLLoader.load(getClass().getResource("main.fxml"));
        Application.setUserAgentStylesheet(getClass().getResource("cupertino-dark.css").toExternalForm());
        root.getStylesheets().add(getClass().getResource("cupertino-dark.css").toExternalForm());
        stage.setScene(new Scene(root));
        stage.show();
    }
    public void new_simple(ActionEvent e) {
        // If empty, do not parse. Warn user.
        if (propField.getText().isEmpty()) {
            userAlert("Empty Input Error", "Cannot parse an empty string. Try again!", new IOException("Empty Input is Invalid."));
        } else {
            // Attempt to parse the user input.
            PropositionInterpreter interp;
            try {
                interp = new PropositionInterpreter(propField.getText(), true);
            } catch (Exception error) {
                userAlert("Interpreting Error", ("Error when interpreting string '" + propField.getText() + "':"), error);
                return;
            }

            // Print out for debugging purposes.
            consoleprintln("Interpreted \"" + propField.getText() + "\" as: " + interp.parse + "\tMode: Simple.");

            // Create the new HBox to put inside the ListView
            FlowPane prop = new FlowPane();
            prop.setVgap(10);
            prop.setHgap(25);
            prop.setPrefWidth(Region.USE_COMPUTED_SIZE);
            prop.setPrefHeight(Region.USE_COMPUTED_SIZE);

            Button remove = new Button("X");
            Label proposition = new Label(propField.getText());
            Label result = new Label(interp.truth_value ? "TRUE" : "FALSE");

            remove.setOnAction(new EventHandler<>() {
                @Override
                public void handle(ActionEvent e) { props.remove(prop); }
            });

            prop.setAlignment(Pos.CENTER_LEFT);
            prop.getChildren().addAll(remove, proposition, new Label("->"), result);

            props.add(prop);
            propView.setItems(props);
        }
        propField.clear();
    }
    public void new_complex(ActionEvent e) {
        // If it's empty, do not parse. Warn user.
        if (propField.getText().isEmpty()) {
            userAlert("Empty Input Error", "Cannot parse an empty string. Try again!", new IOException("Empty Input is Invalid."));
        }
        else {
            // Attempt to parse the user input.
            PropositionInterpreter interp;
            // Implementation hidden for brevity

            // Create the new HBox to put inside the ListView
            HBox prop = new HBox(25);
            Button remove = new Button("X");
            remove.setOnAction(new EventHandler<>() {
                @Override
                public void handle(ActionEvent e) { props.remove(prop); }
            });
            Label proposition = new Label(propField.getText());

            // Create new text fields for each proposition
            VBox propvalues = new VBox(10);
            for (String i : interp.truthmaps.keySet()) {
                Label proplabel = new Label(i + ": ");
                TextField input = new TextField();
                input.setPromptText("\"" + i + "\"'s Truth Value");
                input.setId(i);

                propvalues.getChildren().add(proplabel);
                propvalues.getChildren().add(input);
            }
            propvalues.setAlignment(Pos.CENTER_LEFT);

            Label result = new Label("");
            VBox.setVgrow(result, Priority.ALWAYS);
            prop.setAlignment(Pos.CENTER_LEFT);

            Button interpret = new Button("INTERPRET");
            interpret.setOnAction(new EventHandler<>() {
                  // Implementation hidden for brevity.
            });

            // Add all elements to the proposition
            prop.getChildren().addAll(remove, proposition, propvalues, interpret, new Label("->"), result);

            // Add it to the list view & update it
            props.add(prop);
            propView.setItems(props);
        }
        // Clear user-input field.
        propField.clear();
    }
mkpaz commented 1 year ago

That's interesting indeed. One thing I can say for sure is that AtlantaFX does not apply any styles to the layout containers like HBox, VBox, etc., so the behavior should not be different from the default theme. Judging from the screenshot, it seems like you're using a ListView. Its styling is quite complex and could potentially be the source of the problem. Sorry, but I can't use your code since it's only partial; I don't see the entire ListView layout. Could you please provide a minimal reproducible example?

alexanderjalexander commented 1 year ago

Sure thing! Just made a small IntelliJ project to see how the issue comes around, pictures and code below. Forgive me if this isn't the best way, still kind of a newbie at GitHub

Side note, going off of what you said in your comment:

One thing I can say for sure is that AtlantaFX does not apply any styles to the layout containers like HBox, VBox, etc., so the behavior should not be different from the default theme

That's one of the things I definitely noticed, I installed AtlantaFX into my SceneBuilder and installed the desired CSS files locally, yet it still produced a mostly white application, with none of the panes taking up the Cupertino Dark styling. I ended up having to put everything inside a TitledPane or a TabPane to get the dark mode style. Luckily, it worked first try on a new project, so maybe it's a fluke with my project? Not sure, but

Project Hierarchy:

image

The JavaFX library is stored in a folder inside my computer, and was added as to the project libraries path, and the project dependencies in IntelliJ.

NewMain:

public class NewMain {
    public static void main(String[] args) {
        Main.main(args);
    }
}

Main:

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.fxml.FXMLLoader;

import java.io.IOException;

public class Main extends Application {
    public static void main(String[] args) {launch(args);}

    public void start(Stage stage) throws IOException {
        stage.setTitle("AtlantaFX Example");
        stage.setMinHeight(480);
        stage.setMinWidth(480);
        Parent root = FXMLLoader.load(getClass().getResource("main.fxml"));
        Application.setUserAgentStylesheet(getClass().getResource("cupertino-dark.css").toExternalForm());
        stage.setScene(new Scene(root));
        stage.show();
    }
}

MainController:

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.*;

import java.io.IOException;

public class MainController {

    @FXML
    private ListView<Pane> testView;
    private final ObservableList<Pane> test_items = FXCollections.observableArrayList();

    public void add_to_testView_1() {
        // Create the new FlowPane to put inside the ListView
        FlowPane prop = new FlowPane();
        prop.setVgap(10);
        prop.setHgap(25);
        prop.setPrefWidth(Region.USE_COMPUTED_SIZE);
        prop.setPrefHeight(Region.USE_COMPUTED_SIZE);

        Button remove = new Button("X");
        Label proposition = new Label("ATLANTAFX TESTING");
        Label result = new Label("MORE TEST TEXT MORE TEST TEXT LOREM IPSUM BLAH BLAH BLAH");

        remove.setOnAction(new EventHandler<>() {
            @Override
            public void handle(ActionEvent e) { test_items.remove(prop); }
        });

        prop.setAlignment(Pos.CENTER_LEFT);
        prop.getChildren().addAll(remove, proposition, new Label("->"), result);

        test_items.add(prop);
        testView.setItems(test_items);
    }

    public void add_to_testView_2() {
        // Create the new HBox to put inside the ListView
        HBox horizontal_box = new HBox(25);
        Button remove = new Button("X");
        remove.setOnAction(new EventHandler<>() {
            @Override
            public void handle(ActionEvent e) { test_items.remove(horizontal_box); }
        });
        Label proposition = new Label("ATLANTAFX TESTING NUMBER 2 ELECTRIC BOOGALOO");

        // Create new text fields for each proposition
        VBox vertical_box = new VBox(10);
        for (int i = 0; i < 5; i++) {
            Label proplabel = new Label("EXAMPLE TEXT " + Integer.toString(i));
            vertical_box.getChildren().add(proplabel);
        }
        vertical_box.setAlignment(Pos.CENTER_LEFT);

        Label result = new Label("initial text ");
        VBox.setVgrow(result, Priority.ALWAYS);
        horizontal_box.setAlignment(Pos.CENTER_LEFT);

        Button interpret = new Button("add more text :D");
        interpret.setOnAction(new EventHandler<>() {
            @Override
            public void handle(ActionEvent e) {
                result.setText(result.getText() + "more text ");
            }
        });

        // Add all elements to the proposition
        horizontal_box.getChildren().addAll(remove, proposition, vertical_box, interpret, new Label("->"), result);

        // Add it to the list view & update it
        test_items.add(horizontal_box);
        testView.setItems(test_items);
    }
}

main.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="MainController">
   <right>
      <GridPane BorderPane.alignment="CENTER_LEFT">
         <BorderPane.margin>
            <Insets bottom="25.0" left="25.0" right="25.0" top="25.0" />
         </BorderPane.margin>
         <columnConstraints>
            <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" />
         </columnConstraints>
         <rowConstraints>
            <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
            <RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
         </rowConstraints>
         <children>
            <Button mnemonicParsing="false" onAction="#add_to_testView_2" text="Do Stuff 2" GridPane.rowIndex="1" />
            <Button minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" onAction="#add_to_testView_1" text="Do Stuff" />
         </children>
      </GridPane>
   </right>
   <center>
      <ListView fx:id="testView" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" BorderPane.alignment="CENTER">
         <BorderPane.margin>
            <Insets bottom="25.0" left="25.0" right="25.0" top="25.0" />
         </BorderPane.margin>
      </ListView>
   </center>
</BorderPane>

Issue Reproduction Results

Creating stuff with the "Do Stuff" button produces the following results if the window is too short. With Modena, it's supposed to wrap and overflow, with the VBox stretching in height to fit the content, then creating a scrollbar if it can't wrap the content anymore. Cupertino Dark does this however image

Pressing "Do Stuff 2" sort of fixes the previous issue, but then creates another issue. image

Pressing "add more text" works though, which is good! image

As soon as I remove the "Do Stuff 2" occurrence, then it recreates the first issue. image

mkpaz commented 1 year ago

Thanks for the example. Unlike Modena, AtlantaFX uses a fixed cell height for all virtualized controls (Modena do this only for tables). If you want the dynamic cell height, reset cell size via CSS:

testView.setId("testView");
root.getStylesheets().add(Styles.toDataURI("""
                #testView .list-cell {
                    -fx-cell-size: -1;
                }
                """));

image

alexanderjalexander commented 1 year ago

Awesome, thank you so much! One last question, what class/package is the Styles.toDataURI from? Getting errors for it, and it's not suggesting any imports at the moment

Edit: Nevermind, all good! Fixed it by doing a Maven IntelliJ dependency workaround of AtlantaFX. Tysm for your help!