From a2ff0da4cf242e70de7f6ceb5f1242f59fcb9a8e Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 6 Apr 2021 19:06:09 +0200 Subject: [PATCH] History of actions (CTRL+Z, CTRL+SHIFT+Z)! Fixing a lot of bugs in the process --- .gitignore | 2 + qml/AppMenuBar.qml | 21 ++++++- qml/History.qml | 72 +++++++++++++++++++++++ qml/HistoryBrowser.qml | 1 + qml/LogGraph.qml | 3 +- qml/ObjectLists.qml | 63 ++++++++++++++++---- qml/TextSetting.qml | 5 +- qml/js/historylib.js | 128 +++++++++++++++++++++++++++++++++++++++++ qml/js/mathlib.js | 2 +- qml/js/objects.js | 50 ++++++++++------ 10 files changed, 311 insertions(+), 36 deletions(-) create mode 100644 qml/History.qml create mode 100644 qml/HistoryBrowser.qml create mode 100644 qml/js/historylib.js diff --git a/.gitignore b/.gitignore index ce5ff32..099b0a6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ docs/html .directory *.kdev4 *.json +*.lpf +*.lgg .kdev4 AccountFree.pro AccountFree.pro.user diff --git a/qml/AppMenuBar.qml b/qml/AppMenuBar.qml index 71373b6..5550c16 100644 --- a/qml/AppMenuBar.qml +++ b/qml/AppMenuBar.qml @@ -19,6 +19,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import "js/objects.js" as Objects +import "js/historylib.js" as HistoryLib MenuBar { Menu { @@ -53,6 +54,23 @@ MenuBar { } Menu { title: qsTr("&Edit") + Action { + text: qsTr("&Undo") + shortcut: StandardKey.Undo + onTriggered: history.undo() + icon.name: 'edit-undo' + icon.color: enabled ? sysPalette.windowText : sysPaletteIn.windowText + enabled: history.undoCount > 0 + } + Action { + text: qsTr("&Redo") + shortcut: StandardKey.Redo + onTriggered: history.redo() + icon.name: 'edit-redo' + icon.color: enabled ? sysPalette.windowText : sysPaletteIn.windowText + enabled: history.redoCount > 0 + } + MenuSeparator { } Action { text: qsTr("&Copy diagram") shortcut: StandardKey.Copy @@ -74,7 +92,8 @@ MenuBar { icon.name: modelData icon.color: sysPalette.windowText onTriggered: { - Objects.createNewRegisteredObject(modelData) + var newObj = Objects.createNewRegisteredObject(modelData) + history.addToHistory(new HistoryLib.CreateNewObject(newObj.name, modelData, newObj.export())) objectLists.update() } } diff --git a/qml/History.qml b/qml/History.qml new file mode 100644 index 0000000..b1dbc39 --- /dev/null +++ b/qml/History.qml @@ -0,0 +1,72 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.12 +import "js/objects.js" as Objects +import "js/historylib.js" as HistoryLib + + +QtObject { + // Using a QtObject is necessary in order to have proper property propagation in QML + id: historyObj + property int undoCount: 0 + property int redoCount: 0 + property var undoStack: [] + property var redoStack: [] + + function empty() { + undoStack = [] + redoStack = [] + } + + function addToHistory(action) { + if(action instanceof HistoryLib.Action) { + console.log("Added new entry to history: " + action.getReadableString()) + undoStack.push(action) + undoCount++; + redoStack = [] + } + } + + function undo() { + if(undoStack.length > 0) { + var action = undoStack.pop() + action.undo() + objectLists.update() + redoStack.push(action) + undoCount--; + redoCount++; + } + } + + function redo() { + if(redoStack.length > 0) { + var action = redoStack.pop() + action.redo() + objectLists.update() + undoStack.push(action) + undoCount++; + redoCount--; + } + } + + Component.onCompleted: { + Objects.history = historyObj + Objects.HistoryLib = HistoryLib + } +} diff --git a/qml/HistoryBrowser.qml b/qml/HistoryBrowser.qml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/qml/HistoryBrowser.qml @@ -0,0 +1 @@ + diff --git a/qml/LogGraph.qml b/qml/LogGraph.qml index 865c9a0..fd4947a 100644 --- a/qml/LogGraph.qml +++ b/qml/LogGraph.qml @@ -33,6 +33,7 @@ ApplicationWindow { SystemPalette { id: sysPalette; colorGroup: SystemPalette.Active } SystemPalette { id: sysPaletteIn; colorGroup: SystemPalette.Disabled } + History { id: history } menuBar: AppMenuBar {} @@ -200,6 +201,4 @@ ApplicationWindow { drawCanvas.save(file) Helper.copyImageToClipboard() } - - } diff --git a/qml/ObjectLists.qml b/qml/ObjectLists.qml index cb5c0db..6aad4db 100644 --- a/qml/ObjectLists.qml +++ b/qml/ObjectLists.qml @@ -22,6 +22,7 @@ import QtQuick.Controls 2.12 import "js/objects.js" as Objects import "js/mathlib.js" as MathLib import "js/utils.js" as Utils +import "js/historylib.js" as HistoryLib ListView { @@ -88,6 +89,9 @@ ListView { anchors.left: parent.left anchors.leftMargin: 5 onClicked: { + history.addToHistory(new HistoryLib.EditedVisibility( + objEditor.obj.name, objEditor.objType, this.checked + )) Objects.currentObjects[objType][index].visible = this.checked objectListList.changed() controlRow.obj = Objects.currentObjects[objType][index] @@ -129,6 +133,9 @@ ListView { icon.name: 'delete' onClicked: { + history.addToHistory(new HistoryLib.DeleteObject( + objEditor.obj.name, objEditor.objType, objEditor.obj.export() + )) Objects.currentObjects[objType][index].delete() Objects.currentObjects[objType].splice(index, 1) objectListList.update() @@ -158,7 +165,11 @@ ListView { color: obj.color title: `Pick new color for ${objType} ${obj.name}` onAccepted: { - Objects.currentObjects[objType][index].color = color.toString() + history.addToHistory(new HistoryLib.EditedProperty( + objEditor.obj.name, objEditor.objType, "color", + objEditor.obj.color, color.toString() + )) + objEditor.obj.color = color.toString() controlRow.obj = Objects.currentObjects[objType][index] objectListList.update() } @@ -209,9 +220,7 @@ ListView { newName = Objects.getNewName(newName) } Objects.currentObjects[objEditor.objType][objEditor.objIndex].name = newName - // TODO Resolve dependencies objEditor.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] - //objEditor.editingRow.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] objectListList.update() } } @@ -227,8 +236,6 @@ ListView { currentIndex: model.indexOf(objEditor.obj.labelContent) onActivated: function(newIndex) { Objects.currentObjects[objEditor.objType][objEditor.objIndex].labelContent = model[newIndex] - //objEditor.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] - //objEditor.editingRow.obj = objEditor.obj objectListList.update() } } @@ -267,12 +274,18 @@ ListView { 'number': () => objEditor.obj[modelData[0]] }[modelData[1]]() : "" onChanged: function(newValue) { - Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = { + var newValue = { 'Expression': () => new MathLib.Expression(newValue), 'Domain': () => MathLib.parseDomain(newValue), 'string': () => newValue, 'number': () => parseFloat(newValue) }[modelData[1]]() + history.addToHistory(new HistoryLib.EditedProperty( + objEditor.obj.name, objEditor.objType, modelData[0], + objEditor.obj[modelData[0]], newValue + )) + //Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = newValue + objEditor.obj[modelData[0]] = newValue Objects.currentObjects[objEditor.objType][objEditor.objIndex].update() objectListList.update() } @@ -288,6 +301,10 @@ ListView { checked: visible ? objEditor.obj[modelData[0]] : false onClicked: { + history.addToHistory(new HistoryLib.EditedProperty( + objEditor.obj.name, objEditor.objType, modelData[0], + objEditor.obj[modelData[0]], this.checked + )) objEditor.obj[modelData[0]] = this.checked Objects.currentObjects[objEditor.objType][objEditor.objIndex].update() objectListList.update() @@ -302,6 +319,7 @@ ListView { icon: `icons/settings/custom/${parent.label}.svg` // True to select an object of type, false for enums. property bool selectObjMode: paramTypeIn(modelData[1], ['ObjectType']) + model: visible ? (selectObjMode ? Objects.getObjectsName(modelData[1].objType).concat(['+ Create new ' + modelData[1].objType]) : modelData[1].values) : [] @@ -314,14 +332,26 @@ ListView { var selectedObj = Objects.getObjectByName(model[newIndex], modelData[1].objType) if(selectedObj == null) { selectedObj = Objects.createNewRegisteredObject(modelData[1].objType) + history.addToHistory(new HistoryLib.CreateNewObject(selectedObj.name, modelData[1].objType, selectedObj.export())) model = Objects.getObjectsName(modelData[1].objType).concat(['+ Create new ' + modelData[1].objType]) currentIndex = model.indexOf(selectedObj.name) } - Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]].requiredBy = objEditor.obj[modelData[0]].filter((obj) => objEditor.obj.name != obj.name) + //Objects.currentObjects[objEditor.objType][objEditor.objIndex].requiredBy = objEditor.obj[modelData[0]].filter((obj) => objEditor.obj.name != obj.name) + objEditor.obj.requiredBy = objEditor.obj.requiredBy.filter((obj) => objEditor.obj.name != obj.name) selectedObj.requiredBy.push(Objects.currentObjects[objEditor.objType][objEditor.objIndex]) - Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = selectedObj + history.addToHistory(new HistoryLib.EditedProperty( + objEditor.obj.name, objEditor.objType, modelData[0], + objEditor.obj[modelData[0]], selectedObj + )) + //Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = selectedObj + objEditor.obj[modelData[0]] = selectedObj } else { - Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = model[newIndex] + history.addToHistory(new HistoryLib.EditedProperty( + objEditor.obj.name, objEditor.objType, modelData[0], + objEditor.obj[modelData[0]], model[newIndex] + )) + //Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = model[newIndex] + objEditor.obj[modelData[0]] = model[newIndex] } // Refreshing Objects.currentObjects[objEditor.objType][objEditor.objIndex].update() @@ -347,8 +377,15 @@ ListView { forbidAdding: visible ? modelData[1].forbidAdding : false onChanged: { - Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = exportModel() - Objects.currentObjects[objEditor.objType][objEditor.objIndex].update() + var exported = exportModel() + history.addToHistory(new HistoryLib.EditedProperty( + objEditor.obj.name, objEditor.objType, modelData[0], + objEditor.obj[modelData[0]], exported + )) + //Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = exported + objEditor.obj[modelData[0]] = exported + //Objects.currentObjects[objEditor.objType][objEditor.objIndex].update() + objEditor.obj.update() objectListList.update() } @@ -368,6 +405,7 @@ ListView { } } + // Create items footer: Column { id: createRow width: parent.width @@ -400,7 +438,8 @@ ListView { icon.color: sysPalette.windowText onClicked: { - Objects.createNewRegisteredObject(modelData) + var newObj = Objects.createNewRegisteredObject(modelData) + history.addToHistory(new HistoryLib.CreateNewObject(newObj.name, modelData, newObj.export())) objectListList.update() objEditor.obj = Objects.currentObjects[modelData][Objects.currentObjects[modelData].length - 1] objEditor.objType = modelData diff --git a/qml/TextSetting.qml b/qml/TextSetting.qml index 0e7eccd..2fd705d 100644 --- a/qml/TextSetting.qml +++ b/qml/TextSetting.qml @@ -71,7 +71,10 @@ Item { var value = text if(control.isInt) value = Math.max(control.min,parseInt(value).toString()=="NaN"?control.min:parseInt(value)) if(control.isDouble) value = Math.max(control.min,parseFloat(value).toString()=="NaN"?control.min:parseFloat(value)) - if(value != "") control.changed(value) + if(value != "" && value.toString() != defValue) { + control.changed(value) + defValue = value.toString() + } } } } diff --git a/qml/js/historylib.js b/qml/js/historylib.js new file mode 100644 index 0000000..fd97c59 --- /dev/null +++ b/qml/js/historylib.js @@ -0,0 +1,128 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// This library helps containing actions to be undone or redone (in other words, editing history) +// Each type of event is repertoried as an action that can be listed for everything that's undoable. +.pragma library + +.import "objects.js" as Objects +.import "utils.js" as Utils + +class Action { + // Type of the action done. + static type(){return 'Unknown'} + + // TargetName is the name of the object that's targeted by the event. + constructor(targetName = "", targetType = "Point") { + this.targetName = targetName + this.targetType = targetType + } + + undo() {} + + redo() {} + + getReadableString() { + return 'Unknown action' + } +} + +class CreateNewObject extends Action { + // Action used for the creation of an object + static type(){return 'CreateNewObject'} + + constructor(targetName = "", targetType = "Point", properties = []) { + super(targetName, targetType) + this.targetProperties = properties + } + + undo() { + var targetIndex = Objects.getObjectsName(this.targetType).indexOf(this.targetName) + Objects.currentObjects[this.targetType][targetIndex].delete() + Objects.currentObjects[this.targetType].splice(targetIndex, 1) + } + + redo() { + Objects.createNewRegisteredObject(this.targetType, this.targetProperties) + } + + getReadableString() { + return `New ${this.targetType} ${this.targetName} created.` + } +} + +class DeleteObject extends CreateNewObject { + // Action used at the deletion of an object. Basicly the same thing as creating a new object, except Redo & Undo are reversed. + static type(){return 'DeleteObject'} + + undo() { + super.redo() + } + + redo() { + super.undo() + } + + getReadableString() { + return `${this.targetType} ${this.targetName} deleted.` + } +} + +class EditedProperty extends Action { + // Action used everytime an object's property has been changed + static type(){return 'EditedProperty'} + + constructor(targetName = "", targetType = "Point", targetProperty = "visible", previousValue = false, newValue = true) { + super(targetName, targetType) + this.targetProperty = targetProperty + this.targetPropertyReadable = Utils.camelCase2readable(this.targetProperty) + this.previousValue = previousValue + this.newValue = newValue + } + + undo() { + Objects.getObjectByName(this.targetName, this.targetType)[this.targetProperty] = this.previousValue + } + + redo() { + Objects.getObjectByName(this.targetName, this.targetType)[this.targetProperty] = this.newValue + } + + getReadableString() { + var prev = this.previousValue == null ? ""+this.previousValue : this.previousValue.toString() + var next = this.newValue == null ? ""+this.newValue : this.newValue.toString() + return `${this.targetPropertyReadable} of ${this.targetType} ${this.targetName} changed from "${prev}" to "${next}".` + } +} + +class EditedVisibility extends EditedProperty { + // Action used everytime an object's property has been changed + static type(){return 'EditedVisibility'} + + constructor(targetName = "", targetType = "Point", newValue = true) { + super(targetName, targetType, "visible", !newValue, newValue) + } + + getReadableString() { + if(this.newValue) { + return `${this.targetType} ${this.targetName} shown.` + } else { + return `${this.targetType} ${this.targetName} hidden.` + } + } +} diff --git a/qml/js/mathlib.js b/qml/js/mathlib.js index 195796e..a4af435 100644 --- a/qml/js/mathlib.js +++ b/qml/js/mathlib.js @@ -580,7 +580,7 @@ class MinusDomain extends Domain { static import(frm) { var domains = frm.trim().split("∖") if(domains.length == 1) domains = frm.trim().split("\\") // Fallback - var dom1 = parseDomain(domains.pop()) + var dom1 = parseDomain(domains.shift()) var dom2 = parseDomain(domains.join('∪')) return new MinusDomain(dom1, dom2) } diff --git a/qml/js/objects.js b/qml/js/objects.js index bb9c9ec..e4edb69 100644 --- a/qml/js/objects.js +++ b/qml/js/objects.js @@ -22,6 +22,8 @@ .import "mathlib.js" as MathLib .import "parameters.js" as P +var history = null +var HistoryLib = null function getNewName(allowedLetters) { var newid = 0 @@ -375,6 +377,7 @@ class GainBode extends ExecutableObject { om_0.name = getNewName('ω') om_0.labelContent = 'name' om_0.color = this.color + history.addToHistory(new HistoryLib.CreateNewObject(om_0.name, 'Point', om_0.export())) labelPosition = 'below' } om_0.requiredBy.push(this) @@ -654,6 +657,7 @@ class PhaseBode extends ExecutableObject { om_0.color = this.color om_0.labelContent = 'name' om_0.labelPosition = this.phase.execute() >= 0 ? 'bottom' : 'top' + history.addToHistory(new HistoryLib.CreateNewObject(om_0.name, 'Point', om_0.export())) labelPosition = 'below' } om_0.requiredBy.push(this) @@ -886,14 +890,9 @@ class CursorX extends DrawableObject { static type(){return 'X Cursor'} static typeMultiple(){return 'X Cursors'} static properties() { - var elementTypes = Object.keys(currentObjects).filter(objType => types[objType].prototype instanceof ExecutableObject) - var elementNames = [''] - elementTypes.forEach(function(elemType){ - elementNames = elementNames.concat(currentObjects[elemType].map(obj => obj.name)) - }) return { 'x': 'Expression', - 'targetElement': new P.Enum(...elementNames), + 'targetElement': new P.ObjectType('ExecutableObject'), 'labelPosition': new P.Enum('left', 'right'), 'approximate': 'Boolean', 'rounding': 'number', @@ -917,6 +916,9 @@ class CursorX extends DrawableObject { if(typeof x == 'number' || typeof x == 'string') x = new MathLib.Expression(x.toString()) this.x = x this.targetElement = targetElement + if(typeof targetElement == "string") { + this.targetElement = getObjectByName(targetElement, elementTypes) + } this.labelPosition = labelPosition this.displayStyle = displayStyle this.targetValuePosition = targetValuePosition @@ -924,17 +926,17 @@ class CursorX extends DrawableObject { export() { return [this.name, this.visible, this.color.toString(), this.labelContent, - this.x.toEditableString(), this.targetElement, this.labelPosition, + this.x.toEditableString(), this.targetElement == null ? null : this.targetElement.name, this.labelPosition, this.approximate, this.rounding, this.displayStyle, this.targetValuePosition] } getReadableString() { - if(this.getTargetElement() == null) return `${this.name} = ${this.x.toString()}` + if(this.targetElement == null) return `${this.name} = ${this.x.toString()}` return `${this.name} = ${this.x.toString()}\n${this.getTargetValueLabel()}` } getTargetValueLabel() { - var t = this.getTargetElement() + var t = this.targetElement var approx = '' if(this.approximate) { approx = t.execute(this.x.execute()) @@ -944,12 +946,6 @@ class CursorX extends DrawableObject { (this.approximate ? ' ≈ ' + approx : '') } - getTargetElement() { - // TODO: Use the dependency system instead. - var elementTypes = Object.keys(currentObjects).filter(objType => types[objType].prototype instanceof ExecutableObject) - return getObjectByName(this.targetElement, elementTypes) - } - getLabel() { switch(this.labelContent) { case 'name': @@ -1005,10 +1001,10 @@ class CursorX extends DrawableObject { break; } - if(this.targetValuePosition == 'Next to target' && this.getTargetElement() != null) { + if(this.targetValuePosition == 'Next to target' && this.targetElement != null) { var text = this.getTargetValueLabel() var textSize = canvas.measureText(ctx, text) - var ypox = canvas.y2px(this.getTargetElement().execute(this.x.execute())) + var ypox = canvas.y2px(this.targetElement.execute(this.x.execute())) switch(this.labelPosition) { case 'left': canvas.drawVisibleText(ctx, text, xpos-textSize.width-5, ypox+textSize.height) @@ -1303,8 +1299,11 @@ var currentObjects = {} function getObjectByName(objName, objType = null) { var objectTypes = Object.keys(currentObjects) if(typeof objType == 'string') { - if(currentObjects[objType] == undefined) return null - objectTypes = [objType] + if(objType == "ExecutableObject") { + objectTypes = getExecutableTypes() + } else if(currentObjects[objType] != undefined) { + objectTypes = [objType] + } } if(Array.isArray(objType)) objectTypes = objType var retObj = null @@ -1318,10 +1317,23 @@ function getObjectByName(objName, objType = null) { } function getObjectsName(objType) { + if(objType == "ExecutableObject") { + var types = getExecutableTypes() + var elementNames = [''] + types.forEach(function(elemType){ + elementNames = elementNames.concat(currentObjects[elemType].map(obj => obj.name)) + }) + console.log(elementNames) + return elementNames + } if(currentObjects[objType] == undefined) return [] return currentObjects[objType].map(obj => obj.name) } +function getExecutableTypes() { + return Object.keys(currentObjects).filter(objType => types[objType].prototype instanceof ExecutableObject) +} + function createNewRegisteredObject(objType, args=[]) { if(Object.keys(types).indexOf(objType) == -1) return null // Object type does not exist. var newobj = new types[objType](...args)