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 7406913..513a9ca 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 @@ -6,6 +6,9 @@ import edu.ntnu.idi.idatt2003.gruppe42.model.transaction.Transaction; import edu.ntnu.idi.idatt2003.gruppe42.view.apps.StockApp; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; import javafx.application.Platform; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -17,39 +20,30 @@ import javafx.scene.layout.Region; import javafx.scene.layout.VBox; -/** - * Controller for the Stock application. - * - *

This controller manages: - *

- * - *

It acts as the bridge between the market model, player portfolio, - * and the stock trading UI. - */ +/** Controller for StockApp. */ public final class StockAppController implements AppController { private final MarketController marketController; private final StockApp stockApp; private final Player player; private Stock currentStock; + + private enum SortType { + ALPHA, GAINER, PRICE + } + + private SortType currentSort = SortType.ALPHA; + private boolean isDescending = false; + + /** True while the game is on Saturday or Sunday. */ private boolean isWeekend = false; + + /** Called when the player clicks Trade during a weekend. */ private Runnable onMarketClosed; + private Runnable onInsufficientFunds; - /** - * Constructs the Stock application controller and initializes stock data, - * UI listeners, and trading actions. - * - * @param stockApp the stock application view - * @param marketController the market controller providing stock data and trading logic - * @param player the player performing trades - */ + /** Constructs the controller. */ public StockAppController( final StockApp stockApp, final MarketController marketController, @@ -59,8 +53,7 @@ public StockAppController( this.marketController = marketController; this.player = player; - stockApp.getStockList().getItems() - .addAll(marketController.getExchange().getAllStocks()); + updateStockList(); stockApp.getStockList().setCellFactory(lv -> createStockCell()); @@ -69,10 +62,39 @@ public StockAppController( if (currentStock != null && !newValue.isEmpty()) { navigateToSearch(); } - stockApp.getStockList().getItems() - .setAll(marketController.getMarket(newValue)); + updateStockList(); }); + stockApp.getAlphaButton().setOnAction(e -> { + if (currentSort == SortType.ALPHA) { + isDescending = !isDescending; + } else { + currentSort = SortType.ALPHA; + isDescending = false; + } + updateStockList(); + }); + + stockApp.getGainerButton().setOnAction(e -> { + if (currentSort == SortType.GAINER) { + isDescending = !isDescending; + } else { + currentSort = SortType.GAINER; + isDescending = false; + } + updateStockList(); + }); + + stockApp.getPriceButton().setOnAction(e -> { + if (currentSort == SortType.PRICE) { + isDescending = !isDescending; + } else { + currentSort = SortType.PRICE; + isDescending = true; + } + updateStockList(); + }); + stockApp.getQuantitySpinner().valueProperty().addListener( (obs, old, newValue) -> { if (currentStock != null) { @@ -85,11 +107,7 @@ public StockAppController( stockApp.getBackButton().setOnAction(e -> navigateToSearch()); } - /** - * Navigates the UI to display details for a selected stock. - * - * @param stock the stock to display - */ + /** Navigates to stock detail page. */ public void navigateToStock(final Stock stock) { currentStock = stock; stock.getStockGraph().setVisibility(true); @@ -98,6 +116,7 @@ public void navigateToStock(final Stock stock) { updateReceipt(); } + /** Navigates to search page. */ private void navigateToSearch() { if (currentStock != null) { currentStock.getStockGraph().setVisibility(false); @@ -106,6 +125,7 @@ private void navigateToSearch() { stockApp.showSearchPage(); } + /** Creates list cell for stock row. */ private ListCell createStockCell() { return new ListCell<>() { private final HBox row = new HBox(15); @@ -119,7 +139,7 @@ private ListCell createStockCell() { row.setAlignment(Pos.CENTER_LEFT); row.getStyleClass().add("stock-list-item"); - final VBox names = new VBox(2, symbolLabel, companyLabel); + VBox names = new VBox(2, symbolLabel, companyLabel); symbolLabel.getStyleClass().add("stock-symbol"); companyLabel.getStyleClass().add("stock-company"); @@ -167,20 +187,14 @@ protected void updateItem(final Stock stock, final boolean empty) { }; } - /** - * Sets whether the market is currently in weekend mode. - * - *

When weekend mode is active, trading is disabled. - * - * @param weekend true if it is weekend, false otherwise - */ + /** Sets weekend status. */ public void setWeekend(final boolean weekend) { this.isWeekend = weekend; - Platform.runLater(this::updateTradeUi); + Platform.runLater(this::updateTradeUI); } /** Updates the Trade button and spinner state. */ - private void updateTradeUi() { + private void updateTradeUI() { Button tradeButton = stockApp.getConfirmButton(); var spinner = stockApp.getQuantitySpinner(); @@ -212,24 +226,17 @@ private void updateTradeUi() { } } - /** - * Sets a callback executed when the user attempts to trade while the market is closed. - * - * @param callback action to run when market is closed - */ + /** Sets market closed callback. */ public void setOnMarketClosed(final Runnable callback) { this.onMarketClosed = callback; } - /** - * Sets a callback executed when a buy transaction fails due to insufficient funds. - * - * @param callback action to run when funds are insufficient - */ + /** Sets insufficient funds callback. */ public void setOnInsufficientFunds(final Runnable callback) { this.onInsufficientFunds = callback; } + /** Handles trade button action. */ private void handleTransaction() { if (isWeekend) { if (onMarketClosed != null) { @@ -261,15 +268,18 @@ private void handleTransaction() { } } + /** Buys given stock. */ private void handleBuy(final Stock stock, final BigDecimal quantity) throws Exception { Transaction transaction = marketController.getExchange() .buy(stock.getSymbol(), quantity); if (transaction != null) { transaction.commit(player); player.getPortfolio().addShare(transaction.getShare()); + player.updateStatus(); } } + /** Sells given stock. */ private void handleSell(final Stock stock, final BigDecimal quantity) { player.getPortfolio().getShares().stream() .filter(share -> share.getStock().getSymbol().equals(stock.getSymbol())) @@ -288,28 +298,47 @@ private void handleSell(final Stock stock, final BigDecimal quantity) { System.out.println("Transaction failed"); } player.getPortfolio().removeShare(transaction.getShare()); + player.updateStatus(); } }); } + /** Updates receipt view. */ private void updateReceipt() { if (currentStock != null) { Platform.runLater(() -> { - stockApp.getReceipt().update( + stockApp.getReceipt().update( currentStock, new BigDecimal(stockApp.getQuantitySpinner().getValue()) - ); - updateTradeUi(); + ); + updateTradeUI(); }); } } - /** - * Advances the stock market simulation by one tick. - * - *

This updates stock prices, refreshes the UI list, and updates - * the currently selected stock view if applicable. - */ + /** Updates the stock list with current sort and filter. */ + private void updateStockList() { + List stocks = new ArrayList<>(marketController.getMarket(stockApp.getSearchField().getText())); + Comparator comparator; + switch (currentSort) { + case GAINER: + comparator = Comparator.comparing(Stock::getLatestPriceChange); + break; + case PRICE: + comparator = Comparator.comparing(Stock::getSalesPrice); + break; + default: + comparator = Comparator.comparing(Stock::getSymbol); + break; + } + if (isDescending) { + comparator = comparator.reversed(); + } + stocks.sort(comparator); + stockApp.getStockList().getItems().setAll(stocks); + } + + /** Processes next tick. */ @Override public void nextTick() { marketController.updateMarket(); 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 b866710..7e340f4 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 @@ -121,7 +121,7 @@ public DesktopViewController( () -> { News news = new News(event.company(), event.trend()); mailController.createMessage( - news.generateAuthor(), event.company(), news.generateNews()); + news.generateAuthor(), "BREAKING NEWS", news.generateNews()); })); } @@ -215,8 +215,10 @@ private void setupDesktopUi() { } private void setupListeners() { - timeAndWeatherController.hourProperty().addListener((obs, ov, nv) -> - desktopView.updateClock(timeAndWeatherController.getTimeString())); + timeAndWeatherController.hourProperty().addListener((obs, ov, nv) -> { + desktopView.updateClock(timeAndWeatherController.getTimeString()); + player.updateStatus(); + }); timeAndWeatherController.dayIndexProperty().addListener((obs, ov, nv) -> { String dayName = timeAndWeatherController.getDayOfWeekString(); @@ -235,6 +237,7 @@ private void setupListeners() { player.getNameProperty().addListener((obs, ov, nv) -> desktopView.updatePlayerName(nv)); + player.updateStatus(); player.getStatusProperty().addListener((obs, ov, nv) -> desktopView.updatePlayerStatus(nv.name())); @@ -264,6 +267,7 @@ private void doAdvanceWeek() { marketController.advanceWeek(); timeAndWeatherController.advanceWeek(); mailController.clearMessages(); + player.updateStatus(); } private void showMarketClosedWarning() { diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/News.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/News.java index 87e41ab..8eb8d75 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/News.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/model/News.java @@ -4,49 +4,66 @@ import java.util.List; import java.util.Random; -/** - * Generates news articles and author information for a given company based on its market trend. - */ public class News { private final String company; private final int trend; private final Random random = new Random(); - - /** - * Constructs a News generator for the given company and trend. - * - * @param company the name of the company the news is about. - * @param trend a positive value for good news, negative for bad news. - */ - public News(String company, int trend) { + public News(String company, int trend){ this.company = company; this.trend = trend; } - - /** - * Returns a randomly selected author email address for the news article. - * - * @return the author's email address. - */ - public String generateAuthor() { + public String generateAuthor(){ List authors = new ArrayList<>(); authors.add("market@stocks.com"); + authors.add("myfatherworksatmicrosoft@hotmail.com"); + authors.add("definitely.obama@whitehouse.gov"); + authors.add("bigfoot.financial@yeti.net"); + authors.add("notaciaagent@cia.gov"); + authors.add("iamyourfather@deathstar.empire"); + authors.add("shakespeareswifi@globe.theatre"); + authors.add("dinosaur.investor@jurassic.park"); + authors.add("flat.earth.stonks@edgeoftheworld.com"); + authors.add("timtraveler2089@yahoo.com"); + authors.add("schroedingers.portfolio@maybe.com"); + authors.add("elon@mymusk.biz"); + authors.add("god@heaven.org"); + authors.add("santaclaus@northpole.gifts"); + authors.add("area51.leaks@ufo.gov"); + authors.add("theguywhosneezedonthestockmarket@2008crash.com"); + authors.add("nephew.of.warren.buffet@gmail.com"); + authors.add("batman@notbruce.wayne"); + authors.add("mycatinvestedmysavings@meow.finance"); + authors.add("nsa.definitely.not.reading.this@nsa.gov"); return authors.get(random.nextInt(authors.size())); } - - /** - * Generates a news headline based on the company's trend direction. - * - * @return a positive headline if the trend is good, a negative headline otherwise. - */ - public String generateNews() { + public String generateNews(){ if (trend > 0) { List goodNews = new ArrayList<>(); - goodNews.add("Turns out the product of " + company + " is good for the environment"); + goodNews.add("Turns out the product of " + company + " is good for the environment."); + goodNews.add("Beaver population increased due to the efforts of " + company + "."); + goodNews.add("Eminem dropped new hit single, namedropping " + company + "."); + goodNews.add("Founder of " + company + " seen feeding the homeless."); + goodNews.add(company + " got frontpage in new forbes magazine, on top companies to invest in."); + goodNews.add("IShowSpeed seen using " + company + " products in the background of viral livestream."); + goodNews.add("New studies show that " + company + " took part in inventing the rainbow back in 4,542,997,974 BC"); + goodNews.add(company + " has made breakthroughs in the field of artificial intelligence."); + goodNews.add("After the release of the brand new logo of " + company + ", the logo has been called the \"a glimpse of the future\"."); + goodNews.add("EX-EMPLOYEES of " + company + " REVEAL that the work environment and safety regulations are not up to basic standards but far surpassing them."); + goodNews.add("Founder of " + company + " seen rudely pushing a homeless man out of the road, saving his life."); return goodNews.get(random.nextInt(goodNews.size())); } List badNews = new ArrayList<>(); - badNews.add("Turns out the product of " + company + " is bad for the environment"); + badNews.add("Turns out the product of " + company + " is bad for the environment."); + badNews.add("Founder of " + company + " seen rudely pushing a homeless man out of the way."); + badNews.add(company + " mentioned in epstein files."); + badNews.add(company + " has created many dams for beavers, causing them to lay around in the sun all day and die of laziness."); + badNews.add(company + " initiated a campaign to feed the homeless with burgers, using dog meat. This has decreased the number of starvation deaths among the homeless."); + badNews.add("Turns out " + company + " created the first covid-19 vaccines, which are not safe for humans."); + badNews.add("Extensive research of " + company + " shows financial holes in the company."); + badNews.add("Founder of " + company + " heiling at press conference discussing great ideas."); + badNews.add("EX-EMPLOYEES of " + company + " REVEAL that the work environment and safety regulations are not up to basic standards."); + badNews.add("US-Army found using " + company + " issued weapons that malfunctioned causing mass civilian casualties."); + badNews.add(company + " got frontpage in new forbes magazine, on top companies not to invest in."); return badNews.get(random.nextInt(badNews.size())); } } 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 a8f24c6..d07f6ff 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 @@ -3,18 +3,10 @@ import edu.ntnu.idi.idatt2003.gruppe42.model.exceptions.InsufficientFunds; import edu.ntnu.idi.idatt2003.gruppe42.model.transaction.TransactionArchive; import java.math.BigDecimal; -import javafx.beans.property.IntegerProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; - -/** - * Represents a player in the simulation. - * - *

Tracks the player's name, funds, portfolio, lives, and progression status. - */ + +import javafx.beans.property.*; + +/** Represents a player. */ public class Player { private static final int MAX_LIVES = 3; @@ -27,12 +19,6 @@ public class Player { private final IntegerProperty lives = new SimpleIntegerProperty(MAX_LIVES); private final ObjectProperty status = new SimpleObjectProperty<>(Status.NOVICE); - /** - * Constructs a new Player with the given name and starting funds. - * - * @param name the player's display name. - * @param startingMoney the amount of money the player starts with. - */ public Player(final String name, final BigDecimal startingMoney) { this.name.set(name); this.startingMoney = startingMoney; @@ -41,18 +27,27 @@ public Player(final String name, final BigDecimal startingMoney) { this.transactionArchive = new TransactionArchive(); } + + /** @return name property. */ public StringProperty getNameProperty() { return name; } - + /** @return player's name. */ public String getName() { return name.get(); } + /** Sets player's name. */ + public void setName(final String name) { + this.name.set(name); + } + + /** @return starting money. */ public BigDecimal getStartingMoney() { return startingMoney; } + /** @return current money. */ public BigDecimal getMoney() { return money; } @@ -61,106 +56,77 @@ public String getStatus() { return status.get().name(); } - /** - * Sets the player's display name. - * - * @param name the new name. - */ - public void setName(final String name) { - this.name.set(name); - } - - /** - * Adds the given amount to the player's funds. - * - * @param amount the amount to add. - */ + /** Adds money. */ public void addMoney(final BigDecimal amount) { money = money.add(amount); + updateStatus(); } - /** - * Withdraws the given amount from the player's funds. - * - * @param amount the amount to withdraw. - * @throws InsufficientFunds if the player does not have enough money. - */ + /** Withdraws money. */ 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); + updateStatus(); } - /** - * Deducts rent from the player's funds, allowing the balance to go negative. - * - * @param amount the rent amount to deduct. - */ + /** Deducts rent. */ public void deductRent(final BigDecimal amount) { money = money.subtract(amount); + updateStatus(); } + /** @return true if in debt. */ public boolean isInDebt() { return money.compareTo(BigDecimal.ZERO) < 0; } + /** @return total net worth. */ public BigDecimal getNetWorth() { return money.add(portfolio.getNetWorth()); } + + /** @return lives property. */ public IntegerProperty getLivesProperty() { return lives; } + /** @return number of lives. */ public int getLives() { return lives.get(); } - /** - * Decrements the player's life count by one, to a minimum of zero. - */ + /** Decrements life. */ public void loseLife() { lives.set(Math.max(0, lives.get() - 1)); } - /** - * Returns the player's stock portfolio. - * - * @return the {@link Portfolio}. - */ + + /** @return player's portfolio. */ public Portfolio getPortfolio() { return portfolio; } - /** - * Returns the player's transaction history. - * - * @return the {@link TransactionArchive}. - */ + /** @return transaction archive. */ public TransactionArchive getTransactionArchive() { return transactionArchive; } - /** - * Evaluates and returns the player's current status property. - * - *

Status is determined by the number of weeks the player has been active - * and their net worth relative to their starting money. - * - * @return the status {@link ObjectProperty}, updated to reflect the current progression level. - */ - public ObjectProperty getStatusProperty() { + + /** Updates the player's status. */ + public void updateStatus() { final int investorWeeks = 10; final int speculatorWeeks = 20; - final double investorMultiplier = 1.2; - final double speculatorMultiplier = 2.0; + final double investorMult = 1.2; + final double speculatorMult = 5.0; 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 isSpeculator = weeksActive >= speculatorWeeks && netWorth.compareTo(speculatorTarget) >= 0; @@ -174,6 +140,11 @@ public ObjectProperty getStatusProperty() { } else { status.set(Status.NOVICE); } + } + + + /** @return player status property. */ + public ObjectProperty getStatusProperty() { return status; } } \ No newline at end of file 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 0604a52..9751c9a 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 @@ -6,26 +6,14 @@ import java.util.ArrayList; import java.util.List; -/** - * Represents a publicly traded stock of a company in the simulation. - * - *

Tracks historical prices, provides market data, and manages - * its associated price graph and price update logic. - */ +/** Represents a stock of a company. */ public class Stock { private final String symbol; private final String company; private StockGraph stockGraph; - private final List prices = new ArrayList<>(); - private final StockController stockController; - - /** - * Constructs a new Stock with an initial sales price. - * - * @param symbol the ticker symbol of the stock. - * @param company the name of the company. - * @param salesPrice the initial sales price. - */ + private List prices = new ArrayList<>(); + private StockController stockController; + /** Constructs a new stock. */ public Stock( final String symbol, final String company, @@ -37,82 +25,57 @@ public Stock( stockController = new StockController(this); } + /** @return the symbol. */ public String getSymbol() { return symbol; } + /** @return the company. */ public String getCompany() { return company; } + /** @return current sales price. */ public BigDecimal getSalesPrice() { return prices.isEmpty() ? BigDecimal.ZERO : prices.get(prices.size() - 1); } - /** - * Adds a new sales price to the price history. - * - * @param salesPrice the new price to record. - */ + /** Adds a new sales price. */ public void addNewSalesPrice(final BigDecimal salesPrice) { prices.add(salesPrice); } - /** - * Returns an unmodifiable copy of the full price history. - * - * @return the list of historical prices. - */ + /** @return historical prices. */ public List getHistoricalPrices() { return List.copyOf(prices); } - /** - * Returns the highest price the stock has ever reached. - * - * @return the all-time high price, or {@link BigDecimal#ZERO} if no prices exist. - */ + /** @return highest price. */ public BigDecimal getHighestPrice() { return prices.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO); } - /** - * Returns the lowest price the stock has ever reached. - * - * @return the all-time low price, or {@link BigDecimal#ZERO} if no prices exist. - */ + /** @return lowest price. */ public BigDecimal getLowestPrice() { return prices.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO); } - /** - * Returns the price change between the two most recent recorded prices. - * - * @return the difference between the latest and previous price, - * or {@link BigDecimal#ZERO} if fewer than two prices exist. - */ + /** @return price change. */ public BigDecimal getLatestPriceChange() { - final int minSize = 2; - if (prices.size() < minSize) { + if (prices.isEmpty()) { return BigDecimal.ZERO; } - return prices.get(prices.size() - 1) - .subtract(prices.get(prices.size() - 2)); + int index120Ago = Math.max(0, prices.size() - 121); + return getSalesPrice().subtract(prices.get(index120Ago)); } - /** - * Updates the stock price using the associated {@link StockController}. - */ + /** Updates the price. */ public void updatePrice() { BigDecimal newPrice = stockController.updatePrice(); prices.add(newPrice); } - /** - * Returns the stock's price graph, creating it lazily if not yet initialized. - * - * @return the {@link StockGraph} for this stock. - */ + /** @return stock graph. */ public StockGraph getStockGraph() { if (stockGraph == null) { stockGraph = new StockGraph(); @@ -120,12 +83,7 @@ public StockGraph getStockGraph() { return stockGraph; } - /** - * Generates a simulated price history by advancing the price a given number of steps. - * The history is then reversed so prices appear in chronological order. - * - * @param stockResolution the number of price steps to simulate. - */ + /** Generates fake history. */ public void fakeHistory(int stockResolution) { for (int i = 0; i < stockResolution; i++) { updatePrice(); @@ -133,9 +91,7 @@ public void fakeHistory(int stockResolution) { java.util.Collections.reverse(prices); } - /** - * Updates the stock graph if it has been initialized and is currently visible. - */ + /** Updates the stock graph. */ public void updateStockGraph() { if (stockGraph != null && stockGraph.getVisibility()) { stockGraph.update(this); 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 4899203..e0d27dd 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 @@ -37,7 +37,6 @@ */ public abstract class Popup { - private static final boolean DEV_CSS_RELOAD = true; private static final double ARC = 16; private final App type; @@ -48,6 +47,7 @@ public abstract class Popup { private final Button closeButton; protected final VBox content; + protected final ScrollPane scrollPane; /** * Constructs a popup window with specified size and position. @@ -63,14 +63,13 @@ protected Popup(int width, int height, int x, int y, App type) { content = new VBox(); content.getStyleClass().add("popup-content"); - content.setPrefSize(width, height); + content.setFillWidth(true); - ScrollPane scrollPane = new ScrollPane(content); + scrollPane = new ScrollPane(content); scrollPane.getStyleClass().add("popup-scroll-pane"); - scrollPane.setMinSize(width, height); scrollPane.setFitToWidth(true); - scrollPane.setFitToHeight(true); - scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS); + scrollPane.setFitToHeight(false); + scrollPane.setPrefSize(width, height); closeButton = buildCloseButton(); header = buildHeader(closeButton, type.getDisplayName()); @@ -98,15 +97,6 @@ protected Popup(int width, int height, int x, int y, App type) { loadStylesheets(); - if (DEV_CSS_RELOAD) { - wrapper.setFocusTraversable(true); - wrapper.setOnKeyPressed(event -> { - if (event.getCode() == javafx.scene.input.KeyCode.R) { - loadStylesheets(); - System.out.println("[CSS] Popup stylesheets reloaded"); - } - }); - } } private Rectangle buildClip(BorderPane target) { @@ -140,6 +130,7 @@ private void loadStylesheets() { // Map App to CSS String appCss = switch (type) { case BANK -> "/css/bank.css"; + case MAIL -> "/css/mail.css"; case SETTINGS -> "/css/settings.css"; case STOCK -> "/css/stock.css"; case FILEPICKER -> "/css/filepicker.css"; @@ -157,28 +148,14 @@ private void loadStylesheets() { } try { - if (DEV_CSS_RELOAD) { - String projectDir = System.getProperty("user.dir"); - for (String s : sheets) { - root.getStylesheets().add(devCopyToTemp(projectDir + "/src/main/resources" + s)); - } - } else { - for (String s : sheets) { - root.getStylesheets().add(resource(s)); - } + for (String s : sheets) { + root.getStylesheets().add(resource(s)); } } catch (Exception e) { System.err.println("[CSS] Failed to load popup stylesheets: " + e.getMessage()); } } - private String devCopyToTemp(final String sourcePath) throws Exception { - java.nio.file.Path src = java.nio.file.Paths.get(sourcePath); - java.nio.file.Path tmp = java.nio.file.Files.createTempFile("css_", ".css"); - java.nio.file.Files.copy(src, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - tmp.toFile().deleteOnExit(); - return tmp.toUri().toString(); - } private Button buildCloseButton() { Button button = new Button(); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/BankApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/BankApp.java index c37e588..9d34d70 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/BankApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/BankApp.java @@ -35,7 +35,7 @@ public class BankApp extends Popup { /** Constructs the Bank app. */ public BankApp() { - super(500, 550, 220, 100, App.BANK); + super(600, 550, 220, 100, App.BANK); portfolioList = new ListView<>(); portfolioList.setSelectionModel(null); @@ -73,7 +73,7 @@ private VBox buildSummaryCard() { totalReturnLabel.setMinWidth(Region.USE_PREF_SIZE); GridPane grid = new GridPane(); - grid.setHgap(40); + grid.setHgap(20); grid.setVgap(15); ColumnConstraints c0 = new ColumnConstraints(); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/MailApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/MailApp.java index eec86c4..d420fae 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/MailApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/view/apps/MailApp.java @@ -13,9 +13,6 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import javafx.scene.text.Font; -import javafx.scene.text.FontWeight; /** * Mail app popup for viewing and managing messages. @@ -30,7 +27,7 @@ public class MailApp extends Popup { * @param controller the controller managing mail data */ public MailApp(MailController controller) { - super(450, 400, 140, 140, App.MAIL); + super(450, 600, 140, 140, App.MAIL); this.controller = controller; this.mailList = new VBox(10); buildContent(); @@ -42,9 +39,14 @@ private void buildContent() { content.setPadding(new Insets(20)); content.setSpacing(15); + mailList.setFillWidth(true); + mailList.setMaxWidth(Double.MAX_VALUE); + VBox.setVgrow(mailList, Priority.ALWAYS); + Label title = new Label("Inbox"); - title.setFont(Font.font("System", FontWeight.BOLD, 22)); + title.getStyleClass().add("mail-title"); content.getChildren().add(title); + mailList.getStyleClass().add("mail-list"); content.getChildren().add(mailList); } @@ -60,39 +62,46 @@ private void refreshMailList() { } private VBox createMailItem(Message message) { - VBox item = new VBox(5); - item.setPadding(new Insets(12)); - item.setStyle("-fx-background-color: #ffffff; -fx-border-color: #eeeeee; " - + "-fx-border-radius: 8; -fx-background-radius: 8;"); + VBox item = new VBox(8); + item.setPadding(new Insets(15)); + item.setMaxWidth(Double.MAX_VALUE); + item.getStyleClass().add("mail-item"); + if (!message.isSeen()) { + item.getStyleClass().add("mail-item-unread"); + } HBox top = new HBox(10); top.setAlignment(Pos.CENTER_LEFT); Label senderLabel = new Label(message.getAuthor()); - senderLabel.setFont(Font.font("System", FontWeight.BOLD, 14)); + senderLabel.setMinWidth(0); + senderLabel.getStyleClass().add("mail-sender"); Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); Label timeLabel = new Label(message.getFormattedTime()); - timeLabel.setTextFill(Color.GRAY); - timeLabel.setFont(Font.font(12)); + timeLabel.getStyleClass().add("mail-time"); + timeLabel.setMinWidth(Region.USE_PREF_SIZE); top.getChildren().addAll(senderLabel, spacer, timeLabel); Label subLabel = new Label(message.getTitle()); - subLabel.setFont(Font.font("System", FontWeight.MEDIUM, 13)); + subLabel.getStyleClass().add("mail-subject"); + subLabel.setWrapText(true); + subLabel.setMaxWidth(Double.MAX_VALUE); Label contentLabel = new Label(message.getContent()); - contentLabel.setTextFill(Color.GRAY); + contentLabel.getStyleClass().add("mail-content"); contentLabel.setWrapText(true); + contentLabel.setMaxWidth(Double.MAX_VALUE); item.getChildren().addAll(top, subLabel, contentLabel); if (message.isSeen()) { HBox bottom = new HBox(); bottom.setAlignment(Pos.CENTER_RIGHT); - Button seenButton = new Button("Seen"); + Button seenButton = new Button("Mark as Read"); seenButton.getStyleClass().add("primary-button"); seenButton.setOnAction(event -> { message.setSeen(true); 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 a25141c..c9d30d8 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 @@ -25,6 +25,9 @@ public class StockApp extends Popup { private final ListView stockList; private final TextField searchField; + private final Button alphaButton; + private final Button gainerButton; + private final Button priceButton; private Label priceLabel; private final Spinner quantitySpinner; private final Button confirmButton; @@ -40,6 +43,13 @@ public StockApp(final Player player) { this.searchField.getStyleClass().add("search-field"); this.searchField.setMaxWidth(Double.MAX_VALUE); + this.alphaButton = new Button("A-Z"); + this.alphaButton.getStyleClass().add("sort-button"); + this.gainerButton = new Button("Gainer"); + this.gainerButton.getStyleClass().add("sort-button"); + this.priceButton = new Button("Price"); + this.priceButton.getStyleClass().add("sort-button"); + this.stockList = new ListView<>(); VBox.setVgrow(this.stockList, Priority.ALWAYS); this.stockList.setSelectionModel(null); @@ -70,7 +80,9 @@ public StockApp(final Player player) { public void showSearchPage() { content.setPadding(new Insets(20)); content.setSpacing(20); - content.getChildren().setAll(searchField, stockList); + HBox searchBox = new HBox(10, searchField, alphaButton, gainerButton, priceButton); + HBox.setHgrow(searchField, Priority.ALWAYS); + content.getChildren().setAll(searchBox, stockList); } /** Shows stock detail page. */ @@ -152,4 +164,19 @@ public Receipt getReceipt() { public Button getBackButton() { return backButton; } + + /** @return alpha button. */ + public Button getAlphaButton() { + return alphaButton; + } + + /** @return gainer button. */ + public Button getGainerButton() { + return gainerButton; + } + + /** @return price button. */ + public Button getPriceButton() { + return priceButton; + } } \ No newline at end of file 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 1a3b94d..19f352d 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,7 +30,6 @@ */ public final class DesktopView { - private static final boolean DEV_CSS_RELOAD = true; public enum ModalLayer { RAPPORT, WARNING, GAME_OVER } @@ -94,18 +93,6 @@ public Pane getRoot() { loadStylesheets(); - if (DEV_CSS_RELOAD) { - sceneRoot.setFocusTraversable(true); - sceneRoot.sceneProperty().addListener((obs, oldScene, newScene) -> { - if (newScene != null) sceneRoot.requestFocus(); - }); - sceneRoot.setOnKeyPressed(event -> { - if (event.getCode() == KeyCode.R) { - loadStylesheets(); - System.out.println("[CSS] Stylesheets reloaded"); - } - }); - } } return sceneRoot; } @@ -123,46 +110,24 @@ public Pane getPopupOverlay() { private void loadStylesheets() { sceneRoot.getStylesheets().clear(); try { - if (DEV_CSS_RELOAD) { - String projectDir = System.getProperty("user.dir"); - sceneRoot.getStylesheets().addAll( - devCopyToTemp(projectDir + "/src/main/resources/css/global.css"), - devCopyToTemp(projectDir + "/src/main/resources/css/desktop.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 { - sceneRoot.getStylesheets().addAll( - resource("/css/global.css"), - resource("/css/desktop.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") - ); - } + sceneRoot.getStylesheets().addAll( + resource("/css/global.css"), + resource("/css/desktop.css"), + resource("/css/components.css"), + resource("/css/mail.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) { System.err.println("[CSS] Failed to load stylesheets: " + e.getMessage()); } } - private String devCopyToTemp(final String sourcePath) throws Exception { - Path src = Paths.get(sourcePath); - Path tmp = Files.createTempFile("css_", ".css"); - Files.copy(src, tmp, StandardCopyOption.REPLACE_EXISTING); - tmp.toFile().deleteOnExit(); - return tmp.toUri().toString(); - } /** Registers a popup root. */ public void registerPopup(final Node popupRoot) { 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 4c4d18a..3f04b6f 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 @@ -18,8 +18,6 @@ /** Start screen view. */ public class StartView { - // 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("→"); @@ -101,19 +99,6 @@ public BorderPane getRoot() { loadStylesheets(); - // DEV_CSS_RELOAD: press R to hot-reload CSS from source without restarting - if (DEV_CSS_RELOAD) { - content.setFocusTraversable(true); - content.sceneProperty().addListener((obs, oldScene, newScene) -> { - if (newScene != null) content.requestFocus(); - }); - content.setOnKeyPressed(event -> { - if (event.getCode() == javafx.scene.input.KeyCode.R) { - loadStylesheets(); - System.out.println("[CSS] Stylesheets reloaded"); - } - }); - } root.setCenter(content); return root; @@ -122,32 +107,15 @@ public BorderPane getRoot() { 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( - copyToTemp(projectDir + "/src/main/resources/css/global.css"), - copyToTemp(projectDir + "/src/main/resources/css/login.css") - ); - } else { - root.getStylesheets().addAll( - getClass().getResource("/css/global.css").toExternalForm(), - getClass().getResource("/css/login.css").toExternalForm() - ); - } + root.getStylesheets().addAll( + getClass().getResource("/css/global.css").toExternalForm(), + getClass().getResource("/css/login.css").toExternalForm() + ); } catch (Exception e) { System.err.println("[CSS] Failed to load stylesheets: " + e.getMessage()); } } - // DEV_CSS_RELOAD: copies a CSS file to a unique temp path so JavaFX can't cache it - private String copyToTemp(String sourcePath) throws Exception { - java.nio.file.Path src = java.nio.file.Paths.get(sourcePath); - java.nio.file.Path tmp = java.nio.file.Files.createTempFile("css_", ".css"); - java.nio.file.Files.copy(src, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - tmp.toFile().deleteOnExit(); - return tmp.toUri().toString(); - } /** @return the username field. */ public TextField getUsernameField() { diff --git a/src/main/resources/css/mail.css b/src/main/resources/css/mail.css new file mode 100644 index 0000000..8069bdc --- /dev/null +++ b/src/main/resources/css/mail.css @@ -0,0 +1,57 @@ +.mail-list { + -fx-background-color: transparent; +} + +.mail-item { + -fx-background-color: #ffffff; + -fx-border-color: #eeeeee; + -fx-border-width: 1; + -fx-border-radius: 8; + -fx-background-radius: 8; +} + +.mail-item:hover { + -fx-background-color: #f8f9fa; + -fx-border-color: #e0e0e0; +} + +.mail-item-unread { + -fx-border-color: #3498db; + -fx-border-width: 1.5; +} + +.mail-sender { + -fx-font-weight: bold; + -fx-font-size: 14px; + -fx-text-fill: #2c3e50; + -fx-text-overrun: ellipsis; +} + +.mail-time { + -fx-font-family: "System"; + -fx-font-size: 11px; + -fx-text-fill: #7f8c8d; +} + +.mail-subject { + -fx-font-family: "System"; + -fx-font-weight: 600; + -fx-font-size: 13px; + -fx-text-fill: #34495e; + -fx-wrap-text: true; +} + +.mail-content { + -fx-font-family: "System"; + -fx-font-size: 12px; + -fx-text-fill: #7f8c8d; + -fx-wrap-text: true; +} + +.mail-title { + -fx-font-family: "System"; + -fx-font-weight: bold; + -fx-font-size: 24px; + -fx-text-fill: #2c3e50; + -fx-wrap-text: true; +} diff --git a/src/main/resources/css/popup.css b/src/main/resources/css/popup.css index f15ca7b..d9b3508 100644 --- a/src/main/resources/css/popup.css +++ b/src/main/resources/css/popup.css @@ -37,12 +37,16 @@ } .popup-scroll-pane { - -fx-background-color: -popup-bg; - -fx-viewport-background-color: -popup-bg; + -fx-background-color: white; + -fx-background: white; } -.popup-scroll-pane:focused { - -fx-background-insets: 0; +.popup-scroll-pane > .viewport { + -fx-background-color: white; +} + +.popup-content { + -fx-background-color: white; } .popup-content { diff --git a/src/main/resources/css/stock.css b/src/main/resources/css/stock.css index add33f1..987276f 100644 --- a/src/main/resources/css/stock.css +++ b/src/main/resources/css/stock.css @@ -54,6 +54,14 @@ -fx-padding: 5 15 5 15; } +.sort-button { + -fx-background-color: -surface; + -fx-background-radius: 15; + -fx-border-color: -popup-border; + -fx-border-radius: 15; + -fx-cursor: hand; +} + .stock-price-large { -fx-font-weight: bold; -fx-font-size: 24px;