From 0a0c456faa3d58bc437852798741d3c1c27ce7d8 Mon Sep 17 00:00:00 2001 From: Felicia Hummel Date: Wed, 27 Oct 2021 22:41:15 +0200 Subject: [PATCH] feat: Added modified file tracking and a confirmation (#393) dialog on unsaved changes. This can be disabled if the user wishes to do so. Also allows the user to configure if the editor can be closed by hitting the "ESC" key. --- src/components/TheEditor.vue | 77 ++++++++++++++++++- src/components/TheSettingsMenu.vue | 10 ++- src/components/settings/SettingsEditorTab.vue | 45 +++++++++++ src/locales/en.json | 14 +++- src/store/editor/index.ts | 4 +- src/store/editor/mutations.ts | 28 +++++++ src/store/editor/types.ts | 4 +- src/store/gui/index.ts | 6 +- 8 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 src/components/settings/SettingsEditorTab.vue diff --git a/src/components/TheEditor.vue b/src/components/TheEditor.vue index 5dcc24aa..271706f0 100644 --- a/src/components/TheEditor.vue +++ b/src/components/TheEditor.vue @@ -6,19 +6,19 @@ + + + + + + {{ $t('Editor.UnsavedChanges') }} + + + + mdi-close-thick + + + + + + +

{{ $t('Editor.UnsavedChangesMessage', {filename: filename}) }}

+

{{ $t('Editor.UnsavedChangesSubMessage') }}

+
+
+ + + + + {{ $t('Editor.Cancel') }} + + + {{ $t('Editor.DontSave') }} + + + + {{ $t('Editor.SaveClose') }} + + +
+
+
+
@@ -65,16 +107,23 @@ import {Component, Mixins} from 'vue-property-decorator' import BaseMixin from '@/components/mixins/base' import {formatFilesize} from '@/plugins/helpers' import Codemirror from '@/components/inputs/Codemirror.vue' + @Component({ components: {Codemirror} }) export default class TheEditor extends Mixins(BaseMixin) { + private dialogConfirmChange = false + formatFilesize = formatFilesize refs!: { editor: any } + get changed() { + return this.$store.state.editor.changed ? '*' : '' + } + get show() { return this.$store.state.editor.bool ?? false } @@ -146,11 +195,33 @@ export default class TheEditor extends Mixins(BaseMixin) { this.$store.dispatch('editor/cancelLoad') } + escClose() { + if (this.$store.state.gui.editor.escToClose) + this.close() + } + close() { + if (this.$store.state.gui.editor.confirmUnsavedChanges) + this.promptUnsavedChanges() + else + this.$store.dispatch('editor/close') + } + + discardChanges() { + this.dialogConfirmChange = false this.$store.dispatch('editor/close') } + promptUnsavedChanges() { + if (!this.$store.state.editor.changed) + this.$store.dispatch('editor/close') + else + this.dialogConfirmChange = true + } + save(restartServiceName: string | null = null) { + this.dialogConfirmChange = false + this.$store.dispatch('editor/saveFile', { content: this.sourcecode, restartServiceName: restartServiceName diff --git a/src/components/TheSettingsMenu.vue b/src/components/TheSettingsMenu.vue index 66c134bc..e55860d1 100644 --- a/src/components/TheSettingsMenu.vue +++ b/src/components/TheSettingsMenu.vue @@ -59,6 +59,8 @@ import SettingsRemotePrintersTab from '@/components/settings/SettingsRemotePrint import SettingsThemeTab from '@/components/settings/SettingsThemeTab.vue' import SettingsDashboardTab from '@/components/settings/SettingsDashboardTab.vue' import SettingsGCodeViewerTab from '@/components/settings/SettingsGCodeViewerTab.vue' +import SettingsEditorTab from '@/components/settings/SettingsEditorTab.vue' + import Panel from '@/components/ui/Panel.vue' @Component({ components: { @@ -72,7 +74,8 @@ import Panel from '@/components/ui/Panel.vue' SettingsWebcamTab, SettingsGeneralTab, SettingsDashboardTab, - SettingsGCodeViewerTab + SettingsGCodeViewerTab, + SettingsEditorTab } }) export default class TheSettingsMenu extends Mixins(BaseMixin) { @@ -134,6 +137,11 @@ export default class TheSettingsMenu extends Mixins(BaseMixin) { icon: 'mdi-video-3d', name: 'g-code-viewer', title: this.$t('Settings.GCodeViewerTab.GCodeViewer') + }, + { + icon: 'mdi-file-document-edit-outline', + name: 'editor', + title: this.$t('Settings.EditorTab.Editor') } ] } diff --git a/src/components/settings/SettingsEditorTab.vue b/src/components/settings/SettingsEditorTab.vue new file mode 100644 index 00000000..f00039dc --- /dev/null +++ b/src/components/settings/SettingsEditorTab.vue @@ -0,0 +1,45 @@ + + + \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index e5cd4aaf..34e71557 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -105,7 +105,12 @@ "SaveRestart": "Save & Restart", "SaveClose": "Save & close", "SuccessfullySaved": "{filename} successfully saved.", - "FailedSave": "{filename} could not be uploaded!" + "FailedSave": "{filename} could not be uploaded!", + "UnsavedChanges": "Unsaved Changes", + "UnsavedChangesMessage": "Do you want to save your changes made to {filename}?", + "UnsavedChangesSubMessage": "Your changes will be lost if you don't save them. You can disable this message in the editor settings.", + "DontSave": "Don't save", + "Cancel": "Cancel" }, "Files": { "GCodeFiles": "G-Code Files", @@ -664,6 +669,13 @@ "ShowAxes": "Show Axes", "MinFeed" :"Min Feed Rate", "MaxFeed" : "Max Feed Rate" + }, + "EditorTab": { + "Editor": "Editor", + "UseEscToClose": "Use ESC to close editor", + "UseEscToCloseDescription": "Allows the ESC key to close the editor", + "ConfirmUnsavedChanges": "Prompt to save or discard unsaved changes", + "ConfirmUnsavedChangesDescription": "If enabled, the editor requires a confirmation to either save or discard the changes made. If disabled, changes are silently discarded." } }, "GCodeViewer":{ diff --git a/src/store/editor/index.ts b/src/store/editor/index.ts index cfb7b252..5dff1e16 100644 --- a/src/store/editor/index.ts +++ b/src/store/editor/index.ts @@ -20,7 +20,9 @@ export const getDefaultState = (): EditorState => { total: 0, speed: '', }, - cancelToken: null + cancelToken: null, + loadedHash: '', + changed: false } } diff --git a/src/store/editor/mutations.ts b/src/store/editor/mutations.ts index be8fcc23..874fb38a 100644 --- a/src/store/editor/mutations.ts +++ b/src/store/editor/mutations.ts @@ -2,8 +2,10 @@ import { getDefaultState } from './index' import {MutationTree} from 'vuex' import {EditorState} from '@/store/editor/types' import Vue from 'vue' +import { sha256 } from 'js-sha256' export const mutations: MutationTree = { + reset(state) { Object.assign(state, getDefaultState()) }, @@ -25,6 +27,13 @@ export const mutations: MutationTree = { Vue.set(state, 'fileroot', payload.fileroot) Vue.set(state, 'filepath', payload.filepath) Vue.set(state, 'sourcecode', payload.file) + + // Because the used editor converts all Windows-Style line endings with unix ones on load, + // the hash is computed with the source always having unix-style line endings. + // https://github.com/codemirror/CodeMirror/issues/3395 + + Vue.set(state, 'loadedHash', sha256(payload.file.replace(/(?:\r\n|\r|\n)/g, '\n'))) + Vue.set(state, 'changed', false) Vue.set(state, 'bool', true) }, @@ -42,5 +51,24 @@ export const mutations: MutationTree = { updateSourcecode(state, payload) { Vue.set(state, 'sourcecode', payload) + + // To check if a file has been changed by the user, we need to calculate a hash + // (or otherwise we would need to save the full file in memory twice). Simply listening + // to the changed event is not enough, because if the user types an "a" and then deletes + // the "a" again, the file would still be shown as changed, even though the edited file + // is equal to the stored file. + + // I've tested this functionality with huge text files (60MB G-Code) and while calculating + // the hash took 2 seconds per run, the editor itself is pretty laggy even without hash + // calculations. Hash calculations with typical config file sizes (50KB) only take 1 or 2ms + // on my machine, so I guess this is acceptable for most use cases. + + if (sha256(payload) != state.loadedHash) + state.changed = true + else + state.changed = false } + } + + diff --git a/src/store/editor/types.ts b/src/store/editor/types.ts index d8e622cb..fa2d6f3d 100644 --- a/src/store/editor/types.ts +++ b/src/store/editor/types.ts @@ -13,5 +13,7 @@ export interface EditorState { total: number speed: string }, - cancelToken: any + cancelToken: any, + loadedHash: string, + changed: boolean } \ No newline at end of file diff --git a/src/store/gui/index.ts b/src/store/gui/index.ts index 9c5d13f7..1cc5600f 100644 --- a/src/store/gui/index.ts +++ b/src/store/gui/index.ts @@ -167,7 +167,9 @@ export const getDefaultState = (): GuiState => { } }, editor: { - minimap: false + minimap: false, + escToClose: true, + confirmUnsavedChanges: true }, //moonraker DB api dont accept camel case key names remotePrinters: [], @@ -194,7 +196,7 @@ export const getDefaultState = (): GuiState => { voxelWidth: 1, voxelHeight: 1, specularLighting: false, - } + }, } }