diff --git a/modules/web/src/main/ui/help.scala b/modules/web/src/main/ui/help.scala index f3eeed66de71c..63a5f545f5b73 100644 --- a/modules/web/src/main/ui/help.scala +++ b/modules/web/src/main/ui/help.scala @@ -107,7 +107,8 @@ object help: row(kbd("n"), trans.study.nextChapter()), row(kbd("p"), trans.study.prevChapter()), row(frag((1 to 6).map(kbd(_))), trans.site.toggleGlyphAnnotations()), - row(frag(kbd("shift"), (1 to 8).map(kbd(_))), trans.site.togglePositionAnnotations()) + row(frag(kbd("shift"), (1 to 8).map(kbd(_))), trans.site.togglePositionAnnotations()), + row(frag(kbd("ctrl"), kbd("z")), "Undo arrow changes") ) ), header(trans.site.mouseTricks()), diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index aa88e519b2078..db8afb4cc215e 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -330,7 +330,7 @@ export default class AnalyseCtrl { this.withCg(cg => { cg.set(this.makeCgOpts()); this.setAutoShapes(); - if (this.node.shapes) cg.setShapes(this.node.shapes as DrawShape[]); + if (this.node.shapes) cg.setShapes(this.node.shapes.slice() as DrawShape[]); }); } @@ -395,7 +395,7 @@ export default class AnalyseCtrl { } this.setAutoShapes(); - if (this.node.shapes) this.chessground.setShapes(this.node.shapes as DrawShape[]); + if (this.node.shapes) this.chessground.setShapes(this.node.shapes.slice() as DrawShape[]); this.cgVersion.dom = this.cgVersion.js; }; diff --git a/ui/analyse/src/keyboard.ts b/ui/analyse/src/keyboard.ts index 331a57f441d2e..0c232dcb3eea1 100644 --- a/ui/analyse/src/keyboard.ts +++ b/ui/analyse/src/keyboard.ts @@ -140,6 +140,8 @@ export const bind = (ctrl: AnalyseCtrl) => { // = ∞ ⩲ ⩱ ± ∓ +- -+ for (let i = 1; i < 9; i++) kbd.bind(`shift+${i}`, () => ctrl.study?.glyphForm.toggleGlyph(i === 1 ? 10 : 11 + i)); + + kbd.bind('mod+z', ctrl.study.undoShapeChange); } }; diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index 8a0721bd7e37c..661e575ddb2cd 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -86,6 +86,7 @@ export default class StudyCtrl { relayRecProp = prop(false); nonRelayRecMapProp = storedMap('study.rec', 100, () => true); chapterFlipMapProp = storedMap('chapter.flip', 400, () => false); + arrowHistory: Tree.Shape[][] = []; data: StudyData; vm: StudyVm; notif: NotifCtrl; @@ -271,6 +272,25 @@ export default class StudyCtrl { isWriting = (): boolean => this.vm.mode.write && !this.isGamebookPlay(); + private updateShapes = (shapes: Tree.Shape[]) => { + this.ctrl.tree.setShapes(shapes, this.ctrl.path); + this.makeChange( + 'shapes', + this.addChapterId({ + path: this.ctrl.path, + shapes: shapes, + }), + ); + }; + + undoShapeChange = () => { + if (!this.vm.mode.write) return; + const last = this.arrowHistory.pop(); + if (!last) return; + this.updateShapes(last); + this.ctrl.withCg(cg => cg.setShapes(last.slice() as DrawShape[])); + }; + makeChange = (...args: StudySocketSendParams): boolean => { if (this.isWriting()) { this.send(...args); @@ -413,14 +433,8 @@ export default class StudyCtrl { mutateCgConfig = (config: Required>) => { config.drawable.onChange = (shapes: Tree.Shape[]) => { if (this.vm.mode.write) { - this.ctrl.tree.setShapes(shapes, this.ctrl.path); - this.makeChange( - 'shapes', - this.addChapterId({ - path: this.ctrl.path, - shapes, - }), - ); + this.arrowHistory.push(this.ctrl.node.shapes?.slice() ?? []); + this.updateShapes(shapes); } this.gamebookPlay?.onShapeChange(shapes); }; @@ -543,6 +557,7 @@ export default class StudyCtrl { this.isRelayAwayFromLive() && !treePath.contains(this.data.chapter.relayPath!, this.ctrl.path); setPath = (path: Tree.Path, node: Tree.Node) => { + this.arrowHistory = []; this.onSetPath(path); this.commentForm.onSetPath(this.vm.chapterId, path, node); }; diff --git a/ui/lib/src/tree/tree.ts b/ui/lib/src/tree/tree.ts index 46160a45ad311..f78787dc54b4c 100644 --- a/ui/lib/src/tree/tree.ts +++ b/ui/lib/src/tree/tree.ts @@ -218,7 +218,7 @@ export function build(root: Tree.Node): TreeWrapper { }, setShapes(shapes: Tree.Shape[], path: Tree.Path) { return updateAt(path, function (node: Tree.Node) { - node.shapes = shapes; + node.shapes = shapes.slice(); }); }, setCommentAt,