package edu.ntnu.stud.chaosgame.view; import edu.ntnu.stud.chaosgame.controller.game.ChaosGame; import edu.ntnu.stud.chaosgame.controller.game.GuiButtonController; import edu.ntnu.stud.chaosgame.controller.utility.Formatter; import edu.ntnu.stud.chaosgame.model.game.ChaosCanvas; import edu.ntnu.stud.chaosgame.model.game.ChaosGameDescription; import edu.ntnu.stud.chaosgame.model.generators.ChaosGameDescriptionFactory; import java.io.IOException; 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.ImageView; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.stage.Stage; import javafx.stage.Window; import javafx.util.Duration; // todo: look through GUI and get rid of redundancies, clean up code public class ChaosGameGui implements ChaosGameObserver, GuiButtonObserver { /** The primary stage for the GUI. */ private final Stage primaryStage; /** The aspect ratio of the GUI. */ private final double aspectRatio; private final int currentLine = 0; /** The controller for the GUI. */ private final GuiButtonController controller; /** 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 button which opens a menu to modify the game */ private Button modifyGameButton; /** A description ComboBox for choosing different fractal descriptions. */ private ComboBox<String> descriptionComboBox; /** The step count text field for the GUI. */ private TextField stepCountTextField; /** The iteration limiter text field for the GUI */ private TextField iterationLimitTextField; /** The color check box for the GUI. */ private CheckBox colorCheckBox; /** Button to save an image of the fractal. */ private Button saveImageButton; /** * 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); } /** 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); 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() { this.descriptionComboBox = new ComboBox<>(); // 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 for the GUI, including all its buttons and other components. */ 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); // Stylize Buttons startButton.setStyle("-fx-background-color: #006400;" + " -fx-padding: 8 16; -fx-background-radius: 16; -fx-background-insets: 1px;" + " -fx-border-width: 2px; -fx-border-color: white; -fx-border-radius: 16;" + "-fx-font-size: 14; -fx-text-fill: white;"); stopButton.setStyle("-fx-background-color: #D2691E;" + " -fx-padding: 8 16; -fx-background-radius: 16; -fx-background-insets: 1px;" + " -fx-border-width: 2px; -fx-border-color: white; -fx-border-radius: 16;" + "-fx-font-size: 14; -fx-text-fill: white;"); clearButton.setStyle("-fx-background-color: #00008B;" + " -fx-padding: 8 16; -fx-background-radius: 16; -fx-background-insets: 1px;" + " -fx-border-width: 2px; -fx-border-color: white; -fx-border-radius: 16;" + "-fx-font-size: 14; -fx-text-fill: white;"); quitButton.setStyle("-fx-background-color: #980007;" + " -fx-padding: 8 16; -fx-background-radius: 16; -fx-background-insets: 1px;" + " -fx-border-width: 2px; -fx-border-color: white; -fx-border-radius: 16;" + "-fx-font-size: 14; -fx-text-fill: white;"); 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)); Label steppingSpeedLabel = new Label("Stepping Speed"); stepCountLabel.setAlignment(Pos.CENTER); this.stepCountTextField = new TextField(); this.stepCountTextField.setTextFormatter(Formatter.getIntFormatter()); Formatter.limitTextFieldSize(stepCountTextField, 6); stepCountTextField.setPrefHeight(5); stepCountTextField.setPrefWidth(50); stepCountTextField.setText("1000"); Label iterationLimterLabel = new Label("Iteration Limit"); this.iterationLimitTextField = new TextField(); this.iterationLimitTextField.setTextFormatter(Formatter.getIntFormatter()); Formatter.limitTextFieldSize(iterationLimitTextField, 4); iterationLimitTextField.setText("500"); stepCountBox .getChildren() .addAll( stepCountLabel, steppingSpeedLabel, stepCountTextField, iterationLimterLabel, iterationLimitTextField); stepCountBox.setAlignment(Pos.CENTER); stepCountBox.setPadding(new Insets(5, 5, 5, 5)); stepCountBox.setBorder(blackBorder); // Create a Box for Coordinate Controls VBox modifyGameBox = new VBox(); modifyGameBox.setPadding(new Insets(5, 5, 5, 5)); // Coordinate Control GUI Label coordinateHeader = new Label("Game Modification"); coordinateHeader.setFont(new Font("Arial", 20)); coordinateHeader.setAlignment(Pos.CENTER); modifyGameBox.getChildren().add(coordinateHeader); // Button for game modification popup modifyGameButton = new Button("Create Modified Game"); Tooltip modifyGameButtonTooltip = new Tooltip("Create New Chaos Game From Current"); Tooltip.install(modifyGameButton, modifyGameButtonTooltip); modifyGameBox.getChildren().addAll(modifyGameButton); modifyGameBox.setAlignment(Pos.CENTER); modifyGameBox.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); colorBox.setAlignment(Pos.CENTER); 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, modifyGameBox); 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); // 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, descriptionComboBox); descriptionBox.setSpacing(5); descriptionBox.setAlignment(Pos.CENTER); 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); saveImageButton = new Button("Save Image"); bottomButtonBox .getChildren() .addAll( menuButtonLabel, saveImageButton, 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); // Stylize buttons modifyGameButton.setStyle("-fx-background-color: #00008B;" + " -fx-padding: 8 16; -fx-background-radius: 16; -fx-background-insets: 1px;" + " -fx-border-width: 2px; -fx-border-color: white; -fx-border-radius: 16;" + "-fx-font-size: 14; -fx-text-fill: white;"); loadFractalFromFileButton.setStyle("-fx-background-color: #00008B;" + " -fx-padding: 8 16; -fx-background-radius: 16; -fx-background-insets: 1px;" + " -fx-border-width: 2px; -fx-border-color: white; -fx-border-radius: 16;" + "-fx-font-size: 14; -fx-text-fill: white;"); saveImageButton.setStyle("-fx-background-color: #00008B;" + " -fx-padding: 8 16; -fx-background-radius: 16; -fx-background-insets: 1px;" + " -fx-border-width: 2px; -fx-border-color: white; -fx-border-radius: 16;" + "-fx-font-size: 14; -fx-text-fill: white;"); writeFractalToFileButton.setStyle("-fx-background-color: #00008B;" + " -fx-padding: 8 16; -fx-background-radius: 16; -fx-background-insets: 1px;" + " -fx-border-width: 2px; -fx-border-color: white; -fx-border-radius: 16;" + "-fx-font-size: 14; -fx-text-fill: white;"); // 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("<<"); } }); } /** * Get the image view of this GUI. * * @return the image view. */ public ImageView getImageView() { return this.imageView; } /** * 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) { this.chaosCanvas = canvas; } /** * 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(); } // GuiButtonObserver methods @Override public void onStartButtonPressed() { controller.startGame(); } @Override public void onStopButtonPressed() { controller.stopGame(); } @Override public void onClearButtonPressed() { controller.clearCanvas(); } @Override public void onQuitButtonPressed() { controller.quitGame(); } @Override public void onSaveImageButtonPressed() { controller.saveImage(); } @Override public void onLoadFractalFromFileButtonPressed() { controller.loadFractalFromFile(); } @Override public void onWriteToFileButtonPressed() { controller.writeFractalToFile(); } @Override public void onModifyGameButtonPressed() { controller.modifyGame(); } /** * Get the step count text field for this GUI. * * @return the step count text field. */ public TextField getStepCountTextField() { return this.stepCountTextField; } /** * Get the iteration limit text field for this GUI. * * @return the iteration limit text field. */ public TextField getIterationLimitTextField() { return this.iterationLimitTextField; } /** * Get the color check box for this GUI. * * @return the color check box. */ public CheckBox getColorCheckBox() { return this.colorCheckBox; } /** * Get the canvas for this GUI. * * @return the canvas. */ public Canvas getCanvas() { return this.canvas; } /** * Get the start button for this GUI. * * @return the start button. */ public Button getStartButton() { return this.startButton; } /** * Get the stop button for this GUI. * * @return the stop button. */ public Button getStopButton() { return this.stopButton; } /** * Get the clear button for this GUI. * * @return the clear button. */ public Button getClearButton() { return this.clearButton; } /** * Get the quit button for this GUI. * * @return the quit button. */ public Button getQuitButton() { return this.quitButton; } /** * Get the write fractal to file button. * * @return the write fractal to file button. */ public Button getWriteToFileButton() { return this.writeFractalToFileButton; } /** * Get the primary stage for this GUI. * * @return the primary stage. */ public Window getStage() { return this.primaryStage; } /** Resize the canvas to fit the new dimensions of the scene. */ 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(); } /** * Get the load fractal from file button. * * @return the load fractal from file button. */ public Button getLoadFractalFromFileButton() { return this.loadFractalFromFileButton; } /** * Get the modify game button. * * @return the modify game button. */ public Button getModifyGameButton() { return this.modifyGameButton; } /** * Get the description combo box. * * @return the description combo box. */ public ComboBox getDescriptionComboBox() { return this.descriptionComboBox; } /** * Get the save image button. * * @return the save image button. */ public Button getSaveImageButton() { return this.saveImageButton; } }