mirror of
https://github.com/IvarK/AntimatterDimensionsSourceCode.git
synced 2025-02-18 00:20:13 +00:00
Add undo/redo functionality to text automator
This commit is contained in:
parent
5f48af2c87
commit
13673ae0c7
@ -29,6 +29,7 @@ export default {
|
||||
// AutomatorBackend.deleteScript will create an empty script if necessary
|
||||
player.reality.automator.state.editorScript = scriptList[0].id;
|
||||
}
|
||||
AutomatorData.clearUndoData();
|
||||
EventHub.dispatch(GAME_EVENT.AUTOMATOR_SAVE_CHANGED);
|
||||
},
|
||||
},
|
||||
|
@ -169,6 +169,7 @@ export default {
|
||||
if (storedScripts[this.currentScriptID] === undefined) {
|
||||
this.currentScriptID = Number(Object.keys(storedScripts)[0]);
|
||||
player.reality.automator.state.editorScript = this.currentScriptID;
|
||||
AutomatorData.clearUndoData();
|
||||
}
|
||||
|
||||
// This gets checked whenever the editor pane is foricibly changed to a different script, which may or may not
|
||||
|
@ -59,6 +59,7 @@ export default {
|
||||
if (storedScripts[this.currentScriptID] === undefined) {
|
||||
this.currentScriptID = Number(Object.keys(storedScripts)[0]);
|
||||
player.reality.automator.state.editorScript = this.currentScriptID;
|
||||
AutomatorData.clearUndoData();
|
||||
}
|
||||
// This may happen if the player has errored textmato scripts and switches to them while in blockmato mode
|
||||
if (BlockAutomator.hasUnparsableCommands(this.currentScript) &&
|
||||
|
@ -53,6 +53,7 @@ export default {
|
||||
if (storedScripts[this.currentScriptID] === undefined) {
|
||||
this.currentScriptID = Object.keys(storedScripts)[0];
|
||||
player.reality.automator.state.editorScript = this.currentScriptID;
|
||||
AutomatorData.clearUndoData();
|
||||
}
|
||||
if (BlockAutomator.hasUnparsableCommands(this.currentScript) &&
|
||||
player.reality.automator.type === AUTOMATOR_TYPE.BLOCK) {
|
||||
@ -81,6 +82,7 @@ export default {
|
||||
} else {
|
||||
AutomatorBackend.changeModes(this.currentScriptID);
|
||||
}
|
||||
AutomatorData.clearUndoData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -67,6 +67,7 @@ export default {
|
||||
}
|
||||
if (this.isBlock) this.$nextTick(() => BlockAutomator.fromText(this.currentScript));
|
||||
this.$parent.openRequest = false;
|
||||
AutomatorData.clearUndoData();
|
||||
},
|
||||
dropdownLabel(script) {
|
||||
const labels = [];
|
||||
|
@ -127,16 +127,33 @@ export const AutomatorTextUI = {
|
||||
},
|
||||
setUpEditor() {
|
||||
this.editor = CodeMirror.fromTextArea(this.textArea, this.mode);
|
||||
// CodeMirror has a built-in undo/redo functionality bound to ctrl-z/ctrl-y which doesn't have an
|
||||
// easily-configured history buffer; we need to specifically cancel this event since we have our own undo
|
||||
this.editor.on("beforeChange", (_, event) => {
|
||||
if (event.origin === "undo") event.cancel();
|
||||
});
|
||||
this.editor.on("keydown", (editor, event) => {
|
||||
if (editor.state.completionActive) return;
|
||||
const key = event.key;
|
||||
if (event.ctrlKey && ["z", "y"].includes(key)) {
|
||||
if (key === "z") AutomatorData.undoScriptEdit();
|
||||
if (key === "y") AutomatorData.redoScriptEdit();
|
||||
return;
|
||||
}
|
||||
// This check is related to the drop-down command suggestion menu, but must come after the undo/redo check
|
||||
// as it often evaluates to innocuous false positives which eat the keybinds
|
||||
if (editor.state.completionActive) return;
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || !/^[a-zA-Z0-9 \t]$/u.test(key)) return;
|
||||
CodeMirror.commands.autocomplete(editor, null, { completeSingle: false });
|
||||
});
|
||||
this.editor.on("change", editor => {
|
||||
this.editor.on("change", (editor, event) => {
|
||||
const scriptID = ui.view.tabs.reality.automator.editorScriptID;
|
||||
const scriptText = editor.getDoc().getValue();
|
||||
AutomatorBackend.saveScript(scriptID, scriptText);
|
||||
// Undo/redo directly changes the editor contents, which also causes this event to be fired; we have a few
|
||||
// things which we specifically only want to do on manual typing changes
|
||||
if (event.origin !== "setValue") {
|
||||
AutomatorBackend.saveScript(scriptID, scriptText);
|
||||
AutomatorData.redoBuffer = [];
|
||||
}
|
||||
|
||||
AutomatorData.recalculateErrors();
|
||||
const errors = AutomatorData.currentErrors().length;
|
||||
|
@ -180,6 +180,9 @@ export const AutomatorData = {
|
||||
cachedErrors: 0,
|
||||
// This is to hold finished script templates as text in order to make the custom blocks for blockmato
|
||||
blockTemplates: [],
|
||||
undoBuffer: [],
|
||||
redoBuffer: [],
|
||||
charsSinceLastUndoState: 0,
|
||||
|
||||
MAX_ALLOWED_SCRIPT_CHARACTERS: 10000,
|
||||
MAX_ALLOWED_TOTAL_CHARACTERS: 60000,
|
||||
@ -189,6 +192,8 @@ export const AutomatorData = {
|
||||
// Note that a study string with ALL studies in unshortened form without duplicated studies is ~230 characters
|
||||
MAX_ALLOWED_CONSTANT_VALUE_LENGTH: 250,
|
||||
MAX_ALLOWED_CONSTANT_COUNT: 30,
|
||||
MIN_CHARS_BETWEEN_UNDOS: 10,
|
||||
MAX_UNDO_ENTRIES: 30,
|
||||
|
||||
scriptIndex() {
|
||||
return player.reality.automator.state.editorScript;
|
||||
@ -204,6 +209,7 @@ export const AutomatorData = {
|
||||
const newScript = AutomatorScript.create(name, content);
|
||||
GameUI.notify.automator(`Imported Script "${name}"`);
|
||||
player.reality.automator.state.editorScript = newScript.id;
|
||||
AutomatorData.clearUndoData();
|
||||
EventHub.dispatch(GAME_EVENT.AUTOMATOR_SAVE_CHANGED);
|
||||
},
|
||||
recalculateErrors() {
|
||||
@ -255,6 +261,51 @@ export const AutomatorData = {
|
||||
return this.singleScriptCharacters() <= this.MAX_ALLOWED_SCRIPT_CHARACTERS &&
|
||||
this.totalScriptCharacters() <= this.MAX_ALLOWED_TOTAL_CHARACTERS;
|
||||
},
|
||||
|
||||
// This must be called every time the current script or editor mode are changed
|
||||
clearUndoData() {
|
||||
this.undoBuffer = [];
|
||||
this.redoBuffer = [];
|
||||
this.charsSinceLastUndoState = 0;
|
||||
},
|
||||
// We only save an undo state every so often based on the number of characters that have been modified
|
||||
// since the last state. This gets passed in as a parameter and gets called every time any typing is done,
|
||||
// but only actually does something when that threshold is reached.
|
||||
pushUndoData(data, newChars) {
|
||||
this.charsSinceLastUndoState += newChars;
|
||||
if (this.charsSinceLastUndoState <= this.MIN_CHARS_BETWEEN_UNDOS) return;
|
||||
if (this.undoBuffer[this.undoBuffer.length - 1] !== data) this.undoBuffer.push(data);
|
||||
if (this.undoBuffer.length > this.MAX_UNDO_ENTRIES) this.undoBuffer.shift();
|
||||
this.charsSinceLastUndoState = 0;
|
||||
},
|
||||
pushRedoData(data) {
|
||||
if (this.redoBuffer[this.redoBuffer.length - 1] !== data) this.redoBuffer.push(data);
|
||||
},
|
||||
// These following two methods pop the top entry off of the undo/redo stack and then push it
|
||||
// onto the *other* stack before modifying all the relevant UI elements and player props. These
|
||||
// could in principle be combined into one function to reduce boilerplace, but keeping them
|
||||
// separate is probably more readable externally
|
||||
undoScriptEdit() {
|
||||
if (this.undoBuffer.length === 0) return;
|
||||
|
||||
const undoContent = this.undoBuffer.pop();
|
||||
this.pushRedoData(this.currentScriptText());
|
||||
player.reality.automator.scripts[this.scriptIndex()].content = undoContent;
|
||||
|
||||
if (AutomatorTextUI.editor) AutomatorTextUI.editor.setValue(undoContent);
|
||||
AutomatorBackend.saveScript(this.scriptIndex(), undoContent);
|
||||
},
|
||||
redoScriptEdit() {
|
||||
if (this.redoBuffer.length === 0) return;
|
||||
|
||||
const redoContent = this.redoBuffer.pop();
|
||||
// We call this with a value which is always higher than said threshold, forcing the current text to be pushed
|
||||
this.pushUndoData(this.currentScriptText(), 2 * this.MIN_CHARS_BETWEEN_UNDOS);
|
||||
player.reality.automator.scripts[this.scriptIndex()].content = redoContent;
|
||||
|
||||
if (AutomatorTextUI.editor) AutomatorTextUI.editor.setValue(redoContent);
|
||||
AutomatorBackend.saveScript(this.scriptIndex(), redoContent);
|
||||
}
|
||||
};
|
||||
|
||||
export const LineEnum = { Active: "active", Event: "event", Error: "error" };
|
||||
@ -806,9 +857,20 @@ export const AutomatorBackend = {
|
||||
}
|
||||
},
|
||||
|
||||
// Note: This gets run every time any edit or mode conversion is done
|
||||
saveScript(id, data) {
|
||||
if (!this.findScript(id)) return;
|
||||
this.findScript(id).save(data);
|
||||
const script = this.findScript(id);
|
||||
if (!script) return;
|
||||
|
||||
// Add the old data to the undo buffer; there are internal checks which prevent it from saving too often.
|
||||
// For performance, the contents of the script aren't actually checked (this would be an unavoidable O(n) cost).
|
||||
// Instead we naively assume length changes are pure insertions and deletions, which does mean we're ignoring
|
||||
// a few edge cases when changes are really substitutions that massively change the content
|
||||
const oldData = script.persistent.content;
|
||||
const lenChange = Math.abs(oldData.length - data.length);
|
||||
AutomatorData.pushUndoData(oldData, lenChange);
|
||||
|
||||
script.save(data);
|
||||
if (id === this.state.topLevelScript) this.stop();
|
||||
},
|
||||
|
||||
|
@ -124,13 +124,25 @@ export const shortcuts = [
|
||||
keys: ["u"],
|
||||
type: "bindHotkey",
|
||||
function: () => keyboardAutomatorToggle(),
|
||||
visible: () => PlayerProgress.realityUnlocked()
|
||||
visible: () => Player.automatorUnlocked
|
||||
}, {
|
||||
name: "Restart Automator",
|
||||
keys: ["shift", "u"],
|
||||
type: "bindHotkey",
|
||||
function: () => keyboardAutomatorRestart(),
|
||||
visible: () => PlayerProgress.realityUnlocked()
|
||||
visible: () => Player.automatorUnlocked
|
||||
}, {
|
||||
name: "Undo Edit (Automator)",
|
||||
keys: ["mod", "z"],
|
||||
type: "bind",
|
||||
function: () => AutomatorData.undoScriptEdit(),
|
||||
visible: () => Player.automatorUnlocked
|
||||
}, {
|
||||
name: "Redo Edit (Automator)",
|
||||
keys: ["mod", "y"],
|
||||
type: "bind",
|
||||
function: () => AutomatorData.redoScriptEdit(),
|
||||
visible: () => Player.automatorUnlocked
|
||||
}, {
|
||||
name: "Toggle Black Hole",
|
||||
keys: ["b"],
|
||||
|
Loading…
Reference in New Issue
Block a user