import { computed , observable } from 'mobx' ; import _ from 'lodash' ; import logging , { Events } from './logging' ; import { Note , SerializedNote } from './note' ; import { NoteSequence } from './note-sequence' ; import { editor , generator , masks } from './index' ; import { IMasks } from './masks' ; export interface UndoStep { notes : SerializedNote [ ] ; candidateNotes : SerializedNote [ ] [ ] ; selectedCandidateIndex : number ; masks : IMasks ; } class Undo { @ observable undoStack : UndoStep [ ] = [ ] ; @ observable redoStack : UndoStep [ ] = [ ] ; private pendingUndoStep : UndoStep | null = null ; private undoableNesting : string [ ] = [ ] ; private undoLocked = false ; @ computed get canUndo ( ) { return this . undoStack . length > 0 ; } @ computed get canRedo ( ) { return this . redoStack . length > 0 ; } getSerializedNotes ( ) { return editor . mainNotes . map ( note => note . serialize ( ) ) ; } getSerializedCandidateNotes ( ) { return generator . candidateSequences . map ( sequence => { return sequence . notes . map ( note => note . serialize ( ) ) ; } ) ; } getSerializedMasks ( ) { return masks . userMasks . map ( mask => [ ... mask ] ) as IMasks ; } getUndoStep ( ) { return { notes : this . getSerializedNotes ( ) , candidateNotes : this . getSerializedCandidateNotes ( ) , masks : this . getSerializedMasks ( ) , selectedCandidateIndex : generator . selectedCandidateSequenceIndex , } ; } beginUndoable ( label ) { if ( this . undoLocked ) return ; this . undoableNesting . push ( label ) ; if ( this . pendingUndoStep ) { return ; } this . pendingUndoStep = this . getUndoStep ( ) ; } completeUndoable ( label ) { if ( this . undoLocked ) return ; const topLabel = this . undoableNesting . pop ( ) ; if ( label !== topLabel ) { console . error ( `Undoables of types ${ label }, ${ topLabel } completed in incorrect order ` ) ; } if ( this . undoableNesting . length === 0 ) { this . undoStack . push ( this . pendingUndoStep ) ; this . redoStack = [ ] ; this . pendingUndoStep = null ; } } private lockUndo ( ) { this . undoLocked = true ; } private unlockUndo ( ) { this . undoLocked = false ; } undo ( ) { if ( this . undoStack . length ) { this . redoStack . push ( this . getUndoStep ( ) ) ; const lastStep = this . undoStack . pop ( ) ; this . rehydrateStep ( lastStep ) ; logging . logEvent ( Events . UNDO , lastStep ) ; } } redo ( ) { if ( this . redoStack . length ) { this . undoStack . push ( this . getUndoStep ( ) ) ; const lastStep = this . redoStack . pop ( ) ; this . rehydrateStep ( lastStep ) ; logging . logEvent ( Events . REDO , lastStep ) ; } } rehydrateStep ( step ) { this . lockUndo ( ) ; const deserializedNotes = step . notes . map ( note => Note . fromSerialized ( note ) ) ; editor . replaceAllNotes ( deserializedNotes ) ; const sequences = step . candidateNotes . map ( serialized => { const sequence = new NoteSequence ( serialized . map ( note => Note . fromSerialized ( note ) ) ) ; return sequence ; } ) ; generator . setCandidateSequences ( sequences ) ; generator . selectedCandidateSequenceIndex = step . selectedCandidateIndex ; masks . setMasks ( step . masks ) ; this . unlockUndo ( ) ; } } const undo = new Undo ( ) ; export default undo ; export function undoable ( label ) { return function ( target , propertyKey , descriptor ) { const originalMethod = descriptor . value ; descriptor . value = function ( ) { undo . beginUndoable ( label ) ; originalMethod . apply ( this , arguments ) ; undo . completeUndoable ( label ) ; } ; return descriptor ; } ; }