Skip to content

fixed usertested bugs #141

Merged
merged 5 commits into from
May 20, 2026
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import edu.ntnu.idi.idatt2003.gruppe42.view.Popup;
import java.util.List;
import java.util.Optional;
import javafx.application.Platform;
import javafx.geometry.Point2D;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
Expand Down Expand Up @@ -37,11 +38,14 @@ private void initPopup(Popup popup) {
makeDraggable(popup);
popup.getCloseButton().setOnAction(event -> hide(popup.getType()));

Pane root = popup.getRoot();
root.parentProperty().addListener((obs, oldP, newP) -> {
popup.getRoot().parentProperty().addListener((obs, oldP, newP) -> {
if (newP instanceof Pane parent) {
parent.widthProperty().addListener((o, ov, nv) -> keepInBounds(popup));
parent.heightProperty().addListener((o, ov, nv) -> keepInBounds(popup));
parent.widthProperty().addListener((
o, ov, nv) ->
Platform.runLater(() -> keepInBounds(popup)));
parent.heightProperty().addListener((
o, ov, nv) ->
Platform.runLater(() -> keepInBounds(popup)));
}
});
}
Expand All @@ -61,7 +65,7 @@ public boolean show(App type) {
.findFirst()
.ifPresent(popup -> {
popup.show();
keepInBounds(popup);
Platform.runLater(() -> keepInBounds(popup));
});
audioManager.playSfx(Audio.OPEN_APP);
return true;
Expand Down Expand Up @@ -105,7 +109,6 @@ private void makeDraggable(Popup popup) {
popup.getHeader().setOnMousePressed(event -> {
root.layoutXProperty().unbind();
root.layoutYProperty().unbind();

if (root.getParent() instanceof Pane parent) {
Point2D local = parent.sceneToLocal(event.getSceneX(), event.getSceneY());
offset[0] = local.getX() - root.getLayoutX();
Expand All @@ -119,15 +122,9 @@ private void makeDraggable(Popup popup) {
Point2D local = parent.sceneToLocal(event.getSceneX(), event.getSceneY());
double newX = local.getX() - offset[0];
double newY = local.getY() - offset[1];

double width = root.getWidth() > 0 ? root.getWidth() : root.prefWidth(-1);
double height = root.getHeight() > 0 ? root.getHeight() : root.prefHeight(-1);

double maxX = Math.max(0, parent.getWidth() - width);
double maxY = Math.max(0, parent.getHeight() - height);

root.setLayoutX(Math.max(0, Math.min(newX, maxX)));
root.setLayoutY(Math.max(0, Math.min(newY, maxY)));
double[] clamped = clamp(root, parent, newX, newY);
root.setLayoutX(clamped[0]);
root.setLayoutY(clamped[1]);
}
event.consume();
});
Expand All @@ -144,17 +141,29 @@ private void keepInBounds(Popup popup) {
return;
}
if (root.getParent() instanceof Pane parent) {
double width = root.getWidth() > 0 ? root.getWidth() : root.prefWidth(-1);
double height = root.getHeight() > 0 ? root.getHeight() : root.prefHeight(-1);

double maxX = Math.max(0, parent.getWidth() - width);
double maxY = Math.max(0, parent.getHeight() - height);

root.setLayoutX(Math.max(0, Math.min(root.getLayoutX(), maxX)));
root.setLayoutY(Math.max(0, Math.min(root.getLayoutY(), maxY)));
double[] clamped = clamp(root, parent, root.getLayoutX(), root.getLayoutY());
root.setLayoutX(clamped[0]);
root.setLayoutY(clamped[1]);
}
}

/**
* Clamps the given x/y coordinates so the popup stays within its parent bounds.
*
* @param root the popup root pane
* @param parent the parent pane
* @param x the desired x position
* @param y the desired y position
* @return a two-element array containing the clamped x and y values
*/
private double[] clamp(Pane root, Pane parent, double x, double y) {
double width = root.getWidth() > 0 ? root.getWidth() : root.prefWidth(-1);
double height = root.getHeight() > 0 ? root.getHeight() : root.prefHeight(-1);
double maxX = Math.max(0, parent.getWidth() - width);
double maxY = Math.max(0, parent.getHeight() - height);
return new double[]{Math.max(0, Math.min(x, maxX)), Math.max(0, Math.min(y, maxY))};
}

/**
* Adds multiple popups to the controller.
*
Expand All @@ -181,8 +190,8 @@ public void addPopup(Popup popup) {
* @return the matching popup, or null if none exists
*/
public Popup getPopup(App type) {
Optional<Popup> match = popups.stream().filter(popup -> popup.getType()
.equals(type)).findFirst();
Optional<Popup> match = popups.stream().filter(
popup -> popup.getType().equals(type)).findFirst();
return match.orElse(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class MailController implements AppController {
public MailController(TimeAndWeatherController timeAndWeatherController) {
this.timeAndWeatherController = timeAndWeatherController;
unreadCount = Bindings.createIntegerBinding(() ->
(int) messages.stream().filter(Message::isSeen).count(),
(int) messages.stream().filter(m -> !m.isSeen()).count(),
messages
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ public StockAppController(

stockApp.getQuantitySpinner().valueProperty().addListener(
(obs, old, newValue) -> {
if (isWeekend) {
onMarketClosed.run();
}
if (currentStock != null) {
updateReceipt();
}
Expand Down Expand Up @@ -199,7 +202,6 @@ private void updateTradeUi() {
var spinner = stockApp.getQuantitySpinner();

if (isWeekend) {
tradeButton.setDisable(true);
tradeButton.getStyleClass().add("trade-button-weekend");
tradeButton.setOpacity(0.45);
spinner.setDisable(true);
Expand Down Expand Up @@ -253,7 +255,7 @@ private void handleTransaction() {
}

if (value > 0 && currentStock.getSalesPrice().compareTo(BigDecimal.ZERO) <= 0) {
return; // Buying bankrupt stock disabled
return;
}

BigDecimal quantity = new BigDecimal(Math.abs(value));
Expand All @@ -268,18 +270,16 @@ 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);
.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()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,33 @@
import edu.ntnu.idi.idatt2003.gruppe42.controller.PopupsController;
import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.MailController;
import edu.ntnu.idi.idatt2003.gruppe42.model.App;
import java.util.Objects;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
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;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;

/**
* Factory class for creating desktop user interface components.
*/
public final class DesktopComponentFactory {

private final PopupsController popupsController;
private final MailController mailController;
public record DesktopComponentFactory(
PopupsController popupsController, MailController mailController) {

/**
* Constructs a DesktopComponentFactory.
*
* @param popupsController the popups controller
* @param mailController the mail controller
*/
public DesktopComponentFactory(PopupsController popupsController, MailController mailController) {
this.popupsController = popupsController;
this.mailController = mailController;
public DesktopComponentFactory {
}

/**
Expand All @@ -46,57 +39,70 @@ public DesktopComponentFactory(PopupsController popupsController, MailController
* @return the created application button node
*/
public Node createAppButton(final App type) {
Button button = new Button(type.getDisplayName());
button.getStyleClass().add("app-button");
Button button = new Button();
button.setStyle(
"-fx-background-color: transparent; -fx-padding: 0; -fx-border-color: transparent;"
);
button.setMinSize(0, 0);

Node appNode = button;

if (type == App.MAIL) {
final StackPane container = new StackPane();

final Circle badge = new Circle(10, Color.RED);
Label countLabel = new Label();
countLabel.setTextFill(Color.WHITE);
countLabel.setFont(Font.font("System", FontWeight.BOLD, 10));
countLabel.textProperty().bind(mailController.unreadCountProperty().asString());

StackPane badgeUi = new StackPane(badge, countLabel);
badgeUi.setMouseTransparent(true);
badgeUi.setAlignment(Pos.CENTER);
badgeUi.visibleProperty().bind(mailController.unreadCountProperty().greaterThan(0));

container.getChildren().addAll(button, badgeUi);
StackPane.setAlignment(badgeUi, Pos.TOP_RIGHT);
StackPane.setMargin(badgeUi, new Insets(-10, -10, 0, 0));

// Bind container size to button size so badge is aligned to button corners
container.prefWidthProperty().bind(button.prefWidthProperty());
container.prefHeightProperty().bind(button.prefHeightProperty());
container.maxWidthProperty().bind(button.prefWidthProperty());
container.maxHeightProperty().bind(button.prefHeightProperty());

appNode = container;
try {
String path = iconPath(type);
Image img = new javafx.scene.image.Image(
Objects.requireNonNull(getClass().getResourceAsStream(Objects.requireNonNull(path))));

ImageView icon = new javafx.scene.image.ImageView(img);
icon.setPreserveRatio(false);
icon.fitWidthProperty().bind(button.prefWidthProperty());
icon.fitHeightProperty().bind(button.prefHeightProperty());

button.setGraphic(icon);
button.setPadding(javafx.geometry.Insets.EMPTY);

button.prefWidthProperty().addListener((
obs, ov, nv) ->
applyIconClip(icon, nv.doubleValue(), button.getPrefHeight()));
button.prefHeightProperty().addListener((
obs, ov, nv) ->
applyIconClip(icon, button.getPrefWidth(), nv.doubleValue()));
button.setGraphic(icon);
} catch (Exception e) {
button.setText(type.getDisplayName());
}

final Node finalNode = appNode;
finalNode.parentProperty().addListener((observable, oldParent, newParent) -> {
if (newParent instanceof Region region) {
button.prefWidthProperty().bind(
Bindings.min(
region.widthProperty().multiply(0.8),
region.heightProperty()
).multiply(0.8)
);
button.prefHeightProperty().bind(button.prefWidthProperty());
} else {
button.prefWidthProperty().unbind();
button.prefHeightProperty().unbind();
}
});
button.parentProperty().addListener((obs, oldParent, newParent) ->
bindButtonSize(button, newParent));

popupsController.bindOpenButton(button, type);
setupDrag(button);
return button;
}

private String iconPath(final App type) {
return switch (type) {
case MAIL -> "/Images/mailApp_icon.png";
case BANK -> "/Images/bankApp_icon.png";
case STOCK -> "/Images/stockApp_icon.png";
case HUSTLERS -> "/Images/hustlersApp_icon.png";
default -> null;
};
}

private void bindButtonSize(final Button button, final Object parent) {
if (parent instanceof Region region) {
button.prefWidthProperty().bind(
Bindings.min(
region.widthProperty().multiply(0.8),
region.heightProperty()
).multiply(0.8)
);
button.prefHeightProperty().bind(button.prefWidthProperty());
} else {
button.prefWidthProperty().unbind();
button.prefHeightProperty().unbind();
}
}

private void setupDrag(final Button button) {
button.setOnDragDetected(event -> {
Dragboard dragboard = button.startDragAndDrop(TransferMode.MOVE);
SnapshotParameters params = new SnapshotParameters();
Expand All @@ -109,12 +115,19 @@ public Node createAppButton(final App type) {
dragboard.setContent(content);
event.consume();
});
}

if (type == App.MAIL) {
return appNode;
private void applyIconClip(
final javafx.scene.image.ImageView icon,
final double w,
final double h) {
if (w <= 0 || h <= 0) {
return;
}

return button;
javafx.scene.shape.Rectangle clip = new javafx.scene.shape.Rectangle(w, h);
clip.setArcWidth(w * 0.45);
clip.setArcHeight(h * 0.45);
icon.setClip(clip);
}

/**
Expand All @@ -141,4 +154,4 @@ public void configureCellAsDropTarget(final StackPane cell) {
event.consume();
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ private void wireWeekendRapport() {
*/
public void showDebtWarning() {
warningApp.configure(
"💔",
"Starting Week in Debt",
"Your balance is negative. Proceeding to next week will cost you a life.",
"Stay on Sunday",
Expand All @@ -138,9 +137,8 @@ public void showDebtWarning() {
*/
public void showMarketClosedWarning() {
warningApp.configure(
"🔒",
"It's the WEEKEND!",
"Market Closed",
"Trading is suspended on weekends.\nThe market reopens on Monday.",
"Got it"
);
warningApp.getPrimaryButton().setOnAction(event -> dismissWarning());
Expand All @@ -152,7 +150,7 @@ public void showMarketClosedWarning() {
*/
public void showInsufficientFundsWarning() {
warningApp.configure(
"🥺",
"NO MORE MONEY!",
"Insufficient Funds",
"You don't have enough cash to complete this purchase.",
"I'll be back"
Expand Down
Loading