Skip to content

ny stil :) #125

Merged
merged 1 commit into from
May 25, 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
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