summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/java/design/controller/userinput/UndoActions.java51
-rw-r--r--src/main/java/design/controller/userinput/menus/CourseSearch.java8
-rw-r--r--src/main/java/design/controller/userinput/menus/HolePlayMenu.java46
-rw-r--r--src/main/java/design/controller/userinput/menus/MainMenu.java12
-rw-r--r--src/main/java/design/controller/userinput/menus/ManageClubs.java9
-rw-r--r--src/main/java/design/controller/userinput/menus/ManageCourses.java7
-rw-r--r--src/main/java/design/controller/userinput/menus/SelectUser.java2
-rw-r--r--src/main/java/design/controller/userinput/menus/UserSettings.java6
-rw-r--r--src/main/java/design/model/Golfer.java51
-rw-r--r--src/main/java/design/model/holeplay/HolePlayContext.java61
-rw-r--r--src/main/java/design/model/undo/Memento.java4
-rw-r--r--src/main/java/design/model/undo/Originator.java7
-rw-r--r--src/main/java/design/model/undo/UndoManager.java91
13 files changed, 335 insertions, 20 deletions
diff --git a/src/main/java/design/controller/userinput/UndoActions.java b/src/main/java/design/controller/userinput/UndoActions.java
new file mode 100644
index 0000000..6417284
--- /dev/null
+++ b/src/main/java/design/controller/userinput/UndoActions.java
@@ -0,0 +1,51 @@
+package design.controller.userinput;
+
+import design.model.undo.UndoManager;
+import design.persistence.PersonalDatabase;
+import design.runtime.Session;
+import java.io.IOException;
+
+public final class UndoActions {
+
+ private UndoActions() {
+ }
+
+ public static void undoWithSave() {
+ UndoManager um = UndoManager.instance();
+ if (!um.canUndo()) {
+ System.out.println("Nothing to undo.");
+ return;
+ }
+
+ String label = um.peekUndoLabel();
+ um.undo();
+ System.out.println("Undo: " + label);
+
+ saveCurrentGolfer();
+ }
+
+ public static void redoWithSave() {
+ UndoManager um = UndoManager.instance();
+ if (!um.canRedo()) {
+ System.out.println("Nothing to redo.");
+ return;
+ }
+
+ String label = um.peekRedoLabel();
+ um.redo();
+ System.out.println("Redo: " + label);
+
+ saveCurrentGolfer();
+ }
+
+ private static void saveCurrentGolfer() {
+ try {
+ var g = Session.getCurrentGolfer();
+ if (g != null) {
+ PersonalDatabase.instance().updateGolfer(g);
+ }
+ } catch (IOException e) {
+ System.err.println("Failed to save after undo/redo: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/design/controller/userinput/menus/CourseSearch.java b/src/main/java/design/controller/userinput/menus/CourseSearch.java
index b84247a..4dd2cad 100644
--- a/src/main/java/design/controller/userinput/menus/CourseSearch.java
+++ b/src/main/java/design/controller/userinput/menus/CourseSearch.java
@@ -9,6 +9,7 @@ import design.model.course_search.CurrentSearchQuery;
import design.model.course_search.ICourse;
import design.persistence.PersonalDatabase;
import design.runtime.Session;
+import design.model.undo.UndoManager;
import java.io.IOException;
import java.util.ArrayList;
@@ -71,7 +72,7 @@ public class CourseSearch extends Menu {
}
// recursively go through tree structure of courselist to make menu options.
- // this is all for displaying the menu options, not the actual sorting.
+ // this is all for displaying the menu options, not the actual sorting.
private void addCoursesRecursive(List<MenuOption> menuOptions, CourseList list) {
for (ICourse icourse : list.getCourses()) {
// if we find a leaf (course), display it as a menu option
@@ -79,6 +80,8 @@ public class CourseSearch extends Menu {
var name = String.format("%s, %s, Difficulty: %s, %s holes, %s total par",
c.getName(), c.getLocation(), c.getDifficultyRating(), c.getHoleCount(), c.getTotalPar());
menuOptions.add(new MenuOption(name, () -> {
+ UndoManager.instance().capture(golfer, "Add course " + c.getName());
+
// add the course, try to save to DB.
golfer.addCourse(c);
try {
@@ -89,8 +92,7 @@ public class CourseSearch extends Menu {
System.out.println("\n Course added to profile. \n");
new MainMenu().present();
- }
- ));
+ }));
}
// if not, we need to traverse another courselist
else if (icourse instanceof CourseList sublist) {
diff --git a/src/main/java/design/controller/userinput/menus/HolePlayMenu.java b/src/main/java/design/controller/userinput/menus/HolePlayMenu.java
index e0d1572..6651500 100644
--- a/src/main/java/design/controller/userinput/menus/HolePlayMenu.java
+++ b/src/main/java/design/controller/userinput/menus/HolePlayMenu.java
@@ -3,12 +3,15 @@ package design.controller.userinput.menus;
import design.controller.userinput.Menu;
import design.controller.userinput.MenuOption;
import design.model.Course;
+import design.model.Golfer;
import design.model.Hole;
import design.runtime.Session;
import design.model.Club;
import design.model.Round;
import design.model.holeplay.HolePlayContext;
import design.persistence.PersonalDatabase;
+import design.model.undo.UndoManager;
+import design.controller.userinput.UndoActions;
import java.util.ArrayList;
import java.util.List;
@@ -18,10 +21,11 @@ public class HolePlayMenu extends Menu {
private final Round round;
private final HolePlayContext ctx;
+ private final Golfer golfer = Session.getCurrentGolfer();
public HolePlayMenu() {
this.round = createRound();
- this.ctx = new HolePlayContext(Session.getCurrentGolfer(), round, PersonalDatabase.instance());
+ this.ctx = new HolePlayContext(this.golfer, round, PersonalDatabase.instance());
}
@Override
@@ -34,17 +38,37 @@ public class HolePlayMenu extends Menu {
public List<MenuOption> getMenuOptions() {
List<MenuOption> opts = new ArrayList<>();
+ // End round (always shown) 1
+ opts.add(new MenuOption("end round", () -> {
+ UndoManager.instance().capture(golfer, "End round on " + round.getCourse().getName());
+ ctx.endRoundNow();
+ System.out.println("Round ended.");
+ new MainMenu().present();
+ }));
+
+ // 2
+ opts.add(new MenuOption("undo", () -> {
+ UndoActions.undoWithSave();
+ this.present();
+ }));
+
+ // 3
+ opts.add(new MenuOption("redo", () -> {
+ UndoActions.redoWithSave();
+ this.present();
+ }));
+
boolean inSetup = (ctx.getCurrentPlay() == null);
if (inSetup) {
- // 0) Start hole
+ // 4) Start hole
opts.add(new MenuOption("start hole", () -> {
ctx.startHole();
System.out.println("Started hole " + round.getCurrentHole().getNumber() + ".");
this.present();
}));
} else {
- // 0) Take a shot
+ // 4) Take a shot
opts.add(new MenuOption("take a shot", () -> {
var selector = new SelectClub();
selector.present();
@@ -60,6 +84,8 @@ public class HolePlayMenu extends Menu {
dist = Integer.parseInt(ds);
}
+ UndoManager.instance().capture(ctx, "Shot with " + club.getNickname() + " for " + dist + " yds");
+
// Records shot
ctx.recordShot(club, dist);
System.out.println("Shot recorded: " + dist + " yds with " + club.getNickname());
@@ -67,23 +93,19 @@ public class HolePlayMenu extends Menu {
this.present();
}));
- // 1) Hole out
+ // 5) Hole out
opts.add(new MenuOption("hole out", () -> {
// Precedes to next hole
int prev = round.getCurrentHole().getNumber();
+
+ UndoManager.instance().capture(golfer, "Hole out on " + prev + " at " + round.getCourse().getName());
+
ctx.holeOut();
System.out.println("Holed out on " + prev + ". Next: " + round.getCurrentHole().getNumber());
this.present();
}));
}
- // End round (always shown)
- opts.add(new MenuOption("end round", () -> {
- ctx.endRoundNow();
- System.out.println("Round ended.");
- new MainMenu().present();
- }));
-
return opts;
}
@@ -118,6 +140,8 @@ public class HolePlayMenu extends Menu {
// Starts round and sends user to HolePlayMenu
Hole startHole = course.getHoles().get(startHoleNum - 1);
Round r = new Round(course, Session.getDateTime(), startHole);
+ // Undo puts golfer back to before round existed.
+ UndoManager.instance().capture(golfer, "Start round on " + course.getName() + " (hole " + startHoleNum + ")");
golfer.addRound(r);
return r;
}
diff --git a/src/main/java/design/controller/userinput/menus/MainMenu.java b/src/main/java/design/controller/userinput/menus/MainMenu.java
index 7522ceb..4a221f0 100644
--- a/src/main/java/design/controller/userinput/menus/MainMenu.java
+++ b/src/main/java/design/controller/userinput/menus/MainMenu.java
@@ -2,6 +2,7 @@ package design.controller.userinput.menus;
import design.controller.userinput.Menu;
import design.controller.userinput.MenuOption;
+import design.controller.userinput.UndoActions;
import java.util.List;
@@ -15,11 +16,18 @@ public class MainMenu extends Menu {
public List<MenuOption> getMenuOptions() {
return List.of(
new MenuOption("quit", () -> System.exit(0)),
+ new MenuOption("undo", () -> {
+ UndoActions.undoWithSave();
+ this.present();
+ }),
+ new MenuOption("redo", () -> {
+ UndoActions.redoWithSave();
+ this.present();
+ }),
new MenuOption("user settings...", () -> new UserSettings().present()),
new MenuOption("manage courses...", () -> new ManageCourses().present()),
new MenuOption("manage clubs...", () -> new ManageClubs().present()),
new MenuOption("statistics...", () -> new StatisticsMenu().present()),
- new MenuOption("log round...", () -> new HolePlayMenu().present())
- );
+ new MenuOption("log round...", () -> new HolePlayMenu().present()));
}
}
diff --git a/src/main/java/design/controller/userinput/menus/ManageClubs.java b/src/main/java/design/controller/userinput/menus/ManageClubs.java
index 27b011c..d186cac 100644
--- a/src/main/java/design/controller/userinput/menus/ManageClubs.java
+++ b/src/main/java/design/controller/userinput/menus/ManageClubs.java
@@ -6,6 +6,7 @@ import design.model.Club;
import design.model.Golfer;
import design.persistence.PersonalDatabase;
import design.runtime.Session;
+import design.model.undo.UndoManager;
import java.io.IOException;
import java.util.Arrays;
@@ -36,7 +37,11 @@ public class ManageClubs extends Menu {
opts.add(new MenuOption("remove club...", () -> {
var selector = new SelectClub();
selector.present();
- golfer.removeClub(selector.getResult());
+ Club toRemove = selector.getResult();
+
+ UndoManager.instance().capture(golfer, "Remove club " + toRemove);
+
+ golfer.removeClub(toRemove);
try {
personalDB.updateGolfer(golfer);
} catch (IOException ex) {
@@ -46,6 +51,8 @@ public class ManageClubs extends Menu {
}));
opts.add(new MenuOption("add club...", () -> {
+ UndoManager.instance().capture(golfer, "Add club");
+
Scanner sc = new Scanner(System.in);
System.out.print("Manufacturer: ");
diff --git a/src/main/java/design/controller/userinput/menus/ManageCourses.java b/src/main/java/design/controller/userinput/menus/ManageCourses.java
index b592cd2..4f3efe5 100644
--- a/src/main/java/design/controller/userinput/menus/ManageCourses.java
+++ b/src/main/java/design/controller/userinput/menus/ManageCourses.java
@@ -6,6 +6,7 @@ import design.model.Course;
import design.model.Golfer;
import design.persistence.PersonalDatabase;
import design.runtime.Session;
+import design.model.undo.UndoManager;
import java.io.IOException;
import java.util.List;
@@ -34,7 +35,11 @@ public class ManageCourses extends Menu {
opts.add(new MenuOption("remove course...", () -> {
var selector = new SelectCourse();
selector.present();
- golfer.removeCourse(selector.getResult());
+ Course course = selector.getResult();
+
+ UndoManager.instance().capture(golfer, "Remove course " + course.getName());
+
+ golfer.removeCourse(course);
try {
personalDB.updateGolfer(golfer);
} catch (IOException ex) {
diff --git a/src/main/java/design/controller/userinput/menus/SelectUser.java b/src/main/java/design/controller/userinput/menus/SelectUser.java
index 4b7928c..a2c7071 100644
--- a/src/main/java/design/controller/userinput/menus/SelectUser.java
+++ b/src/main/java/design/controller/userinput/menus/SelectUser.java
@@ -6,6 +6,7 @@ import design.controller.userinput.MenuOption;
import design.model.Golfer;
import design.runtime.Session;
import design.persistence.PersonalDatabase;
+import design.model.undo.UndoManager;
import java.io.IOException;
import java.util.ArrayList;
@@ -61,6 +62,7 @@ public class SelectUser extends Menu {
if (g.checkPassword(password)) {
// Sets user in Session too
Session.setCurrentGolfer(g);
+ UndoManager.instance().purge();
System.out.printf("Loaded user: %s%n", g);
doAfter.onAction();
} else {
diff --git a/src/main/java/design/controller/userinput/menus/UserSettings.java b/src/main/java/design/controller/userinput/menus/UserSettings.java
index 47b0a5b..36d8d70 100644
--- a/src/main/java/design/controller/userinput/menus/UserSettings.java
+++ b/src/main/java/design/controller/userinput/menus/UserSettings.java
@@ -12,6 +12,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
+import design.model.undo.UndoManager;
+
public class UserSettings extends Menu {
private final Golfer golfer = Session.getCurrentGolfer();
@@ -35,6 +37,8 @@ public class UserSettings extends Menu {
}));
opts.add(new MenuOption("set password...", () -> {
+ UndoManager.instance().capture(golfer, "Change password for @" + golfer.getUsername());
+
System.out.print("Enter password: ");
var password = scanner.nextLine();
@@ -48,6 +52,8 @@ public class UserSettings extends Menu {
}));
opts.add(new MenuOption("set full name...", () -> {
+ UndoManager.instance().capture(golfer, "Change full name for @" + golfer.getUsername());
+
System.out.print("Enter full name: ");
var fullName = scanner.nextLine();
diff --git a/src/main/java/design/model/Golfer.java b/src/main/java/design/model/Golfer.java
index 48daae8..0f4c914 100644
--- a/src/main/java/design/model/Golfer.java
+++ b/src/main/java/design/model/Golfer.java
@@ -6,8 +6,11 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import java.util.ArrayList;
import java.util.List;
+import design.model.undo.Memento;
+import design.model.undo.Originator;
+
@JsonPropertyOrder({ "clubs", "nextClubId" })
-public class Golfer {
+public class Golfer implements Originator {
private String username;
private int passwordHash;
private String fullName;
@@ -112,4 +115,50 @@ public class Golfer {
public void removeClub(Club c) {
clubs.remove(c);
}
+
+ // Takes a snapshot of the golfer
+ private static class GolferMemento implements Memento {
+ private final String username;
+ private final int passwordHash;
+ private final String fullName;
+ private final List<Course> courses;
+ private final List<Round> rounds;
+ private final List<Club> clubs;
+ private final int nextClubId;
+
+ GolferMemento(Golfer g) {
+ this.username = g.username;
+ this.passwordHash = g.passwordHash;
+ this.fullName = g.fullName;
+ this.courses = new ArrayList<>(g.courses);
+ this.rounds = new ArrayList<>(g.rounds);
+ this.clubs = new ArrayList<>(g.clubs);
+ this.nextClubId = g.nextClubId;
+ }
+ }
+
+ @Override
+ public Memento createMemento() {
+ return new GolferMemento(this);
+ }
+
+ @Override
+ public void restore(Memento memento) {
+ GolferMemento gm = (GolferMemento) memento;
+
+ this.username = gm.username;
+ this.passwordHash = gm.passwordHash;
+ this.fullName = gm.fullName;
+
+ this.courses.clear();
+ this.courses.addAll(gm.courses);
+
+ this.rounds.clear();
+ this.rounds.addAll(gm.rounds);
+
+ this.clubs.clear();
+ this.clubs.addAll(gm.clubs);
+
+ this.nextClubId = gm.nextClubId;
+ }
}
diff --git a/src/main/java/design/model/holeplay/HolePlayContext.java b/src/main/java/design/model/holeplay/HolePlayContext.java
index 7a5d8ef..71fba32 100644
--- a/src/main/java/design/model/holeplay/HolePlayContext.java
+++ b/src/main/java/design/model/holeplay/HolePlayContext.java
@@ -6,7 +6,10 @@ import java.util.ArrayList;
import design.model.*;
import design.persistence.PersonalDatabase;
-public class HolePlayContext {
+import design.model.undo.Memento;
+import design.model.undo.Originator;
+
+public class HolePlayContext implements Originator {
private final Golfer golfer;
private final Round round;
private final PersonalDatabase pdb;
@@ -109,4 +112,60 @@ public class HolePlayContext {
void setStrokes(int s) {
this.strokes = s;
}
+
+ // Captures current State, current play (hole# + swings), and stroke counter
+ private static class HolePlayMemento implements Memento {
+ private final String stateName;
+ private final Play currentPlayCopy;
+ private final int strokesCopy;
+
+ HolePlayMemento(HolePlayContext ctx) {
+ this.stateName = ctx.state.name();
+
+ // Copy currentPlay if it exists
+ Play src = ctx.currentPlay;
+ if (src != null) {
+ var copySwings = new ArrayList<Swing>();
+ for (Swing s : src.getSwings()) {
+ copySwings.add(new Swing(s.getDistance(), s.getClubUsed()));
+ }
+ this.currentPlayCopy = new Play(src.getHoleNumber(), copySwings);
+ } else {
+ this.currentPlayCopy = null;
+ }
+
+ this.strokesCopy = ctx.strokes;
+ }
+ }
+
+ @Override
+ public Memento createMemento() {
+ return new HolePlayMemento(this);
+ }
+
+ @Override
+ public void restore(Memento m) {
+ HolePlayMemento hm = (HolePlayMemento) m;
+
+ // restore strokes
+ this.strokes = hm.strokesCopy;
+
+ // restore currentPlay
+ if (hm.currentPlayCopy == null) {
+ this.currentPlay = null;
+ } else {
+ var copySwings = new ArrayList<Swing>();
+ for (Swing s : hm.currentPlayCopy.getSwings()) {
+ copySwings.add(new Swing(s.getDistance(), s.getClubUsed()));
+ }
+ this.currentPlay = new Play(hm.currentPlayCopy.getHoleNumber(), copySwings);
+ }
+
+ // restore state based on name
+ switch (hm.stateName) {
+ case "SetupState" -> this.state = new SetupState();
+ case "PlayState" -> this.state = new PlayState();
+ case "HoleCompleteState" -> this.state = new HoleCompleteState();
+ }
+ }
} \ No newline at end of file
diff --git a/src/main/java/design/model/undo/Memento.java b/src/main/java/design/model/undo/Memento.java
new file mode 100644
index 0000000..162c3f3
--- /dev/null
+++ b/src/main/java/design/model/undo/Memento.java
@@ -0,0 +1,4 @@
+package design.model.undo;
+
+public interface Memento {
+}
diff --git a/src/main/java/design/model/undo/Originator.java b/src/main/java/design/model/undo/Originator.java
new file mode 100644
index 0000000..63eac38
--- /dev/null
+++ b/src/main/java/design/model/undo/Originator.java
@@ -0,0 +1,7 @@
+package design.model.undo;
+
+public interface Originator {
+ Memento createMemento();
+
+ void restore(Memento memento);
+} \ No newline at end of file
diff --git a/src/main/java/design/model/undo/UndoManager.java b/src/main/java/design/model/undo/UndoManager.java
new file mode 100644
index 0000000..5c6a4a0
--- /dev/null
+++ b/src/main/java/design/model/undo/UndoManager.java
@@ -0,0 +1,91 @@
+package design.model.undo;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.List;
+import java.util.stream.Collectors;
+
+// Singleton caretaker that handles undo and redo stacks.
+public final class UndoManager {
+
+ private static final UndoManager INSTANCE = new UndoManager();
+
+ public static UndoManager instance() {
+ return INSTANCE;
+ }
+
+ private UndoManager() {
+ }
+
+ // Stack entry
+ private static class Entry {
+ final Originator originator;
+ final Memento memento;
+ final String label;
+
+ Entry(Originator originator, Memento memento, String label) {
+ this.originator = originator;
+ this.memento = memento;
+ this.label = label;
+ }
+ }
+
+ private final Deque<Entry> undoStack = new ArrayDeque<>();
+ private final Deque<Entry> redoStack = new ArrayDeque<>();
+
+ // Capture state
+ public void capture(Originator originator, String label) {
+ undoStack.push(new Entry(originator, originator.createMemento(), label));
+ redoStack.clear();
+ }
+
+ // Undo / Redo ops
+ public boolean canUndo() {
+ return !undoStack.isEmpty();
+ }
+
+ public boolean canRedo() {
+ return !redoStack.isEmpty();
+ }
+
+ public String peekUndoLabel() {
+ return canUndo() ? undoStack.peek().label : null;
+ }
+
+ public String peekRedoLabel() {
+ return canRedo() ? redoStack.peek().label : null;
+ }
+
+ public List<String> getUndoHistoryLabels() {
+ return undoStack.stream()
+ .map(e -> e.label)
+ .collect(Collectors.toList());
+ }
+
+ public void undo() {
+ if (!canUndo()) {
+ System.out.println("Nothing to undo.");
+ return;
+ }
+
+ Entry entry = undoStack.pop();
+ redoStack.push(new Entry(entry.originator, entry.originator.createMemento(), entry.label));
+ entry.originator.restore(entry.memento);
+ }
+
+ public void redo() {
+ if (!canRedo()) {
+ System.out.println("Nothing to redo.");
+ return;
+ }
+
+ Entry entry = redoStack.pop();
+ undoStack.push(new Entry(entry.originator, entry.originator.createMemento(), entry.label));
+ entry.originator.restore(entry.memento);
+ }
+
+ public void purge() {
+ undoStack.clear();
+ redoStack.clear();
+ }
+}