From 31eb876f840abeb072a5f365e0d28aa4d3602385 Mon Sep 17 00:00:00 2001 From: Per Eric Trapnes Date: Fri, 15 May 2026 20:54:50 +0200 Subject: [PATCH] made choosing file native and added and stylised the settings to actullay change the enviroment --- .../idi/idatt2003/gruppe42/Audio/Audio.java | 11 + .../gruppe42/Audio/AudioManager.java | 125 +++++- .../AppControllers/AppStoreController.java | 3 + .../AppControllers/BankAppController.java | 3 + .../AppControllers/SettingsAppController.java | 185 +++++++- .../AppControllers/StockAppController.java | 9 +- .../gruppe42/Controller/GameController.java | 68 +-- .../gruppe42/Controller/MarketController.java | 8 + .../gruppe42/Controller/PopupsController.java | 39 +- .../Controller/TimeAndWeatherController.java | 46 +- .../DesktopViewController.java | 243 +++++++++-- .../ViewControllers/StartViewController.java | 20 +- .../ntnu/idi/idatt2003/gruppe42/Millions.java | 6 + .../idi/idatt2003/gruppe42/Model/App.java | 1 + .../Model/Calculator/PurchaseCalculator.java | 12 + .../Model/Calculator/SaleCalculator.java | 12 + .../idi/idatt2003/gruppe42/Model/Day.java | 3 + .../idatt2003/gruppe42/Model/GameState.java | 3 + .../idi/idatt2003/gruppe42/Model/Player.java | 185 ++++---- .../idatt2003/gruppe42/Model/Portfolio.java | 5 + .../idi/idatt2003/gruppe42/Model/Share.java | 6 + .../idi/idatt2003/gruppe42/Model/Stock.java | 15 +- .../gruppe42/Model/Transaction/Purchase.java | 3 + .../gruppe42/Model/Transaction/Sale.java | 3 + .../gruppe42/View/Apps/FilePickerApp.java | 308 +++++++++++++ .../gruppe42/View/Apps/GameOverApp.java | 7 +- .../gruppe42/View/Apps/SettingsApp.java | 352 ++++++++++++++- .../gruppe42/View/Apps/StockApp.java | 17 +- .../gruppe42/View/Apps/WarningApp.java | 7 +- .../gruppe42/View/Apps/WeekendRapportApp.java | 12 +- .../idi/idatt2003/gruppe42/View/Popup.java | 80 +++- .../View/Views/Components/StockGraph.java | 10 + .../gruppe42/View/Views/DesktopView.java | 224 ++++++---- .../gruppe42/View/Views/StartView.java | 25 +- src/main/resources/css/apps.css | 412 ------------------ src/main/resources/css/bank.css | 68 +++ src/main/resources/css/components.css | 50 +++ src/main/resources/css/filepicker.css | 189 ++++++++ src/main/resources/css/rapport.css | 107 +++++ src/main/resources/css/receipt.css | 51 +++ src/main/resources/css/settings.css | 113 +++++ src/main/resources/css/stock.css | 111 +++++ src/main/resources/css/warning.css | 32 ++ .../gruppe42/CriticalFeaturesTest.java | 68 +++ 44 files changed, 2512 insertions(+), 745 deletions(-) create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/FilePickerApp.java delete mode 100644 src/main/resources/css/apps.css create mode 100644 src/main/resources/css/bank.css create mode 100644 src/main/resources/css/components.css create mode 100644 src/main/resources/css/filepicker.css create mode 100644 src/main/resources/css/rapport.css create mode 100644 src/main/resources/css/receipt.css create mode 100644 src/main/resources/css/settings.css create mode 100644 src/main/resources/css/stock.css create mode 100644 src/main/resources/css/warning.css create mode 100644 src/test/java/edu/ntnu/idi/idatt2003/gruppe42/CriticalFeaturesTest.java diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/Audio.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/Audio.java index fdc1711..36b9809 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/Audio.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/Audio.java @@ -1,5 +1,8 @@ package edu.ntnu.idi.idatt2003.gruppe42.Audio; +/** + * Represents a Audio enum. + */ public enum Audio { // Sound effects @@ -19,10 +22,18 @@ public enum Audio { this.isSFX = isSFX; } + /** + * Returns the path. + * + * @return the value + */ public String getPath() { return path; } + /** + * Issfx method. + */ public boolean isSFX() { return isSFX; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/AudioManager.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/AudioManager.java index c828c00..a9c3b53 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/AudioManager.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/AudioManager.java @@ -1,60 +1,104 @@ package edu.ntnu.idi.idatt2003.gruppe42.Audio; -import javax.sound.sampled.*; -import javafx.scene.media.Media; -import javafx.scene.media.MediaPlayer; import java.io.IOException; import java.util.EnumMap; import java.util.Map; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import javax.sound.sampled.FloatControl; +import javax.sound.sampled.LineEvent; + +/** + * Represents a AudioManager class. + */ +public final class AudioManager { -public class AudioManager { + private static final AudioManager INSTANCE = new AudioManager(); - private static final AudioManager instance = new AudioManager(); private final Map sfxCache = new EnumMap<>(Audio.class); private MediaPlayer bgPlayer; + private final DoubleProperty masterVolume = new SimpleDoubleProperty(0.8); + private final DoubleProperty sfxVolume = new SimpleDoubleProperty(0.8); + private AudioManager() { for (Audio audio : Audio.values()) { if (audio.isSFX()) { try (var stream = getClass().getResourceAsStream(audio.getPath())) { - sfxCache.put(audio, stream.readAllBytes()); + if (stream != null) { + sfxCache.put(audio, stream.readAllBytes()); + } } catch (IOException e) { - e.printStackTrace(); + System.err.println("[Audio] Failed to cache " + audio + ": " + e.getMessage()); } } } + + // Keep background music volume in sync with master × 0.5 (bg is quieter) + masterVolume.addListener((obs, ov, nv) -> { + if (bgPlayer != null) { + bgPlayer.setVolume(nv.doubleValue() * 0.5); + } + }); } - public static AudioManager getInstance() { return instance; } + public static AudioManager get() { return INSTANCE; } + - public void playSFX(Audio audio) { + /** + * Playsfx method. + */ + public void playSFX(final Audio audio) { byte[] data = sfxCache.get(audio); if (data == null) return; + double volume = masterVolume.get() * sfxVolume.get(); + new Thread(() -> { try { - var stream = AudioSystem.getAudioInputStream( - new java.io.ByteArrayInputStream(data)); - Clip clip = AudioSystem.getClip(); + var bais = new java.io.ByteArrayInputStream(data); + var stream = AudioSystem.getAudioInputStream(bais); + Clip clip = AudioSystem.getClip(); clip.open(stream); + + // Apply volume via MASTER_GAIN + if (clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) { + FloatControl gain = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN); + float db = volumeToDb(volume); + gain.setValue(Math.max(gain.getMinimum(), Math.min(gain.getMaximum(), db))); + } + clip.addLineListener(e -> { if (e.getType() == LineEvent.Type.STOP) clip.close(); }); clip.start(); + } catch (Exception e) { - e.printStackTrace(); + System.err.println("[Audio] SFX playback failed: " + e.getMessage()); } - }).start(); + }, "sfx-thread").start(); } - public void playBgMusic(Audio audio) { + + /** + * Playbgmusic method. + */ + public void playBgMusic(final Audio audio) { stopBgMusic(); - var url = getClass().getResource(audio.getPath()).toExternalForm(); + String url = getClass().getResource(audio.getPath()).toExternalForm(); bgPlayer = new MediaPlayer(new Media(url)); - bgPlayer.setVolume(0.5); + bgPlayer.setVolume(masterVolume.get() * 0.5); + bgPlayer.setCycleCount(MediaPlayer.INDEFINITE); bgPlayer.play(); } + /** + * Stopbgmusic method. + */ public void stopBgMusic() { if (bgPlayer != null) { bgPlayer.stop(); @@ -62,4 +106,51 @@ public void stopBgMusic() { bgPlayer = null; } } + + + /** + * Mastervolumeproperty method. + */ + public DoubleProperty masterVolumeProperty() { return masterVolume; } + /** + * Sfxvolumeproperty method. + */ + public DoubleProperty sfxVolumeProperty() { return sfxVolume; } + + /** + * Returns the mastervolume. + * + * @return the value + */ + public double getMasterVolume() { return masterVolume.get(); } + /** + * Returns the sfxvolume. + * + * @return the value + */ + public double getSfxVolume() { return sfxVolume.get(); } + + /** + * Sets the mastervolume. + * + * @param value the new value + */ + public void setMasterVolume(final double v) { masterVolume.set(clamp(v)); } + /** + * Sets the sfxvolume. + * + * @param value the new value + */ + public void setSfxVolume(final double v) { sfxVolume.set(clamp(v)); } + + + /** Converts a 0.0–1.0 linear volume to decibels for FloatControl. */ + private static float volumeToDb(final double volume) { + if (volume <= 0.0) return -80.0f; + return (float) (20.0 * Math.log10(volume)); + } + + private static double clamp(final double v) { + return Math.max(0.0, Math.min(1.0, v)); + } } \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/AppStoreController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/AppStoreController.java index 2f83fa6..115e6ac 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/AppStoreController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/AppStoreController.java @@ -15,6 +15,9 @@ public AppStoreController(final AppStoreApp appStoreApp) { } @Override + /** + * Nexttick method. + */ public void nextTick() { } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/BankAppController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/BankAppController.java index 2637639..3901ab9 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/BankAppController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/BankAppController.java @@ -20,6 +20,9 @@ public record BankAppController(BankApp bankApp, Player player) implements AppCo } @Override + /** + * Nexttick method. + */ public void nextTick() { Platform.runLater(() -> { bankApp.updateStatus( diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/SettingsAppController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/SettingsAppController.java index a3e6c04..d78bcff 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/SettingsAppController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/SettingsAppController.java @@ -1,51 +1,122 @@ package edu.ntnu.idi.idatt2003.gruppe42.Controller.AppControllers; +import edu.ntnu.idi.idatt2003.gruppe42.Audio.AudioManager; import edu.ntnu.idi.idatt2003.gruppe42.Model.Exchange; import edu.ntnu.idi.idatt2003.gruppe42.Model.StockFileHandler; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.FilePickerApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.SettingsApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.SettingsApp.GradientConfig; import java.nio.file.Path; import java.util.Objects; +import java.util.function.Consumer; +import javafx.scene.layout.Pane; +/** + * Controller for the Settings app, handling volume changes, file picking, and profile updates. + */ public class SettingsAppController implements AppController { private Path userSelectedPath; private Runnable onUserSelection; private Runnable onSelectedFileFailed; + private Runnable onLogout; + private Runnable onPowerOff; + private Consumer onPlayerNameChanged; + private Consumer onGradientChanged; private String errorMessage; private final SettingsApp settingsApp; - - public SettingsAppController(final SettingsApp settingsApp) { + private final FilePickerApp filePickerApp; + + /** + * Creates a new SettingsAppController. + * + * @param settingsApp the settings app view + * @param filePickerApp the file picker app view + */ + public SettingsAppController(final SettingsApp settingsApp, final FilePickerApp filePickerApp) { this.settingsApp = settingsApp; + this.filePickerApp = filePickerApp; + userSelectedPath = loadDefaultPath(); refreshView(); - - if (!settingsApp.isLoggedIn()) { - settingsApp.getOpenFileChooserButton().setOnAction(event -> openFileChooser()); - } + wireControls(); + + // Confirm: validate the picked file then propagate or revert + filePickerApp.setOnFileConfirmed(path -> { + if (isValidStockFile(path)) { + userSelectedPath = path; + if (onUserSelection != null) onUserSelection.run(); + } else { + if (onSelectedFileFailed != null) onSelectedFileFailed.run(); + userSelectedPath = loadDefaultPath(); + } + refreshView(); + }); } - private void openFileChooser() { - var file = settingsApp.getFileChooser() - .showOpenDialog(settingsApp.getRoot().getScene().getWindow()); - if (file == null) { - return; // user cancelled — keep current path - } + private void wireControls() { + settingsApp.getMasterVolumeSlider().valueProperty().addListener((obs, ov, nv) -> + AudioManager.get().setMasterVolume(nv.doubleValue() / 100.0)); + + settingsApp.getSfxVolumeSlider().valueProperty().addListener((obs, ov, nv) -> + AudioManager.get().setSfxVolume(nv.doubleValue() / 100.0)); - Path selected = file.toPath(); + settingsApp.getMasterVolumeSlider().setValue(AudioManager.get().getMasterVolume() * 100.0); + settingsApp.getSfxVolumeSlider().setValue(AudioManager.get().getSfxVolume() * 100.0); + + if (!settingsApp.isLoggedIn()) { + settingsApp.getOpenFileChooserButton().setOnAction(event -> openFilePicker()); - if (isValidStockFile(selected)) { - userSelectedPath = selected; - onUserSelection.run(); } else { - onSelectedFileFailed.run(); - userSelectedPath = loadDefaultPath(); + settingsApp.getApplyGradientButton().setOnAction(event -> { + if (onGradientChanged != null) { + onGradientChanged.accept(settingsApp.getGradientConfig()); + } + }); + + settingsApp.getApplyNameButton().setOnAction(event -> { + String name = settingsApp.getPlayerNameField().getText().trim(); + if (!name.isEmpty() && onPlayerNameChanged != null) { + onPlayerNameChanged.accept(name); + settingsApp.getPlayerNameField().clear(); + } + }); + + settingsApp.getLogoutButton().setOnAction(event -> { + if (onLogout != null) onLogout.run(); + }); + settingsApp.getPowerOffButton().setOnAction(event -> { + if (onPowerOff != null) onPowerOff.run(); + }); } + } - refreshView(); + /** + * Sets the login status of the app. + * + * @param status the new login status + */ + public void setLoggedIn(final boolean status) { + settingsApp.setLoggedIn(status); + wireControls(); + } + + + private void openFilePicker() { + // Lazily attach the picker to the same parent pane as the settings popup + if (filePickerApp.getRoot().getParent() == null) { + var parent = settingsApp.getRoot().getParent(); + if (parent instanceof Pane pane) { + pane.getChildren().add(filePickerApp.getRoot()); + filePickerApp.centerInParent(); + } + } + filePickerApp.show(); } + private boolean isValidStockFile(final Path path) { try { new Exchange(StockFileHandler.readFromFile(path)); @@ -71,22 +142,92 @@ private void refreshView() { settingsApp.updateContent(); } - public Path getUserSelectedPath() { - return userSelectedPath; - } + /** + * Sets the onuserpathselected. + * + * @param callback the new callback + */ public void setOnUserPathSelected(final Runnable callback) { onUserSelection = callback; } + /** + * Sets the onselectedfilefailed. + * + * @param callback the new callback + */ public void setOnSelectedFileFailed(final Runnable callback) { onSelectedFileFailed = callback; } + /** + * Sets the onlogout. + * + * @param callback the new callback + */ + public void setOnLogout(final Runnable callback) { + onLogout = callback; + } + + /** + * Sets the onpoweroff. + * + * @param callback the new callback + */ + public void setOnPowerOff(final Runnable callback) { + onPowerOff = callback; + } + + /** + * Sets the onplayernamechanged. + * + * @param callback the new callback + */ + public void setOnPlayerNameChanged(final Consumer callback) { + onPlayerNameChanged = callback; + } + + /** + * Sets the ongradientchanged. + * + * @param callback the new callback + */ + public void setOnGradientChanged(final Consumer callback) { + onGradientChanged = callback; + } + + + /** + * Returns the userselectedpath. + * + * @return the value + */ + public Path getUserSelectedPath() { + return userSelectedPath; + } + + /** + * Returns the filepickerapp. + * + * @return the value + */ + public FilePickerApp getFilePickerApp() { + return filePickerApp; + } + + /** + * Returns the errormessage. + * + * @return the value + */ public String getErrorMessage() { return errorMessage; } @Override + /** + * Nexttick method. + */ public void nextTick() {} } \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/StockAppController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/StockAppController.java index 2a1603e..bfbbf8c 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/StockAppController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/StockAppController.java @@ -76,9 +76,9 @@ public StockAppController( } }); - stockApp.getConfirmButton().setOnAction(e -> { - handleTransaction(); - }); + stockApp.getConfirmButton().setOnAction(e -> handleTransaction()); + + stockApp.getBackButton().setOnAction(e -> navigateToSearch()); } /** @@ -300,6 +300,9 @@ private void updateReceipt() { * Updates the app state on each simulation tick. */ @Override + /** + * Nexttick method. + */ public void nextTick() { marketController.updateMarket(); Platform.runLater(() -> stockApp.getStockList().refresh()); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java index 201c4f3..8f9ba2d 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java @@ -4,74 +4,77 @@ import edu.ntnu.idi.idatt2003.gruppe42.Audio.AudioManager; import edu.ntnu.idi.idatt2003.gruppe42.Controller.AppControllers.AppController; import edu.ntnu.idi.idatt2003.gruppe42.Model.GameState; - import java.util.ArrayList; import java.util.List; import java.util.Timer; import java.util.TimerTask; /** - * Controller for managing the game state and timing. + * Represents a GameController class. */ public final class GameController { + private final List appControllers = new ArrayList<>(); private Timer timer; private GameState gameState; - private AudioManager audioManager = AudioManager.getInstance(); - private boolean workMusicPlaying = false; + private final AudioManager audioManager = AudioManager.get(); // ← was getInstance() + private boolean workMusicPlaying = false; private boolean weekendMusicPlaying = false; /** - * Adds an application controller to the list of controllers to be updated. - * - * @param appController the app controller to add + * Addappcontroller method. */ public void addAppController(final AppController appController) { appControllers.add(appController); } - public void setGameState(GameState gameState) { + /** + * Sets the game state and handles music transitions. + * + * @param gameState the new game state + */ + public void setGameState(final GameState gameState) { this.gameState = gameState; if (gameState == GameState.WORKDAY && !workMusicPlaying) { audioManager.playBgMusic(Audio.WORK_THEME); - workMusicPlaying = true; + workMusicPlaying = true; weekendMusicPlaying = false; } - if ((gameState == GameState.FREEDAY) && !weekendMusicPlaying) { + if (gameState == GameState.FREEDAY && !weekendMusicPlaying) { audioManager.playBgMusic(Audio.WEEKEND_THEME); weekendMusicPlaying = true; - workMusicPlaying = false; + workMusicPlaying = false; } } /** - * Starts the game simulation timer. + * Startgame method. */ public void startGame() { timer = new Timer(true); - final int delay = 0; - final int period = 1000; //TODO: This has to be 1000 (1 second) on release + final int delay = 0; + final int period = 1000; - timer.scheduleAtFixedRate( - new TimerTask() { - @Override - public void run() { - for (AppController controller : appControllers) { - controller.nextTick(); - } - if (isWeekend()) { - stopGame(); - } - } - }, - delay, - period); + timer.scheduleAtFixedRate(new TimerTask() { + @Override + /** + * Run method. + */ + public void run() { + for (AppController controller : appControllers) { + controller.nextTick(); + } + if (isWeekend()) { + stopGame(); + } + } + }, delay, period); } /** - * Stops the game simulation timer. + * Stopgame method. */ public void stopGame() { if (timer != null) { @@ -80,7 +83,10 @@ public void stopGame() { } } + /** + * Isweekend method. + */ public boolean isWeekend() { - return !(gameState == GameState.WORKDAY); + return gameState != GameState.WORKDAY; } -} +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java index d738a83..2ff6f1a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java @@ -35,6 +35,11 @@ public MarketController(Path path) { } } + /** + * Returns the exchange. + * + * @return the value + */ public Exchange getExchange() { return exchange; } @@ -81,6 +86,9 @@ public static void printMarket(final List stocks) { } } + /** + * Advanceweek method. + */ public void advanceWeek() { exchange.advance(); } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupsController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupsController.java index a11f1db..523d8f3 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupsController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupsController.java @@ -17,7 +17,7 @@ public final class PopupsController { private final List popups; - private AudioManager audioManager = AudioManager.getInstance(); + private AudioManager audioManager = AudioManager.get(); public PopupsController(List popups) { this.popups = popups; @@ -29,6 +29,9 @@ private void initPopup(Popup popup) { popup.getCloseButton().setOnAction(event -> hide(popup.getType())); } + /** + * Show method. + */ public boolean show(App type) { if (type == null) { return false; @@ -36,15 +39,14 @@ public boolean show(App type) { popups.stream() .filter(popup -> popup.getType().equals(type)) .findFirst() - .ifPresent(popup -> { - popup.getRoot().setVisible(true); - popup.getRoot().autosize(); - popup.getRoot().toFront(); - }); + .ifPresent(Popup::show); audioManager.playSFX(Audio.OPEN_APP); return true; } + /** + * Hide method. + */ public boolean hide(App type) { if (type == null) { return false; @@ -52,13 +54,14 @@ public boolean hide(App type) { popups.stream() .filter(popup -> popup.getType().equals(type)) .findFirst() - .ifPresent(popup -> { - popup.getRoot().setVisible(false); - }); + .ifPresent(Popup::hide); audioManager.playSFX(Audio.CLOSE_APP); return true; } + /** + * Bindopenbutton method. + */ public void bindOpenButton(Button button, App type) { button.setOnAction(event -> show(type)); } @@ -72,7 +75,7 @@ private void makeDraggable(Popup popup) { offset[0] = event.getSceneX() - root.getLayoutX(); offset[1] = event.getSceneY() - root.getLayoutY(); if (root.getParent() instanceof Pane parent) { - maxXY[0] = parent.getWidth() - root.getWidth(); + maxXY[0] = parent.getWidth() - root.getWidth(); maxXY[1] = parent.getHeight() - root.getHeight(); } }); @@ -83,20 +86,36 @@ private void makeDraggable(Popup popup) { }); } + /** + * Addpopups method. + */ public void addPopups(List popups) { popups.forEach(this::addPopup); } + /** + * Addpopup method. + */ public void addPopup(Popup popup) { initPopup(popup); this.popups.add(popup); } + /** + * Returns the popup. + * + * @return the value + */ public Popup getPopup(App type) { Optional match = popups.stream().filter(popup -> popup.getType().equals(type)).findFirst(); return match.orElse(null); } + /** + * Returns the popups. + * + * @return the value + */ public List getPopups() { return popups; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/TimeAndWeatherController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/TimeAndWeatherController.java index 8a3bb15..2cfe15a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/TimeAndWeatherController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/TimeAndWeatherController.java @@ -25,7 +25,7 @@ public class TimeAndWeatherController implements AppController { private final StringProperty weather = new SimpleStringProperty("Sunny"); private final Random random = new Random(); private final GameController gameController; - private AudioManager audioManager = AudioManager.getInstance(); + private AudioManager audioManager = AudioManager.get(); private static final Day[] DAYS = {Day.SUN, Day.MON, Day.TUE, Day.WED, Day.THU, Day.FRI, Day.SAT}; @@ -36,6 +36,9 @@ public TimeAndWeatherController(GameController gameController) { } @Override + /** + * Nexttick method. + */ public void nextTick() { int nextHour = hour.get() + 1; if (nextHour >= 24) { @@ -67,18 +70,30 @@ private void updateWeather() { } } + /** + * Hourproperty method. + */ public IntegerProperty hourProperty() { return hour; } + /** + * Dayindexproperty method. + */ public IntegerProperty dayIndexProperty() { return dayIndex; } + /** + * Weekindexproperty method. + */ public IntegerProperty weekIndexProperty() { return weekNumber; } + /** + * Advanceweek method. + */ public void advanceWeek() { weekNumber.set(weekNumber.get() + 1); dayIndex.set(1); @@ -86,26 +101,50 @@ public void advanceWeek() { gameController.startGame(); } + /** + * Temperatureproperty method. + */ public IntegerProperty temperatureProperty() { return temperature; } + /** + * Weatherproperty method. + */ public StringProperty weatherProperty() { return weather; } + /** + * Returns the timestring. + * + * @return the value + */ public String getTimeString() { return String.format("%02d:00", hour.get()); } + /** + * Returns the dayofweekstring. + * + * @return the value + */ public String getDayOfWeekString() { return DAYS[dayIndex.get()].toString(); } + /** + * Returns the weekstring. + * + * @return the value + */ public String getWeekString() { return "Week " + weekNumber.get(); } + /** + * Updategamestate method. + */ public void updateGameState() { if (dayIndex.get() == 6) { gameController.setGameState(GameState.RENTDAY); @@ -117,6 +156,11 @@ public void updateGameState() { gameController.setGameState(GameState.WORKDAY); } + /** + * Sets the day. + * + * @param value the new value + */ public void setDay(int index) { dayIndex.set(index); updateGameState(); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/DesktopViewController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/DesktopViewController.java index a4ff879..f52e0c4 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/DesktopViewController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/DesktopViewController.java @@ -9,12 +9,21 @@ import edu.ntnu.idi.idatt2003.gruppe42.Controller.PopupsController; import edu.ntnu.idi.idatt2003.gruppe42.Controller.TimeAndWeatherController; import edu.ntnu.idi.idatt2003.gruppe42.Model.App; -import edu.ntnu.idi.idatt2003.gruppe42.Model.Exceptions.StockFileParseException; import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Purchase; import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Sale; import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Transaction; -import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.*; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.AppStoreApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.BankApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.FilePickerApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.GameOverApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.HustlersApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.MailApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.NewsApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.SettingsApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.StockApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.WarningApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.WeekendRapportApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; import edu.ntnu.idi.idatt2003.gruppe42.View.Views.DesktopView; import edu.ntnu.idi.idatt2003.gruppe42.View.Views.DesktopView.ModalLayer; @@ -24,6 +33,7 @@ import java.util.List; import javafx.application.Platform; import javafx.beans.binding.Bindings; +import javafx.scene.SnapshotParameters; import javafx.scene.control.Button; import javafx.scene.input.ClipboardContent; import javafx.scene.input.Dragboard; @@ -31,10 +41,10 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; /** - * Controller for the desktop view. Manages app popup lifecycle, drag and drop, - * the weekend rapport modal, warning popups, and gameover state. + * Controller for the desktop view, handling week advancement, app opening, and UI updates. */ public final class DesktopViewController { @@ -43,26 +53,38 @@ public final class DesktopViewController { private static final int SUNDAY_INDEX = 0; private final PopupsController popupsController; - private final DesktopView desktopView; private final Player player; private final TimeAndWeatherController timeAndWeatherController; private final MarketController marketController; private final StockAppController stockAppController; - + private final SettingsAppController settingsAppController; // ← added private final WeekendRapportApp weekendRapportApp = new WeekendRapportApp(); private final WarningApp warningApp = new WarningApp(); private final GameOverApp gameOverApp = new GameOverApp(); + private DesktopView desktopView = null; private Runnable onGameOver; - public DesktopViewController(final Player player, final GameController gameController, final Path userSelectedPath, final PopupsController popupsController) { + /** + * Creates a new DesktopViewController. + * + * @param player the player model + * @param gameController the main game controller + * @param userSelectedPath the path to the stock file + * @param popupsController the controller for popups + */ + public DesktopViewController( + final Player player, + final GameController gameController, + final Path userSelectedPath, + final PopupsController popupsController) { + this.player = player; this.timeAndWeatherController = new TimeAndWeatherController(gameController); gameController.addAppController(timeAndWeatherController); this.marketController = new MarketController(userSelectedPath); - AppStoreApp appStoreApp = new AppStoreApp(); gameController.addAppController(new AppStoreController(appStoreApp)); @@ -79,7 +101,17 @@ public DesktopViewController(final Player player, final GameController gameContr gameController.addAppController(new BankAppController(bankApp, player)); SettingsApp settingsApp = (SettingsApp) popupsController.getPopup(App.SETTINGS); - settingsApp.setLoggedIn(true); + FilePickerApp filePickerApp = (FilePickerApp) popupsController.getPopup(App.FILEPICKER); + settingsAppController = new SettingsAppController(settingsApp, filePickerApp); + settingsAppController.setLoggedIn(true); + + settingsAppController.setOnGradientChanged(cfg -> + desktopView.getRoot().setStyle( + SettingsApp.buildCssGradient(cfg.colorA(), cfg.colorB(), cfg.direction()))); + + settingsAppController.setOnPlayerNameChanged(name -> player.setName(name)); + settingsAppController.setOnLogout(this::handleLogout); + settingsAppController.setOnPowerOff(Platform::exit); List popups = new ArrayList<>(); popups.add(appStoreApp); @@ -89,6 +121,7 @@ public DesktopViewController(final Player player, final GameController gameContr popups.add(newsApp); popups.add(bankApp); + this.popupsController = popupsController; this.popupsController.addPopups(popups); @@ -101,6 +134,7 @@ public DesktopViewController(final Player player, final GameController gameContr stockAppController.setOnInsufficientFunds(this::showInsufficientFundsWarning); this.desktopView = new DesktopView(this); + setupListeners(); BorderPane root = desktopView.getRoot(); for (Popup p : popupsController.getPopups()) { @@ -122,9 +156,8 @@ public DesktopViewController(final Player player, final GameController gameContr desktopView.getOverlay(ModalLayer.GAME_OVER), gameOverApp.getRoot() ); - desktopView.getOverlay(ModalLayer.RAPPORT).setOnMouseClicked(event -> - weekendRapportApp.getContinueButton().fire() - ); + desktopView.getOverlay(ModalLayer.RAPPORT).setOnMouseClicked( + event -> weekendRapportApp.getContinueButton().fire()); wireWeekendRapport(); wireGameOver(); @@ -145,6 +178,41 @@ public DesktopViewController(final Player player, final GameController gameContr stockAppController.setWeekend(startDay == SATURDAY_INDEX || startDay == SUNDAY_INDEX); } + + private void setupListeners() { + timeAndWeatherController.hourProperty().addListener((o, ov, nv) -> + desktopView.updateClock(timeAndWeatherController.getTimeString())); + + timeAndWeatherController.dayIndexProperty().addListener((o, ov, nv) -> { + String dayName = timeAndWeatherController.getDayOfWeekString(); + desktopView.updateDay(dayName, dayName.equals("SUN")); + }); + + timeAndWeatherController.weekIndexProperty().addListener((o, ov, nv) -> + desktopView.updateWeek(timeAndWeatherController.getWeekString())); + + timeAndWeatherController.weatherProperty().addListener((o, ov, nv) -> + desktopView.updateWeather(nv)); + + timeAndWeatherController.temperatureProperty().addListener((o, ov, nv) -> + desktopView.updateTemperature(String.valueOf(nv))); + + player.getNameProperty().addListener((obs, ov, nv) -> + desktopView.updatePlayerName(nv)); + + // Initial sync + desktopView.updateClock(timeAndWeatherController.getTimeString()); + String startDay = timeAndWeatherController.getDayOfWeekString(); + desktopView.updateDay(startDay, startDay.equals("SUN")); + desktopView.updateWeek(timeAndWeatherController.getWeekString()); + desktopView.updateWeather(timeAndWeatherController.weatherProperty().get()); + desktopView.updateTemperature(String.valueOf(timeAndWeatherController.temperatureProperty().get())); + desktopView.updatePlayerName(player.getName()); + } + + /** + * Shows the weekend rapport at the end of the week. + */ private void showWeekendRapport() { int week = timeAndWeatherController.weekIndexProperty().get(); List transactions = player.getTransactionArchive().getTransactions(week); @@ -159,6 +227,9 @@ private void showWeekendRapport() { desktopView.enterLayer(ModalLayer.RAPPORT); } + /** + * Wires the weekend rapport buttons. + */ private void wireWeekendRapport() { weekendRapportApp.getContinueButton().setOnAction(event -> { weekendRapportApp.getRoot().setVisible(false); @@ -167,6 +238,10 @@ private void wireWeekendRapport() { }); } + + /** + * Handles the request to advance to the next week. + */ private void handleAdvanceWeek() { if (player.isInDebt()) { showDebtWarning(); @@ -175,6 +250,18 @@ private void handleAdvanceWeek() { } } + /** + * Performs the actual week advancement. + */ + private void doAdvanceWeek() { + marketController.advanceWeek(); + timeAndWeatherController.advanceWeek(); + } + + + /** + * Shows a warning when the player is in debt. + */ private void showDebtWarning() { warningApp.configure( "💔", @@ -196,6 +283,9 @@ private void showDebtWarning() { showWarning(); } + /** + * Shows a warning when the market is closed. + */ private void showMarketClosedWarning() { warningApp.configure( "🔒", @@ -207,72 +297,78 @@ private void showMarketClosedWarning() { showWarning(); } + /** + * Shows a warning when the player has insufficient funds. + */ private void showInsufficientFundsWarning() { warningApp.configure( "\uD83E\uDD7A", "Insufficient Funds", "You don't have enough cash to complete this purchase.", - "I'll be back"); + "I'll be back" + ); warningApp.getPrimaryButton().setOnAction(event -> dismissWarning()); showWarning(); } + /** + * Displays the warning modal. + */ private void showWarning() { warningApp.show(); desktopView.enterLayer(ModalLayer.WARNING); } + /** + * Dismisses the current warning modal. + */ private void dismissWarning() { warningApp.getRoot().setVisible(false); desktopView.exitLayer(ModalLayer.WARNING); } + + /** + * Shows the game over screen. + */ private void showGameOver() { gameOverApp.show(); desktopView.enterLayer(ModalLayer.GAME_OVER); } + /** + * Wires the game over buttons. + */ private void wireGameOver() { gameOverApp.getStartOverButton().setOnAction(event -> { - if (onGameOver != null) { - onGameOver.run(); - } + if (onGameOver != null) onGameOver.run(); }); } + /** + * Sets the callback for when the game is over. + * + * @param callback the callback to run + */ public void setOnGameOver(final Runnable callback) { this.onGameOver = callback; } - private void doAdvanceWeek() { - marketController.advanceWeek(); - timeAndWeatherController.advanceWeek(); - } - - private static BigDecimal calculateNetIncome(final List transactions) { - BigDecimal net = BigDecimal.ZERO; - for (Transaction t : transactions) { - if (t instanceof Sale) { - net = net.add(t.getCalculator().calculateTotal()); - } else if (t instanceof Purchase) { - net = net.subtract(t.getCalculator().calculateTotal()); - } - } - return net; - } - - public Player getPlayer() { - return player; - } - public TimeAndWeatherController getTimeAndWeatherController() { - return timeAndWeatherController; + /** + * Handles the logout action. + */ + private void handleLogout() { + if (onGameOver != null) onGameOver.run(); } - public DesktopView getDesktopView() { - return desktopView; - } + /** + * Creates an app button for the given app type. + * + * @param type the app type + * @return the created Button + */ public Button createAppButton(final App type) { Button button = new Button(type.getDisplayName()); button.getStyleClass().add("app-button"); @@ -297,21 +393,25 @@ public Button createAppButton(final App type) { button.setOnDragDetected(event -> { Dragboard dragboard = button.startDragAndDrop(TransferMode.MOVE); - dragboard.setDragView(button.snapshot(null, null)); + SnapshotParameters params = new SnapshotParameters(); + params.setFill(Color.TRANSPARENT); + dragboard.setDragView(button.snapshot(params, null)); dragboard.setDragViewOffsetX(event.getX()); dragboard.setDragViewOffsetY(event.getY()); - ClipboardContent clipboardContent = new ClipboardContent(); - clipboardContent.putString("app_button"); - dragboard.setContent(clipboardContent); + ClipboardContent content = new ClipboardContent(); + content.putString("app_button"); + dragboard.setContent(content); event.consume(); }); return button; } - public void handleSettings() { - } - + /** + * Configures a StackPane as a drop target for app buttons. + * + * @param cell the StackPane to configure + */ public void configureCellAsDropTarget(final StackPane cell) { cell.setOnDragOver(event -> { if (event.getGestureSource() instanceof Button && cell.getChildren().isEmpty()) { @@ -331,4 +431,51 @@ public void configureCellAsDropTarget(final StackPane cell) { event.consume(); }); } + + + /** + * Returns the player model. + * + * @return the Player + */ + public Player getPlayer() { + return player; + } + + /** + * Returns the time and weather controller. + * + * @return the TimeAndWeatherController + */ + public TimeAndWeatherController getTimeAndWeatherController() { + return timeAndWeatherController; + } + + /** + * Returns the desktop view. + * + * @return the DesktopView + */ + public DesktopView getDesktopView() { + return desktopView; + } + + + /** + * Calculates the net income for a list of transactions. + * + * @param transactions the list of transactions + * @return the net income + */ + private static BigDecimal calculateNetIncome(final List transactions) { + BigDecimal net = BigDecimal.ZERO; + for (Transaction t : transactions) { + if (t instanceof Sale) { + net = net.add(t.getCalculator().calculateTotal()); + } else if (t instanceof Purchase) { + net = net.subtract(t.getCalculator().calculateTotal()); + } + } + return net; + } } \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/StartViewController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/StartViewController.java index 940f7ff..36d1695 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/StartViewController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/StartViewController.java @@ -5,6 +5,7 @@ import edu.ntnu.idi.idatt2003.gruppe42.Millions; import edu.ntnu.idi.idatt2003.gruppe42.Model.App; import edu.ntnu.idi.idatt2003.gruppe42.Model.Difficulty; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.FilePickerApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.SettingsApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.WarningApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; @@ -57,19 +58,25 @@ public StartViewController(final Millions application, final StartView startView List popups = new ArrayList<>(); + FilePickerApp filePickerApp = new FilePickerApp(); SettingsApp settingsApp = new SettingsApp(false); - settingsAppController = new SettingsAppController(settingsApp); + settingsAppController = new SettingsAppController(settingsApp, filePickerApp); warningApp = new WarningApp(); warningApp.centerInParent(); - startView.getRoot().getChildren().addAll(settingsApp.getRoot(), warningApp.getRoot()); + startView.getRoot().getChildren().addAll( + settingsApp.getRoot(), + warningApp.getRoot(), + filePickerApp.getRoot() + ); settingsAppController.setOnUserPathSelected(this::updateUserSelectedPath); settingsAppController.setOnSelectedFileFailed(this::showFileWarning); popups.add(settingsApp); popups.add(warningApp); + popups.add(filePickerApp); popupsController = new PopupsController(popups); startView.getUsernameField().textProperty().addListener( @@ -109,6 +116,15 @@ private void handleStart() { } } + if (Objects.isNull(userSelectedPath)) { + try { + userSelectedPath = Path.of(Objects.requireNonNull( + getClass().getClassLoader().getResource("stocks.csv")).toURI()); + } catch (Exception e) { + System.err.println("Failed to load default stocks.csv: " + e.getMessage()); + } + } + application.initGame(resolvedName, difficulty, userSelectedPath, popupsController); } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java index d571c15..16b6e83 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java @@ -35,6 +35,9 @@ public class Millions extends Application { private DesktopViewController desktopViewController; @Override + /** + * Start method. + */ public void start(final Stage stage) { StartView startView = installStartView(); scene = new Scene(startView.getRoot(), 900, 700); @@ -94,6 +97,9 @@ private StartView installStartView() { } @Override + /** + * Stop method. + */ public void stop() { if (gameController != null) { gameController.stopGame(); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java index 9925085..e44e916 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java @@ -11,6 +11,7 @@ public enum App { NEWS("News"), BANK("Bank"), SETTINGS("Settings"), + FILEPICKER("File Picker"), WEEKENDRAPPORT("Weekend Rapport"), WARNING("Warning"), GAMEOVER("Game Over"); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/PurchaseCalculator.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/PurchaseCalculator.java index ca65050..29a0495 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/PurchaseCalculator.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/PurchaseCalculator.java @@ -21,22 +21,34 @@ public PurchaseCalculator(final Share share) { } @Override + /** + * Calculategross method. + */ public BigDecimal calculateGross() { return purchasePrice.multiply(quantity); } @Override + /** + * Calculatecommission method. + */ public BigDecimal calculateCommission() { final BigDecimal commissionRate = new BigDecimal("0.01"); return calculateGross().multiply(commissionRate); } @Override + /** + * Calculatetax method. + */ public BigDecimal calculateTax() { return BigDecimal.ZERO; } @Override + /** + * Calculatetotal method. + */ public BigDecimal calculateTotal() { return calculateGross().add(calculateCommission()).add(calculateTax()); } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/SaleCalculator.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/SaleCalculator.java index 325bdc9..4bdebbf 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/SaleCalculator.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/SaleCalculator.java @@ -23,17 +23,26 @@ public SaleCalculator(Share share) { } @Override + /** + * Calculategross method. + */ public BigDecimal calculateGross() { return salesPrice.multiply(quantity); } @Override + /** + * Calculatecommission method. + */ public BigDecimal calculateCommission() { final BigDecimal commissionRate = new BigDecimal("0.01"); return calculateGross().multiply(commissionRate); } @Override + /** + * Calculatetax method. + */ public BigDecimal calculateTax() { final BigDecimal taxRate = new BigDecimal("0.22"); BigDecimal differencePrice = salesPrice.subtract(purchasePrice); @@ -42,6 +51,9 @@ public BigDecimal calculateTax() { } @Override + /** + * Calculatetotal method. + */ public BigDecimal calculateTotal() { return calculateGross().subtract(calculateCommission()) .subtract(calculateTax()); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Day.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Day.java index c044d46..a81bebc 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Day.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Day.java @@ -1,5 +1,8 @@ package edu.ntnu.idi.idatt2003.gruppe42.Model; +/** + * Represents a Day enum. + */ public enum Day { MON, TUE, WED, THU, FRI, SAT, SUN; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/GameState.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/GameState.java index a72dd36..08e1f6a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/GameState.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/GameState.java @@ -1,5 +1,8 @@ package edu.ntnu.idi.idatt2003.gruppe42.Model; +/** + * Represents a GameState enum. + */ public enum GameState { WORKDAY, RENTDAY, FREEDAY } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Player.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Player.java index 61923ff..9c1779f 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Player.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Player.java @@ -5,151 +5,186 @@ import java.math.BigDecimal; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; /** - * Represents a player in the stock market simulation. - *

- * A {@code Player} has a name, starting money, current money balance, - * a {@link Portfolio} of shares, and a {@link TransactionArchive} of past transactions. - * The player can add or withdraw money, and manage their portfolio through purchases and sales. - *

+ * A player in the Millions game, tracks money, lives, and portfolio. */ public class Player { + private static final int MAX_LIVES = 3; - private final String name; + private final StringProperty name = new SimpleStringProperty(); // ← was plain String private final BigDecimal startingMoney; private BigDecimal money; private final Portfolio portfolio; private final TransactionArchive transactionArchive; private final IntegerProperty lives = new SimpleIntegerProperty(MAX_LIVES); - /** - * Constructs a new {@code Player} with a name and starting money. - * - * @param name the name of the player - * @param startingMoney the initial amount of money the player has - */ public Player(final String name, final BigDecimal startingMoney) { - this.name = name; + this.name.set(name); this.startingMoney = startingMoney; this.money = startingMoney; this.portfolio = new Portfolio(); this.transactionArchive = new TransactionArchive(); } - public BigDecimal getStartingMoney() { - return startingMoney; - } - public String getName() { + /** + * Returns the name property. + * + * @return the name property + */ + public StringProperty getNameProperty() { return name; } - - public Portfolio getPortfolio() { - return portfolio; + /** + * Returns the player's name. + * + * @return the name + */ + public String getName() { + return name.get(); + } + /** + * Sets the player's name. + * + * @param name the new name + */ + public void setName(final String name) { + this.name.set(name); } - public TransactionArchive getTransactionArchive() { - return transactionArchive; + + /** + * Returns the starting money. + * + * @return the starting money + */ + public BigDecimal getStartingMoney() { + return startingMoney; } + /** + * Returns the current money. + * + * @return the current money + */ public BigDecimal getMoney() { return money; } - public IntegerProperty getLivesProperty() { - return lives; + /** + * Addmoney method. + */ + public void addMoney(final BigDecimal amount) { + money = money.add(amount); } - public int getLives() { - return lives.get(); + /** + * Withdrawmoney method. + */ + public void withdrawMoney(final BigDecimal amount) throws InsufficientFunds { + if (money.compareTo(amount) < 0) { + throw new InsufficientFunds("Not enough money to withdraw " + amount); + } + money = money.subtract(amount); } /** - * Deducts one life. Safe to call on the JavaFX thread only. - * Has no effect if already at 0. + * Deductrent method. */ - public void loseLife() { - lives.set(Math.max(0, lives.get() - 1)); + public void deductRent(final BigDecimal amount) { + money = money.subtract(amount); } - /** Returns true if the player's cash balance is negative. */ + /** + * Isindebt method. + */ public boolean isInDebt() { return money.compareTo(BigDecimal.ZERO) < 0; } /** - * Deducts rent from the player's cash balance. - * Unlike {@link #withdrawMoney}, this allows the balance to go negative. + * Returns the networth. * - * @param amount the rent amount to deduct + * @return the value */ - public void deductRent(final BigDecimal amount) { - money = money.subtract(amount); + public BigDecimal getNetWorth() { + return money.add(portfolio.getNetWorth()); } + /** - * Adds the specified amount of money to the player's balance. + * Returns the lives property. * - * @param amount the amount to add + * @return the lives property */ - public void addMoney(final BigDecimal amount) { - money = money.add(amount); + public IntegerProperty getLivesProperty() { + return lives; } /** - * Withdraws the specified amount of money from the player's balance. + * Returns the number of lives. * - * @param amount the amount to withdraw - * @throws InsufficientFunds if the player does not have enough money + * @return the number of lives */ - public void withdrawMoney(final BigDecimal amount) throws InsufficientFunds { - if (money.compareTo(amount) < 0) { - throw new InsufficientFunds("Not enough money to withdraw " + amount); - } - money = money.subtract(amount); + public int getLives() { + return lives.get(); + } + + /** + * Loselife method. + */ + public void loseLife() { + lives.set(Math.max(0, lives.get() - 1)); } + /** - * Calculates the total net worth of a player (portfolio + money). + * Returns the portfolio. * - * @return the player's total net worth + * @return the portfolio */ - public BigDecimal getNetWorth() { - return money.add(portfolio.getNetWorth()); + public Portfolio getPortfolio() { + return portfolio; } /** - * Returns the current status of the player. + * Returns the transaction archive. * - * @return the current status (NOVICE / INVESTOR / SPECULATOR) + * @return the transaction archive + */ + public TransactionArchive getTransactionArchive() { + return transactionArchive; + } + + + /** + * Returns the status. + * + * @return the value */ public Status getStatus() { - final int investorWeeks = 10; - final int speculatorWeeks = 20; - final double investorMultiplier = 1.2; - final double speculatorMultiplier = 2.0; + final int investorWeeks = 10; + final int speculatorWeeks = 20; + final double investorMult = 1.2; + final double speculatorMult = 2.0; - int weeksActive = transactionArchive.countDistinctWeeks(); - BigDecimal netWorth = getNetWorth(); + int weeksActive = transactionArchive.countDistinctWeeks(); + BigDecimal netWorth = getNetWorth(); - BigDecimal investorTarget = startingMoney - .multiply(BigDecimal.valueOf(investorMultiplier)); - BigDecimal speculatorTarget = startingMoney - .multiply(BigDecimal.valueOf(speculatorMultiplier)); + BigDecimal investorTarget = startingMoney.multiply(BigDecimal.valueOf(investorMult)); + BigDecimal speculatorTarget = startingMoney.multiply(BigDecimal.valueOf(speculatorMult)); - boolean isInvestor = weeksActive >= investorWeeks - && netWorth.compareTo(investorTarget) >= 0; boolean isSpeculator = weeksActive >= speculatorWeeks && netWorth.compareTo(speculatorTarget) >= 0; + boolean isInvestor = weeksActive >= investorWeeks + && netWorth.compareTo(investorTarget) >= 0; - if (isSpeculator) { - return Status.SPECULATOR; - } else if (isInvestor) { - return Status.INVESTOR; - } else { - return Status.NOVICE; - } + if (isSpeculator) return Status.SPECULATOR; + if (isInvestor) return Status.INVESTOR; + return Status.NOVICE; } -} +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Portfolio.java index da54723..b759c30 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Portfolio.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Portfolio.java @@ -106,6 +106,11 @@ public boolean removeShare(final Share newShare) { } } + /** + * Returns the shares. + * + * @return the value + */ public List getShares() { return shares; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Share.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Share.java index cd19f12..a080860 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Share.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Share.java @@ -69,6 +69,9 @@ public BigDecimal getPurchasePrice() { } @Override + /** + * Equals method. + */ public boolean equals(Object object) { if (this == object) { return true; @@ -83,6 +86,9 @@ public boolean equals(Object object) { } @Override + /** + * Hashcode method. + */ public int hashCode() { return Objects.hash(stock.getSymbol(), quantity, purchasePrice); } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Stock.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Stock.java index a8f4f09..243d0bc 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Stock.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Stock.java @@ -35,10 +35,20 @@ public Stock( prices.add(salesPrice); } + /** + * Returns the symbol. + * + * @return the value + */ public String getSymbol() { return symbol; } + /** + * Returns the company. + * + * @return the value + */ public String getCompany() { return company; } @@ -124,11 +134,14 @@ public StockGraph getStockGraph() { return stockGraph; } + /** + * Fakehistory method. + */ public void fakeHistory(int stockResolution) { for (int i = 0; i < stockResolution; i++) { updatePrice(); } - prices = prices.reversed(); + java.util.Collections.reverse(prices); } /** diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Purchase.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Purchase.java index e30b711..5500768 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Purchase.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Purchase.java @@ -22,6 +22,9 @@ public Purchase(final Share share, final int week) { } @Override + /** + * Commit method. + */ public void commit(final Player player) throws Exception { setCommitted(true); player.withdrawMoney(getCalculator().calculateTotal()); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Sale.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Sale.java index 426c5f7..7383179 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Sale.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Transaction/Sale.java @@ -19,6 +19,9 @@ public Sale(final Share share, final int week) { } @Override + /** + * Commit method. + */ public void commit(final Player player) { setCommitted(true); player.addMoney(getCalculator().calculateTotal()); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/FilePickerApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/FilePickerApp.java new file mode 100644 index 0000000..bf6f117 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/FilePickerApp.java @@ -0,0 +1,308 @@ +package edu.ntnu.idi.idatt2003.gruppe42.View.Apps; + +import edu.ntnu.idi.idatt2003.gruppe42.Model.App; +import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; +import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Comparator; +import java.util.function.Consumer; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * Represents a FilePickerApp class. + */ +public class FilePickerApp extends Popup { + + private static final String FILTER = ".csv"; + + private final Button backButton = new Button("←"); + private final Button forwardButton = new Button("→"); + private final Button upButton = new Button("↑"); + private final TextField addressBar = new TextField(); + + private static final File[] QUICK_ACCESS = { + new File(System.getProperty("user.home")), + new File(System.getProperty("user.home") + "/Desktop"), + new File(System.getProperty("user.home") + "/Documents"), + new File(System.getProperty("user.home") + "/Downloads"), + }; + + private final VBox fileList = new VBox(1); + + private final TextField selectedField = new TextField(); + private final Button openButton = new Button("Open"); + private final Button cancelButton = new Button("Cancel"); + private final Label filterLabel = new Label("CSV Files (*.csv)"); + + private File currentDir = new File(System.getProperty("user.home")); + private File prevDir = null; + private File selectedFile; + private Consumer onFileConfirmed; + + public FilePickerApp() { + super(580, 420, 80, 60, App.FILEPICKER); + buildLayout(); + wireToolbar(); + populateDir(currentDir); + } + + + private void buildLayout() { + backButton.getStyleClass().add("fp-toolbar-button"); + forwardButton.getStyleClass().add("fp-toolbar-button"); + upButton.getStyleClass().add("fp-toolbar-button"); + backButton.setDisable(true); + forwardButton.setDisable(true); + + addressBar.getStyleClass().add("fp-address-bar"); + addressBar.setEditable(false); + HBox.setHgrow(addressBar, Priority.ALWAYS); + + HBox toolbar = new HBox(4, backButton, forwardButton, upButton, addressBar); + toolbar.setAlignment(Pos.CENTER_LEFT); + toolbar.setPadding(new Insets(6, 8, 6, 8)); + toolbar.getStyleClass().add("fp-toolbar"); + + VBox sidebar = new VBox(2); + sidebar.getStyleClass().add("fp-sidebar"); + sidebar.setPrefWidth(130); + sidebar.setPadding(new Insets(8, 6, 8, 6)); + + Label quickLabel = new Label("QUICK ACCESS"); + quickLabel.getStyleClass().add("fp-sidebar-section"); + sidebar.getChildren().add(quickLabel); + + String[] icons = { "🏠", "🖥", "📄", "⬇" }; + String[] names = { "Home", "Desktop", "Documents", "Downloads" }; + for (int i = 0; i < QUICK_ACCESS.length; i++) { + File dir = QUICK_ACCESS[i]; + if (!dir.exists()) continue; + Button btn = new Button(icons[i] + " " + names[i]); + btn.getStyleClass().add("fp-sidebar-item"); + btn.setMaxWidth(Double.MAX_VALUE); + btn.setAlignment(Pos.CENTER_LEFT); + int idx = i; + btn.setOnAction(e -> { + prevDir = currentDir; + backButton.setDisable(false); + populateDir(QUICK_ACCESS[idx]); + }); + sidebar.getChildren().add(btn); + } + + ScrollPane scroll = new ScrollPane(fileList); + scroll.setFitToWidth(true); + scroll.getStyleClass().add("fp-scroll"); + VBox.setVgrow(scroll, Priority.ALWAYS); + + // Column header + HBox header = buildColumnHeader(); + + VBox mainArea = new VBox(0, header, scroll); + mainArea.getStyleClass().add("fp-main"); + HBox.setHgrow(mainArea, Priority.ALWAYS); + + HBox body = new HBox(0, sidebar, new Separator(), mainArea); + VBox.setVgrow(body, Priority.ALWAYS); + + selectedField.getStyleClass().add("settings-text-field"); + selectedField.setEditable(false); + selectedField.setPromptText("File name"); + HBox.setHgrow(selectedField, Priority.ALWAYS); + + filterLabel.getStyleClass().add("fp-filter-label"); + filterLabel.setPrefWidth(140); + + openButton.getStyleClass().add("primary-button"); + cancelButton.getStyleClass().add("secondary-button"); + openButton.setDisable(true); + + openButton.setOnAction(e -> confirm()); + cancelButton.setOnAction(e -> hide()); + + HBox nameRow = new HBox(8, new Label("File name:"), selectedField, filterLabel); + nameRow.setAlignment(Pos.CENTER_LEFT); + ((Label) nameRow.getChildren().get(0)).getStyleClass().add("fp-bottom-label"); + + HBox buttonRow = new HBox(6, openButton, cancelButton); + buttonRow.setAlignment(Pos.CENTER_RIGHT); + + VBox bottomBar = new VBox(6, nameRow, buttonRow); + bottomBar.getStyleClass().add("fp-bottom-bar"); + bottomBar.setPadding(new Insets(8, 10, 8, 10)); + + VBox root = new VBox(0, toolbar, body, new Separator(), bottomBar); + VBox.setVgrow(body, Priority.ALWAYS); + content.getChildren().setAll(root); + content.setPrefSize(580, 420); + } + + private HBox buildColumnHeader() { + Label nameCol = new Label("Name"); + Label typeCol = new Label("Type"); + Label sizeCol = new Label("Size"); + nameCol.getStyleClass().add("fp-col-header"); + typeCol.getStyleClass().add("fp-col-header"); + sizeCol.getStyleClass().add("fp-col-header"); + HBox.setHgrow(nameCol, Priority.ALWAYS); + typeCol.setPrefWidth(80); + sizeCol.setPrefWidth(70); + + HBox header = new HBox(0, nameCol, typeCol, sizeCol); + header.getStyleClass().add("fp-col-header-row"); + header.setPadding(new Insets(3, 8, 3, 36)); + return header; + } + + + private void wireToolbar() { + backButton.setOnAction(e -> { + if (prevDir != null) { + populateDir(prevDir); + prevDir = null; + backButton.setDisable(true); + } + }); + upButton.setOnAction(e -> { + if (currentDir.getParentFile() != null) { + prevDir = currentDir; + backButton.setDisable(false); + populateDir(currentDir.getParentFile()); + } + }); + addressBar.setOnAction(e -> { + File typed = new File(addressBar.getText()); + if (typed.isDirectory()) populateDir(typed); + }); + } + + + private void populateDir(final File dir) { + currentDir = dir; + addressBar.setText(dir.getAbsolutePath()); + upButton.setDisable(dir.getParentFile() == null); + fileList.getChildren().clear(); + selectedFile = null; + selectedField.clear(); + openButton.setDisable(true); + + File[] entries = dir.listFiles(); + if (entries == null) return; + + Arrays.stream(entries) + .filter(f -> f.isDirectory() || f.getName().endsWith(FILTER)) + .sorted(Comparator + .comparing(File::isDirectory).reversed() + .thenComparing(f -> f.getName().toLowerCase())) + .forEach(f -> fileList.getChildren().add(buildRow(f))); + } + + private HBox buildRow(final File file) { + String icon = file.isDirectory() ? "📁" : "📄"; + String type = file.isDirectory() ? "Folder" : "CSV File"; + String size = file.isDirectory() ? "" + : formatSize(file.length()); + + Label iconLabel = new Label(icon); + iconLabel.getStyleClass().add("fp-row-icon"); + iconLabel.setMinWidth(28); + + Label nameLabel = new Label(file.getName()); + nameLabel.getStyleClass().add( + file.isDirectory() ? "fp-row-dir" : "fp-row-file"); + nameLabel.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(nameLabel, Priority.ALWAYS); + + Label typeLabel = new Label(type); + typeLabel.getStyleClass().add("fp-row-meta"); + typeLabel.setPrefWidth(80); + + Label sizeLabel = new Label(size); + sizeLabel.getStyleClass().add("fp-row-meta"); + sizeLabel.setPrefWidth(70); + + HBox row = new HBox(0, iconLabel, nameLabel, typeLabel, sizeLabel); + row.setAlignment(Pos.CENTER_LEFT); + row.setPadding(new Insets(4, 8, 4, 8)); + row.getStyleClass().add("fp-row"); + + row.setOnMouseClicked(e -> { + if (e.getClickCount() == 1) { + if (file.isDirectory()) { + prevDir = currentDir; + backButton.setDisable(false); + populateDir(file); + } else { + clearSelection(); + row.getStyleClass().add("fp-row-selected"); + selectedFile = file; + selectedField.setText(file.getName()); + openButton.setDisable(false); + } + } else if (e.getClickCount() == 2 && !file.isDirectory()) { + selectedFile = file; + confirm(); + } + }); + + return row; + } + + private void confirm() { + if (selectedFile != null && onFileConfirmed != null) { + onFileConfirmed.accept(selectedFile.toPath()); + hide(); + } + } + + private void clearSelection() { + fileList.getChildren() + .forEach(n -> n.getStyleClass().remove("fp-row-selected")); + } + + private static String formatSize(final long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return (bytes / 1024) + " KB"; + return (bytes / (1024 * 1024)) + " MB"; + } + + + /** + * Sets the onfileconfirmed. + * + * @param callback the new callback + */ + public void setOnFileConfirmed(final Consumer callback) { + this.onFileConfirmed = callback; + } + + /** + * Show method. + */ + @Override + public void show() { + selectedFile = null; + selectedField.clear(); + openButton.setDisable(true); + populateDir(currentDir); + super.show(); + } + + /** + * Hide method. + */ + @Override + public void hide() { + super.hide(); + } +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/GameOverApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/GameOverApp.java index 033a7f9..64c5e2a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/GameOverApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/GameOverApp.java @@ -52,8 +52,11 @@ public Button getStartOverButton() { return startOverButton; } + /** + * Show method. + */ + @Override public void show(){ - getRoot().setVisible(true); - getRoot().toFront(); + super.show(); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/SettingsApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/SettingsApp.java index 78be2d0..ea4b10f 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/SettingsApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/SettingsApp.java @@ -3,56 +3,368 @@ import edu.ntnu.idi.idatt2003.gruppe42.Model.App; import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; import java.nio.file.Path; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.Button; +import javafx.scene.control.ColorPicker; +import javafx.scene.control.ComboBox; import javafx.scene.control.Label; -import javafx.stage.FileChooser; +import javafx.scene.control.Separator; +import javafx.scene.control.Slider; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +/** + * Settings app: allows the user to change audio volume, appearance, and profile. + */ public class SettingsApp extends Popup { - private Button openFileChooserButton; - private FileChooser fileChooser; + /** Gradient configuration produced by the appearance section. */ + public record GradientConfig(Color colorA, Color colorB, String direction) {} + + private final Button openFileChooserButton; + private final javafx.stage.FileChooser fileChooser; + private final Slider masterVolumeSlider; + private final Slider sfxVolumeSlider; + + private final ColorPicker gradientColorA; + private final ColorPicker gradientColorB; + private final ComboBox gradientDirection; + private final Button applyGradientButton; + + private final TextField playerNameField; + private final Button applyNameButton; + private final Button logoutButton; + private final Button powerOffButton; + private boolean isLoggedIn; private Path userSelectedPath; - public SettingsApp(boolean isLoggedIn) { - super(720, 480, 80, 60, App.SETTINGS); + /** + * Creates a new SettingsApp. + * + * @param isLoggedIn whether the user is logged in + */ + public SettingsApp(final boolean isLoggedIn) { + super(400, 500, 80, 400, App.SETTINGS); this.isLoggedIn = isLoggedIn; - openFileChooserButton = new Button("Import stock file"); - fileChooser = new FileChooser(); + + openFileChooserButton = new Button("Import stock file…"); + openFileChooserButton.getStyleClass().add("secondary-button"); + + fileChooser = new javafx.stage.FileChooser(); + fileChooser.setTitle("Select .csv stock file"); + fileChooser.getExtensionFilters().add( + new javafx.stage.FileChooser.ExtensionFilter("CSV files", "*.csv")); + + masterVolumeSlider = createSlider(); + sfxVolumeSlider = createSlider(); + + gradientColorA = new ColorPicker(Color.web("#3a5068")); + gradientColorB = new ColorPicker(Color.web("#1a2a3a")); + gradientDirection = new ComboBox<>(); + gradientDirection.getItems().addAll( + "Top → Bottom", + "Bottom → Top", + "Left → Right", + "Right → Left", + "Diagonal ↘", + "Diagonal ↙" + ); + gradientDirection.setValue("Top → Bottom"); + gradientDirection.getStyleClass().add("settings-combo"); + + applyGradientButton = new Button("Apply"); + applyGradientButton.getStyleClass().add("primary-button"); + + playerNameField = new TextField(); + playerNameField.setPromptText("Enter new name…"); + playerNameField.getStyleClass().add("settings-text-field"); + + applyNameButton = new Button("Apply"); + applyNameButton.getStyleClass().add("primary-button"); + + logoutButton = new Button("⇤ Log out"); + logoutButton.getStyleClass().add("settings-logout-button"); + logoutButton.setMaxWidth(Double.MAX_VALUE); + + powerOffButton = new Button("⏻ Power off"); + powerOffButton.getStyleClass().add("settings-poweroff-button"); + powerOffButton.setMaxWidth(Double.MAX_VALUE); + updateContent(); } + + /** + * Returns the button for importing a stock file. + * + * @return the Button + */ public Button getOpenFileChooserButton() { return openFileChooserButton; } - public FileChooser getFileChooser() { + /** + * Returns the file chooser for selecting a stock file. + * + * @return the FileChooser + */ + public javafx.stage.FileChooser getFileChooser() { return fileChooser; } + /** + * Returns whether the user is logged in. + * + * @return true if logged in, false otherwise + */ public boolean isLoggedIn() { return isLoggedIn; } - public void setLoggedIn(boolean status) { + /** + * Returns the master volume slider. + * + * @return the Slider + */ + public Slider getMasterVolumeSlider() { + return masterVolumeSlider; + } + + /** + * Returns the SFX volume slider. + * + * @return the Slider + */ + public Slider getSfxVolumeSlider() { + return sfxVolumeSlider; + } + + /** + * Returns the button for applying the gradient. + * + * @return the Button + */ + public Button getApplyGradientButton() { + return applyGradientButton; + } + + /** + * Returns the field for entering the player name. + * + * @return the TextField + */ + public TextField getPlayerNameField() { + return playerNameField; + } + + /** + * Returns the button for applying the player name. + * + * @return the Button + */ + public Button getApplyNameButton() { + return applyNameButton; + } + + /** + * Returns the button for logging out. + * + * @return the Button + */ + public Button getLogoutButton() { + return logoutButton; + } + + /** + * Returns the button for powering off. + * + * @return the Button + */ + public Button getPowerOffButton() { + return powerOffButton; + } + + /** Returns the current gradient config as chosen in the UI. */ + public GradientConfig getGradientConfig() { + return new GradientConfig( + gradientColorA.getValue(), + gradientColorB.getValue(), + gradientDirection.getValue() + ); + } + + /** + * Sets the login status and updates the view. + * + * @param status the new login status + */ + public void setLoggedIn(final boolean status) { this.isLoggedIn = status; updateContent(); } + /** + * Sets the path of the selected stock file and updates the view. + * + * @param path the new path + */ + public void setPath(final Path path) { + this.userSelectedPath = path; + updateContent(); + } + + + /** + * Rebuilds the content of the settings app based on current state. + */ public void updateContent() { + VBox layout = new VBox(16); + layout.setPadding(new Insets(24, 28, 24, 28)); + layout.getStyleClass().add("settings-layout"); + + layout.getChildren().add(sectionTitle("Audio")); + layout.getChildren().add(sliderRow("Master volume", masterVolumeSlider)); + layout.getChildren().add(sliderRow("SFX volume", sfxVolumeSlider)); + if (!isLoggedIn) { - fileChooser.setTitle("Select .csv stock file"); - fileChooser.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("CSV files","*.csv")); - Label currentPathLabel = new Label("Current path: " + userSelectedPath); - content.getChildren().setAll(openFileChooserButton, currentPathLabel); + layout.getChildren().add(new Separator()); + layout.getChildren().add(sectionTitle("Data")); + + String pathText = userSelectedPath != null + ? userSelectedPath.getFileName().toString() + : "No file selected"; + Label pathLabel = new Label(pathText); + pathLabel.getStyleClass().add("settings-path-label"); + layout.getChildren().add(new VBox(8, openFileChooserButton, pathLabel)); + } else { - //TODO: settings in desktopView - content.getChildren().setAll(); + layout.getChildren().add(new Separator()); + layout.getChildren().add(sectionTitle("Appearance")); + layout.getChildren().add(buildGradientSection()); + + layout.getChildren().add(new Separator()); + layout.getChildren().add(sectionTitle("Profile")); + HBox nameRow = new HBox(8, playerNameField, applyNameButton); + HBox.setHgrow(playerNameField, Priority.ALWAYS); + nameRow.setAlignment(Pos.CENTER_LEFT); + layout.getChildren().add(labeledRow("Player name", nameRow)); + + layout.getChildren().add(new Separator()); + layout.getChildren().add(sectionTitle("Session")); + HBox sessionRow = new HBox(10, logoutButton, powerOffButton); + HBox.setHgrow(logoutButton, Priority.ALWAYS); + HBox.setHgrow(powerOffButton, Priority.ALWAYS); + layout.getChildren().add(sessionRow); } + + content.getChildren().setAll(layout); } - public void setPath(Path path) { - userSelectedPath = path; - updateContent(); + + private VBox buildGradientSection() { + Region preview = new Region(); + preview.setPrefHeight(24); + preview.setMaxWidth(Double.MAX_VALUE); + preview.getStyleClass().add("settings-gradient-preview"); + preview.setStyle(buildCssGradient( + gradientColorA.getValue(), + gradientColorB.getValue(), + gradientDirection.getValue())); + + Runnable refreshPreview = () -> preview.setStyle(buildCssGradient( + gradientColorA.getValue(), + gradientColorB.getValue(), + gradientDirection.getValue())); + + gradientColorA.setOnAction(e -> refreshPreview.run()); + gradientColorB.setOnAction(e -> refreshPreview.run()); + gradientDirection.setOnAction(e -> refreshPreview.run()); + + gradientColorA.setMaxWidth(Double.MAX_VALUE); + gradientColorB.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(gradientColorA, Priority.ALWAYS); + HBox.setHgrow(gradientColorB, Priority.ALWAYS); + + Label fromLabel = new Label("From"); + Label toLabel = new Label("To"); + fromLabel.getStyleClass().add("settings-row-label"); + toLabel.getStyleClass().add("settings-row-label"); + + HBox colorRow = new HBox(8, fromLabel, gradientColorA, toLabel, gradientColorB); + colorRow.setAlignment(Pos.CENTER_LEFT); + + gradientDirection.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(gradientDirection, Priority.ALWAYS); + + HBox controlRow = new HBox(8, gradientDirection, applyGradientButton); + controlRow.setAlignment(Pos.CENTER_LEFT); + + VBox box = new VBox(8, colorRow, controlRow, preview); + return box; + } + + /** Converts a direction label to a CSS linear-gradient string. */ + public static String buildCssGradient( + final Color a, final Color b, final String direction) { + + String angle = switch (direction) { + case "Bottom → Top" -> "to top"; + case "Left → Right" -> "to right"; + case "Right → Left" -> "to left"; + case "Diagonal ↘" -> "to bottom right"; + case "Diagonal ↙" -> "to bottom left"; + default -> "to bottom"; // "Top → Bottom" + }; + return String.format( + "-fx-background-color: linear-gradient(%s, %s, %s);", + angle, toHex(a), toHex(b)); + } + + private static String toHex(final Color c) { + return String.format("#%02x%02x%02x", + (int) (c.getRed() * 255), + (int) (c.getGreen() * 255), + (int) (c.getBlue() * 255)); + } + + + private static Slider createSlider() { + Slider s = new Slider(0, 100, 80); + s.getStyleClass().add("settings-slider"); + HBox.setHgrow(s, Priority.ALWAYS); + return s; + } + + private static Label sectionTitle(final String text) { + Label l = new Label(text.toUpperCase()); + l.getStyleClass().add("settings-section-title"); + return l; + } + + private static HBox sliderRow(final String labelText, final Slider slider) { + Label valueLabel = new Label("80%"); + valueLabel.getStyleClass().add("settings-value-label"); + valueLabel.setMinWidth(36); + slider.valueProperty().addListener( + (obs, ov, nv) -> valueLabel.setText((int) nv.doubleValue() + "%")); + return labeledRow(labelText, slider, valueLabel); + } + + private static HBox labeledRow(final String labelText, final Node... nodes) { + Label label = new Label(labelText); + label.getStyleClass().add("settings-row-label"); + label.setMinWidth(120); + + HBox row = new HBox(10); + row.setAlignment(Pos.CENTER_LEFT); + row.getChildren().add(label); + row.getChildren().addAll(nodes); + return row; } -} +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/StockApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/StockApp.java index 6bc423f..ac89570 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/StockApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/StockApp.java @@ -30,6 +30,7 @@ public class StockApp extends Popup { private Label priceLabel; private final Spinner quantitySpinner; private final Button confirmButton; + private final Button backButton; private final Receipt receipt; /** @@ -63,6 +64,9 @@ public StockApp(final Player player) { this.confirmButton.getStyleClass().add("primary-button"); this.confirmButton.setPrefHeight(35); + this.backButton = new Button("← Back"); + this.backButton.getStyleClass().add("back-button"); + this.receipt = new Receipt(player); showSearchPage(); @@ -85,10 +89,6 @@ public void showSearchPage() { public void showStockPage(final Stock stock) { content.getChildren().clear(); - Button backButton = new Button("← Back"); - backButton.getStyleClass().add("back-button"); - backButton.setOnAction(e -> showSearchPage()); - HBox topControls = new HBox(10); topControls.setAlignment(Pos.CENTER_LEFT); HBox.setHgrow(searchField, Priority.ALWAYS); @@ -183,4 +183,13 @@ public Button getConfirmButton() { public Receipt getReceipt() { return receipt; } + + /** + * Returns the back button. + * + * @return the back button + */ + public Button getBackButton() { + return backButton; + } } \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WarningApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WarningApp.java index 153930c..7195ad1 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WarningApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WarningApp.java @@ -124,9 +124,12 @@ public void configure( getCloseButton().setOnAction(event -> secondaryButton.fire()); } + /** + * Show method. + */ + @Override public void show() { - getRoot().setVisible(true); - getRoot().toFront(); + super.show(); } /** The right-side action button (always visible). Wire with {@code setOnAction}. */ diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WeekendRapportApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WeekendRapportApp.java index 89dc2f5..1485e07 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WeekendRapportApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WeekendRapportApp.java @@ -181,6 +181,11 @@ public void populate( ); } + /** + * Returns the continuebutton. + * + * @return the value + */ public Button getContinueButton() { return continueButton; } @@ -245,8 +250,11 @@ static String formatBalance(BigDecimal amount) { return "$" + String.format("%,.2f", amount); } + /** + * Show method. + */ + @Override public void show(){ - getRoot().setVisible(true); - getRoot().toFront(); + super.show(); } } \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java index e2a19b3..680339e 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java @@ -1,6 +1,9 @@ package edu.ntnu.idi.idatt2003.gruppe42.View; import edu.ntnu.idi.idatt2003.gruppe42.Model.App; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -108,20 +111,40 @@ private Rectangle buildBorderOverlay() { private void loadStylesheets() { root.getStylesheets().clear(); + List sheets = new ArrayList<>(); + sheets.add("/css/global.css"); + sheets.add("/css/popup.css"); + sheets.add("/css/components.css"); + + // Map App to CSS + String appCss = switch (type) { + case BANK -> "/css/bank.css"; + case SETTINGS -> "/css/settings.css"; + case STOCK -> "/css/stock.css"; + case FILEPICKER -> "/css/filepicker.css"; + case WEEKENDRAPPORT -> "/css/rapport.css"; + case WARNING, GAMEOVER -> "/css/warning.css"; + default -> null; + }; + if (appCss != null) { + sheets.add(appCss); + } + + // Some apps need receipt.css + if (type == App.STOCK || type == App.WEEKENDRAPPORT) { + sheets.add("/css/receipt.css"); + } + try { if (DEV_CSS_RELOAD) { String projectDir = System.getProperty("user.dir"); - root.getStylesheets().addAll( - devCopyToTemp(projectDir + "/src/main/resources/css/global.css"), - devCopyToTemp(projectDir + "/src/main/resources/css/popup.css"), - devCopyToTemp(projectDir + "/src/main/resources/css/apps.css") - ); + for (String s : sheets) { + root.getStylesheets().add(devCopyToTemp(projectDir + "/src/main/resources" + s)); + } } else { - root.getStylesheets().addAll( - resource("/css/global.css"), - resource("/css/popup.css"), - resource("/css/apps.css") - ); + for (String s : sheets) { + root.getStylesheets().add(resource(s)); + } } } catch (Exception e) { System.err.println("[CSS] Failed to load popup stylesheets: " + e.getMessage()); @@ -161,7 +184,7 @@ private HBox buildHeader(Button closeButton, String title) { } private String resource(final String path) { - return getClass().getResource(path).toExternalForm(); + return Objects.requireNonNull(getClass().getResource(path)).toExternalForm(); } /** @@ -181,18 +204,53 @@ public void centerInParent() { wrapper.parentProperty().addListener((obs, oldParent, newParent) -> bind.run()); } + /** + * Shows the popup by setting its visibility to true and bringing it to the front. + */ + public void show() { + wrapper.setVisible(true); + wrapper.toFront(); + } + + /** + * Hides the popup by setting its visibility to false. + */ + public void hide() { + wrapper.setVisible(false); + } + + /** + * Returns the type. + * + * @return the value + */ public App getType() { return type; } + /** + * Returns the root. + * + * @return the value + */ public StackPane getRoot() { return wrapper; } + /** + * Returns the header. + * + * @return the value + */ public HBox getHeader() { return header; } + /** + * Returns the closebutton. + * + * @return the value + */ public Button getCloseButton() { return closeButton; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/StockGraph.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/StockGraph.java index a14a4e5..15693f8 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/StockGraph.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/StockGraph.java @@ -77,10 +77,20 @@ public void update(final Stock stock) { }); } + /** + * Returns the visibility. + * + * @return the value + */ public boolean getVisibility() { return isVisible; } + /** + * Sets the visibility. + * + * @param value the new value + */ public void setVisibility(final boolean visible) { isVisible = visible; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java index 63c2109..9d8cefc 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java @@ -30,19 +30,14 @@ import javafx.scene.layout.StackPane; /** - * Desktop view: 10x6 app-icon grid, bottom status bar, and a layered - * modal overlay system. + * Desktop view: 10x6 app-icon grid, bottom status bar, and a layered modal overlay system. */ public final class DesktopView { - // ── DEV FLAG ───────────────────────────────────────────────────────────── - // Set to false (or delete this + all DEV_CSS_RELOAD blocks) before release. private static final boolean DEV_CSS_RELOAD = true; - // ───────────────────────────────────────────────────────────────────────── /** - * Z-stack of modal levels, ordered from lowest to highest. Each value - * is its own named token; no numeric z-index lives anywhere else. + * The different modal layers that can be active on the desktop. */ public enum ModalLayer { RAPPORT, WARNING, GAME_OVER } @@ -69,15 +64,30 @@ public enum ModalLayer { RAPPORT, WARNING, GAME_OVER } private final EnumSet activeLayers = EnumSet.noneOf(ModalLayer.class); + private Label weatherLabel; + private Label tempLabel; + private Label dayLabel; + private Label weekLabel; + private Label clockLabel; + private Label playerLabel; private Button nextWeekButton; private Button settingsButton; + /** + * Creates a new DesktopView with the given controller. + * + * @param controller the controller for this view + */ public DesktopView(final DesktopViewController controller) { this.controller = controller; this.root = new BorderPane(); } - /** Builds and returns the root node (lazy, built only on first call). */ + /** + * Returns the root node of this view. + * + * @return the root BorderPane + */ public BorderPane getRoot() { if (root.getCenter() == null) { gridArea = createGrid(); @@ -88,7 +98,6 @@ public BorderPane getRoot() { loadStylesheets(); - // DEV_CSS_RELOAD: press R to hot-reload CSS from source without restarting if (DEV_CSS_RELOAD) { root.setFocusTraversable(true); root.sceneProperty().addListener((obs, oldScene, newScene) -> { @@ -109,18 +118,31 @@ private void loadStylesheets() { root.getStylesheets().clear(); try { if (DEV_CSS_RELOAD) { - // DEV_CSS_RELOAD: reads directly from source, copies to temp to bust JavaFX cache String projectDir = System.getProperty("user.dir"); root.getStylesheets().addAll( devCopyToTemp(projectDir + "/src/main/resources/css/global.css"), devCopyToTemp(projectDir + "/src/main/resources/css/desktop.css"), - devCopyToTemp(projectDir + "/src/main/resources/css/apps.css") + devCopyToTemp(projectDir + "/src/main/resources/css/components.css"), + devCopyToTemp(projectDir + "/src/main/resources/css/stock.css"), + devCopyToTemp(projectDir + "/src/main/resources/css/bank.css"), + devCopyToTemp(projectDir + "/src/main/resources/css/receipt.css"), + devCopyToTemp(projectDir + "/src/main/resources/css/rapport.css"), + devCopyToTemp(projectDir + "/src/main/resources/css/filepicker.css"), + devCopyToTemp(projectDir + "/src/main/resources/css/settings.css"), + devCopyToTemp(projectDir + "/src/main/resources/css/warning.css") ); } else { root.getStylesheets().addAll( resource("/css/global.css"), resource("/css/desktop.css"), - resource("/css/apps.css") + resource("/css/components.css"), + resource("/css/stock.css"), + resource("/css/bank.css"), + resource("/css/receipt.css"), + resource("/css/rapport.css"), + resource("/css/filepicker.css"), + resource("/css/settings.css"), + resource("/css/warning.css") ); } } catch (Exception e) { @@ -128,7 +150,6 @@ private void loadStylesheets() { } } - // DEV_CSS_RELOAD: copies a CSS file to a unique temp path so JavaFX can't cache it private String devCopyToTemp(final String sourcePath) throws Exception { Path src = Paths.get(sourcePath); Path tmp = Files.createTempFile("css_", ".css"); @@ -137,18 +158,21 @@ private String devCopyToTemp(final String sourcePath) throws Exception { return tmp.toUri().toString(); } - // Layer registration: the controller adds nodes to the scene graph in - // the exact z-order required for the layering rules to work. - /** Registers a non-modal popup (Stock, Bank, etc.) for blur tracking. */ + /** + * Registers a popup root to be managed by this view. + * + * @param popupRoot the root node of the popup + */ public void registerPopup(final Node popupRoot) { popupRoots.add(popupRoot); } /** - * Records the modal root for the given layer so the layer manager can - * blur it when a higher modal is active. Does NOT add it to the scene - * graph; the caller is responsible for that. + * Registers a modal root for a specific layer. + * + * @param layer the layer to register for + * @param modalRoot the root node of the modal */ public void registerModal(final ModalLayer layer, final Node modalRoot) { switch (layer) { @@ -158,7 +182,12 @@ public void registerModal(final ModalLayer layer, final Node modalRoot) { } } - /** Returns the overlay {@code Pane} for the given layer (for click wiring). */ + /** + * Returns the overlay pane for a specific layer. + * + * @param layer the layer to get the overlay for + * @return the overlay Pane + */ public Pane getOverlay(final ModalLayer layer) { return switch (layer) { case RAPPORT -> rapportOverlay; @@ -167,20 +196,26 @@ public Pane getOverlay(final ModalLayer layer) { }; } - /** Marks the given layer as active and re-applies blur / disable state. */ + /** + * Activates a modal layer. + * + * @param layer the layer to activate + */ public void enterLayer(final ModalLayer layer) { activeLayers.add(layer); recompute(); } - /** Marks the given layer as inactive and re-applies blur / disable state. */ + /** + * Deactivates a modal layer. + * + * @param layer the layer to deactivate + */ public void exitLayer(final ModalLayer layer) { activeLayers.remove(layer); recompute(); } - // Single source of truth: derive overlay visibility, blur, and disable - // state from the active-layer set. private void recompute() { ModalLayer top = topmostActive(); @@ -191,6 +226,7 @@ private void recompute() { boolean anyModal = top != null; setLocked(gridArea, anyModal); setLocked(bottomBar, anyModal); + for (Node p : popupRoots) { if (p instanceof StackPane wrapper && !wrapper.getChildren().isEmpty()) { Node content = wrapper.getChildren().get(0); @@ -204,17 +240,14 @@ private void recompute() { } if (rapportRoot != null) { - boolean rapportLocked = activeLayers.contains(ModalLayer.RAPPORT) - && top != ModalLayer.RAPPORT; - setLocked(rapportRoot, rapportLocked); + setLocked(rapportRoot, + activeLayers.contains(ModalLayer.RAPPORT) && top != ModalLayer.RAPPORT); } if (warningRoot != null) { - boolean warningLocked = activeLayers.contains(ModalLayer.WARNING) - && top != ModalLayer.WARNING; - setLocked(warningRoot, warningLocked); + setLocked(warningRoot, + activeLayers.contains(ModalLayer.WARNING) && top != ModalLayer.WARNING); } if (gameOverRoot != null) { - // Game Over is always topmost when active, never locked. setLocked(gameOverRoot, false); } } @@ -232,10 +265,6 @@ private ModalLayer topmostActive() { return null; } - /** - * Applies (or removes) the blur effect AND the disable flag together, - * so blur and pointer-events are never out of sync. - */ private static void setLocked(final Node node, final boolean locked) { if (node == null) { return; @@ -249,14 +278,12 @@ private static Pane createOverlayPane() { pane.setManaged(false); pane.setLayoutX(0); pane.setLayoutY(0); - // Big enough to cover any viewport; the dim background blocks clicks. pane.setPrefSize(8000, 8000); pane.getStyleClass().add("modal-overlay"); pane.setVisible(false); return pane; } - // Grid private GridPane createGrid() { GridPane grid = new GridPane(); @@ -289,22 +316,22 @@ private GridPane createGrid() { return grid; } - /** Returns true for apps that should appear as icons on the desktop grid. */ private static boolean isDesktopApp(final App app) { return app != App.WEEKENDRAPPORT && app != App.WARNING - && app != App.GAMEOVER; + && app != App.GAMEOVER + && app != App.SETTINGS + && app != App.FILEPICKER; } - // Bottom bar private HBox createBottomBar() { settingsButton = new Button("⚙"); settingsButton.getStyleClass().add("desktop-settings-button"); - settingsButton.setOnAction(e -> controller.handleSettings()); Player player = controller.getPlayer(); - Label playerLabel = new Label("User: " + player.getName()); + + playerLabel = new Label("User: " + player.getName()); playerLabel.getStyleClass().add("desktop-label-bold"); playerLabel.setMinWidth(Region.USE_PREF_SIZE); @@ -329,9 +356,6 @@ private HBox createBottomBar() { return bar; } - /** - * Builds the hearts display as an HBox of three filled-heart labels. - */ private HBox buildHeartsBox(final Player player) { HBox box = new HBox(4); box.setAlignment(Pos.CENTER_LEFT); @@ -358,48 +382,20 @@ private HBox buildHeartsBox(final Player player) { return box; } - // Status box (clock / weather) private HBox createStatusBox() { - TimeAndWeatherController twc = controller.getTimeAndWeatherController(); - - Label weather = styledLabel("desktop-label"); - Label temp = styledLabel("desktop-label"); - Label day = styledLabel("desktop-label-bold"); - Label week = styledLabel("desktop-label-bold"); - Label clock = styledLabel("desktop-label-bold"); - - weather.setText(twc.weatherProperty().get()); - temp.setText(twc.temperatureProperty().get() + "°C"); - day.setText(twc.getDayOfWeekString()); - week.setText(twc.getWeekString()); - clock.setText(twc.getTimeString()); - - twc.hourProperty().addListener((o, ov, nv) - -> Platform.runLater(() -> clock.setText(twc.getTimeString()))); - twc.dayIndexProperty() - .addListener( - (o, ov, nv) -> - Platform.runLater( - () -> { - String dayName = twc.getDayOfWeekString(); - day.setText(dayName); - nextWeekButton.setVisible(dayName.equals("SUN")); - })); - twc.weekIndexProperty().addListener((o, ov, nv) - -> Platform.runLater(() -> week.setText(twc.getWeekString()))); - twc.weatherProperty().addListener((o, ov, nv) - -> Platform.runLater(() -> weather.setText(nv))); - twc.temperatureProperty().addListener((o, ov, nv) - -> Platform.runLater(() -> temp.setText(nv + "°C"))); - - HBox box = new HBox(10, weather, temp, clock, day, week); + weatherLabel = styledLabel("desktop-label"); + tempLabel = styledLabel("desktop-label"); + dayLabel = styledLabel("desktop-label-bold"); + weekLabel = styledLabel("desktop-label-bold"); + clockLabel = styledLabel("desktop-label-bold"); + + HBox box = new HBox(10, weatherLabel, tempLabel, clockLabel, dayLabel, weekLabel); box.setAlignment(Pos.CENTER_RIGHT); box.setMinWidth(Region.USE_PREF_SIZE); return box; } - // Helpers private Label styledLabel(final String styleClass) { Label l = new Label(); @@ -411,10 +407,78 @@ private String resource(final String path) { return getClass().getResource(path).toExternalForm(); } + /** + * Updates the player name in the UI. + * + * @param name the new player name + */ + public void updatePlayerName(final String name) { + Platform.runLater(() -> playerLabel.setText("User: " + name)); + } + + /** + * Updates the clock in the UI. + * + * @param time the new time string + */ + public void updateClock(final String time) { + Platform.runLater(() -> clockLabel.setText(time)); + } + + /** + * Updates the day in the UI. + * + * @param day the new day string + * @param isSunday whether the new day is Sunday + */ + public void updateDay(final String day, final boolean isSunday) { + Platform.runLater(() -> { + dayLabel.setText(day); + nextWeekButton.setVisible(isSunday); + }); + } + + /** + * Updates the week in the UI. + * + * @param week the new week string + */ + public void updateWeek(final String week) { + Platform.runLater(() -> weekLabel.setText(week)); + } + + /** + * Updates the weather in the UI. + * + * @param weather the new weather description + */ + public void updateWeather(final String weather) { + Platform.runLater(() -> weatherLabel.setText(weather)); + } + + /** + * Updates the temperature in the UI. + * + * @param temperature the new temperature + */ + public void updateTemperature(final String temperature) { + Platform.runLater(() -> tempLabel.setText(temperature + "°C")); + } + + /** + * Returns the nextweekbutton. + * + * @return the value + */ public Button getNextWeekButton() { return nextWeekButton; } + /** + * Returns the settingsbutton. + * + * @return the value + */ public Button getSettingsButton() { return settingsButton; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/StartView.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/StartView.java index 527c5e0..d5f6125 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/StartView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/StartView.java @@ -12,12 +12,13 @@ import java.util.Objects; +/** + * Represents a StartView class. + */ public class StartView { - // ── DEV FLAG ───────────────────────────────────────────────────────────── // Set to false (or delete this + all DEV_CSS_RELOAD blocks) before release. private static final boolean DEV_CSS_RELOAD = true; - // ───────────────────────────────────────────────────────────────────────── private final TextField usernameField = new TextField(); private final Button loginButton = new Button("→"); @@ -27,6 +28,11 @@ public class StartView { private final BorderPane root = new BorderPane(); + /** + * Returns the root. + * + * @return the value + */ public BorderPane getRoot() { if (!root.getChildren().isEmpty()) { return root; // already built @@ -143,6 +149,11 @@ private String copyToTemp(String sourcePath) throws Exception { return tmp.toUri().toString(); } + /** + * Returns the usernamefield. + * + * @return the value + */ public TextField getUsernameField() { return usernameField; } @@ -159,10 +170,20 @@ public String getSelectedMode() { return selectedMode.getText(); } + /** + * Returns the loginbutton. + * + * @return the value + */ public Button getLoginButton() { return loginButton; } + /** + * Returns the settingsbutton. + * + * @return the value + */ public Button getSettingsButton() { return settingsButton; } diff --git a/src/main/resources/css/apps.css b/src/main/resources/css/apps.css deleted file mode 100644 index 8a54649..0000000 --- a/src/main/resources/css/apps.css +++ /dev/null @@ -1,412 +0,0 @@ - -.primary-button { - -fx-background-color: -primary; - -fx-text-fill: white; - -fx-font-weight: bold; - -fx-background-radius: 10; - -fx-padding: 8 20 8 20; - -fx-cursor: hand; -} - -.primary-button:hover { - -fx-background-color: -primary-hover; -} - -.back-button { - -fx-background-color: transparent; - -fx-text-fill: -primary; - -fx-cursor: hand; -} - -.trade-button-weekend { - -fx-cursor: default; -} - -.stock-list-item { - -fx-border-color: #f0f0f0; - -fx-border-width: 0 0 1 0; - -fx-cursor: hand; - -fx-background-color: transparent; -} - -.stock-symbol { - -fx-font-weight: bold; - -fx-font-size: 14px; -} - -.stock-company { - -fx-font-size: 14px; - -fx-text-fill: -text-muted; -} - -.stock-price { - -fx-font-weight: bold; - -fx-font-size: 14px; -} - -.stock-price-change { - -fx-font-size: 12px; -} - -.stock-price-positive { - -fx-text-fill: -success; -} - -.stock-price-negative { - -fx-text-fill: -danger; -} - -.stock-title { - -fx-font-weight: bold; - -fx-font-size: 24px; - -fx-text-fill: -text-strong; -} - -.stock-symbol-large { - -fx-text-fill: -text-muted; -} - -.search-field { - -fx-background-color: -surface; - -fx-background-radius: 15; - -fx-border-color: -popup-border; - -fx-border-radius: 15; - -fx-padding: 5 15 5 15; -} - -.stock-price-large { - -fx-font-weight: bold; - -fx-font-size: 24px; -} - -.stock-quantity-spinner { - -fx-background-radius: 5; - -fx-background-color: -surface; - -fx-border-color: -popup-border; - -fx-border-radius: 5; -} - -.stock-quantity-spinner .text-field { - -fx-background-color: transparent; - -fx-alignment: center; - -fx-font-size: 18px; -} - -.stock-price-change-large { - -fx-font-size: 18px; -} - -.trade-panel { - -fx-background-color: -surface-soft; - -fx-background-radius: 10; -} - -.stock-graph-container { - -fx-padding: 10; - -fx-background-color: -surface; -} - -.stock-area-chart { - -fx-create-symbols: false; - -fx-horizontal-grid-lines-visible: false; - -fx-vertical-grid-lines-visible: false; -} - -.stock-area-chart .chart-series-area-line { - -fx-stroke: #004e92; - -fx-stroke-width: 2px; -} - -.stock-area-chart .chart-series-area-fill { - -fx-fill: linear-gradient(to bottom, #004e9280, transparent 80%); -} - -.stock-area-chart .chart-plot-background { - -fx-background-color: transparent; -} - -.stock-area-chart .axis { - -fx-tick-label-fill: -text-muted; - -fx-tick-length: 5; -} - -.bank-summary-card { - -fx-background-color: linear-gradient(to bottom right, #004e92, #000428); - -fx-background-radius: 25; - -fx-padding: 25; - -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 5); -} - -.bank-summary-title { - -fx-font-weight: bold; - -fx-font-size: 14px; - -fx-text-fill: rgba(255, 255, 255, 0.8); - -fx-text-transform: uppercase; -} - -.bank-summary-label-bold { - -fx-font-weight: bold; - -fx-font-size: 32px; - -fx-text-fill: white; -} - -.bank-summary-label-normal { - -fx-font-size: 18px; - -fx-text-fill: white; - -fx-font-weight: bold; -} - -.bank-summary-item-title { - -fx-font-size: 12px; - -fx-text-fill: rgba(255, 255, 255, 0.7); -} - -.bank-portfolio-title { - -fx-font-weight: bold; - -fx-font-size: 18px; - -fx-padding: 10 0 5 0; -} - -.portfolio-list, .stock-list { - -fx-background-color: -surface; - -fx-background-radius: 10; - -fx-border-color: #e0e0e0; - -fx-border-radius: 10; - -fx-padding: 5; -} - -.portfolio-list:focused, .stock-list:focused { - -fx-focus-color: transparent; - -fx-faint-focus-color: transparent; -} - -.portfolio-list .list-cell, .stock-list .list-cell { - -fx-background-color: transparent; -} - -.portfolio-list .list-cell:hover, .stock-list .list-cell:hover { - -fx-cursor: hand; -} - -.bank-share-quantity, .bank-share-value, .bank-share-gain { - -fx-font-weight: bold; - -fx-font-size: 14px; -} - -.bank-share-avg-price, .bank-share-current-price { - -fx-font-size: 12px; - -fx-text-fill: -text-muted; -} - -.receipt-container { - -fx-background-color: -surface; - -fx-border-color: -popup-border; - -fx-border-style: dashed; - -fx-padding: 20; - -fx-background-radius: 4; - -fx-border-radius: 4; - -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.08), 4, 0, 0, 1); -} - -.receipt-text { - -fx-font-family: "Courier New", monospace; - -fx-font-size: 12px; - -fx-text-fill: -text-base; -} - -.receipt-divider { - -fx-pref-height: 1; - -fx-min-height: 1; - -fx-max-height: 1; - -fx-background-color: -text-muted; - -fx-opacity: 0.35; -} - -.receipt-divider-strong { - -fx-pref-height: 1; - -fx-min-height: 1; - -fx-max-height: 1; - -fx-background-color: -text-base; - -fx-opacity: 0.7; -} - -.receipt-header { - -fx-font-family: "Courier New", monospace; - -fx-font-weight: bold; - -fx-font-size: 14px; - -fx-alignment: center; - -fx-text-fill: -text-strong; -} - -.receipt-total { - -fx-font-weight: bold; - -fx-border-color: -text-base transparent transparent transparent; - -fx-border-width: 1 0 0 0; -} - -.receipt-total .label { - -fx-font-family: "Courier New", monospace; - -fx-font-weight: bold; - -fx-font-size: 12px; -} - -.rapport-section-header { - -fx-font-weight: bold; - -fx-font-size: 11px; - -fx-text-fill: -text-faint; - -fx-text-transform: uppercase; - -fx-letter-spacing: 1; -} - -.rapport-hero-block { - -fx-background-color: -surface-soft; - -fx-background-radius: 12; - -fx-border-color: -surface-line; - -fx-border-radius: 12; - -fx-border-width: 1; -} - -.rapport-hero-amount { - -fx-font-size: 44px; - -fx-font-weight: bold; - -fx-text-fill: -text-strong; -} - -.rapport-hero-caption { - -fx-font-size: 12px; - -fx-text-fill: -text-muted; - -fx-text-transform: uppercase; - -fx-letter-spacing: 1; -} - -.rapport-verdict { - -fx-font-size: 14px; - -fx-font-weight: bold; - -fx-padding: 4 14 4 14; - -fx-background-radius: 12; -} - -.mood-positive { - -fx-text-fill: -success; -} - -.mood-positive.rapport-verdict { - -fx-background-color: rgba(52, 199, 89, 0.15); - -fx-text-fill: derive(-success, -20%); -} - -.mood-negative { - -fx-text-fill: -danger; -} - -.mood-negative.rapport-verdict { - -fx-background-color: -danger-soft; - -fx-text-fill: derive(-danger, -20%); -} - -.mood-neutral { - -fx-text-fill: -text-base; -} - -.mood-neutral.rapport-verdict { - -fx-background-color: -surface-line; - -fx-text-fill: -text-base; -} - -.rapport-line-key { - -fx-font-size: 13px; - -fx-text-fill: -text-muted; -} - -.rapport-line-value { - -fx-font-size: 14px; - -fx-font-weight: bold; - -fx-text-fill: -text-base; -} - -.rapport-rent-row { - -fx-padding: 0 0 4 0; -} - -.rapport-rent-value { - -fx-text-fill: -danger; -} - -.rapport-balance-header { - -fx-font-weight: bold; - -fx-font-size: 16px; - -fx-text-fill: -text-strong; -} - -.rapport-balance-value { - -fx-font-weight: bold; - -fx-font-size: 22px; -} - -.rapport-rule { - -fx-background-color: -surface-line; -} - -/* Scrollable receipt list */ -.rapport-scroll { - -fx-background-color: -surface-soft; - -fx-background-radius: 10; - -fx-border-color: -surface-line; - -fx-border-radius: 10; -} - -.rapport-scroll > .viewport { - -fx-background-color: transparent; -} - -.warning-title { - -fx-font-weight: bold; - -fx-font-size: 17px; - -fx-text-fill: -text-strong; - -fx-text-alignment: center; -} - -.warning-message { - -fx-font-size: 13px; - -fx-text-fill: -text-base; - -fx-text-alignment: center; - -fx-line-spacing: 2; -} - -.warning-scroll, -.warning-scroll .viewport, -.warning-scroll .scroll-pane { - -fx-background-color: transparent; - -fx-border-color: transparent; -} -.game-over-title { - -fx-font-weight: bold; - -fx-font-size: 36px; - -fx-text-fill: -text-strong; -} - -.game-over-subtitle { - -fx-font-size: 14px; - -fx-text-fill: -text-muted; - -fx-text-alignment: center; -} - -.modal-overlay { - -fx-background-color: -overlay; -} - -.hearts-display { - -fx-padding: 0 4 0 4; -} - -.heart-icon { - -fx-font-size: 22px; -} - -.heart-active { - -fx-text-fill: -accent; -} - -.heart-empty { - -fx-text-fill: rgba(255, 255, 255, 0.22); -} diff --git a/src/main/resources/css/bank.css b/src/main/resources/css/bank.css new file mode 100644 index 0000000..2602b04 --- /dev/null +++ b/src/main/resources/css/bank.css @@ -0,0 +1,68 @@ +.bank-summary-card { + -fx-background-color: linear-gradient(to bottom right, #004e92, #000428); + -fx-background-radius: 25; + -fx-padding: 25; + -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 5); +} + +.bank-summary-title { + -fx-font-weight: bold; + -fx-font-size: 14px; + -fx-text-fill: rgba(255, 255, 255, 0.8); + -fx-text-transform: uppercase; + -fx-letter-spacing: 1; +} + +.bank-summary-label-bold { + -fx-font-weight: bold; + -fx-font-size: 32px; + -fx-text-fill: white; +} + +.bank-summary-label-normal { + -fx-font-size: 18px; + -fx-text-fill: white; + -fx-font-weight: bold; +} + +.bank-summary-item-title { + -fx-font-size: 12px; + -fx-text-fill: rgba(255, 255, 255, 0.7); +} + +.bank-portfolio-title { + -fx-font-weight: bold; + -fx-font-size: 18px; + -fx-padding: 10 0 5 0; +} + +.portfolio-list, .stock-list { + -fx-background-color: -surface; + -fx-background-radius: 10; + -fx-border-color: #e0e0e0; + -fx-border-radius: 10; + -fx-padding: 5; +} + +.portfolio-list:focused, .stock-list:focused { + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; +} + +.portfolio-list .list-cell, .stock-list .list-cell { + -fx-background-color: transparent; +} + +.portfolio-list .list-cell:hover, .stock-list .list-cell:hover { + -fx-cursor: hand; +} + +.bank-share-quantity, .bank-share-value, .bank-share-gain { + -fx-font-weight: bold; + -fx-font-size: 14px; +} + +.bank-share-avg-price, .bank-share-current-price { + -fx-font-size: 12px; + -fx-text-fill: -text-muted; +} \ No newline at end of file diff --git a/src/main/resources/css/components.css b/src/main/resources/css/components.css new file mode 100644 index 0000000..ec20997 --- /dev/null +++ b/src/main/resources/css/components.css @@ -0,0 +1,50 @@ +.primary-button { + -fx-background-color: -primary; + -fx-text-fill: white; + -fx-font-weight: bold; + -fx-background-radius: 10; + -fx-padding: 8 20 8 20; + -fx-cursor: hand; +} + +.primary-button:hover { + -fx-background-color: -primary-hover; +} + +.secondary-button { + -fx-background-color: -surface-line; + -fx-text-fill: -text-base; + -fx-background-radius: 8; + -fx-padding: 6 16; + -fx-cursor: hand; +} + +.secondary-button:hover { + -fx-background-color: #dedede; +} + +.back-button { + -fx-background-color: transparent; + -fx-text-fill: -primary; + -fx-cursor: hand; +} + +.modal-overlay { + -fx-background-color: -overlay; +} + +.hearts-display { + -fx-padding: 0 4 0 4; +} + +.heart-icon { + -fx-font-size: 22px; +} + +.heart-active { + -fx-text-fill: -accent; +} + +.heart-empty { + -fx-text-fill: rgba(255, 255, 255, 0.22); +} \ No newline at end of file diff --git a/src/main/resources/css/filepicker.css b/src/main/resources/css/filepicker.css new file mode 100644 index 0000000..ba41dc3 --- /dev/null +++ b/src/main/resources/css/filepicker.css @@ -0,0 +1,189 @@ +.filepicker-path { + -fx-font-size: 11px; + -fx-text-fill: #666666; + -fx-font-style: italic; + -fx-padding: 0 0 4 0; +} + +.filepicker-scroll { + -fx-background-color: #f9f9f9; + -fx-border-color: #e0e0e0; + -fx-border-radius: 6; + -fx-background-radius: 6; +} + +.filepicker-scroll .viewport { + -fx-background-color: transparent; +} + +.filepicker-row { + -fx-background-radius: 5; + -fx-cursor: hand; +} + +.filepicker-row:hover { + -fx-background-color: #f0f4ff; +} + +.filepicker-row-selected { + -fx-background-color: #dce8ff; +} + +.filepicker-icon { + -fx-font-size: 13px; + -fx-min-width: 20px; +} + +.filepicker-dir { + -fx-font-size: 13px; + -fx-text-fill: #1a1a1a; + -fx-font-weight: bold; +} + +.filepicker-file { + -fx-font-size: 13px; + -fx-text-fill: #2a2a2a; +} + +.filepicker-selected { + -fx-font-size: 12px; + -fx-text-fill: #444444; + -fx-font-style: italic; +} + +.fp-toolbar { + -fx-background-color: #f0f0f0; + -fx-border-color: transparent transparent #e0e0e0 transparent; + -fx-border-width: 0 0 1 0; +} + +.fp-toolbar-button { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-text-fill: #333333; + -fx-font-size: 14px; + -fx-cursor: hand; + -fx-padding: 3 8; + -fx-background-radius: 4; +} + +.fp-toolbar-button:hover { + -fx-background-color: #e0e0e0; +} + +.fp-toolbar-button:disabled { + -fx-text-fill: #bbbbbb; +} + +.fp-address-bar { + -fx-background-color: white; + -fx-border-color: #cccccc; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-text-fill: #1a1a1a; + -fx-font-size: 12px; + -fx-padding: 3 8; +} + +.fp-sidebar { + -fx-background-color: #f7f7f7; + -fx-border-color: transparent; +} + +.fp-sidebar-section { + -fx-font-size: 10px; + -fx-text-fill: #999999; + -fx-font-weight: bold; + -fx-padding: 4 4 6 4; +} + +.fp-sidebar-item { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-text-fill: #222222; + -fx-font-size: 12px; + -fx-cursor: hand; + -fx-padding: 5 8; + -fx-background-radius: 5; +} + +.fp-sidebar-item:hover { + -fx-background-color: #e8eaf0; +} + +.fp-main { + -fx-background-color: white; +} + +.fp-scroll { + -fx-background-color: white; +} + +.fp-scroll .viewport { + -fx-background-color: white; +} + +.fp-col-header-row { + -fx-background-color: #f5f5f5; + -fx-border-color: transparent transparent #e8e8e8 transparent; + -fx-border-width: 0 0 1 0; +} + +.fp-col-header { + -fx-font-size: 11px; + -fx-font-weight: bold; + -fx-text-fill: #555555; +} + +.fp-row { + -fx-background-color: transparent; + -fx-cursor: hand; +} + +.fp-row:hover { + -fx-background-color: #f0f4ff; +} + +.fp-row-selected { + -fx-background-color: #cce0ff; +} + +.fp-row-icon { + -fx-font-size: 13px; +} + +.fp-row-dir { + -fx-font-size: 13px; + -fx-text-fill: #1a1a1a; + -fx-font-weight: bold; +} + +.fp-row-file { + -fx-font-size: 13px; + -fx-text-fill: #2a2a2a; +} + +.fp-row-meta { + -fx-font-size: 12px; + -fx-text-fill: #777777; +} + +.fp-bottom-bar { + -fx-background-color: #f7f7f7; +} + +.fp-bottom-label { + -fx-font-size: 12px; + -fx-text-fill: #333333; + -fx-min-width: 70px; +} + +.fp-filter-label { + -fx-font-size: 12px; + -fx-text-fill: #555555; + -fx-background-color: white; + -fx-border-color: #cccccc; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-padding: 4 8; +} \ No newline at end of file diff --git a/src/main/resources/css/rapport.css b/src/main/resources/css/rapport.css new file mode 100644 index 0000000..8e9bcdd --- /dev/null +++ b/src/main/resources/css/rapport.css @@ -0,0 +1,107 @@ +.rapport-section-header { + -fx-font-weight: bold; + -fx-font-size: 11px; + -fx-text-fill: -text-faint; + -fx-text-transform: uppercase; + -fx-letter-spacing: 1; +} + +.rapport-hero-block { + -fx-background-color: -surface-soft; + -fx-background-radius: 12; + -fx-border-color: -surface-line; + -fx-border-radius: 12; + -fx-border-width: 1; +} + +.rapport-hero-amount { + -fx-font-size: 44px; + -fx-font-weight: bold; + -fx-text-fill: -text-strong; +} + +.rapport-hero-caption { + -fx-font-size: 12px; + -fx-text-fill: -text-muted; + -fx-text-transform: uppercase; + -fx-letter-spacing: 1; +} + +.rapport-verdict { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-padding: 4 14 4 14; + -fx-background-radius: 12; +} + +.mood-positive { + -fx-text-fill: -success; +} + +.mood-positive.rapport-verdict { + -fx-background-color: rgba(52, 199, 89, 0.15); + -fx-text-fill: derive(-success, -20%); +} + +.mood-negative { + -fx-text-fill: -danger; +} + +.mood-negative.rapport-verdict { + -fx-background-color: -danger-soft; + -fx-text-fill: derive(-danger, -20%); +} + +.mood-neutral { + -fx-text-fill: -text-base; +} + +.mood-neutral.rapport-verdict { + -fx-background-color: -surface-line; + -fx-text-fill: -text-base; +} + +.rapport-line-key { + -fx-font-size: 13px; + -fx-text-fill: -text-muted; +} + +.rapport-line-value { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: -text-base; +} + +.rapport-rent-row { + -fx-padding: 0 0 4 0; +} + +.rapport-rent-value { + -fx-text-fill: -danger; +} + +.rapport-balance-header { + -fx-font-weight: bold; + -fx-font-size: 16px; + -fx-text-fill: -text-strong; +} + +.rapport-balance-value { + -fx-font-weight: bold; + -fx-font-size: 22px; +} + +.rapport-rule { + -fx-background-color: -surface-line; +} + +.rapport-scroll { + -fx-background-color: -surface-soft; + -fx-background-radius: 10; + -fx-border-color: -surface-line; + -fx-border-radius: 10; +} + +.rapport-scroll > .viewport { + -fx-background-color: transparent; +} \ No newline at end of file diff --git a/src/main/resources/css/receipt.css b/src/main/resources/css/receipt.css new file mode 100644 index 0000000..75dedc4 --- /dev/null +++ b/src/main/resources/css/receipt.css @@ -0,0 +1,51 @@ +.receipt-container { + -fx-background-color: -surface; + -fx-border-color: -popup-border; + -fx-border-style: dashed; + -fx-padding: 20; + -fx-background-radius: 4; + -fx-border-radius: 4; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.08), 4, 0, 0, 1); +} + +.receipt-text { + -fx-font-family: "Courier New", monospace; + -fx-font-size: 12px; + -fx-text-fill: -text-base; +} + +.receipt-divider { + -fx-pref-height: 1; + -fx-min-height: 1; + -fx-max-height: 1; + -fx-background-color: -text-muted; + -fx-opacity: 0.35; +} + +.receipt-divider-strong { + -fx-pref-height: 1; + -fx-min-height: 1; + -fx-max-height: 1; + -fx-background-color: -text-base; + -fx-opacity: 0.7; +} + +.receipt-header { + -fx-font-family: "Courier New", monospace; + -fx-font-weight: bold; + -fx-font-size: 14px; + -fx-alignment: center; + -fx-text-fill: -text-strong; +} + +.receipt-total { + -fx-font-weight: bold; + -fx-border-color: -text-base transparent transparent transparent; + -fx-border-width: 1 0 0 0; +} + +.receipt-total .label { + -fx-font-family: "Courier New", monospace; + -fx-font-weight: bold; + -fx-font-size: 12px; +} \ No newline at end of file diff --git a/src/main/resources/css/settings.css b/src/main/resources/css/settings.css new file mode 100644 index 0000000..4958632 --- /dev/null +++ b/src/main/resources/css/settings.css @@ -0,0 +1,113 @@ +.settings-layout { + -fx-background-color: transparent; +} + +.settings-section-title { + -fx-font-size: 11px; + -fx-font-weight: bold; + -fx-text-fill: #555f6e; + -fx-padding: 4 0 0 0; +} + +.settings-row-label { + -fx-font-size: 13px; + -fx-text-fill: #1a1a1a; +} + +.settings-value-label { + -fx-font-size: 12px; + -fx-text-fill: #333333; + -fx-min-width: 36px; + -fx-alignment: center-right; +} + +.settings-slider .track { + -fx-background-color: #cccccc; + -fx-pref-height: 4px; +} + +.settings-slider .thumb { + -fx-background-color: #5b9bd5; + -fx-pref-width: 14px; + -fx-pref-height: 14px; +} + +.settings-text-field { + -fx-background-color: #f5f5f5; + -fx-text-fill: #1a1a1a; + -fx-prompt-text-fill: #999999; + -fx-border-color: #cccccc; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-padding: 6 10; +} + +.settings-path-label { + -fx-font-size: 11px; + -fx-text-fill: #444444; + -fx-font-style: italic; +} + +.settings-color-picker { + -fx-pref-width: 120px; +} + +.settings-logout-button { + -fx-background-color: #e8f0fe; + -fx-text-fill: #1a1a1a; + -fx-border-color: #b0c4e8; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-font-size: 13px; + -fx-padding: 6 16; + -fx-cursor: hand; +} + +.settings-logout-button:hover { + -fx-background-color: #d0e0f8; +} + +.settings-poweroff-button { + -fx-background-color: #fdecea; + -fx-text-fill: #c0392b; + -fx-border-color: #f0b8b2; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-padding: 6 16; + -fx-cursor: hand; +} + +.settings-poweroff-button:hover { + -fx-background-color: #f8d7d4; +} + +.settings-layout .color-picker { + -fx-pref-width: 110px; + -fx-max-width: 110px; +} + +.settings-layout .color-picker .color-picker-label { + -fx-text-fill: #1a1a1a; +} + +.settings-combo { + -fx-pref-width: 0; + -fx-background-color: #f5f5f5; + -fx-border-color: #cccccc; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-text-fill: #1a1a1a; +} + +.settings-combo .list-cell { + -fx-text-fill: #1a1a1a; +} + +.settings-gradient-preview { + -fx-background-radius: 6; + -fx-border-radius: 6; + -fx-border-color: #cccccc; + -fx-border-width: 1; +} \ No newline at end of file diff --git a/src/main/resources/css/stock.css b/src/main/resources/css/stock.css new file mode 100644 index 0000000..add33f1 --- /dev/null +++ b/src/main/resources/css/stock.css @@ -0,0 +1,111 @@ +.trade-button-weekend { + -fx-cursor: default; +} + +.stock-list-item { + -fx-border-color: #f0f0f0; + -fx-border-width: 0 0 1 0; + -fx-cursor: hand; + -fx-background-color: transparent; +} + +.stock-symbol { + -fx-font-weight: bold; + -fx-font-size: 14px; +} + +.stock-company { + -fx-font-size: 14px; + -fx-text-fill: -text-muted; +} + +.stock-price { + -fx-font-weight: bold; + -fx-font-size: 14px; +} + +.stock-price-change { + -fx-font-size: 12px; +} + +.stock-price-positive { + -fx-text-fill: -success; +} + +.stock-price-negative { + -fx-text-fill: -danger; +} + +.stock-title { + -fx-font-weight: bold; + -fx-font-size: 24px; + -fx-text-fill: -text-strong; +} + +.stock-symbol-large { + -fx-text-fill: -text-muted; +} + +.search-field { + -fx-background-color: -surface; + -fx-background-radius: 15; + -fx-border-color: -popup-border; + -fx-border-radius: 15; + -fx-padding: 5 15 5 15; +} + +.stock-price-large { + -fx-font-weight: bold; + -fx-font-size: 24px; +} + +.stock-quantity-spinner { + -fx-background-radius: 5; + -fx-background-color: -surface; + -fx-border-color: -popup-border; + -fx-border-radius: 5; +} + +.stock-quantity-spinner .text-field { + -fx-background-color: transparent; + -fx-alignment: center; + -fx-font-size: 18px; +} + +.stock-price-change-large { + -fx-font-size: 18px; +} + +.trade-panel { + -fx-background-color: -surface-soft; + -fx-background-radius: 10; +} + +.stock-graph-container { + -fx-padding: 10; + -fx-background-color: -surface; +} + +.stock-area-chart { + -fx-create-symbols: false; + -fx-horizontal-grid-lines-visible: false; + -fx-vertical-grid-lines-visible: false; +} + +.stock-area-chart .chart-series-area-line { + -fx-stroke: #004e92; + -fx-stroke-width: 2px; +} + +.stock-area-chart .chart-series-area-fill { + -fx-fill: linear-gradient(to bottom, #004e9280, transparent 80%); +} + +.stock-area-chart .chart-plot-background { + -fx-background-color: transparent; +} + +.stock-area-chart .axis { + -fx-tick-label-fill: -text-muted; + -fx-tick-length: 5; +} \ No newline at end of file diff --git a/src/main/resources/css/warning.css b/src/main/resources/css/warning.css new file mode 100644 index 0000000..57cedbf --- /dev/null +++ b/src/main/resources/css/warning.css @@ -0,0 +1,32 @@ +.warning-title { + -fx-font-weight: bold; + -fx-font-size: 17px; + -fx-text-fill: -text-strong; + -fx-text-alignment: center; +} + +.warning-message { + -fx-font-size: 13px; + -fx-text-fill: -text-base; + -fx-text-alignment: center; + -fx-line-spacing: 2; +} + +.warning-scroll, +.warning-scroll .viewport, +.warning-scroll .scroll-pane { + -fx-background-color: transparent; + -fx-border-color: transparent; +} + +.game-over-title { + -fx-font-weight: bold; + -fx-font-size: 36px; + -fx-text-fill: -text-strong; +} + +.game-over-subtitle { + -fx-font-size: 14px; + -fx-text-fill: -text-muted; + -fx-text-alignment: center; +} \ No newline at end of file diff --git a/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/CriticalFeaturesTest.java b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/CriticalFeaturesTest.java new file mode 100644 index 0000000..e258808 --- /dev/null +++ b/src/test/java/edu/ntnu/idi/idatt2003/gruppe42/CriticalFeaturesTest.java @@ -0,0 +1,68 @@ +package edu.ntnu.idi.idatt2003.gruppe42; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Portfolio; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Share; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Stock; +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for critical features of the Millions game. + */ +public class CriticalFeaturesTest { + private Player player; + + @BeforeEach + void setUp() { + player = new Player("Test User", new BigDecimal("1000")); + } + + @Test + void testDebtAndLifeLoss() { + // Deduct more than the player has using deductRent + player.deductRent(new BigDecimal("1500")); + assertTrue(player.isInDebt(), "Player should be in debt"); + + int initialLives = player.getLives(); + player.loseLife(); + assertEquals(initialLives - 1, player.getLives(), "Player should have lost a life"); + } + + @Test + void testPortfolioManagement() { + Stock apple = new Stock("AAPL", "Apple Inc.", new BigDecimal("150")); + BigDecimal quantity = new BigDecimal("10"); + BigDecimal buyPrice = new BigDecimal("150"); + + Portfolio portfolio = player.getPortfolio(); + portfolio.addShare(new Share(apple, quantity, buyPrice)); + + assertEquals(1, portfolio.getShares().size(), "Portfolio should have one share"); + assertEquals(quantity, portfolio.getShares().get(0).getQuantity(), "Quantity should match"); + + portfolio.removeShare(new Share(apple, new BigDecimal("5"), buyPrice)); + assertEquals(new BigDecimal("5"), portfolio.getShares().get(0).getQuantity(), "Quantity should be reduced"); + } + + @Test + void testNetWorthCalculation() { + Stock apple = new Stock("AAPL", "Apple Inc.", new BigDecimal("200")); + player.getPortfolio().addShare(new Share(apple, new BigDecimal("5"), new BigDecimal("150"))); + + // Net worth = Cash + Portfolio.getNetWorth() + // Cash = 1000 + // Portfolio.getNetWorth() uses SaleCalculator for each share: + // Gross = 5 * 200 = 1000 + // Commission = 1% of 1000 = 10 + // Tax = 22% of profit. Profit = (200 - 150) * 5 = 250. Tax = 0.22 * 250 = 55 + // Total Sale Value = 1000 - 10 - 55 = 935 + // Net Worth = 1000 + 935 = 1935 + + assertEquals(new BigDecimal("1935.00"), player.getNetWorth(), "Net worth calculation incorrect"); + } +}