package edu.ntnu.stud.chaosgame.view; import edu.ntnu.stud.chaosgame.model.game.ChaosCanvas; import edu.ntnu.stud.chaosgame.controller.game.ChaosGame; import edu.ntnu.stud.chaosgame.model.game.ChaosGameDescription; import edu.ntnu.stud.chaosgame.controller.game.GuiButtonController; import edu.ntnu.stud.chaosgame.controller.utility.Formatter; import edu.ntnu.stud.chaosgame.model.data.Vector2D; import edu.ntnu.stud.chaosgame.model.generators.ChaosGameDescriptionFactory; import javafx.animation.Timeline; import javafx.animation.TranslateTransition; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.stage.Window; import javafx.util.Duration; import java.io.IOException; // todo: look through GUI and get rid of redundancies, clean up code public class ChaosGameGui implements ChaosGameObserver { private int currentLine = 0; /** * The primary stage for the GUI. */ private Stage primaryStage; /** * The aspect ratio of the GUI. */ private double aspectRatio; /** * The canvas for this GUI. */ private Canvas canvas; /** * The ChaosCanvas for this GUI. */ private ChaosCanvas chaosCanvas; /** * The ChaosGameDescription. */ private ChaosGameDescription description; /** * The ChaosGameDescriptionFactory. */ private ChaosGameDescriptionFactory factory; /** * The ImageView for the GUI.. */ private ChaosGameImageView imageView; /** * The Scene for the GUI.. */ private Scene scene; /** * The width of the GUI. */ private int width; /** * The height of the GUI. */ private int height; /** * The ChaosGame for this GUI. */ private ChaosGame game; /** * The Timeline for the GUI. */ private Timeline timeline; /** * The BorderPane for the GUI. */ private BorderPane borderPane; /** * The side menu for the GUI. */ private VBox sideMenu; /** * The start, stop, new, clear, quit and show sidebar buttons for the GUI. */ private Button startButton; /** * The stop button for the GUI. */ private Button stopButton; /** * The button for clearing */ private Button clearButton; /** * The quit button for the GUI. */ private Button quitButton; /** * The side menu button for the GUI. */ private Button sideMenuButton; /** * The load fractal from file and write fractal to file buttons for the GUI. */ private Button loadFractalFromFileButton; /** * The write fractal to file button for the GUI. */ private Button writeFractalToFileButton; /** * The radio buttons for the fractal type for the GUI. */ private RadioButton sierpinskiRadioButton; /** * The Barnsley radio button. */ private RadioButton barnsleyRadioButton; /** * The Julia radio button. */ private RadioButton juliaRadioButton; /** * The improved Barnsley radio button. */ private RadioButton improvedBarnsleyButton; /** * A description ComboBox for choosing different fractal descriptions. */ private ComboBox<String> descriptionComboBox; /** * The step count text field for the GUI. */ private TextField stepCountTextField; /** * The color check box for the GUI. */ private CheckBox colorCheckBox; /** * The controller for the GUI. */ private GuiButtonController controller; /** * Constructor for the ChaosGameGui. * * @param primaryStage the primary stage for the GUI. * @throws IOException if the GUI fails to initialize. */ public ChaosGameGui(Stage primaryStage) throws IOException { this.primaryStage = primaryStage; this.initializeComponents(); this.initializeGameComponents(); this.controller = new GuiButtonController(game, this); // Initialize controller here primaryStage.setTitle("Fractal Chaos Game"); primaryStage.setScene(scene); primaryStage.setOnShown(event -> this.imageView.requestFocus()); primaryStage.show(); // Initialize aspect ratio based on initial dimensions this.aspectRatio = (double) width / height; // Add listeners to handle window size changes scene.widthProperty().addListener((observable, oldValue, newValue) -> { resizeCanvas(); }); scene.heightProperty().addListener((observable, oldValue, newValue) -> { resizeCanvas(); }); // Bind the width of the sideMenu to the width of the scene sideMenu.prefWidthProperty().bind(scene.widthProperty().multiply(0.2)); // 20% of the scene width // Bind the height of the sideMenu to the height of the scene sideMenu.prefHeightProperty().bind(scene.heightProperty()); } /** * Initialize the components of the GUI. */ private void initializeComponents() { // Timeline //this.timeline = new Timeline(new KeyFrame(Duration.seconds(0.05), event -> controller.drawChaosGame())); this.initializeImageView(); //this.initializeMainButtons(); this.initializeFractalComponents(); this.initializeSideMenu(); this.scene = new Scene(this.borderPane,1700,1000); } /** * Creates a TextField of specific size. */ private TextField createCoordinateTextField(String promptText) { TextField textField = new TextField(); textField.setPrefHeight(5); textField.setPrefWidth(90); textField.setPromptText(promptText); return textField; } /** * Initialize the components related to the chaos game itself. */ private void initializeGameComponents() { // Description this.factory = new ChaosGameDescriptionFactory(); this.description = factory.getDescriptions().get(0); this.chaosCanvas = new ChaosCanvas(1000, 1000, this.description.getMinCoords(), this.description.getMaxCoords()); game = new ChaosGame(this.description, chaosCanvas); //controller.startGame(); // Start the game after it's created } /** * Initialize components related to the image view and zoom function. */ private void initializeImageView() { // Image view this.imageView = new ChaosGameImageView(this); width = 1000; height = 1000; this.canvas = new Canvas(width, height); //this.imageView.setImage(canvas.snapshot(null, null)); canvas.widthProperty().bind(imageView.fitWidthProperty()); canvas.heightProperty().bind(imageView.fitHeightProperty()); this.clearImageView(); } /** * Color the entire image view white. */ public void clearImageView() { GraphicsContext gc = canvas.getGraphicsContext2D(); gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); imageView.setImage(null); } /** * Initialize the buttons related to managing the fractals. */ private void initializeFractalComponents() { // Radio buttons for choosing fractal type ToggleGroup group = new ToggleGroup(); this.sierpinskiRadioButton = new RadioButton("Sierpinski"); sierpinskiRadioButton.setToggleGroup(group); //sierpinskiRadioButton.setSelected(true); this.barnsleyRadioButton = new RadioButton("Barnsley"); barnsleyRadioButton.setToggleGroup(group); this.juliaRadioButton = new RadioButton("Julia"); juliaRadioButton.setToggleGroup(group); this.improvedBarnsleyButton = new RadioButton("Improved Barnsley"); improvedBarnsleyButton.setToggleGroup(group); this.descriptionComboBox = new ComboBox<>(); // Set action for Sierpinski radio button. sierpinskiRadioButton.setOnAction(event -> { controller.updateDescription(0); }); // Set action for Barnsley radio button. barnsleyRadioButton.setOnAction(event -> { controller.updateDescription(1); }); // Set action for Julia radio button. juliaRadioButton.setOnAction(event -> { controller.updateDescription(2); }); improvedBarnsleyButton.setOnAction(event -> { controller.updateDescription(3); }); // Recreate the ChaosGameDescriptionFactory and update the ComboBox items each time the ComboBox is shown descriptionComboBox.setOnShowing(event -> { factory = new ChaosGameDescriptionFactory(); descriptionComboBox.getItems().clear(); for (ChaosGameDescription description : factory.getDescriptions()) { descriptionComboBox.getItems().add(description.getName()); } }); // Update the controller's description when a new item is selected in the ComboBox descriptionComboBox.getSelectionModel().selectedItemProperty() .addListener((observable, oldValue, newValue) -> { for (int i = 0; i < factory.getDescriptions().size(); i++) { ChaosGameDescription description = factory.getDescriptions().get(i); if (description.getName().equals(newValue)) { controller.updateDescription(i); break; } } }); // Load fractal file button and tooltip this.loadFractalFromFileButton = new Button("Load Fractal"); Tooltip loadFractalFromFileButtonTooltip = new Tooltip("Load a text file describing a new fractal chaos game"); Tooltip.install(loadFractalFromFileButton,loadFractalFromFileButtonTooltip); // Write fractal to file button and tooltip this.writeFractalToFileButton = new Button("Write to File"); Tooltip writeFractalToFileButtonTooltip = new Tooltip("Write a text file defining the current fractal chaos game to chosen location"); Tooltip.install(writeFractalToFileButton, writeFractalToFileButtonTooltip); } /** * Initialize the side menu. */ private void initializeSideMenu() { // Create a Border style Border blackBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, new CornerRadii(10), new BorderWidths(5))); // Create Canvas Header Label canvasLabel = new Label("Play Controls"); canvasLabel.setAlignment(Pos.CENTER); canvasLabel.setFont(new Font("Arial",20)); // Create Canvas Buttons this.startButton = new Button("Start"); this.stopButton = new Button("Pause"); this.clearButton = new Button("New"); this.quitButton = new Button("Quit"); //this.sideMenuButton = new Button("Side Menu"); // Create Tooltips Tooltip startButtonTooltip = new Tooltip("Starts drawing the current fractal from the selected chaos game"); Tooltip stopButtonTooltip = new Tooltip("Pause drawing current fractal"); Tooltip clearButtonTooltip = new Tooltip("Clear the current fracal"); Tooltip quitButtonTooltip = new Tooltip("Quit the application"); // Attach Tooltips to Buttons Tooltip.install(startButton,startButtonTooltip); Tooltip.install(stopButton,stopButtonTooltip); Tooltip.install(clearButton,clearButtonTooltip); Tooltip.install(quitButton, quitButtonTooltip); this.sideMenu = new VBox(); //this.sideMenu.setAlignment(Pos.CENTER); // Parameters VBox parameterBox = new VBox(); VBox controlButtonBox = new VBox(); controlButtonBox.setBorder(blackBorder); controlButtonBox.setPadding(new Insets(5,5,5,5)); VBox descriptionBox = new VBox(); descriptionBox.setBorder(blackBorder); descriptionBox.setPadding(new Insets(5,5,5,5)); VBox bottomButtonBox = new VBox(); // Step Count GUI VBox stepCountBox = new VBox(); Label stepCountLabel = new Label("Step Count"); stepCountLabel.setFont(new Font("Arial",20)); stepCountLabel.setAlignment(Pos.CENTER); this.stepCountTextField = new TextField(); this.stepCountTextField.setTextFormatter(Formatter.getIntFormatter()); // Set formatter Formatter.limitTextFieldSize(stepCountTextField, 6); stepCountTextField.setPrefHeight(5); stepCountTextField.setPrefWidth(50); stepCountBox.getChildren().addAll(stepCountLabel,stepCountTextField); stepCountBox.setAlignment(Pos.CENTER); stepCountBox.setPadding(new Insets(5,5,5,5)); stepCountBox.setBorder(blackBorder); // Create a Box for Coordinate Controls VBox coordinateControlBox = new VBox(); coordinateControlBox.setPadding(new Insets(5, 5, 5, 5)); // Coordinate Control GUI Label coordinateHeader = new Label("Coordinate Control"); coordinateHeader.setFont(new Font("Arial", 20)); coordinateHeader.setAlignment(Pos.CENTER); coordinateControlBox.getChildren().add(coordinateHeader); // Minimum Coordinates GUI Label minCoordinatesLabel = new Label("Min. Coordinates"); VBox minCoordinatesBox = new VBox(); TextField minimumCoordinatesTextFieldX = createCoordinateTextField("x"); TextField minimumCoordinatesTextFieldY = createCoordinateTextField("y"); HBox minCoordsHBox = new HBox(); minCoordsHBox.getChildren().addAll(minimumCoordinatesTextFieldX,minimumCoordinatesTextFieldY); Button changeMinimumCoordinatesButton = new Button("Change Min. Coordinates"); Tooltip changeMinimumCoordinatesButtonTooltip = new Tooltip("Change the minimum x and y coordinates for the fractal"); Tooltip.install(changeMinimumCoordinatesButton,changeMinimumCoordinatesButtonTooltip); minCoordinatesBox.getChildren().addAll(minCoordinatesLabel, minCoordsHBox, changeMinimumCoordinatesButton); // Maximum Coordinates GUI VBox maxCoordinatesBox = new VBox(); Label maxCoordinatesLabel = new Label("Max Coordinates"); TextField maximumCoordinatesTextFieldX = createCoordinateTextField("x"); TextField maximumCoordinatesTextFieldY = createCoordinateTextField("y"); HBox maxCoordsHBox = new HBox(); maxCoordsHBox.getChildren().addAll(maximumCoordinatesTextFieldX,maximumCoordinatesTextFieldY); Button changeMaximumCoordinatesButton = new Button("Change Max Coordinates"); Tooltip changeMaximumCoordinatesButtonTooltip = new Tooltip("Change the maximum x and y coordinates of the fractal"); Tooltip.install(changeMaximumCoordinatesButton,changeMaximumCoordinatesButtonTooltip); maxCoordinatesBox.getChildren().addAll(maxCoordinatesLabel, maxCoordsHBox,changeMaximumCoordinatesButton); coordinateControlBox.getChildren().addAll(minCoordinatesBox,maxCoordinatesBox); coordinateControlBox.setAlignment(Pos.CENTER); coordinateControlBox.setBorder(blackBorder); Label colorHeaderLabel = new Label("Color Control"); colorHeaderLabel.setFont(new Font("Arial",20)); colorHeaderLabel.setAlignment(Pos.CENTER); VBox colorVBox = new VBox(); HBox colorBox = new HBox(); Label colorLabel = new Label("Show Redrawn Pixels"); this.colorCheckBox = new CheckBox(); Tooltip colorCheckBoxTooltip = new Tooltip("Change pixel color for pixels drawn multiple times"); Tooltip.install(colorCheckBox,colorCheckBoxTooltip); Region colorRegion = new Region(); colorRegion.setMinWidth(30); colorBox.getChildren().addAll(colorCheckBox, colorRegion, colorLabel); colorVBox.setPadding(new Insets(5,5,5,5)); colorVBox.getChildren().addAll(colorHeaderLabel,colorBox); colorVBox.setAlignment(Pos.CENTER); colorVBox.setBorder(blackBorder); Region separator1 = new Region(); separator1.setMinHeight(10); Region separator2 = new Region(); separator2.setMinHeight(10); //Create spacing Region space = new Region(); Region spacer = new Region(); space.setMinHeight(10); spacer.setMinHeight(10); // Fill parameter box parameterBox.getChildren().addAll(stepCountBox, spacer, coordinateControlBox); parameterBox.setPadding(new Insets(10)); // Add basic control buttons controlButtonBox.setAlignment(Pos.CENTER); controlButtonBox.getChildren().addAll(canvasLabel,startButton, stopButton,clearButton); controlButtonBox.setSpacing(5); sideMenu.getChildren().add(controlButtonBox); // Add spacing sideMenu.getChildren().add(space); sideMenu.getChildren().addAll(startButton,stopButton, clearButton); //Radio Button header label Label chaosGameTypeLabel = new Label("Chaos Game Selection"); chaosGameTypeLabel.setFont(new Font("Arial",20)); chaosGameTypeLabel.setAlignment(Pos.CENTER); // Add fractal radio buttons descriptionBox.getChildren().addAll(chaosGameTypeLabel,sierpinskiRadioButton, barnsleyRadioButton, juliaRadioButton, improvedBarnsleyButton, descriptionComboBox); descriptionBox.setSpacing(5); sideMenu.getChildren().add(descriptionBox); sideMenu.getChildren().addAll(separator1, colorVBox, separator2); //this.initializeColorButtonHandler(); // Add parameter VBox sideMenu.getChildren().add(parameterBox); // Add file buttons and quit button Label menuButtonLabel = new Label("Menu Controls"); menuButtonLabel.setFont(new Font("Arial",20)); menuButtonLabel.setAlignment(Pos.CENTER); bottomButtonBox.getChildren().addAll(menuButtonLabel,loadFractalFromFileButton,writeFractalToFileButton,quitButton); bottomButtonBox.setSpacing(5); bottomButtonBox.setBorder(blackBorder); bottomButtonBox.setAlignment(Pos.CENTER); bottomButtonBox.setPadding(new Insets(5,5,5,5)); sideMenu.getChildren().add(bottomButtonBox); // Add padding sideMenu.setPadding(new Insets(10)); // Create split pane and button to toggle sidebar this.sideMenuButton = new Button(">>"); Tooltip sideMenuButtonTooltip = new Tooltip("Hide/Unhide menu"); Tooltip.install(sideMenuButton, sideMenuButtonTooltip); this.initializeSideButtonHandler(); Region sideMenuButtonRegion = new Region(); sideMenuButtonRegion.setMinWidth(400); HBox sideMenuButtonBox = new HBox(); sideMenuButtonBox.getChildren().addAll(sideMenuButtonRegion, sideMenuButton); // The right VBox containing both the sidebar and the sidebar toggle button. VBox rightVBox = new VBox(); rightVBox.getChildren().addAll(sideMenuButtonBox, sideMenu); this.sideMenu.setStyle("-fx-background-color: lightblue; -fx-background-radius: 5;"); this.borderPane = new BorderPane(); this.borderPane.setCenter(imageView); this.borderPane.setRight(rightVBox); imageView.setFocusTraversable(true); rightVBox.setFocusTraversable(false); borderPane.setFocusTraversable(false); } /** * Initialise the side bar button handler, allowing the user * to show or hide the right sidebar. */ private void initializeSideButtonHandler() { TranslateTransition openNav = new TranslateTransition(new Duration(350), sideMenu); openNav.setToX(0); TranslateTransition closeNav = new TranslateTransition(new Duration(350), sideMenu); this.sideMenuButton.setOnAction(e -> { if(sideMenu.getTranslateX() != 0){ this.sideMenuButton.setText(">>"); openNav.play(); } else { closeNav.setToX(sideMenu.getWidth()); closeNav.play(); this.sideMenuButton.setText("<<"); } }); } /** * Initialize the color button handler. */ private void initializeColorButtonHandler() { this.colorCheckBox.setOnAction(event -> { controller.game.setUseColor(colorCheckBox.isSelected()); this.clearImageView(); this.chaosCanvas.clearCanvas(); }); } /** * Get the chaos canvas of this GUI view. * * @return the canvas. */ public ChaosCanvas getChaosCanvas() { return this.chaosCanvas; } public int getWidth(){ return this.width; } public int getHeight(){ return this.height; } public ImageView getImageView(){ return this.imageView; } public void setCurrentLine(int currentLine) { this.currentLine = currentLine; } public void setImageViewFromImage(Image inputView) { this.imageView.setImage(inputView); } /** * Update the canvas and set a new zoom factor for the image view based on the ratio * between the old and new canvas heights. * * @param canvas the canvas to update with. */ @Override public void updateCanvas(ChaosCanvas canvas) { float zoomRatio = (float) this.chaosCanvas.getHeight() / canvas.getHeight(); //this.imageView.fixedZoom(zoomRatio); // Set new zoom factor. this.chaosCanvas = canvas; } /** * Update which parts of the fractal are rendered and at what level of detail. * * @param zoomLevel the number of recursive zoom levels. * @param centreX the x-coordinate of the centre of the image view. * @param centreY the y-coordinate of the centre of the image view. */ public void updateDetail(int zoomLevel, double centreX, double centreY) { this.clearImageView(); this.chaosCanvas.clearCanvas(); this.chaosCanvas.updateCoords(centreX, centreY, zoomLevel); controller.game.setCurrentPoint(new Vector2D(centreX, centreY)); } /** * Update the observer based on changes to the chaos game. * TODO: this method may need to be changed depending on how we implement the UI. The update method may need to be split. * * @param game the game this observer is monitoring. */ @Override public void updateGame(ChaosGame game) { controller.drawChaosGame(); } public TextField getStepCountTextField() { return this.stepCountTextField; } public CheckBox getColorCheckBox() { return this.colorCheckBox; } /** * Get the canvas for this GUI. * * @return the canvas. */ public Canvas getCanvas() { return this.canvas; } public Button getStartButton() { return this.startButton; } public Button getStopButton() { return this.stopButton; } public Button getClearButton() { return this.clearButton; } public Button getQuitButton() { return this.quitButton; } public ButtonBase getWriteToFileButton() { return this.writeFractalToFileButton; } public Window getStage() { return this.primaryStage; } public RadioButton getSierpinskiRadioButton() { return this.sierpinskiRadioButton; } public RadioButton getBarnsleyRadioButton() { return this.barnsleyRadioButton; } public RadioButton getJuliaRadioButton() { return this.juliaRadioButton; } public RadioButton getImprovedBarnsleyButton(){ return this.improvedBarnsleyButton; } private void resizeCanvas() { double newWidth = scene.getWidth() - sideMenu.getWidth(); double newHeight = scene.getHeight(); if (newWidth / newHeight > aspectRatio) { newWidth = newHeight * aspectRatio; } else { newHeight = newWidth / aspectRatio; } // Update imageView size to new calculated dimensions imageView.setFitWidth(newWidth); imageView.setFitHeight(newHeight); // Redraw the fractal to fit the new canvas size controller.drawChaosGame(); } }