diff --git a/client/src/actions/action.ts b/client/src/actions/action.ts index 6a486c9..755ea1e 100644 --- a/client/src/actions/action.ts +++ b/client/src/actions/action.ts @@ -1,3 +1,4 @@ export interface Action { run(): void; + undo?(): void; } diff --git a/client/src/actions/notefield/placeTap.ts b/client/src/actions/notefield/placeTap.ts index 422ea08..b4783af 100644 --- a/client/src/actions/notefield/placeTap.ts +++ b/client/src/actions/notefield/placeTap.ts @@ -37,4 +37,9 @@ export class PlaceTapAction implements Action { removeIfExists: true, }); } + + undo(): void { + // FIXME: This won't work correctly if placing a note gets rid of a hold. + this.run(); + } } diff --git a/client/src/actions/notefield/playPause.ts b/client/src/actions/notefield/playPause.ts index 81d9cb8..0010a26 100644 --- a/client/src/actions/notefield/playPause.ts +++ b/client/src/actions/notefield/playPause.ts @@ -15,4 +15,8 @@ export class PlayPauseAction implements Action { const { notefield } = this.store; notefield.setPlaying(!notefield.data.isPlaying); } + + undo(): void { + this.run(); + } } diff --git a/client/src/actions/notefield/scroll.ts b/client/src/actions/notefield/scroll.ts index 74f4c90..950b266 100644 --- a/client/src/actions/notefield/scroll.ts +++ b/client/src/actions/notefield/scroll.ts @@ -1,6 +1,6 @@ import assert from "assert"; -import { BeatTime } from "../../charting"; +import { Beat, BeatTime } from "../../charting"; import { RootStore } from "../../store"; import { Action } from "../action"; @@ -17,6 +17,8 @@ export interface ScrollArgs { */ export class ScrollAction implements Action { args: ScrollArgs; + oldScroll!: Beat; + store: RootStore; constructor(store: RootStore, args: ScrollArgs) { @@ -28,6 +30,7 @@ export class ScrollAction implements Action { run(): void { const args = this.args; + this.oldScroll = this.store.notefield.data.scroll.beat; if (args.by) { this.store.notefield.scrollBy(args.by); @@ -35,4 +38,8 @@ export class ScrollAction implements Action { this.store.notefield.setScroll(args.to); } } + + undo(): void { + this.store.notefield.setScroll({ beat: this.oldScroll }); + } } diff --git a/client/src/actions/notefield/scrollDirection.ts b/client/src/actions/notefield/scrollDirection.ts index 4db2b7b..ffa109d 100644 --- a/client/src/actions/notefield/scrollDirection.ts +++ b/client/src/actions/notefield/scrollDirection.ts @@ -13,6 +13,8 @@ export interface ScrollDirectionArgs { */ export class ScrollDirectionAction implements Action { args: ScrollDirectionArgs; + oldDirection!: ScrollDirection; + store: RootStore; constructor(store: RootStore, args: ScrollDirectionArgs) { @@ -24,6 +26,8 @@ export class ScrollDirectionAction implements Action { const { to } = this.args; const { data } = this.store.notefieldDisplay; + this.oldDirection = this.store.notefieldDisplay.data.scrollDirection; + if (to === "swap") { if (data.scrollDirection === "up") { this.store.notefieldDisplay.update({ scrollDirection: "down" }); @@ -34,4 +38,8 @@ export class ScrollDirectionAction implements Action { this.store.notefieldDisplay.update({ scrollDirection: to }); } } + + undo(): void { + this.store.notefieldDisplay.update({ scrollDirection: this.oldDirection }); + } } diff --git a/client/src/actions/notefield/snapAdjust.ts b/client/src/actions/notefield/snapAdjust.ts index bfb8c1c..42a2842 100644 --- a/client/src/actions/notefield/snapAdjust.ts +++ b/client/src/actions/notefield/snapAdjust.ts @@ -1,5 +1,6 @@ import assert from "assert"; import Fraction from "fraction.js"; +import { BeatSnap } from "../../notefield/beatsnap"; import { RootStore } from "../../store"; import { Action } from "../action"; @@ -17,6 +18,8 @@ export interface SnapAdjustArgs { */ export class SnapAdjustAction implements Action { args: SnapAdjustArgs; + oldSnap!: BeatSnap; + store: RootStore; constructor(store: RootStore, args: SnapAdjustArgs) { @@ -30,6 +33,8 @@ export class SnapAdjustAction implements Action { const args = this.args; const { snap } = this.store.notefield.data; + this.oldSnap = snap; + if (args.adjust === "next") { snap.nextSnap(); } else if (args.adjust === "prev") { @@ -38,4 +43,8 @@ export class SnapAdjustAction implements Action { snap.setSnap(args.to); } } + + undo(): void { + this.store.notefield.data.snap.setSnap(this.oldSnap.current); + } } diff --git a/client/src/actions/notefield/snapScroll.ts b/client/src/actions/notefield/snapScroll.ts index 5babfee..5001210 100644 --- a/client/src/actions/notefield/snapScroll.ts +++ b/client/src/actions/notefield/snapScroll.ts @@ -1,3 +1,4 @@ +import { Beat } from "../../charting"; import { RootStore } from "../../store"; import { Action } from "../action"; @@ -14,6 +15,8 @@ export interface SnapScrollArgs { */ export class SnapScrollAction implements Action { args: SnapScrollArgs; + oldScroll!: Beat; + store: RootStore; constructor(store: RootStore, args: SnapScrollArgs) { @@ -34,6 +37,8 @@ export class SnapScrollAction implements Action { return; } + this.oldScroll = notefield.data.scroll.beat; + const { scroll, snap } = notefield.data; let dir = this.args.direction; @@ -45,4 +50,8 @@ export class SnapScrollAction implements Action { this.store.notefield.setScroll({ beat }); } + + undo(): void { + this.store.notefield.setScroll({ beat: this.oldScroll }); + } } diff --git a/client/src/actions/notefield/zoom.ts b/client/src/actions/notefield/zoom.ts index be0aa61..e8d7575 100644 --- a/client/src/actions/notefield/zoom.ts +++ b/client/src/actions/notefield/zoom.ts @@ -16,6 +16,8 @@ export interface ZoomArgs { */ export class ZoomAction implements Action { args: ZoomArgs; + oldZoom!: Fraction; + store: RootStore; /** @@ -31,6 +33,13 @@ export class ZoomAction implements Action { } run(): void { - this.store.notefield.setZoom(this.args.to); + const { notefield } = this.store; + + this.oldZoom = notefield.data.zoom; + notefield.setZoom(this.args.to); + } + + undo(): void { + this.store.notefield.setZoom(this.oldZoom); } } diff --git a/client/src/store/action.ts b/client/src/store/action.ts new file mode 100644 index 0000000..e9c17d3 --- /dev/null +++ b/client/src/store/action.ts @@ -0,0 +1,59 @@ +import { Action } from "../actions/action"; +import { RootStore } from "./root"; + +export class ActionStore { + readonly root: RootStore; + + stack: { + undo: Action[]; + redo: Action[]; + }; + + constructor(root: RootStore) { + this.root = root; + + this.stack = { + undo: [], + redo: [], + }; + } + + canUndo(): boolean { + return this.stack.undo.length > 0; + } + + canRedo(): boolean { + return this.stack.redo.length > 0; + } + + run(action: Action) { + action.run(); + + if (action.undo) { + this.stack.redo = []; + this.stack.undo.push(action); + } + } + + undo() { + const action = this.stack.undo.pop(); + + if (!action) { + return; + } + + action.undo!(); + this.stack.redo.push(action); + } + + redo() { + const action = this.stack.redo.pop(); + + if (!action) { + return; + } + + action.run(); + this.stack.undo.push(action); + } +} diff --git a/client/src/store/root.ts b/client/src/store/root.ts index 18c09d6..180c8f2 100644 --- a/client/src/store/root.ts +++ b/client/src/store/root.ts @@ -1,3 +1,4 @@ +import { ActionStore } from "./action"; import { NotefieldStore } from "./notefield"; import { NotefieldDisplayStore } from "./notefieldDisplay"; import { ProjectStore } from "./project"; @@ -8,6 +9,7 @@ import { WaveformStore } from "./waveform"; * The root store for the application that contains all of the application data. */ export class RootStore { + readonly actions: ActionStore; readonly notefieldDisplay: NotefieldDisplayStore; readonly notefield: NotefieldStore; readonly project: ProjectStore; @@ -15,6 +17,7 @@ export class RootStore { readonly ui: UIStore; constructor() { + this.actions = new ActionStore(this); this.ui = new UIStore(this); this.notefieldDisplay = new NotefieldDisplayStore(this); this.project = new ProjectStore(this);