diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java index 6162207..d6c8602 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java @@ -9,6 +9,8 @@ import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; import edu.ntnu.idi.idatt2003.g40.mappe.utils.ConfigValues; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewManager; +import edu.ntnu.idi.idatt2003.g40.mappe.view.creategame.CreateGameController; +import edu.ntnu.idi.idatt2003.g40.mappe.view.creategame.CreateGameView; import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameController; import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameView; import edu.ntnu.idi.idatt2003.g40.mappe.view.mainmenu.MainMenuController; @@ -68,7 +70,7 @@ static void main(String[] args) { public void start(final Stage stage) throws Exception { Scene scene = new Scene(new Pane()); scene.getStylesheets() - .add(Objects.requireNonNull(getClass().getResource("/styles.css")).toExternalForm()); + .add(Objects.requireNonNull(getClass().getResource("/styles.css")).toExternalForm()); Font.loadFont(getClass().getResourceAsStream("/Fonts/Aptos.ttf"), 16); stage.setScene(scene); stage.setWidth(ConfigValues.VIEWPORT_WIDTH.getValue()); @@ -92,11 +94,18 @@ public void start(final Stage stage) throws Exception { // Play game (mellom hovedmeny og spillet) PlayGameView playGameView = new PlayGameView(); - new PlayGameController(playGameView, eventManager); - - // Last lagrede spill fra disk. SaveGameService saveGameService = new SaveGameService(); - playGameView.setSaves(saveGameService.loadSaves()); + PlayGameController playGameController = + new PlayGameController(playGameView, eventManager, saveGameService); + playGameController.refresh(); + + + // Create game (mellom play-game og selve spillet) + CreateGameView createGameView = new CreateGameView(); + CreateGameController createGameController = + new CreateGameController(createGameView, eventManager, saveGameService); + // Refresh save-listen etter at en ny save er skrevet til disk. + createGameController.setOnSaveCreated(playGameController::refresh); // Settings SettingsView settingsView = new SettingsView(); @@ -117,10 +126,10 @@ public void start(final Stage stage) throws Exception { // Dashboard (default center-view in-game - første siden du ser) DashBoardView dashBoardView = new DashBoardView(); new DashBoardController(dashBoardView, - eventManager, - player, - exchange, - stocksInFile); + eventManager, + player, + exchange, + stocksInFile); // Stats page (Stats-knappen i topbaren tar deg hit) StatsView statsView = new StatsView(); @@ -129,10 +138,10 @@ public void start(final Stage stage) throws Exception { // Market page (Market-knappen tar deg hit) MarketView marketView = new MarketView(); new MarketController(marketView, - eventManager, - player, - exchange, - stocksInFile); + eventManager, + player, + exchange, + stocksInFile); // In-game (Change "topBarView" to "topBarView2" if no summary section). // Dashboard er default center-view. @@ -152,8 +161,8 @@ public void start(final Stage stage) throws Exception { ClickerGame clickerGame = new ClickerGame(); FindStockGame findStockGame = new FindStockGame( stocksInFile.stream() - .map(Stock::getSymbol) - .toList() + .map(Stock::getSymbol) + .toList() ); TimeInputsGame timeInputsGame = new TimeInputsGame(); @@ -180,18 +189,19 @@ public void start(final Stage stage) throws Exception { // Wire top bar buttons til å bytte mellom dashboard / stats / market / // transactions. Stats-knappen tar deg til stats-siden. topBarController.setMarketIntegration( - inGameView::changeCenterView, - dashBoardView.getRootPane(), - marketView.getRootPane(), - statsView.getRootPane(), - transactionsView.getRootPane(), - transactionsController::refresh, - miniGamesView.getRootPane() + inGameView::changeCenterView, + dashBoardView.getRootPane(), + marketView.getRootPane(), + statsView.getRootPane(), + transactionsView.getRootPane(), + transactionsController::refresh, + miniGamesView.getRootPane() ); // Register all views viewManager.addView(mainMenuView); viewManager.addView(playGameView); + viewManager.addView(createGameView); viewManager.addView(settingsView); viewManager.addView(inGameView); viewManager.setScene(mainMenuView); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java index e22273f..ff00e77 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java @@ -4,44 +4,102 @@ * Represents one save game entry. * *
- * Holds the display name and the current balance for a single + * Holds the display name, current balance, starting capital and + * (optionally) the path to a custom stock data file for a single * saved game. *
+ * + *+ * When {@link #getStockDataPath()} returns {@code null} the save is + * expected to be loaded with the default bundled stock data file. + *
*/ public class SaveGame { - /** Display name of the save. */ - private final String name; - - /** Current balance in the save. */ - private final double balance; - - /** - * Constructor. - * - * @param name the display name of the save. - * @param balance the current balance value. - */ - public SaveGame(final String name, final double balance) { - this.name = name; - this.balance = balance; - } - - /** - * Getter method for the name. - * - * @return the save name. - */ - public String getName() { - return name; - } - - /** - * Getter method for the balance. - * - * @return the balance value. - */ - public double getBalance() { - return balance; - } -} \ No newline at end of file + /** Display name of the save. */ + private final String name; + + /** Current balance in the save. */ + private final double balance; + + /** The starting capital chosen when the save was created. */ + private final double startingCapital; + + /** + * Absolute path to a custom stock data file, or {@code null} + * if the save should use the default bundled stock data. + * */ + private final String stockDataPath; + + /** + * Full constructor. + * + * @param name the display name of the save. + * @param balance the current balance value. + * @param startingCapital the starting capital chosen on creation. + * @param stockDataPath absolute path to a custom stock data file, + * or {@code null} to use the default file. + */ + public SaveGame(final String name, + final double balance, + final double startingCapital, + final String stockDataPath) { + this.name = name; + this.balance = balance; + this.startingCapital = startingCapital; + this.stockDataPath = stockDataPath; + } + + /** + * Convenience constructor matching the legacy "name + balance" format. + * + *+ * Starting capital is defaulted to the balance value and + * {@code stockDataPath} is left {@code null} so the default stock + * data file is used. + *
+ * + * @param name the display name of the save. + * @param balance the current balance value. + */ + public SaveGame(final String name, final double balance) { + this(name, balance, balance, null); + } + + /** + * Getter method for the name. + * + * @return the save name. + */ + public String getName() { + return name; + } + + /** + * Getter method for the balance. + * + * @return the balance value. + */ + public double getBalance() { + return balance; + } + + /** + * Getter method for the starting capital. + * + * @return the starting capital chosen on creation. + */ + public double getStartingCapital() { + return startingCapital; + } + + /** + * Getter method for the custom stock data path. + * + * @return the absolute path to a custom stock data file, + * or {@code null} if the save uses the default data. + */ + public String getStockDataPath() { + return stockDataPath; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java index 82b310d..2c6aaed 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java @@ -3,105 +3,459 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame; import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Stream; /** * Service for loading and saving {@link SaveGame} entries from disk. * *- * Save file format (one entry per line): + * Each save is stored as a separate JSON file in a single directory, + * which lets several saves co-exist independently. *
- * + * + *JSON file format:
+ * *
- * # Comment lines start with hash
- * SaveName, 1234567.89
+ * {
+ * "name": "MySave",
+ * "balance": 10000.00,
+ * "startingCapital": 10000.00,
+ * "stockDataPath": null
+ * }
*
*
* - * Lines that don't match the expected format are skipped. + * The service exposes both a "load all saves" entry point (used by + * {@link edu.ntnu.idi.idatt2003.g40.mappe.view.playgame.PlayGameView}) + * and a "save one" entry point (used by the create-game flow). The + * JSON parsing logic is intentionally minimal and tailored to this + * format — invalid files are skipped rather than throwing. *
*/ public class SaveGameService { - /** Default location of the save file. */ - private static final String DEFAULT_PATH = "src/main/resources/saves.txt"; - - /** Path to the save file. */ - private final String filePath; - - /** - * Constructor with default path. - */ - public SaveGameService() { - this(DEFAULT_PATH); - } - - /** - * Constructor with custom path. - * - * @param filePath the path to the save file. - */ - public SaveGameService(final String filePath) { - this.filePath = filePath; - } - - /** - * Loads all save games from the file. - * - *- * Returns an empty list if the file cannot be read or is empty. - *
- * - * @return the loaded {@link SaveGame} entries. - */ - public List+ * Walks the saves directory, parses every file with a + * {@code .json} extension and returns the resulting list. + * Files that fail to parse are silently skipped. + *
+ * + *+ * Returns an empty list if the directory does not exist or cannot + * be read. + *
+ * + * @return the loaded {@link SaveGame} entries. + */ + public List- * Returns null for invalid lines, comment lines, and blank lines. - *
- * - * @param line the raw line from the file. - * @return the parsed {@link SaveGame}, or null if the line is invalid. - */ - private SaveGame parseLine(final String line) { - if (line == null || line.isBlank() || line.trim().startsWith("#")) { - return null; + } + } catch (IOException e) { + System.err.println("Could not read saves directory: " + e.getMessage()); + } + return saves; + } + + /** + * Writes a single {@link SaveGame} to disk as a JSON file. + * + *+ * The file name is derived from {@link SaveGame#getName()} sanitised + * to filesystem-safe characters and given the {@code .json} + * extension. The saves directory is created automatically if it + * doesn't exist. + *
+ * + * @param save the {@link SaveGame} object to write. + * + * @throws IllegalArgumentException if save is null or has a blank name. + * @throws IOException if the file cannot be written. + */ + public void saveGame(final SaveGame save) + throws IllegalArgumentException, IOException { + if (save == null) { + throw new IllegalArgumentException("Save game is null!"); + } + if (save.getName() == null || save.getName().trim().isEmpty()) { + throw new IllegalArgumentException("Save name is empty!"); + } + + Path dir = Paths.get(directoryPath); + if (!Files.isDirectory(dir)) { + Files.createDirectories(dir); + } + + String fileName = sanitiseFileName(save.getName()) + JSON_EXTENSION; + Path target = dir.resolve(fileName); + Files.writeString(target, toJson(save), StandardCharsets.UTF_8); + } + + /** + * Checks whether a save with the given name already exists on disk. + * + * @param name the save display name. + * + * @return {@code true} if a matching file is present, otherwise + * {@code false}. + */ + public boolean saveExists(final String name) { + if (name == null || name.trim().isEmpty()) { + return false; + } + String fileName = sanitiseFileName(name) + JSON_EXTENSION; + Path target = Paths.get(directoryPath).resolve(fileName); + return Files.isRegularFile(target); + } + + /** + * Loads a single save from an explicit file path. + * + *+ * Used by the upload flow on the play-game screen so a save file + * located anywhere on disk can be added to the displayed list + * without having to live inside the saves directory. + *
+ * + * @param file the file to parse. + * @return the parsed {@link SaveGame}, or {@code null} if the file + * does not contain a valid save record. + */ + public SaveGame loadSaveFromFile(final Path file) { + if (file == null || !Files.isRegularFile(file)) { + return null; + } + return parseFile(file); + } + + /** + * Parses a single JSON file into a {@link SaveGame}. + * + *+ * Returns {@code null} for files that don't contain a valid save + * record (missing required fields or malformed JSON). + *
+ * + * @param file path to the file to parse. + * @return the parsed {@link SaveGame}, or {@code null}. + */ + private SaveGame parseFile(final Path file) { + try { + String content = Files.readString(file, StandardCharsets.UTF_8); + Map+ * Output is pretty-printed with two-space indentation and a trailing + * newline so the files are human-readable. Numeric values are + * written via {@link BigDecimal#toPlainString()} so they never + * surface as scientific notation. + *
+ * + * @param save the save to convert. + * @return JSON object string. + */ + private String toJson(final SaveGame save) { + StringBuilder sb = new StringBuilder(); + sb.append("{\n"); + sb.append(" \"name\": ").append(quote(save.getName())).append(",\n"); + sb.append(" \"balance\": ") + .append(formatNumber(save.getBalance())).append(",\n"); + sb.append(" \"startingCapital\": ") + .append(formatNumber(save.getStartingCapital())).append(",\n"); + sb.append(" \"stockDataPath\": "); + if (save.getStockDataPath() == null) { + sb.append("null"); + } else { + sb.append(quote(save.getStockDataPath())); + } + sb.append("\n"); + sb.append("}\n"); + return sb.toString(); + } + + /** + * Formats a double as a plain (never scientific) decimal string. + * Uses {@link BigDecimal#valueOf(double)} for the conversion so the + * shortest round-tripping representation is preserved. + */ + private String formatNumber(final double value) { + return BigDecimal.valueOf(value).toPlainString(); + } + + /** + * Parses a flat (one level deep) JSON object into a map. + * + *+ * Only supports string, number, boolean and null values, which is + * everything {@link SaveGame} needs. Returns {@code null} if the + * content can't be parsed. + *
+ * + *+ * Values are returned as strings; {@code null} values are stored as + * an empty string so callers can distinguish "absent" from "null" + * using {@link Map#containsKey(Object)}. + *
+ * + * @param content the raw file content. + * @return a map of field name to raw value, or {@code null}. + */ + private Map+ * All characters outside {@code [A-Za-z0-9_-]} are replaced with + * underscores. This keeps the on-disk file name predictable while + * preserving the original display name in the JSON content. + *
+ */ + private String sanitiseFileName(final String name) { + String trimmed = name.trim(); + String safe = trimmed.replaceAll("[^A-Za-z0-9_-]", "_"); + if (safe.isEmpty()) { + safe = "save"; } -} \ No newline at end of file + return safe; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewEnum.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewEnum.java index b411d57..aab273e 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewEnum.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewEnum.java @@ -19,6 +19,11 @@ public enum ViewEnum { * */ PLAY_GAME, + /** + * {@link edu.ntnu.idi.idatt2003.g40.mappe.view.creategame.CreateGameView}. + * */ + CREATE_GAME, + /** * {@link edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameView}. * */ diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameActions.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameActions.java new file mode 100644 index 0000000..366aa7d --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameActions.java @@ -0,0 +1,8 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.creategame; + +public enum CreateGameActions { + USE_DEFAULT_STOCKS, + CHOOSE_STOCK_FILE, + CANCEL, + CREATE_GAME, +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java new file mode 100644 index 0000000..e671f6c --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java @@ -0,0 +1,194 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.creategame; + +import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame; +import edu.ntnu.idi.idatt2003.g40.mappe.service.SaveGameService; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; +import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController; +import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewEnum; + +import java.io.File; +import java.io.IOException; + +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.stage.FileChooser; +import javafx.stage.Window; + +/** + * Controller for the {@link CreateGameView}. + * + *Extends {@link ViewController}.
+ * + *+ * Handles the four interactions on the create-game screen: picking + * the default stock data, choosing a custom stock data file, cancel + * (back to the play-game screen), and finally creating the save. + *
+ * + *+ * When a save is successfully written to disk a callback can be + * notified so the play-game view can refresh its save list. + *
+ */ +public class CreateGameController extends ViewControllerWires the four create-game buttons.
+ */ + @Override + protected void initInteractions() { + getViewElement().setOnAction(CreateGameActions.USE_DEFAULT_STOCKS, + () -> getViewElement().selectDefaultStocks()); + + getViewElement().setOnAction(CreateGameActions.CHOOSE_STOCK_FILE, + this::handleChooseStockFile); + + getViewElement().setOnAction(CreateGameActions.CANCEL, + () -> changeScene(ViewEnum.PLAY_GAME)); + + getViewElement().setOnAction(CreateGameActions.CREATE_GAME, + this::handleCreateGame); + } + + /** + * Opens a {@link FileChooser} so the user can pick a custom stock + * data file. The chosen file is stored on the view so the resulting + * save can reference it. + */ + private void handleChooseStockFile() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Velg aksje fil"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Stock data (*.txt)", "*.txt"), + new FileChooser.ExtensionFilter("All files", "*.*") + ); + + Window window = getOwnerWindow(); + File selectedFile = fileChooser.showOpenDialog(window); + if (selectedFile == null) { + // User cancelled the dialog. + return; + } + + getViewElement().selectCustomStockFile(selectedFile); + } + + /** + * Validates the form state, builds a {@link SaveGame} from the + * input, writes it to disk via {@link SaveGameService} and + * navigates back to the play-game screen on success. + */ + private void handleCreateGame() { + CreateGameView view = getViewElement(); + String name = view.getFileName(); + Double capital = view.getStartingCapital(); + CreateGameView.StockSelection selection = view.getStockSelection(); + + // The button should be disabled in this case, but guard anyway. + if (name.isEmpty() + || capital == null + || selection == CreateGameView.StockSelection.NONE) { + return; + } + + if (saveGameService.saveExists(name)) { + showAlert(AlertType.WARNING, + "Save finnes allerede", + "Det finnes allerede en lagret fil med navnet \"" + + name + "\". Velg et annet filnavn."); + return; + } + + String stockDataPath = null; + if (selection == CreateGameView.StockSelection.CUSTOM) { + File file = view.getCustomStockFile(); + if (file != null) { + stockDataPath = file.getAbsolutePath(); + } + } + + SaveGame newSave = new SaveGame( + name, + capital, + capital, + stockDataPath + ); + + try { + saveGameService.saveGame(newSave); + } catch (IOException | IllegalArgumentException e) { + showAlert(AlertType.ERROR, + "Kunne ikke lagre", + "Klarte ikke å skrive save-filen:\n" + e.getMessage()); + return; + } + + onSaveCreated.run(); + changeScene(ViewEnum.PLAY_GAME); + } + + /** + * Looks up the {@link Window} that hosts the view, so we can parent + * the file chooser to it. Returns null if the view is not yet + * attached to a scene (in which case the FileChooser opens + * un-parented, which still works). + */ + private Window getOwnerWindow() { + Scene scene = getViewElement().getRootPane().getScene(); + return (scene != null) ? scene.getWindow() : null; + } + + /** + * Shows a modal {@link Alert} with the given type, header and content. + */ + private void showAlert(final AlertType type, + final String header, + final String content) { + Alert alert = new Alert(type); + alert.setTitle("Create game"); + alert.setHeaderText(header); + alert.setContentText(content); + alert.showAndWait(); + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java new file mode 100644 index 0000000..4ee8f53 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java @@ -0,0 +1,381 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.creategame; + +import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewElement; +import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewEnum; +import javafx.beans.value.ChangeListener; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.util.StringConverter; + +import java.io.File; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Objects; + +/** + * View shown after the player clicks "Create new game" in the + * {@link edu.ntnu.idi.idatt2003.g40.mappe.view.playgame.PlayGameView}. + * + *+ * Layout, top-down: + *
+ *+ * Extends {@link ViewElement} with a {@link StackPane} root so a + * background image can sit behind the central panel. + *
+ */ +public class CreateGameView extends ViewElement+ * Constructs with name "CreateGameView". + *
+ */ + public CreateGameView() { + super(new StackPane(), ViewEnum.CREATE_GAME, CreateGameActions.class); + } + + /** + * Returns the filename entered by the user, trimmed. + * + * @return the entered filename, or an empty string if blank. + */ + public String getFileName() { + return fileNameField.getText() == null + ? "" : fileNameField.getText().trim(); + } + + /** + * Returns the starting capital currently selected, or {@code null} + * if nothing is selected yet. + * + * @return the selected starting capital, or {@code null}. + */ + public Double getStartingCapital() { + return startingCapitalChoice.getValue(); + } + + /** + * Returns the currently active stock-data selection mode. + * + * @return the {@link StockSelection} state. + */ + public StockSelection getStockSelection() { + return stockSelection; + } + + /** + * Returns the custom stock-data file picked by the user, or + * {@code null} if the default file is selected or no file has + * been picked yet. + * + * @return the picked stock file or {@code null}. + */ + public File getCustomStockFile() { + return customStockFile; + } + + /** + * Marks the "default stock data" option as chosen and updates + * the visual indicators accordingly. + */ + public void selectDefaultStocks() { + this.stockSelection = StockSelection.DEFAULT; + this.customStockFile = null; + stockSelectionLabel.setText("Default aksje data valgt"); + refreshStockButtonStyles(); + refreshCreateButtonState(); + } + + /** + * Records the custom stock file selection and updates the + * visual indicators. + * + * @param file the file the user picked; must not be {@code null}. + */ + public void selectCustomStockFile(final File file) { + if (file == null) { + return; + } + this.stockSelection = StockSelection.CUSTOM; + this.customStockFile = file; + stockSelectionLabel.setText("Egen fil: " + file.getName()); + refreshStockButtonStyles(); + refreshCreateButtonState(); + } + + /** + * Resets all fields back to their initial state. Called when the + * view becomes active so a previous abandoned session doesn't + * leak into the next one. + */ + public void resetFields() { + if (fileNameField != null) { + fileNameField.clear(); + } + if (startingCapitalChoice != null) { + startingCapitalChoice.getSelectionModel().clearSelection(); + } + this.stockSelection = StockSelection.NONE; + this.customStockFile = null; + if (stockSelectionLabel != null) { + stockSelectionLabel.setText("Ingen aksje data valgt"); + } + refreshStockButtonStyles(); + refreshCreateButtonState(); + } + + /** {@inheritDoc} */ + @Override + protected void initLayout() { + backgroundImage = new ImageView(new Image(Objects.requireNonNull( + getClass().getResourceAsStream("/millionsbackground.png")))); + backgroundImage.setPreserveRatio(false); + + Text title = new Text("Create new game"); + title.getStyleClass().add("create-game-title"); + + // Row 1 - filename + Label fileNameLabel = new Label("Filnavn"); + fileNameLabel.getStyleClass().add("create-game-label"); + fileNameField = new TextField(); + fileNameField.setPromptText("Skriv inn filnavn..."); + fileNameField.getStyleClass().add("create-game-input"); + VBox fileNameRow = new VBox(6, fileNameLabel, fileNameField); + + // Row 2 - starting capital + Label capitalLabel = new Label("Startkapital"); + capitalLabel.getStyleClass().add("create-game-label"); + startingCapitalChoice = new ChoiceBox<>(); + startingCapitalChoice.getItems().addAll( + 1_000.0, + 10_000.0, + 100_000.0, + 1_000_000.0, + 10_000_000.0 + ); + startingCapitalChoice.setConverter(buildCapitalConverter()); + startingCapitalChoice.getStyleClass().add("create-game-input"); + VBox capitalRow = new VBox(6, capitalLabel, startingCapitalChoice); + + // Row 3 - stock data choice (two buttons side by side) + Label stockLabel = new Label("Aksje data"); + stockLabel.getStyleClass().add("create-game-label"); + + useDefaultStocksButton = new Button("Bruk default aksje data"); + chooseStockFileButton = new Button("Velg egen aksje fil"); + + useDefaultStocksButton.setMaxWidth(Double.MAX_VALUE); + chooseStockFileButton.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(useDefaultStocksButton, Priority.ALWAYS); + HBox.setHgrow(chooseStockFileButton, Priority.ALWAYS); + + HBox stockButtonsRow = new HBox(15, + useDefaultStocksButton, chooseStockFileButton); + stockButtonsRow.setAlignment(Pos.CENTER); + + stockSelectionLabel = new Label("Ingen aksje data valgt"); + stockSelectionLabel.getStyleClass().add("create-game-selection-label"); + + VBox stockRow = new VBox(6, + stockLabel, stockButtonsRow, stockSelectionLabel); + + // Bottom row - cancel (left) / create-game (right) + cancelButton = new Button("Avbryt"); + createGameButton = new Button("Create game"); + + Region bottomSpacer = new Region(); + HBox.setHgrow(bottomSpacer, Priority.ALWAYS); + + HBox bottomRow = new HBox(20, + cancelButton, bottomSpacer, createGameButton); + bottomRow.setAlignment(Pos.CENTER); + + // Push the bottom row to the bottom of the panel. + Region middleSpacer = new Region(); + VBox.setVgrow(middleSpacer, Priority.ALWAYS); + + mainPanel = new VBox(20, + title, fileNameRow, capitalRow, stockRow, + middleSpacer, bottomRow); + mainPanel.setAlignment(Pos.TOP_CENTER); + mainPanel.setPadding(new Insets(30)); + mainPanel.setMaxWidth(620); + mainPanel.setMaxHeight(560); + + HBox centerWrapper = new HBox(mainPanel); + centerWrapper.setAlignment(Pos.CENTER); + + getRootPane().getChildren().addAll(backgroundImage, centerWrapper); + + // Make the background fill the root pane. + backgroundImage.fitWidthProperty().bind(getRootPane().widthProperty()); + backgroundImage.fitHeightProperty().bind(getRootPane().heightProperty()); + + // Hook listeners so the create-game button enables only when + // every required field has a valid value. + ChangeListener