diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Launcher.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Launcher.java index 828f455..f80af23 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Launcher.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Launcher.java @@ -1,6 +1,14 @@ package edu.ntnu.idi.idatt2003.gruppe42; +/** + * Main class for launching the application. + */ public class Launcher { + /** + * Launches the application. + * + * @param args command-line arguments + */ public static void main(String[] args) { Millions.launch(Millions.class, args); } 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 a3ea37b..3365bb3 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java @@ -9,7 +9,6 @@ import edu.ntnu.idi.idatt2003.gruppe42.model.Player; import edu.ntnu.idi.idatt2003.gruppe42.view.views.DesktopView; import edu.ntnu.idi.idatt2003.gruppe42.view.views.StartView; - import java.io.InputStream; import javafx.application.Application; import javafx.scene.Scene; @@ -68,7 +67,8 @@ public void start(final Stage stage) { * @param popupsController controller managing popups */ public void initGame(final String username, final Difficulty difficulty, - InputStream stocksStream, PopupsController popupsController) { + InputStream stocksStream, PopupsController popupsController) + throws Exception { player = GameFactory.createPlayer(username, difficulty); gameController = new GameController(); @@ -77,8 +77,6 @@ public void initGame(final String username, final Difficulty difficulty, desktopViewController.setOnGameOver(this::resetGame); desktopView = desktopViewController.getDesktopView(); scene.setRoot(desktopView.getRoot()); - - gameController.startGame(); } /** 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 22ef4ef..b36ef66 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 @@ -64,7 +64,7 @@ public void setGameState(final GameState gameState) { public void startGame() { timer = new Timer(true); final int delay = 0; - final int period = 100; + final int period = 1000; timer.scheduleAtFixedRate(new TimerTask() { @Override 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 0865672..ff738ef 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 @@ -7,6 +7,7 @@ import edu.ntnu.idi.idatt2003.gruppe42.model.exceptions.StockFileParseException; import java.io.IOException; import java.io.InputStream; +import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -78,11 +79,16 @@ public void nextTick() { public void startMarket() { for (Stock stock : exchange.getAllStocks()) { StockController controller = stockControllers.get(stock.getSymbol()); - if (controller != null) { - for (int i = 0; i < STOCK_RESOLUTION; i++) { - stock.addNewSalesPrice(controller.updatePrice()); - } + if (controller == null) { + continue; } + + for (int i = 0; i < STOCK_RESOLUTION; i++) { + stock.addNewSalesPrice(controller.updatePrice()); + } + + stock.reverseHistory(); + controller.syncPrice(); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockController.java index a101e46..36f93d2 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/StockController.java @@ -146,4 +146,11 @@ public BigDecimal getPrice() { return price; } + /** + * Resets the internal price to match the stock's current sales price after history manipulation. + */ + public void syncPrice() { + this.price = stock.getSalesPrice(); + } + } \ No newline at end of file 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 76fcc9d..54cca7e 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 @@ -16,8 +16,8 @@ * Controller responsible for managing in-game time, days, weeks, and weather. */ public class TimeAndWeatherController implements AppController { - private final IntegerProperty hour = new SimpleIntegerProperty(HOURS_PER_DAY - 1); - private final IntegerProperty dayIndex = new SimpleIntegerProperty(SATURDAY_INDEX); + private final IntegerProperty hour = new SimpleIntegerProperty(0); + private final IntegerProperty dayIndex = new SimpleIntegerProperty(0); private final IntegerProperty weekNumber = new SimpleIntegerProperty(0); private final IntegerProperty temperature = new SimpleIntegerProperty(15); private final StringProperty weather = new SimpleStringProperty("Sunny"); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/HustlersContentRenderer.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/HustlersContentRenderer.java new file mode 100644 index 0000000..37c23d2 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/appcontrollers/HustlersContentRenderer.java @@ -0,0 +1,83 @@ +package edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers; + +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +/** + * Renders formatted tutorial chapter text into a VBox content area. + */ +public record HustlersContentRenderer(VBox contentBody) { + + private static final String ACCENT = "#a0720a"; + private static final String ACCENT_DIM = "#c9a84c"; + private static final String TEXT_PRIMARY = "#1a1a1a"; + + /** + * Creates a renderer targeting the given content body. + * + * @param contentBody the VBox to render content into + */ + public HustlersContentRenderer { + } + + /** + * Clears the content body and renders the given raw text. + * + * @param rawText the chapter body text to render + */ + public void render(final String rawText) { + contentBody.getChildren().clear(); + for (String para : rawText.split("\n\n")) { + para = para.trim(); + if (para.isEmpty()) { + continue; + } + + if (isSectionHeader(para)) { + renderSectionHeader(para); + } else { + renderParagraph(para); + } + } + } + + private void renderSectionHeader(final String text) { + Region divider = new Region(); + divider.setPrefHeight(1); + divider.setMaxWidth(32); + divider.setStyle("-fx-background-color: " + ACCENT_DIM + ";"); + VBox.setMargin(divider, new Insets(4, 0, 5, 0)); + + Label header = new Label(text); + header.setStyle( + "-fx-text-fill: " + ACCENT + ";" + + "-fx-font-size: 10px;" + + "-fx-font-weight: bold;" + + "-fx-font-family: 'Georgia';" + ); + contentBody.getChildren().addAll(divider, header); + } + + private void renderParagraph(final String para) { + TextFlow flow = new TextFlow(); + flow.setLineSpacing(3); + String[] lines = para.split("\n"); + for (int i = 0; i < lines.length; i++) { + Text t = new Text((i > 0 ? "\n" : "") + lines[i]); + t.setStyle("-fx-fill: " + TEXT_PRIMARY + ";"); + t.setFont(Font.font("Georgia", 12.5)); + flow.getChildren().add(t); + } + contentBody.getChildren().add(flow); + } + + private boolean isSectionHeader(final String text) { + String stripped = text.replaceAll("[^a-zA-Z]", ""); + return !stripped.isEmpty() && stripped.equals(stripped.toUpperCase()) && text.length() < 60; + } +} \ No newline at end of file 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 b216313..34cb8e6 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 @@ -5,7 +5,6 @@ import edu.ntnu.idi.idatt2003.gruppe42.model.StockFileHandler; import edu.ntnu.idi.idatt2003.gruppe42.view.apps.SettingsApp; import edu.ntnu.idi.idatt2003.gruppe42.view.apps.SettingsApp.GradientConfig; - import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -30,6 +29,11 @@ public class SettingsAppController implements AppController { private final SettingsApp settingsApp; + /** + * Constructor for the SettingsAppController. + * + * @param settingsApp the SettingsApp instance to control + */ public SettingsAppController( final SettingsApp settingsApp) { @@ -144,6 +148,11 @@ private void wireLoggedInControls() { }); } + /** + * Sets the logged-in status of the SettingsApp. + * + * @param status the new logged-in status + */ public void setLoggedIn(boolean status) { settingsApp.setLoggedIn(status); wireControls(); @@ -159,6 +168,11 @@ private boolean isValidStockFile(Path path) { } } + /** + * Returns stock file. + * + * @return the selected stock file as an InputStream, or null if no file is selected + */ public InputStream getSelectedStockStream() { if (userSelectedPath == null) { return null; 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 10add7b..e350b79 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 @@ -300,7 +300,9 @@ private void handleSell(final Stock stock, final BigDecimal quantity) { transaction.commit(player); player.updateStatus(); } catch (Exception e) { - LOGGER.log(System.Logger.Level.WARNING, "Unexpected error during sell transaction", e); + LOGGER.log( + System.Logger.Level.WARNING, + "Unexpected error during sell transaction", e); } } }); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopBindings.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopBindings.java new file mode 100644 index 0000000..c4c7b04 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopBindings.java @@ -0,0 +1,108 @@ +package edu.ntnu.idi.idatt2003.gruppe42.controller.viewcontrollers; + +import edu.ntnu.idi.idatt2003.gruppe42.controller.PopupsController; +import edu.ntnu.idi.idatt2003.gruppe42.controller.TimeAndWeatherController; +import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.StockAppController; +import edu.ntnu.idi.idatt2003.gruppe42.model.App; +import edu.ntnu.idi.idatt2003.gruppe42.model.Player; +import edu.ntnu.idi.idatt2003.gruppe42.view.views.DesktopView; +import javafx.application.Platform; + +/** + * Responsible for binding UI components and listeners to game state in the desktop view. + */ +public record DesktopBindings(DesktopView desktopView, Player player, + TimeAndWeatherController timeAndWeatherController, + StockAppController stockAppController, + PopupsController popupsController, + DesktopDialogController dialogController, + Runnable onAdvanceWeek) { + + private static final int SATURDAY_INDEX = 6; + private static final int SUNDAY_INDEX = 0; + + /** + * Creates a new DesktopBindings instance. + * + * @param desktopView the desktop view + * @param player the player model + * @param timeAndWeatherController the time and weather controller + * @param stockAppController the stock app controller + * @param popupsController the popups controller + * @param dialogController the dialog controller + * @param onAdvanceWeek callback to advance the week + */ + public DesktopBindings { + + } + + /** + * Binds all UI components and listeners. + */ + public void setup() { + setupDesktopUi(); + setupListeners(); + syncInitialState(); + } + + private void setupDesktopUi() { + desktopView.getNextWeekButton().setOnAction(event -> handleAdvanceWeek()); + desktopView.getSettingsButton().setOnAction(event -> popupsController.show(App.SETTINGS)); + + timeAndWeatherController.dayIndexProperty().addListener((obs, oldDay, newDay) -> { + int day = newDay.intValue(); + boolean isWeekend = day == SATURDAY_INDEX || day == SUNDAY_INDEX; + Platform.runLater(() -> stockAppController.setWeekend(isWeekend)); + if (day == SATURDAY_INDEX) { + Platform.runLater(dialogController::showWeekendRapport); + } + }); + } + + private void setupListeners() { + timeAndWeatherController.hourProperty().addListener((obs, ov, nv) -> { + desktopView.updateClock(timeAndWeatherController.getTimeString()); + player.updateStatus(); + }); + + timeAndWeatherController.dayIndexProperty().addListener((obs, ov, nv) -> { + String dayName = timeAndWeatherController.getDayOfWeekString(); + desktopView.updateDay(dayName, dayName.equals("SUN")); + }); + + timeAndWeatherController.weekIndexProperty().addListener((obs, ov, nv) -> + desktopView.updateWeek(timeAndWeatherController.getWeekString())); + + timeAndWeatherController.weatherProperty().addListener((obs, ov, nv) -> + desktopView.updateWeather(nv)); + + timeAndWeatherController.temperatureProperty().addListener((obs, ov, nv) -> + desktopView.updateTemperature(String.valueOf(nv))); + + player.addNameListener(name -> + Platform.runLater(() -> desktopView.updatePlayerName(name))); + + player.updateStatus(); + player.addStatusListener(s -> + Platform.runLater(() -> desktopView.updatePlayerStatus(s.name()))); + } + + private void syncInitialState() { + 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()); + } + + private void handleAdvanceWeek() { + if (player.isInDebt()) { + dialogController.showDebtWarning(); + } else { + onAdvanceWeek.run(); + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopComponentFactory.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopComponentFactory.java index 98d9631..4409e8a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopComponentFactory.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopComponentFactory.java @@ -8,6 +8,7 @@ import javafx.scene.Node; import javafx.scene.SnapshotParameters; import javafx.scene.control.Button; +import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.ClipboardContent; @@ -15,6 +16,7 @@ import javafx.scene.input.TransferMode; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.scene.paint.Color; /** @@ -38,7 +40,8 @@ public record DesktopComponentFactory( * @param type the application type * @return the created application button node */ - public Node createAppButton(final App type) { + public VBox createAppButton(final App type) { + final VBox buttonContainer = new VBox(); Button button = new Button(); button.setStyle( "-fx-background-color: transparent; -fx-padding: 0; -fx-border-color: transparent;" @@ -73,8 +76,15 @@ public Node createAppButton(final App type) { bindButtonSize(button, newParent)); popupsController.bindOpenButton(button, type); - setupDrag(button); - return button; + setupDrag(button, buttonContainer); + + Label appName = new Label(type.getDisplayName()); + appName.setStyle( + "-fx-font-size: 12px; -fx-font-weight: bold; -fx-text-fill: white; -fx-wrap-text: true;"); + buttonContainer.setAlignment(javafx.geometry.Pos.CENTER); + buttonContainer.setSpacing(5); + buttonContainer.getChildren().addAll(button, appName); + return buttonContainer; } private String iconPath(final App type) { @@ -102,12 +112,12 @@ private void bindButtonSize(final Button button, final Object parent) { } } - private void setupDrag(final Button button) { + private void setupDrag(final Button button, final VBox container) { button.setOnDragDetected(event -> { - Dragboard dragboard = button.startDragAndDrop(TransferMode.MOVE); + Dragboard dragboard = container.startDragAndDrop(TransferMode.MOVE); SnapshotParameters params = new SnapshotParameters(); params.setFill(Color.TRANSPARENT); - dragboard.setDragView(button.snapshot(params, null)); + dragboard.setDragView(container.snapshot(params, null)); dragboard.setDragViewOffsetX(event.getX()); dragboard.setDragViewOffsetY(event.getY()); ClipboardContent content = new ClipboardContent(); @@ -119,14 +129,14 @@ private void setupDrag(final Button button) { private void applyIconClip( final javafx.scene.image.ImageView icon, - final double w, - final double h) { - if (w <= 0 || h <= 0) { + final double width, + final double height) { + if (width <= 0 || height <= 0) { return; } - javafx.scene.shape.Rectangle clip = new javafx.scene.shape.Rectangle(w, h); - clip.setArcWidth(w * 0.45); - clip.setArcHeight(h * 0.45); + javafx.scene.shape.Rectangle clip = new javafx.scene.shape.Rectangle(width, height); + clip.setArcWidth(width * 0.45); + clip.setArcHeight(height * 0.45); icon.setClip(clip); } @@ -137,7 +147,7 @@ private void applyIconClip( */ public void configureCellAsDropTarget(final StackPane cell) { cell.setOnDragOver(event -> { - if (event.getGestureSource() instanceof Button && cell.getChildren().isEmpty()) { + if (event.getGestureSource() instanceof VBox && cell.getChildren().isEmpty()) { event.acceptTransferModes(TransferMode.MOVE); } event.consume(); @@ -145,9 +155,10 @@ public void configureCellAsDropTarget(final StackPane cell) { cell.setOnDragDropped(event -> { boolean success = false; - if (event.getGestureSource() instanceof Button button) { - ((StackPane) button.getParent()).getChildren().remove(button); - cell.getChildren().add(button); + if (event.getGestureSource() instanceof VBox container) { + StackPane source = (StackPane) container.getParent(); + source.getChildren().remove(container); + cell.getChildren().add(container); success = true; } event.setDropCompleted(success); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopDialogController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopDialogController.java index 6c03fef..bf0fa0f 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopDialogController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopDialogController.java @@ -21,9 +21,9 @@ public final class DesktopDialogController { private final Player player; private final TimeAndWeatherController timeAndWeatherController; - private final WeekendReportApp weekendReportApp; - private final WarningApp warningApp; - private final GameOverApp gameOverApp; + private final WeekendReportApp weekendReportApp = new WeekendReportApp(); + private final WarningApp warningApp = new WarningApp(); + private final GameOverApp gameOverApp = new GameOverApp(); private DesktopView desktopView; private Runnable onGameOver; private Runnable onAdvanceWeek; @@ -31,24 +31,14 @@ public final class DesktopDialogController { /** * Constructs a new desktop dialog controller. * - * @param player the player model + * @param player the player model * @param timeAndWeatherController the time and weather controller - * @param weekendReportApp the weekend report UI - * @param warningApp the warning UI - * @param gameOverApp the game over UI */ public DesktopDialogController( - Player player, - TimeAndWeatherController timeAndWeatherController, - WeekendReportApp weekendReportApp, - WarningApp warningApp, - GameOverApp gameOverApp) { + final Player player, + final TimeAndWeatherController timeAndWeatherController) { this.player = player; this.timeAndWeatherController = timeAndWeatherController; - this.weekendReportApp = weekendReportApp; - this.warningApp = warningApp; - this.gameOverApp = gameOverApp; - wireWeekendRapport(); wireGameOver(); } @@ -191,4 +181,31 @@ private void wireGameOver() { } }); } + + /** Centers all modal apps within their parent. */ + public void centerModals() { + weekendReportApp.centerInParent(); + warningApp.centerInParent(); + gameOverApp.centerInParent(); + } + + public javafx.scene.Parent getWeekendReportRoot() { + return weekendReportApp.getRoot(); + } + + public javafx.scene.Parent getWarningRoot() { + return warningApp.getRoot(); + } + + public javafx.scene.Parent getGameOverRoot() { + return gameOverApp.getRoot(); + } + + /** + * Fires the continue button of the weekend report modal. + */ + public void fireWeekendReportContinue() { + weekendReportApp.getContinueButton().fire(); + } + } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopInitializer.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopInitializer.java new file mode 100644 index 0000000..f2e3ca8 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopInitializer.java @@ -0,0 +1,121 @@ +package edu.ntnu.idi.idatt2003.gruppe42.controller.viewcontrollers; + +import edu.ntnu.idi.idatt2003.gruppe42.controller.GameController; +import edu.ntnu.idi.idatt2003.gruppe42.controller.MarketController; +import edu.ntnu.idi.idatt2003.gruppe42.controller.PopupsController; +import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.AppStoreController; +import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.BankAppController; +import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.MailController; +import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.SettingsAppController; +import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.StockAppController; +import edu.ntnu.idi.idatt2003.gruppe42.model.App; +import edu.ntnu.idi.idatt2003.gruppe42.model.EventBus; +import edu.ntnu.idi.idatt2003.gruppe42.model.News; +import edu.ntnu.idi.idatt2003.gruppe42.model.Player; +import edu.ntnu.idi.idatt2003.gruppe42.model.StockEvent; +import edu.ntnu.idi.idatt2003.gruppe42.view.Popup; +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.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.views.DesktopView; +import java.util.ArrayList; +import java.util.List; +import javafx.application.Platform; + +/** + * Responsible for initializing all desktop applications and their controllers. + */ +public class DesktopInitializer { + + private final StockAppController stockAppController; + private final List popups; + + /** + * Initializes all apps and their controllers. + * + * @param player the player model + * @param gameController the main game controller + * @param marketController the market controller + * @param mailController the mail controller + * @param popupsController the popups controller + * @param desktopView the desktop view + * @param onMarketClosed callback for when market is closed + * @param onInsufficientFunds callback for when funds are insufficient + * @param onLogout callback for logout + */ + public DesktopInitializer( + final Player player, + final GameController gameController, + final MarketController marketController, + final MailController mailController, + final PopupsController popupsController, + final DesktopView desktopView, + final Runnable onMarketClosed, + final Runnable onInsufficientFunds, + final Runnable onLogout) { + + gameController.addAppController(new AppStoreController()); + + StockApp stockApp = new StockApp(player); + this.stockAppController = new StockAppController(stockApp, marketController, player); + gameController.addAppController(stockAppController); + gameController.addAppController(marketController); + + BankApp bankApp = new BankApp(); + gameController.addAppController(new BankAppController(bankApp, player)); + + SettingsApp settingsApp = (SettingsApp) popupsController.getPopup(App.SETTINGS); + SettingsAppController settingsAppController = new SettingsAppController(settingsApp); + settingsAppController.setLoggedIn(true); + settingsAppController.setOnGradientChanged(cfg -> + desktopView.getDesktopPane().setStyle( + SettingsApp.buildCssGradient(cfg.colorA(), cfg.colorB(), cfg.direction()))); + settingsAppController.setOnPlayerNameChanged(player::setName); + settingsAppController.setOnLogout(onLogout); + settingsAppController.setOnPowerOff(Platform::exit); + + bankApp.setOnShareSelected(share -> { + popupsController.show(App.STOCK); + stockAppController.navigateToStock(share.getStock()); + }); + + stockAppController.setOnMarketClosed(onMarketClosed); + stockAppController.setOnInsufficientFunds(onInsufficientFunds); + + this.popups = new ArrayList<>(); + popups.add(new AppStoreApp()); + popups.add(new HustlersApp()); + popups.add(stockApp); + popups.add(new MailApp(mailController)); + popups.add(new NewsApp()); + popups.add(bankApp); + + EventBus.get().subscribe(StockEvent.class, event -> + Platform.runLater(() -> { + News news = new News(event.company(), event.trend()); + mailController.createMessage(news.generateAuthor(), "BREAKING NEWS", news.generateNews()); + })); + } + + /** + * Returns the initialized stock app controller. + * + * @return the stock app controller + */ + public StockAppController getStockAppController() { + return stockAppController; + } + + /** + * Returns the list of initialized popups. + * + * @return the popup list + */ + public List getPopups() { + return popups; + } +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopOverlaySetup.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopOverlaySetup.java new file mode 100644 index 0000000..7dfb539 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/controller/viewcontrollers/DesktopOverlaySetup.java @@ -0,0 +1,69 @@ +package edu.ntnu.idi.idatt2003.gruppe42.controller.viewcontrollers; + +import edu.ntnu.idi.idatt2003.gruppe42.controller.PopupsController; +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; +import javafx.scene.layout.Pane; + +/** + * Responsible for setting up the popup and modal overlay layer of the desktop view. + */ +public class DesktopOverlaySetup { + + private final DesktopView desktopView; + private final PopupsController popupsController; + private final DesktopDialogController dialogController; + + /** + * Creates a new DesktopOverlaySetup instance. + * + * @param desktopView the desktop view + * @param popupsController the popups controller + * @param dialogController the dialog controller + */ + public DesktopOverlaySetup( + final DesktopView desktopView, + final PopupsController popupsController, + final DesktopDialogController dialogController) { + + this.desktopView = desktopView; + this.popupsController = popupsController; + this.dialogController = dialogController; + } + + /** + * Registers all popups and modals with the desktop view overlay. + */ + public void setup() { + registerPopups(); + registerModals(); + } + + private void registerPopups() { + Pane overlay = desktopView.getPopupOverlay(); + for (Popup p : popupsController.getPopups()) { + desktopView.registerPopup(p.getRoot()); + overlay.getChildren().add(p.getRoot()); + } + } + + private void registerModals() { + final Pane overlay = desktopView.getPopupOverlay(); + + dialogController.centerModals(); + + desktopView.registerModal(ModalLayer.RAPPORT, dialogController.getWeekendReportRoot()); + desktopView.registerModal(ModalLayer.WARNING, dialogController.getWarningRoot()); + desktopView.registerModal(ModalLayer.GAME_OVER, dialogController.getGameOverRoot()); + + overlay.getChildren().addAll( + desktopView.getOverlay(ModalLayer.RAPPORT), dialogController.getWeekendReportRoot(), + desktopView.getOverlay(ModalLayer.WARNING), dialogController.getWarningRoot(), + desktopView.getOverlay(ModalLayer.GAME_OVER), dialogController.getGameOverRoot() + ); + + desktopView.getOverlay(ModalLayer.RAPPORT).setOnMouseClicked( + event -> dialogController.fireWeekendReportContinue()); + } +} \ No newline at end of file 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 4b8e991..c203abc 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 @@ -4,52 +4,21 @@ import edu.ntnu.idi.idatt2003.gruppe42.controller.MarketController; import edu.ntnu.idi.idatt2003.gruppe42.controller.PopupsController; import edu.ntnu.idi.idatt2003.gruppe42.controller.TimeAndWeatherController; -import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.AppStoreController; -import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.BankAppController; import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.MailController; -import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.SettingsAppController; import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.StockAppController; import edu.ntnu.idi.idatt2003.gruppe42.model.App; -import edu.ntnu.idi.idatt2003.gruppe42.model.EventBus; -import edu.ntnu.idi.idatt2003.gruppe42.model.News; import edu.ntnu.idi.idatt2003.gruppe42.model.Player; -import edu.ntnu.idi.idatt2003.gruppe42.model.StockEvent; -import edu.ntnu.idi.idatt2003.gruppe42.view.Popup; -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.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.WeekendReportApp; +import edu.ntnu.idi.idatt2003.gruppe42.model.exceptions.StockFileParseException; import edu.ntnu.idi.idatt2003.gruppe42.view.views.DesktopView; -import edu.ntnu.idi.idatt2003.gruppe42.view.views.DesktopView.ModalLayer; - +import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import javafx.application.Platform; import javafx.scene.Node; -import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; /** * Controller responsible for managing the entire desktop view, including * application initialization, popup handling, modal dialogs, game progression, * and coordination between UI components and game logic. - * - *

This class acts as the central integration layer between: - *

- * - *

It also handles event-driven updates such as stock events and day/week transitions. */ public final class DesktopViewController { @@ -63,209 +32,57 @@ public final class DesktopViewController { private final MailController mailController; private final DesktopDialogController dialogController; private final DesktopComponentFactory componentFactory; - private final WeekendReportApp weekendReportApp = new WeekendReportApp(); - private final WarningApp warningApp = new WarningApp(); - private final GameOverApp gameOverApp = new GameOverApp(); - private StockAppController stockAppController; - + private final StockAppController stockAppController; private final DesktopView desktopView; /** * Constructs the desktop view controller and initializes all applications, * popups, event listeners, and UI bindings. * - * @param player the player model - * @param gameController the main game controller - * @param stocksStream path to stock data file selected by user + * @param player the player model + * @param gameController the main game controller + * @param stocksStream path to stock data file selected by user * @param popupsController controller responsible for popup management */ public DesktopViewController( final Player player, final GameController gameController, final InputStream stocksStream, - final PopupsController popupsController) { + final PopupsController popupsController) throws IOException, StockFileParseException { this.player = player; + this.popupsController = popupsController; + this.timeAndWeatherController = new TimeAndWeatherController(gameController); gameController.addAppController(timeAndWeatherController); - try { - this.marketController = new MarketController(stocksStream); - } catch (Exception e) { - // In a real app we'd show an error dialog, but here we'll at least not swallow it silently - throw new RuntimeException("Failed to load market", e); - } - + this.marketController = new MarketController(stocksStream); this.mailController = new MailController(timeAndWeatherController); gameController.addAppController(mailController); - this.popupsController = popupsController; + this.dialogController = new DesktopDialogController(player, timeAndWeatherController); this.componentFactory = new DesktopComponentFactory(popupsController, mailController); - this.dialogController = new DesktopDialogController( - player, timeAndWeatherController, weekendReportApp, warningApp, gameOverApp); - this.desktopView = new DesktopView(this); + this.dialogController.setDesktopView(desktopView); this.dialogController.setOnAdvanceWeek(this::doAdvanceWeek); - List popups = initializeApps(gameController); - this.popupsController.addPopups(popups); + DesktopInitializer initializer = new DesktopInitializer( + player, gameController, marketController, mailController, + popupsController, desktopView, + dialogController::showMarketClosedWarning, + dialogController::showInsufficientFundsWarning, + dialogController::showGameOver); - setupPopupOverlay(); - setupDesktopUi(); - setupListeners(); + this.stockAppController = initializer.getStockAppController(); + this.popupsController.addPopups(initializer.getPopups()); + new DesktopOverlaySetup(desktopView, popupsController, dialogController).setup(); + new DesktopBindings(desktopView, player, timeAndWeatherController, stockAppController, + popupsController, dialogController, this::doAdvanceWeek).setup(); int startDay = timeAndWeatherController.dayIndexProperty().get(); stockAppController.setWeekend(startDay == SATURDAY_INDEX || startDay == SUNDAY_INDEX); - - EventBus.get() - .subscribe( - StockEvent.class, - event -> - Platform.runLater( - () -> { - News news = new News(event.company(), event.trend()); - mailController.createMessage( - news.generateAuthor(), "BREAKING NEWS", news.generateNews()); - })); - } - - private List initializeApps(GameController gameController) { - final AppStoreApp appStoreApp = new AppStoreApp(); - gameController.addAppController(new AppStoreController()); - - final HustlersApp hustlersApp = new HustlersApp(); - - StockApp stockApp = new StockApp(player); - this.stockAppController = new StockAppController(stockApp, marketController, player); - gameController.addAppController(stockAppController); - gameController.addAppController(marketController); - - final MailApp mailApp = new MailApp(mailController); - final NewsApp newsApp = new NewsApp(); - - BankApp bankApp = new BankApp(); - gameController.addAppController(new BankAppController(bankApp, player)); - - SettingsApp settingsApp = (SettingsApp) popupsController.getPopup(App.SETTINGS); - SettingsAppController settingsAppController = - new SettingsAppController(settingsApp); - settingsAppController.setLoggedIn(true); - settingsAppController.setOnGradientChanged(cfg -> - desktopView.getDesktopPane().setStyle( - SettingsApp.buildCssGradient(cfg.colorA(), cfg.colorB(), cfg.direction()))); - - settingsAppController.setOnPlayerNameChanged(player::setName); - settingsAppController.setOnLogout(this::handleLogout); - settingsAppController.setOnPowerOff(Platform::exit); - - bankApp.setOnShareSelected(share -> { - popupsController.show(App.STOCK); - stockAppController.navigateToStock(share.getStock()); - }); - - stockAppController.setOnMarketClosed(this::showMarketClosedWarning); - stockAppController.setOnInsufficientFunds(this::showInsufficientFundsWarning); - - List popups = new ArrayList<>(); - popups.add(appStoreApp); - popups.add(hustlersApp); - popups.add(stockApp); - popups.add(mailApp); - popups.add(newsApp); - popups.add(bankApp); - - return popups; - } - - private void setupPopupOverlay() { - Pane overlay = desktopView.getPopupOverlay(); - - for (Popup p : popupsController.getPopups()) { - desktopView.registerPopup(p.getRoot()); - overlay.getChildren().add(p.getRoot()); - } - - weekendReportApp.centerInParent(); - warningApp.centerInParent(); - gameOverApp.centerInParent(); - - desktopView.registerModal(ModalLayer.RAPPORT, weekendReportApp.getRoot()); - desktopView.registerModal(ModalLayer.WARNING, warningApp.getRoot()); - desktopView.registerModal(ModalLayer.GAME_OVER, gameOverApp.getRoot()); - - overlay.getChildren().addAll( - desktopView.getOverlay(ModalLayer.RAPPORT), weekendReportApp.getRoot(), - desktopView.getOverlay(ModalLayer.WARNING), warningApp.getRoot(), - desktopView.getOverlay(ModalLayer.GAME_OVER), gameOverApp.getRoot() - ); - - desktopView.getOverlay(ModalLayer.RAPPORT).setOnMouseClicked( - event -> weekendReportApp.getContinueButton().fire()); - } - - private void setupDesktopUi() { - desktopView.getNextWeekButton().setOnAction(event -> handleAdvanceWeek()); - desktopView.getSettingsButton().setOnAction(event -> popupsController.show(App.SETTINGS)); - - timeAndWeatherController.dayIndexProperty().addListener((obs, oldDay, newDay) -> { - int day = newDay.intValue(); - boolean isWeekend = day == SATURDAY_INDEX || day == SUNDAY_INDEX; - Platform.runLater(() -> stockAppController.setWeekend(isWeekend)); - if (day == SATURDAY_INDEX) { - Platform.runLater(this::showWeekendRapport); - } - }); - } - - private void setupListeners() { - timeAndWeatherController.hourProperty().addListener((obs, ov, nv) -> { - desktopView.updateClock(timeAndWeatherController.getTimeString()); - player.updateStatus(); - }); - - timeAndWeatherController.dayIndexProperty().addListener((obs, ov, nv) -> { - String dayName = timeAndWeatherController.getDayOfWeekString(); - desktopView.updateDay(dayName, dayName.equals("SUN")); - }); - - timeAndWeatherController.weekIndexProperty().addListener((obs, ov, nv) -> - desktopView.updateWeek(timeAndWeatherController.getWeekString())); - - timeAndWeatherController.weatherProperty().addListener((obs, ov, nv) -> - desktopView.updateWeather(nv)); - - timeAndWeatherController.temperatureProperty().addListener((obs, ov, nv) -> - desktopView.updateTemperature(String.valueOf(nv))); - - player.addNameListener(name -> - Platform.runLater(() -> desktopView.updatePlayerName(name))); - - player.updateStatus(); - player.addStatusListener(s -> - Platform.runLater(() -> desktopView.updatePlayerStatus(s.name()))); - - 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()); - } - - private void showWeekendRapport() { - dialogController.showWeekendRapport(); - } - - private void handleAdvanceWeek() { - if (player.isInDebt()) { - dialogController.showDebtWarning(); - } else { - doAdvanceWeek(); - } } private void doAdvanceWeek() { @@ -275,14 +92,6 @@ private void doAdvanceWeek() { player.updateStatus(); } - private void showMarketClosedWarning() { - dialogController.showMarketClosedWarning(); - } - - private void showInsufficientFundsWarning() { - dialogController.showInsufficientFundsWarning(); - } - /** * Sets a callback that is executed when the game is over. * @@ -292,10 +101,6 @@ public void setOnGameOver(final Runnable callback) { dialogController.setOnGameOver(callback); } - private void handleLogout() { - dialogController.showGameOver(); // Or appropriate logout logic - } - /** * Creates a UI button representing an application on the desktop. * @@ -332,5 +137,4 @@ public Player getPlayer() { public DesktopView getDesktopView() { return desktopView; } - } \ 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 fb8d6e7..6b30192 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 @@ -3,18 +3,20 @@ import edu.ntnu.idi.idatt2003.gruppe42.Millions; import edu.ntnu.idi.idatt2003.gruppe42.controller.PopupsController; import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.SettingsAppController; -import edu.ntnu.idi.idatt2003.gruppe42.model.Difficulty; import edu.ntnu.idi.idatt2003.gruppe42.model.App; +import edu.ntnu.idi.idatt2003.gruppe42.model.Difficulty; import edu.ntnu.idi.idatt2003.gruppe42.view.Popup; 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.views.StartView; - import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Random; +/** + * Controller responsible for handling the start view and initializing the game. + */ public class StartViewController { private String username = ""; @@ -42,11 +44,17 @@ public class StartViewController { "Money Manager" ); + /** + * Constructor for StartViewController. + * + * @param application the application + * @param startView the start view + */ public StartViewController(final Millions application, final StartView startView) { this.application = application; this.startView = startView; - List popups = new ArrayList<>(); + final List popups = new ArrayList<>(); SettingsApp settingsApp = new SettingsApp(false); @@ -97,13 +105,12 @@ private void handleStart() { Difficulty difficulty = resolveDifficulty(); InputStream stocksStream = resolveStockStream(); + try (stocksStream) { + application.initGame(resolvedName, difficulty, stocksStream, popupsController); + } catch (Exception exception) { + System.out.println(exception.getMessage()); + } - application.initGame( - resolvedName, - difficulty, - stocksStream, - popupsController - ); } private InputStream resolveStockStream() { 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 4b4d772..2e91688 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 @@ -5,7 +5,7 @@ */ public enum App { APPSTORE("App Store"), - HUSTLERS("Hustlers"), + HUSTLERS("Hustlers University"), STOCK("Market"), MAIL("Mail"), NEWS("News"), 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 0bea46a..4036757 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 @@ -2,6 +2,7 @@ import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -77,4 +78,11 @@ public BigDecimal getLatestPriceChange() { int index120Ago = Math.max(0, prices.size() - 121); return getSalesPrice().subtract(prices.get(index120Ago)); } + + /** + * Reverses the price history so that the curve ends on the original starting price. + */ + public void reverseHistory() { + Collections.reverse(prices); + } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java index 31fb04f..008c885 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/StockFileHandler.java @@ -1,12 +1,12 @@ package edu.ntnu.idi.idatt2003.gruppe42.model; import edu.ntnu.idi.idatt2003.gruppe42.model.exceptions.StockFileParseException; - -import java.io.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; @@ -36,7 +36,7 @@ private StockFileHandler() {} * * @param inputStream the path to the stock data file. * @return a list of parsed {@link Stock} objects. - * @throws IOException if the file does not exist or cannot be read. + * @throws IOException if the file does not exist or cannot be read. * @throws StockFileParseException if any line contains invalid or malformed data. */ public static List readFromFile(final InputStream inputStream) @@ -117,31 +117,7 @@ public static List readFromFile(final InputStream inputStream) ); } - BigDecimal price; - - try { - price = new BigDecimal(rawPrice); - } catch (NumberFormatException e) { - throw new StockFileParseException( - lineNumber, - trimmed, - String.format( - "Price \"%s\" could not be parsed", - rawPrice - ) - ); - } - - if (price.compareTo(BigDecimal.ZERO) <= 0) { - throw new StockFileParseException( - lineNumber, - trimmed, - String.format( - "Price \"%s\" must be greater than zero", - rawPrice - ) - ); - } + BigDecimal price = getBigDecimal(rawPrice, lineNumber, trimmed); stocks.add(new Stock(symbol, company, price)); } @@ -157,4 +133,34 @@ public static List readFromFile(final InputStream inputStream) return stocks; } + + private static BigDecimal getBigDecimal(String rawPrice, int lineNumber, String trimmed) + throws StockFileParseException { + BigDecimal price; + + try { + price = new BigDecimal(rawPrice); + } catch (NumberFormatException e) { + throw new StockFileParseException( + lineNumber, + trimmed, + String.format( + "Price \"%s\" could not be parsed", + rawPrice + ) + ); + } + + if (price.compareTo(BigDecimal.ZERO) <= 0) { + throw new StockFileParseException( + lineNumber, + trimmed, + String.format( + "Price \"%s\" must be greater than zero", + rawPrice + ) + ); + } + return price; + } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/HustlersApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/HustlersApp.java index 5271734..3fc4eda 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/HustlersApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/HustlersApp.java @@ -1,8 +1,10 @@ package edu.ntnu.idi.idatt2003.gruppe42.view.apps; +import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.HustlersContentRenderer; import edu.ntnu.idi.idatt2003.gruppe42.model.App; import edu.ntnu.idi.idatt2003.gruppe42.view.Popup; -import java.util.LinkedHashMap; +import edu.ntnu.idi.idatt2003.gruppe42.view.views.components.HustlersChapters; +import edu.ntnu.idi.idatt2003.gruppe42.view.views.components.HustlersSidebarBuilder; import java.util.Map; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -10,164 +12,33 @@ import javafx.scene.control.ScrollPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; import javafx.scene.layout.VBox; -import javafx.scene.text.Font; -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; /** - * Tutorial-style popup application that provides in-game guidance - * for core mechanics such as trading, banking, messaging, and rent. - * - *

The content is structured into chapters displayed in a sidebar, - * with dynamic rendering of formatted instructional text in the main panel.

+ * Tutorial popup application for Hustler's University. */ public class HustlersApp extends Popup { + private static final String BG_ROOT = "#ffffff"; - private static final String BG_SIDEBAR = "#f4f4f4"; private static final String BG_CONTENT = "#ffffff"; private static final String ACCENT = "#a0720a"; - private static final String ACCENT_DIM = "#c9a84c"; - private static final String TEXT_PRIMARY = "#1a1a1a"; - private static final String TEXT_SEC = "#666666"; private static final String DIVIDER = "#e0e0e0"; - private static final String HOVER_BG = "#e8e8e8"; - private static final String SELECTED_BG = "#fef9ec"; - private Label activeNavItem = null; - private VBox contentBody; - private Label chapterTitleLabel; - private final Map chapters = new LinkedHashMap<>(); + + private final Map chapters = HustlersChapters.build(); + private final VBox contentBody = new VBox(14); + private final Label chapterTitleLabel = new Label(); + private final HustlersContentRenderer renderer = new HustlersContentRenderer(contentBody); /** * Constructs the Hustler's University tutorial popup. - * - *

Initializes all tutorial chapters and builds the UI layout, - * including sidebar navigation and content rendering system.

*/ public HustlersApp() { super(720, 480, 400, 200, App.HUSTLERS); - buildChapters(); buildUi(); show(); } - /** - * Builds all tutorial chapters used in the sidebar navigation. - * - *

Each chapter consists of a title and formatted instructional content.

- */ - private void buildChapters() { - chapters.put("Welcome", new String[]{ - "HUSTLER'S UNIVERSITY", - "Knowledge is power, power is influence, influence is money.\n\n" - + "This app covers every system. Read it once, and never forget.\n\n " - + "Use the sidebar to find what you need fast.\n\n" - + "Skip a chapter and you might end up as a brokey.\n" - + "Your call." - }); - - chapters.put("Trading", new String[]{ - "HOW TO TRADE", - "The market is where men is made, and babies run home to mommy.\n\n" - + "BUY LOW, SELL HIGH\n" - + "Open the Market app and browse the available stocks. " - + "Each stock has a current price and a trend indicator. " - + "Green means the price is climbing; red means it's falling.\n\n" - + "HOW TO BUY\n" - + "Select s stock, enter how many units you want, and tap TRADE. " - + "The cost is deducted from your wallet immediately, " - + "unless you don't have enough...\n\n" - + "HOW TO SELL\n" - + "Select a held stock, enter how many units you want to sell, and tap TRADE. " - + "Profit = (sell price minus buy price) times units.\n\n" - + "RISK WARNING\n" - + "Prices move every in-game hour. If a position tanks and you sell at a loss, " - + "that money is gone. Never invest money you need for rent.\n\n" - + "PRO TIPS\n" - + "Watch the trend for at least two days before buying.\n" - + "Diversify - don't put everything in one stock.\n" - + "Always keep enough cash to cover your weekly rent." - }); - - chapters.put("Bank", new String[]{ - "HOW THE BANK WORKS", - "The Bank app is your financial safety net - and leverage tool.\n\n" - + "SAVINGS ACCOUNT\n" - + "Your account displays your total balance, cash available, " - + "cash invested and total return.\n" - + "Remember, green numbers mean you're flying, red means you gotta lock in chief.\n\n" - + "PORTFOLIO\n" - + "If you like to brag about your investments, this is where it's at.\n" - + "If it's empty, what are you even doing reading this?! Get to investing!\n" - + "On the other hand, if it's full, I'm one proud G.\n\n" - + "PRO TIPS\n" - + "Clicking on a share in your portfolio will open its page in the Market app.\n" - + "Bank app + Market app = holy dual wield.\n" - + "Check your current cash balance before rent day." - }); - - chapters.put("Email", new String[]{ - "HOW EMAIL WORKS", - "Email is how the game world communicates with you.\n\n" - + "RECEIVING EMAILS\n" - + "Emails arrive automatically - from employers, contacts, and the system. " - + "A badge on the Email app icon shows unread messages.\n\n" - + "BREAKING NEWS\n" - + "These are like the news, but for emails. " - + "Check your inbox regularly so you don't miss " - + "time-sensitive opportunities and events.\n\n" - + "PRO TIPS\n" - + "Read every email when it arrives - some are one-time chances.\n" - + "Read the entire newsletter carefully - they might fool you.\n" - }); - - chapters.put("Rent", new String[]{ - "HOW RENT WORKS", - "Rent is collected automatically at the end of every in-game week.\n" - + "WHEN IT'S DUE\n" - + "Rent is charged on Saturday 00:00, right before the new week begins, " - + "so you have until Friday to make sure you're set.\n\n" - + "HOW IT'S PAID\n" - + "The rent amount is deducted directly from your wallet - automatically, " - + "no action needed on your part. Make sure the money is in your wallet, " - + "NOT locked in savings/investments.\n\n" - + "WHAT IF YOU CAN'T PAY?\n" - + "If your current cash is lower than the rent amount when Sunday night hits:\n" - + " - You lose ONE heart.\n" - + " - Losing all hearts ends the game.\n\n" - + "HOW MUCH IS RENT?\n" - + "Your rent amount is shown on the Rent Day popup at Saturday 00:00. " - + "You can count on it being $50 the first couple weeks.\n" - + "It may increase as you progress - so keep earning.\n\n" - + "PRO TIPS\n" - + "Check your wallet balance on Friday.\n" - + "Sell shares if you are short." - }); - - chapters.put("Hearts", new String[]{ - "YOUR LIVES - HEARTS", - "Hearts represent how many chances you have left before it's game over.\n\n" - + "STARTING HEARTS\n" - + "You begin the game with a set number of hearts shown in the footer.\n\n" - + "LOSING A HEART\n" - + "You lose one heart if your wallet cannot cover rent on Sunday night.\n\n" - + "GAME OVER\n" - + "When you run out of hearts, the game ends. " - + "There is no revive. Manage your money or start over.\n\n" - + "CAN YOU GAIN HEARTS BACK?\n" - + "Nah bro, work with the cards you've been dealt - protect the ones you have.\n\n" - + "PRO TIPS\n" - + "Treat every heart like real money. One bad week can cascade.\n" - + "If you're at one heart, play conservative - protect your wallet above all." - }); - } - - /** - * Builds the full UI layout including sidebar navigation and content area. - */ private void buildUi() { - chapterTitleLabel = new Label(); chapterTitleLabel.setStyle( "-fx-text-fill: " + ACCENT + ";" + "-fx-font-size: 11px;" @@ -176,109 +47,27 @@ private void buildUi() { + "-fx-letter-spacing: 1px;" ); - contentBody = new VBox(14); contentBody.setPadding(new Insets(22, 26, 22, 26)); contentBody.setStyle("-fx-background-color: " + BG_CONTENT + ";"); - HBox root = new HBox(); - root.setStyle("-fx-background-color: " + BG_ROOT + ";"); - root.setPrefSize(720, 480); - - VBox sidebar = buildSidebar(); + String firstKey = chapters.keySet().iterator().next(); + VBox sidebar = new HustlersSidebarBuilder().build(chapters, this::applyChapter, firstKey); sidebar.setPrefWidth(148); sidebar.setMinWidth(148); sidebar.setMaxWidth(148); - VBox contentWrapper = buildContentWrapper(); - HBox.setHgrow(contentWrapper, Priority.ALWAYS); + HBox root = new HBox(sidebar, buildContentWrapper()); + root.setStyle("-fx-background-color: " + BG_ROOT + ";"); + root.setPrefSize(720, 480); + HBox.setHgrow(root.getChildren().get(1), Priority.ALWAYS); - root.getChildren().addAll(sidebar, contentWrapper); content.getChildren().add(root); - String firstKey = chapters.keySet().iterator().next(); - applyChapter(firstKey); - } - - /** - * Constructs the sidebar containing chapter navigation items. - * - * @return configured sidebar VBox - */ - private VBox buildSidebar() { - VBox sidebar = new VBox(); - sidebar.setStyle( - "-fx-background-color: " + BG_SIDEBAR + ";" - + "-fx-border-color: " + DIVIDER + ";" - + "-fx-border-width: 0 1 0 0;" - ); - - VBox header = new VBox(); - header.setPadding(new Insets(14, 12, 12, 12)); - header.setStyle("-fx-border-color: " + DIVIDER + "; -fx-border-width: 0 0 1 0;"); - - Label title = new Label("Hustler's\nUniversity"); - title.setStyle( - "-fx-text-fill: " + ACCENT + ";" - + "-fx-font-size: 11px;" - + "-fx-font-weight: bold;" - + "-fx-font-family: 'Georgia';" - + "-fx-line-spacing: 2;" - ); - header.getChildren().add(title); - - VBox nav = new VBox(1); - nav.setPadding(new Insets(8, 6, 8, 6)); - - for (String chapter : chapters.keySet()) { - Label item = getLabel(chapter); - - nav.getChildren().add(item); - } - - ScrollPane navScroll = new ScrollPane(nav); - navScroll.setFitToWidth(true); - navScroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); - navScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - VBox.setVgrow(navScroll, Priority.ALWAYS); - - sidebar.getChildren().addAll(header, navScroll); - return sidebar; - } - - private Label getLabel(String chapter) { - Label item = new Label(chapter); - item.setPadding(new Insets(8, 10, 8, 10)); - item.setMaxWidth(Double.MAX_VALUE); - item.setStyle(navStyle(false)); - - item.setOnMouseEntered(e -> { - if (item != activeNavItem) { - item.setStyle(navHoverStyle()); - } - }); - item.setOnMouseExited(e -> { - if (item != activeNavItem) { - item.setStyle(navStyle(false)); - } - }); - item.setOnMouseClicked(e -> { - if (activeNavItem != null) { - activeNavItem.setStyle(navStyle(false)); - } - activeNavItem = item; - item.setStyle(navStyle(true)); - applyChapter(chapter); - }); - return item; + applyChapter(chapters.keySet().iterator().next()); } - /** - * Builds the main content wrapper containing the chapter title and scrollable content area. - * - * @return configured content wrapper VBox - */ private VBox buildContentWrapper() { - HBox titleBar = new HBox(); + HBox titleBar = new HBox(chapterTitleLabel); titleBar.setPadding(new Insets(13, 20, 13, 20)); titleBar.setAlignment(Pos.CENTER_LEFT); titleBar.setStyle( @@ -286,14 +75,15 @@ private VBox buildContentWrapper() { + "-fx-border-color: " + DIVIDER + ";" + "-fx-border-width: 0 0 1 0;" ); - titleBar.getChildren().add(chapterTitleLabel); ScrollPane scroll = new ScrollPane(contentBody); scroll.setFitToWidth(true); scroll.setStyle( - "-fx-background: " + BG_CONTENT + ";" - + "-fx-background-color: " + BG_CONTENT + ";" - ); + "-fx-background: " + + BG_CONTENT + + "; -fx-background-color: " + + BG_CONTENT + + ";"); scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); VBox.setVgrow(scroll, Priority.ALWAYS); @@ -302,105 +92,12 @@ private VBox buildContentWrapper() { return wrapper; } - /** - * Applies the selected chapter and renders its content. - * - * @param key the chapter key to display - */ - private void applyChapter(String key) { - String[] content = chapters.get(key); - if (content == null) { + private void applyChapter(final String key) { + String[] chapter = chapters.get(key); + if (chapter == null) { return; } - chapterTitleLabel.setText(content[0]); - contentBody.getChildren().clear(); - renderContent(content[1]); - } - - /** - * Renders formatted instructional text into the content body. - * - * @param rawText raw chapter text to render - */ - private void renderContent(String rawText) { - for (String para : rawText.split("\n\n")) { - para = para.trim(); - if (para.isEmpty()) { - continue; - } - if (isSectionHeader(para)) { - Region divider = new Region(); - divider.setPrefHeight(1); - divider.setMaxWidth(32); - divider.setStyle("-fx-background-color: " + ACCENT_DIM + ";"); - VBox.setMargin(divider, new Insets(4, 0, 5, 0)); - - Label header = new Label(para); - header.setStyle( - "-fx-text-fill: " + ACCENT + ";" - + "-fx-font-size: 10px;" - + "-fx-font-weight: bold;" - + "-fx-font-family: 'Georgia';" - ); - contentBody.getChildren().addAll(divider, header); - } else { - TextFlow flow = new TextFlow(); - flow.setLineSpacing(3); - String[] lines = para.split("\n"); - for (int i = 0; i < lines.length; i++) { - Text t = new Text((i > 0 ? "\n" : "") + lines[i]); - t.setStyle("-fx-fill: " + TEXT_PRIMARY + ";"); - t.setFont(Font.font("Georgia", 12.5)); - flow.getChildren().add(t); - } - contentBody.getChildren().add(flow); - } - } - } - - /** - * Determines whether a paragraph should be rendered as a section header. - * - * @param text input text - * @return true if text is considered a header, false otherwise - */ - private boolean isSectionHeader(String text) { - String stripped = text.replaceAll("[^a-zA-Z]", ""); - if (stripped.isEmpty()) { - return false; - } - return stripped.equals(stripped.toUpperCase()) && text.length() < 60; - } - - /** - * Generates CSS style string for navigation items. - * - * @param selected whether the item is currently selected - * @return CSS style string - */ - private String navStyle(boolean selected) { - return "-fx-text-fill: " + (selected ? ACCENT : TEXT_SEC) + ";" - + "-fx-font-size: 11px;" - + "-fx-font-family: 'Georgia';" - + "-fx-background-color: " + (selected ? SELECTED_BG : "transparent") + ";" - + "-fx-background-radius: 3;" - + "-fx-cursor: hand;" - + (selected - ? "-fx-border-color: " + ACCENT_DIM + "; -fx-border-width: 0 0 0 2; -fx-border-radius: 0;" - : ""); - } - - /** - * Generates CSS style string for hovered navigation items. - * - * @return CSS style string for hover state - */ - private String navHoverStyle() { - return "-fx-text-fill: " + TEXT_PRIMARY + ";" - + "-fx-font-size: 11px;" - + "-fx-font-family: 'Georgia';" - + "-fx-background-color: " + HOVER_BG + ";" - + "-fx-background-radius: 3;" - + "-fx-cursor: hand;"; + chapterTitleLabel.setText(chapter[0]); + renderer.render(chapter[1]); } } \ No newline at end of file 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 5042f42..6666036 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 @@ -65,8 +65,8 @@ public SettingsApp(final boolean isLoggedIn) { masterVolumeSlider = createSlider(); sfxVolumeSlider = createSlider(); - gradientColorA = new ColorPicker(Color.web("#3a5068")); - gradientColorB = new ColorPicker(Color.web("#1a2a3a")); + gradientColorA = new ColorPicker(Color.web("#263B6A")); + gradientColorB = new ColorPicker(Color.web("#6984A9")); gradientDirection = new ComboBox<>(); gradientDirection.getItems().addAll( "Top to Bottom", 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 5832074..eb188fb 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 @@ -145,7 +145,8 @@ private void loadStylesheets() { resource("/css/rapport.css"), resource("/css/filepicker.css"), resource("/css/settings.css"), - resource("/css/warning.css") + resource("/css/warning.css"), + resource("/css/hustlers.css") ); } catch (Exception e) { System.err.println("[CSS] Failed to load stylesheets: " + e.getMessage()); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/views/components/HustlersChapters.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/views/components/HustlersChapters.java new file mode 100644 index 0000000..d0082c5 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/views/components/HustlersChapters.java @@ -0,0 +1,126 @@ +package edu.ntnu.idi.idatt2003.gruppe42.view.views.components; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Provides the tutorial chapter content for Hustler's University. + */ +public class HustlersChapters { + + private HustlersChapters() {} + + /** + * Returns all chapters as an ordered map of title to [heading, body]. + * + * @return chapter map + */ + public static Map build() { + Map chapters = new LinkedHashMap<>(); + + chapters.put("Welcome", new String[]{ + "HUSTLER'S UNIVERSITY", + "Knowledge is power, power is influence, influence is money.\n\n" + + "This app covers every system. Read it once, and never forget.\n\n " + + "Use the sidebar to find what you need fast.\n\n" + + "Skip a chapter and you might end up as a brokey.\n" + + "Your call." + }); + + chapters.put("Trading", new String[]{ + "HOW TO TRADE", + "The market is where men are made, and babies run home to mommy.\n\n" + + "BUY LOW, SELL HIGH\n" + + "Open the Market app and browse the available stocks. " + + "Each stock has a current price and a trend indicator. " + + "Green means the price is climbing; red means it's falling.\n\n" + + "HOW TO BUY\n" + + "Select a stock, enter how many units you want, and tap TRADE. " + + "The cost is deducted from your wallet immediately, " + + "unless you don't have enough...\n\n" + + "HOW TO SELL\n" + + "Select a held stock, enter how many units you want to sell, and tap TRADE. " + + "Profit = (sell price minus buy price) times units.\n\n" + + "RISK WARNING\n" + + "Prices move every in-game hour. If a position tanks and you sell at a loss, " + + "that money is gone. Never invest money you need for rent.\n\n" + + "PRO TIPS\n" + + "Watch the trend for at least two days before buying.\n" + + "Diversify - don't put everything in one stock.\n" + + "Always keep enough cash to cover your weekly rent." + }); + + chapters.put("Bank", new String[]{ + "HOW THE BANK WORKS", + "The bank app is your financial safety net and leverage tool.\n\n" + + "SAVINGS ACCOUNT\n" + + "Your account displays your total balance, cash available, " + + "cash invested and total return.\n" + + "Remember, green numbers mean you're flying, red means you gotta lock in chief.\n\n" + + "PORTFOLIO\n" + + "If you like to brag about your investments, this is where it's at.\n" + + "If it's empty, what are you even doing reading this?! Get to investing!\n" + + "On the other hand, if it's full, I'm one proud G.\n\n" + + "PRO TIPS\n" + + "Clicking on a share in your portfolio will open its page in the Market app.\n" + + "Bank app + Market app = holy dual wield.\n" + + "Check your current cash balance before rent day." + }); + + chapters.put("Email", new String[]{ + "HOW EMAIL WORKS", + "Email is how the game world communicates with you.\n\n" + + "RECEIVING EMAILS\n" + + "Emails arrive automatically from news services. " + + "BREAKING NEWS\n" + + "These are like the news, but for emails. " + + "Check your inbox regularly so you don't miss " + + "time-sensitive opportunities and events.\n\n" + + "PRO TIPS\n" + + "Read every email when they arrive - some are one-time chances.\n" + + "Read the entire newsletter carefully - they might fool you.\n" + }); + + chapters.put("Rent", new String[]{ + "HOW RENT WORKS", + "Rent is collected automatically at the end of every in-game week.\n\n" + + "WHEN IT'S DUE\n" + + "Rent is charged on Saturday 00:00, " + + "so you have until Friday to make sure you're set.\n\n" + + "HOW IT'S PAID\n" + + "The rent amount is deducted directly from your wallet automatically, " + + "no action needed on your part. Make sure the money is in your wallet and " + + "NOT locked in investments.\n\n" + + "WHAT IF YOU CAN'T PAY?\n" + + "If your current cash is lower than the rent amount when Sunday night hits:\n" + + " - You lose ONE heart.\n" + + " - Losing all hearts ends the game.\n\n" + + "HOW MUCH IS RENT?\n" + + "Your rent amount is shown at Rent Day, Saturday 00:00. " + + "You can count on it being $50 the first couple weeks.\n" + + "It may increase as you progress - so keep earning.\n\n" + + "PRO TIPS\n" + + "Check your wallet balance on Friday.\n" + + "Sell shares if you are short." + }); + + chapters.put("Hearts", new String[]{ + "HOW HEARTS WORK", + "Hearts represent how many chances you have left before it's game over.\n\n" + + "STARTING HEARTS\n" + + "You begin the game with a set number of hearts shown in the footer.\n\n" + + "LOSING A HEART\n" + + "You lose one heart if your wallet cannot cover rent on Sunday night.\n\n" + + "GAME OVER\n" + + "When you run out of hearts, the game ends. " + + "There is no revive. Manage your money or start over.\n\n" + + "CAN YOU GAIN HEARTS BACK?\n" + + "Nah bro, work with the cards you've been dealt - protect the ones you have.\n\n" + + "PRO TIPS\n" + + "Treat every heart like real money. One bad week can cascade.\n" + + "If you're at one heart, play conservative - protect your wallet above all." + }); + + return chapters; + } +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/views/components/HustlersSidebarBuilder.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/views/components/HustlersSidebarBuilder.java new file mode 100644 index 0000000..d557b61 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/views/components/HustlersSidebarBuilder.java @@ -0,0 +1,104 @@ +package edu.ntnu.idi.idatt2003.gruppe42.view.views.components; + +import java.util.Map; +import java.util.function.Consumer; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * Builds the sidebar navigation for Hustler's University. + */ +public class HustlersSidebarBuilder { + + private Label activeNavItem; + + /** + * Builds the sidebar VBox. + * + * @param chapters ordered chapter map + * @param onChapterSelect callback invoked with the selected chapter key + * @param initialKey the initial chapter key to select + * @return the configured sidebar + */ + public VBox build( + final Map chapters, + final Consumer onChapterSelect, + final String initialKey) { + + VBox sidebar = new VBox(); + sidebar.getStyleClass().add("hustlers-sidebar"); + + VBox header = new VBox(); + header.setPadding(new Insets(14, 12, 12, 12)); + header.getStyleClass().add("hustlers-sidebar-header"); + + Label title = new Label("Hustler's\nUniversity"); + title.getStyleClass().add("hustlers-sidebar-title"); + header.getChildren().add(title); + + VBox nav = new VBox(1); + nav.setPadding(new Insets(8, 6, 8, 6)); + for (String chapter : chapters.keySet()) { + nav.getChildren().add(buildNavItem(chapter, onChapterSelect)); + } + + ScrollPane navScroll = new ScrollPane(nav); + navScroll.setFitToWidth(true); + navScroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + navScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + VBox.setVgrow(navScroll, Priority.ALWAYS); + + sidebar.getChildren().addAll(header, navScroll); + + for (javafx.scene.Node node : nav.getChildren()) { + if (node instanceof Label item && item.getText().equals(initialKey)) { + activeNavItem = item; + setSelected(item, true); + break; + } + } + + return sidebar; + } + + private Label buildNavItem(final String chapter, final Consumer onSelect) { + Label item = new Label(chapter); + item.setPadding(new Insets(8, 10, 8, 10)); + item.setMaxWidth(Double.MAX_VALUE); + item.getStyleClass().add("hustlers-nav-item"); + + item.setOnMouseEntered(e -> { + if (item != activeNavItem) { + item.getStyleClass().remove("hustlers-nav-item-selected"); + } + }); + item.setOnMouseExited(e -> { + if (item != activeNavItem) { + setSelected(item, false); + } + }); + item.setOnMouseClicked(e -> { + if (activeNavItem != null) { + setSelected(activeNavItem, false); + } + activeNavItem = item; + setSelected(item, true); + onSelect.accept(chapter); + }); + + return item; + } + + private void setSelected(final Label item, final boolean selected) { + item.getStyleClass().remove( + selected ? "hustlers-nav-item" : "hustlers-nav-item-selected"); + if (!item.getStyleClass().contains( + selected ? "hustlers-nav-item-selected" : "hustlers-nav-item")) { + item.getStyleClass().add( + selected ? "hustlers-nav-item-selected" : "hustlers-nav-item"); + } + } +} \ No newline at end of file diff --git a/src/main/resources/css/hustlers.css b/src/main/resources/css/hustlers.css new file mode 100644 index 0000000..3dc0214 --- /dev/null +++ b/src/main/resources/css/hustlers.css @@ -0,0 +1,44 @@ +.hustlers-sidebar { + -fx-background-color: #f4f4f4; + -fx-border-color: #e0e0e0; + -fx-border-width: 0 1 0 0; +} + +.hustlers-sidebar-header { + -fx-border-color: #e0e0e0; + -fx-border-width: 0 0 1 0; +} + +.hustlers-sidebar-title { + -fx-text-fill: #a0720a; + -fx-font-size: 11px; + -fx-font-weight: bold; + -fx-font-family: 'Georgia'; + -fx-line-spacing: 2; +} + +.hustlers-nav-item { + -fx-text-fill: #666666; + -fx-font-size: 11px; + -fx-font-family: 'Georgia'; + -fx-background-color: transparent; + -fx-background-radius: 3; + -fx-cursor: hand; +} + +.hustlers-nav-item:hover { + -fx-text-fill: #1a1a1a; + -fx-background-color: #e8e8e8; +} + +.hustlers-nav-item-selected { + -fx-text-fill: #a0720a; + -fx-font-size: 11px; + -fx-font-family: 'Georgia'; + -fx-background-color: #fef9ec; + -fx-background-radius: 3; + -fx-cursor: hand; + -fx-border-color: #c9a84c; + -fx-border-width: 0 0 0 2; + -fx-border-radius: 0; +} \ No newline at end of file