Skip to content

Commit

Permalink
Merge pull request #125 from Team-40-IDATT2003/enhancement/122-deign
Browse files Browse the repository at this point in the history
ny stil :)
  • Loading branch information
tommyah authored May 25, 2026
2 parents e730057 + da711e9 commit f7b2825
Show file tree
Hide file tree
Showing 13 changed files with 2,043 additions and 536 deletions.
12 changes: 12 additions & 0 deletions src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import edu.ntnu.idi.idatt2003.g40.mappe.service.SaveGameService;
import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager;
import edu.ntnu.idi.idatt2003.g40.mappe.utils.ConfigValues;
import edu.ntnu.idi.idatt2003.g40.mappe.utils.ThemeManager;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewManager;
import edu.ntnu.idi.idatt2003.g40.mappe.view.creategame.CreateGameController;
import edu.ntnu.idi.idatt2003.g40.mappe.view.creategame.CreateGameView;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameController;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameView;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.settings.InGameSettingsController;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.settings.InGameSettingsView;
import edu.ntnu.idi.idatt2003.g40.mappe.view.mainmenu.MainMenuController;
import edu.ntnu.idi.idatt2003.g40.mappe.view.mainmenu.MainMenuView;
import edu.ntnu.idi.idatt2003.g40.mappe.view.playgame.PlayGameController;
Expand Down Expand Up @@ -76,6 +79,9 @@ public void start(final Stage stage) throws Exception {
stage.setWidth(ConfigValues.VIEWPORT_WIDTH.getValue());
stage.setHeight(ConfigValues.VIEWPORT_HEIGHT.getValue());

// Register the scene with the theme manager so it can toggle dark/light.
ThemeManager.getInstance().registerScene(scene);

EventManager eventManager = new EventManager();
ViewManager viewManager = new ViewManager(stage, eventManager);

Expand Down Expand Up @@ -198,6 +204,12 @@ public void start(final Stage stage) throws Exception {
miniGamesView.getRootPane()
);

// In-game settings overlay (Dark / Light theme toggle).
InGameSettingsView inGameSettingsView = new InGameSettingsView();
InGameSettingsController inGameSettingsController =
new InGameSettingsController(inGameSettingsView, eventManager, inGameView);
topBarController.setSettingsAction(inGameSettingsController::show);

// Register all views
viewManager.addView(mainMenuView);
viewManager.addView(playGameView);
Expand Down
136 changes: 136 additions & 0 deletions src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/utils/ThemeManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package edu.ntnu.idi.idatt2003.g40.mappe.utils;

import javafx.scene.Scene;

/**
* Singleton utility for managing the active visual theme of the application.
*
* <p>The base stylesheet ({@code styles.css}) defines the dark theme as the
* default. When light mode is activated, this manager adds the
* {@code "light-mode"} style class on the scene's root node, which causes the
* light-mode CSS overrides to take effect for all descendant nodes.</p>
*
* <p>The active scene is registered once from {@code Main} after the
* application has been initialized. After that, callers (e.g. the in-game
* settings panel) only need to call {@link #setTheme(Theme)} or
* {@link #toggleTheme()}.</p>
*/
public final class ThemeManager {

/**
* Style class added to the scene root to activate light mode.
*
* <p>Matches the {@code .light-mode} CSS rules in {@code styles.css}.</p>
* */
public static final String LIGHT_MODE_CLASS = "light-mode";

/**
* Available themes.
* */
public enum Theme {
/** Default dark theme (no extra style class). */
DARK,

/** Light theme (adds the {@link #LIGHT_MODE_CLASS} style class). */
LIGHT
}

/** Singleton instance. */
private static final ThemeManager INSTANCE = new ThemeManager();

/** The currently registered scene. */
private Scene scene;

/** The currently active theme. */
private Theme currentTheme = Theme.DARK;

/**
* Private constructor for singleton.
* */
private ThemeManager() {

}

/**
* Getter method for the singleton instance.
*
* @return the {@link ThemeManager} singleton.
* */
public static ThemeManager getInstance() {
return INSTANCE;
}

/**
* Registers the active {@link Scene} with the theme manager.
*
* <p>Should be called once from {@code Main} after the scene is built.
* Re-registering with a new scene applies the current theme to it.</p>
*
* <p>A listener is attached to the scene's root property so that the
* active theme is re-applied whenever the {@code ViewManager} swaps the
* scene root for a view change.</p>
*
* @param scene the active scene.
*
* @throws IllegalArgumentException if scene is null.
* */
public void registerScene(final Scene scene) {
if (scene == null) {
throw new IllegalArgumentException("Scene cannot be null!");
}
this.scene = scene;
scene.rootProperty().addListener((obs, oldRoot, newRoot) -> applyCurrentTheme());
applyCurrentTheme();
}

/**
* Getter method for the active theme.
*
* @return the currently active {@link Theme}.
* */
public Theme getCurrentTheme() {
return currentTheme;
}

/**
* Sets the active theme and applies it to the registered scene.
*
* @param theme the {@link Theme} to set.
*
* @throws IllegalArgumentException if theme is null.
* */
public void setTheme(final Theme theme) {
if (theme == null) {
throw new IllegalArgumentException("Theme cannot be null!");
}
this.currentTheme = theme;
applyCurrentTheme();
}

/**
* Toggles between {@link Theme#DARK} and {@link Theme#LIGHT}.
* */
public void toggleTheme() {
setTheme(currentTheme == Theme.DARK ? Theme.LIGHT : Theme.DARK);
}

/**
* Applies the current theme to the registered scene.
*
* <p>For light mode, the {@link #LIGHT_MODE_CLASS} style class is added
* to the scene root. For dark mode, it is removed.</p>
* */
private void applyCurrentTheme() {
if (scene == null || scene.getRoot() == null) {
return;
}
var styleClasses = scene.getRoot().getStyleClass();
if (currentTheme == Theme.LIGHT) {
if (!styleClasses.contains(LIGHT_MODE_CLASS)) {
styleClasses.add(LIGHT_MODE_CLASS);
}
} else {
styleClasses.remove(LIGHT_MODE_CLASS);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
Expand All @@ -22,7 +20,6 @@
import java.io.File;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Objects;

/**
* View shown after the player clicks "Create new game" in the
Expand Down Expand Up @@ -72,9 +69,6 @@ public class CreateGameView extends ViewElement<StackPane, CreateGameActions> {
/** "Create game" button - bottom-right corner. */
private Button createGameButton;

/** Background image displayed behind the panel. */
private ImageView backgroundImage;

/** Central VBox stacking the input rows and bottom buttons. */
private VBox mainPanel;

Expand Down Expand Up @@ -200,10 +194,6 @@ public void resetFields() {
/** {@inheritDoc} */
@Override
protected void initLayout() {
backgroundImage = new ImageView(new Image(Objects.requireNonNull(
getClass().getResourceAsStream("/millionsbackground.png"))));
backgroundImage.setPreserveRatio(false);

Text title = new Text("Create new game");
title.getStyleClass().add("create-game-title");

Expand Down Expand Up @@ -278,11 +268,7 @@ protected void initLayout() {
HBox centerWrapper = new HBox(mainPanel);
centerWrapper.setAlignment(Pos.CENTER);

getRootPane().getChildren().addAll(backgroundImage, centerWrapper);

// Make the background fill the root pane.
backgroundImage.fitWidthProperty().bind(getRootPane().widthProperty());
backgroundImage.fitHeightProperty().bind(getRootPane().heightProperty());
getRootPane().getChildren().add(centerWrapper);

// Hook listeners so the create-game button enables only when
// every required field has a valid value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,26 @@
import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.topbar.TopBarView;
import javafx.scene.Node;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;

public class InGameView extends ViewElement<VBox, InGameActions> {

private final TopBarView topBarView;
private Node centerView;

/**
* StackPane wrapping the active center view. Lets overlays (e.g. the
* in-game settings panel) sit on top of the current center widget while
* leaving the top bar visible.
* */
private StackPane centerStack;

/**
* Active settings overlay, or {@code null} if none is shown.
* */
private Node settingsOverlay;

public InGameView(final TopBarView topBarView, final Node centerView) {
this.topBarView = topBarView;
this.centerView = centerView;
Expand All @@ -20,19 +33,57 @@ public InGameView(final TopBarView topBarView, final Node centerView) {

@Override
protected void initLayout() {
getRootPane().getChildren().addAll(topBarView.getRootPane(), centerView);
VBox.setVgrow(centerView, Priority.ALWAYS);
centerStack = new StackPane(centerView);
getRootPane().getChildren().addAll(topBarView.getRootPane(), centerStack);
VBox.setVgrow(centerStack, Priority.ALWAYS);
}

@Override
protected void initStyling() {

getRootPane().getStyleClass().add("in-game-root");
}

public void changeCenterView(final Node newCenterView) {
this.centerView = newCenterView;
getRootPane().getChildren().clear();
getRootPane().getChildren().addAll(topBarView.getRootPane(), centerView);
VBox.setVgrow(centerView, Priority.ALWAYS);
centerStack.getChildren().clear();
centerStack.getChildren().add(centerView);
if (settingsOverlay != null) {
centerStack.getChildren().add(settingsOverlay);
}
}

/**
* Shows a settings overlay on top of the current center view.
*
* <p>The top bar remains visible above the overlay. If an overlay is
* already shown, this is a no-op.</p>
*
* @param overlay the overlay root node to show.
* */
public void showSettingsOverlay(final Node overlay) {
if (settingsOverlay != null || overlay == null) {
return;
}
settingsOverlay = overlay;
centerStack.getChildren().add(settingsOverlay);
}

/**
* Hides the active settings overlay if one is shown.
* */
public void hideSettingsOverlay() {
if (settingsOverlay != null) {
centerStack.getChildren().remove(settingsOverlay);
settingsOverlay = null;
}
}

/**
* Returns whether a settings overlay is currently shown.
*
* @return {@code true} if the settings overlay is visible.
* */
public boolean isSettingsOverlayVisible() {
return settingsOverlay != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.settings;

/**
* User-triggered actions available in the in-game settings overlay.
* */
public enum InGameSettingsActions {
/** Closes the settings overlay and returns to the game. */
CLOSE,

/** Selects the dark theme. */
DARK_MODE,

/** Selects the light theme. */
LIGHT_MODE;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.settings;

import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager;
import edu.ntnu.idi.idatt2003.g40.mappe.utils.ThemeManager;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameView;

/**
* Controller for {@link InGameSettingsView}.
*
* <p>Wires the close button to remove the overlay from the
* {@link InGameView}, and the theme toggle buttons to the global
* {@link ThemeManager}.</p>
*/
public final class InGameSettingsController
extends ViewController<InGameSettingsView> {

/** The in-game view hosting this overlay. */
private final InGameView inGameView;

/**
* Constructor.
*
* @param view the {@link InGameSettingsView} this controller is
* attached to.
* @param eventManager the active {@link EventManager}.
* @param inGameView the in-game view that hosts this settings overlay.
* */
public InGameSettingsController(final InGameSettingsView view,
final EventManager eventManager,
final InGameView inGameView) {
this.inGameView = inGameView;
super(view, eventManager);
// Sync the highlighted button with the current theme on construction.
getViewElement().setActiveTheme(ThemeManager.getInstance().getCurrentTheme());
}

/** {@inheritDoc} */
@Override
protected void initInteractions() {
getViewElement().setOnAction(InGameSettingsActions.CLOSE,
inGameView::hideSettingsOverlay);

getViewElement().setOnAction(InGameSettingsActions.DARK_MODE, () -> {
ThemeManager.getInstance().setTheme(ThemeManager.Theme.DARK);
getViewElement().setActiveTheme(ThemeManager.Theme.DARK);
});

getViewElement().setOnAction(InGameSettingsActions.LIGHT_MODE, () -> {
ThemeManager.getInstance().setTheme(ThemeManager.Theme.LIGHT);
getViewElement().setActiveTheme(ThemeManager.Theme.LIGHT);
});
}

/**
* Shows the settings overlay on the host {@link InGameView}.
*
* <p>Re-syncs the active-theme highlight before showing, so that if the
* user has toggled the theme elsewhere, the panel reflects the current
* state.</p>
* */
public void show() {
getViewElement().setActiveTheme(ThemeManager.getInstance().getCurrentTheme());
inGameView.showSettingsOverlay(getViewElement().getRootPane());
}
}
Loading

0 comments on commit f7b2825

Please sign in to comment.