Skip to content

123 quit button pop up #139

Merged
merged 2 commits into from
May 26, 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
21 changes: 21 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 @@ -19,6 +19,8 @@
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.quit.QuitDialogController;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.quit.QuitDialogView;
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;
Expand Down Expand Up @@ -222,6 +224,25 @@ public <T> void handleEvent(final EventData<T> eventData) {
new InGameSettingsController(inGameSettingsView, eventManager, inGameView);
topBarController.setSettingsAction(inGameSettingsController::show);

// In-game quit confirmation dialog: replaces the previous immediate
// "auto-save and return to main menu" behaviour with a popup that
// lets the player choose Continue / Save / Save and quit to main
// menu / Save and quit game.
QuitDialogView quitDialogView = new QuitDialogView();
QuitDialogController quitDialogController =
new QuitDialogController(quitDialogView, eventManager, inGameView,
gameStateLoader, saveGameService);
quitDialogController.setOnExitApplication(() -> {
try {
javafx.application.Platform.exit();
} finally {
// Fallback in case there are non-daemon background threads
// keeping the JVM alive after JavaFX shuts down.
System.exit(0);
}
});
topBarController.setOnQuitRequested(quitDialogController::show);

topBarController.setOnQuitToMainMenu(() -> {
System.out.println("[auto-save] Quit triggered, attempting snapshot...");
SaveGame snapshot = gameStateLoader.snapshotActiveSave();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.quit;

/**
* User-triggered actions available in the in-game quit dialog overlay.
*
* <p>The quit dialog is shown when the player clicks the "Quit" button
* from the in-game dashboard, and lets them choose between continuing,
* saving in place, saving and returning to the main menu, or saving
* and exiting the application.</p>
* */
public enum QuitDialogActions {
/** Closes the dialog and returns the player to the game. */
CONTINUE,

/** Saves the current game state without leaving the in-game session. */
SAVE,

/** Saves the current game state and returns to the main menu. */
SAVE_AND_QUIT_TO_MAIN_MENU,

/** Saves the current game state and exits the application. */
SAVE_AND_QUIT_GAME;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.quit;

import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame;
import edu.ntnu.idi.idatt2003.g40.mappe.service.GameStateLoader;
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.view.ViewController;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewEnum;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameView;

/**
* Controller for {@link QuitDialogView}.
*
* <p>Wires the four dialog buttons to the appropriate save / scene-change
* / application-exit actions:</p>
* <ul>
* <li>{@code CONTINUE} - hides the overlay; the player stays in the
* current game session.</li>
* <li>{@code SAVE} - snapshots and writes the active save to disk, but
* keeps the overlay open so the player sees the confirmation message.
* They can then choose to continue, quit, or exit.</li>
* <li>{@code SAVE_AND_QUIT_TO_MAIN_MENU} - snapshots, writes to disk,
* hides the overlay, and changes the scene back to the main menu.</li>
* <li>{@code SAVE_AND_QUIT_GAME} - snapshots, writes to disk, then
* invokes the configured exit-application runnable to close the JVM.</li>
* </ul>
*
* <p>The save logic mirrors the auto-save hook in {@code Main}: a missing
* active save (e.g. before any game has been loaded) is treated as a
* no-op rather than an error, since there's nothing meaningful to write.</p>
* */
public final class QuitDialogController
extends ViewController<QuitDialogView> {

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

/** Builds {@link SaveGame} snapshots of the current player/exchange. */
private final GameStateLoader gameStateLoader;

/** Persists save games to disk. */
private final SaveGameService saveGameService;

/** Runnable invoked to close the application. */
private Runnable onExitApplication = () -> { };

/**
* Constructor.
*
* @param view the {@link QuitDialogView} this controller is
* attached to.
* @param eventManager the active {@link EventManager}, used to
* publish scene-change events.
* @param inGameView the in-game view that hosts this overlay.
* @param gameStateLoader the loader used to snapshot the active save.
* @param saveGameService the service used to persist saves to disk.
*
* @throws IllegalArgumentException if any constructor argument is null.
* */
public QuitDialogController(final QuitDialogView view,
final EventManager eventManager,
final InGameView inGameView,
final GameStateLoader gameStateLoader,
final SaveGameService saveGameService)
throws IllegalArgumentException {
this.inGameView = inGameView;
this.gameStateLoader = gameStateLoader;
this.saveGameService = saveGameService;
super(view, eventManager);
if (inGameView == null
|| gameStateLoader == null
|| saveGameService == null) {
throw new IllegalArgumentException(
"Invalid QuitDialogController arguments!");
}
}

/**
* Installs the runnable used to close the application when the player
* chooses "Save and quit game".
*
* <p>Typically this is wired to {@code Platform.exit()} (plus a
* {@code System.exit(0)} fallback) from {@code Main}, so the controller
* itself stays decoupled from the JavaFX runtime.</p>
*
* @param exitRunnable runnable invoked after the save completes; a null
* value resets the hook to a no-op.
* */
public void setOnExitApplication(final Runnable exitRunnable) {
this.onExitApplication =
(exitRunnable != null) ? exitRunnable : () -> { };
}

/** {@inheritDoc} */
@Override
protected void initInteractions() {
getViewElement().setOnAction(QuitDialogActions.CONTINUE, this::close);

getViewElement().setOnAction(QuitDialogActions.SAVE, () -> {
boolean ok = performSave();
if (ok) {
getViewElement().setStatus("Game saved.", false);
}
// Failures already wrote an error to the status label inside
// performSave(); nothing more to do here.
});

getViewElement().setOnAction(
QuitDialogActions.SAVE_AND_QUIT_TO_MAIN_MENU, () -> {
if (performSave()) {
close();
changeScene(ViewEnum.MAIN_MENU);
}
});

getViewElement().setOnAction(QuitDialogActions.SAVE_AND_QUIT_GAME, () -> {
if (performSave()) {
close();
onExitApplication.run();
}
});
}

/**
* Shows the quit dialog overlay on the host {@link InGameView}.
*
* <p>Clears any stale status message left over from a previous opening
* so the player doesn't see "Game saved." from a save they made hours
* ago.</p>
* */
public void show() {
getViewElement().setStatus(null, false);
inGameView.showSettingsOverlay(getViewElement().getRootPane());
}

/**
* Hides the overlay if it's currently visible.
* */
private void close() {
inGameView.hideSettingsOverlay();
}

/**
* Snapshots the active save and writes it to disk.
*
* <p>If no save is currently active (e.g. the player got into the
* in-game view through a path that doesn't load a save), this is
* treated as a successful no-op so the quit / exit flows can proceed.
* Any I/O failure is reported back to the player via the dialog's
* status label.</p>
*
* @return {@code true} when the operation can be considered successful
* (a real save was written, or no save was needed); {@code false}
* when an actual error occurred.
* */
private boolean performSave() {
SaveGame snapshot;
try {
snapshot = gameStateLoader.snapshotActiveSave();
} catch (Exception e) {
System.err.println("[quit-dialog] Snapshot failed: "
+ e.getMessage());
getViewElement().setStatus(
"Could not prepare save: " + e.getMessage(), true);
return false;
}

if (snapshot == null) {
System.out.println(
"[quit-dialog] No active save - nothing to write.");
// Nothing to persist, but the user's intent (continue past this
// step) is still valid - treat as success.
return true;
}

try {
saveGameService.saveGame(snapshot);
System.out.println("[quit-dialog] Wrote save '"
+ snapshot.getName() + "' to disk.");
return true;
} catch (Exception e) {
System.err.println("[quit-dialog] Save failed: " + e.getMessage());
getViewElement().setStatus(
"Save failed: " + e.getMessage(), true);
return false;
}
}
}
Loading