From 32db56304bfbbfe9b61cf74282a36d4f2a0eeab0 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 18 Oct 2022 23:24:58 +0200 Subject: [PATCH] Expression editor! --- .../ObjectLists/Editor/CustomPropertyList.qml | 88 +++---- .../Popup/InsertCharacter.qml | 72 ++++++ .../eu/ad5001/LogarithmPlotter/Popup/qmldir | 1 + .../Setting/ExpressionEditor.qml | 214 ++++++++++++++++++ .../LogarithmPlotter/Setting/TextSetting.qml | 54 ++--- .../eu/ad5001/LogarithmPlotter/Setting/qmldir | 2 +- 6 files changed, 345 insertions(+), 86 deletions(-) create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/InsertCharacter.qml create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/ObjectLists/Editor/CustomPropertyList.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/ObjectLists/Editor/CustomPropertyList.qml index 0bb35bb..42499a9 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/ObjectLists/Editor/CustomPropertyList.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/ObjectLists/Editor/CustomPropertyList.qml @@ -18,10 +18,8 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 -import QtQuick.Dialogs 1.3 as D import eu.ad5001.LogarithmPlotter.Setting 1.0 as Setting import "../../js/objects.js" as Objects -import "../../js/objs/common.js" as ObjectsCommons import "../../js/historylib.js" as HistoryLib import "../../js/utils.js" as Utils import "../../js/mathlib.js" as MathLib @@ -46,7 +44,7 @@ Repeater { */ property var obj - readonly property var textTypes: ['Expression', 'Domain', 'string', 'number'] + readonly property var textTypes: ['Domain', 'string', 'number'] readonly property var comboBoxTypes: ['ObjectType', 'Enum'] readonly property var listTypes: ['List', 'Dict'] @@ -66,48 +64,43 @@ Repeater { } } + Component { + id: expressionEditorComponent + + // Setting for expressions + Setting.ExpressionEditor { + height: 30 + label: propertyLabel + icon: `settings/custom/${propertyIcon}.svg` + defValue: Utils.simplifyExpression(obj[propertyName].toEditableString()) + self: obj.name + onChanged: function(newExpr) { + if(obj[propertyName].toString() != newExpr.toString()) { + history.addToHistory(new HistoryLib.EditedProperty( + obj.name, objType, propertyName, + obj[propertyName], newExpr + )) + obj[propertyName] = newExpr + root.changed() + } + } + } + } + + Component { id: textEditorComponent - // Setting for text & number settings as well as domains & expressions + // Setting for text & number settings as well as domains Setting.TextSetting { height: 30 label: propertyLabel icon: `settings/custom/${propertyIcon}.svg` isDouble: propertyType == 'number' - defValue: { - switch(propertyType) { - case 'Expression': - return Utils.simplifyExpression(obj[propertyName].toEditableString()) - break - case 'string': - return obj[propertyName] - break - case 'Domain': - case 'number': - default: - return obj[propertyName].toString() - } - } + defValue: obj[propertyName] == null ? '' : obj[propertyName].toString() onChanged: function(newValue) { try { var newValueParsed = { - 'Expression': () => { - let expr = new MathLib.Expression(newValue) - // Check if the expression is valid, throws error otherwise. - if(!expr.allRequirementsFullfilled()) { - let undefVars = expr.undefinedVariables() - console.log(JSON.stringify(undefVars), undefVars.join(', ')) - if(undefVars.length > 1) - throw new Error(qsTranslate('error', 'No object found with names %1.').arg(undefVars.join(', '))) - else - throw new Error(qsTranslate('error', 'No object found with name %1.').arg(undefVars.join(', '))) - } - if(expr.requiredObjects().includes(obj.name)) - throw new Error(qsTranslate('error', 'Object cannot be dependent on itself.')) - // TODO: Check for recursive dependencies. - return expr - }, 'Domain': () => MathLib.parseDomain(newValue), 'string': () => newValue, 'number': () => parseFloat(newValue) @@ -129,15 +122,15 @@ Repeater { } - D.MessageDialog { - id: parsingErrorDialog - title: qsTr("LogarithmPlotter - Parsing error") - text: "" - function showDialog(propName, propValue, error) { - text = qsTr("Error while parsing expression for property %1:\n%2\n\nEvaluated expression: %3").arg(propName).arg(error).arg(propValue) - open() - } - } + // D.MessageDialog { + // id: parsingErrorDialog + // title: qsTranslate("expression", "LogarithmPlotter - Parsing error") + // text: "" + // function showDialog(propName, propValue, error) { + // text = qsTranslate("error", "Error while parsing expression for property %1:\n%2\n\nEvaluated expression: %3").arg(propName).arg(error).arg(propValue) + // open() + // } + // } } } @@ -150,7 +143,12 @@ Repeater { text: propertyLabel //icon: `settings/custom/${propertyIcon}.svg` - checked: obj[propertyName] + checked: { + //if(obj[propertyName] == null) { + // return false + //} + return obj[propertyName] + } onClicked: { history.addToHistory(new HistoryLib.EditedProperty( obj.name, objType, propertyName, @@ -269,6 +267,8 @@ Repeater { return commentComponent else if(propertyType == 'boolean') return checkboxComponent + else if(propertyType == 'Expression') + return expressionEditorComponent else if(paramTypeIn(propertyType, textTypes)) return textEditorComponent else if(paramTypeIn(propertyType, comboBoxTypes)) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/InsertCharacter.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/InsertCharacter.qml new file mode 100644 index 0000000..bbeb3e0 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/InsertCharacter.qml @@ -0,0 +1,72 @@ +/** + * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions. + * Copyright (C) 2022 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.Controls 2.12 +import QtQuick 2.12 + +/*! + \qmltype InsertCharacter + \inqmlmodule eu.ad5001.LogarithmPlotter.Setting + \brief Popup to insert special character. + + \sa TextSetting, ExpressionEditor +*/ +Popup { + id: insertPopup + + signal selected(string character) + + width: 280 + height: insertGrid.insertChars.length/insertGrid.columns*(width/insertGrid.columns) + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + + Grid { + id: insertGrid + width: parent.width + columns: 7 + + property var insertChars: [ + "α","β","γ","δ","ε","ζ","η", + "π","θ","κ","λ","μ","ξ","ρ", + "ς","σ","τ","φ","χ","ψ","ω", + "Γ","Δ","Θ","Λ","Ξ","Π","Σ", + "Φ","Ψ","Ω","ₐ","ₑ","ₒ","ₓ", + "ₕ","ₖ","ₗ","ₘ","ₙ","ₚ","ₛ", + "ₜ","¹","²","³","⁴","⁵","⁶", + "⁷","⁸","⁹","⁰","₁","₂","₃", + "₄","₅","₆","₇","₈","₉","₀" + ] + Repeater { + model: parent.insertChars.length + + Button { + id: insertBtn + width: insertGrid.width/insertGrid.columns + height: width + text: insertGrid.insertChars[modelData] + flat: text == " " + font.pixelSize: 18 + + onClicked: { + selected(text) + } + } + } + } +} diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/qmldir b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/qmldir index 3f71a18..fb7800e 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/qmldir +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/qmldir @@ -6,3 +6,4 @@ FileDialog 1.0 FileDialog.qml GreetScreen 1.0 GreetScreen.qml Changelog 1.0 Changelog.qml ThanksTo 1.0 ThanksTo.qml +InsertCharacter 1.0 InsertCharacter.qml diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml new file mode 100644 index 0000000..313b071 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml @@ -0,0 +1,214 @@ +/** + * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions. + * Copyright (C) 2022 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.Controls 2.12 +import QtQuick 2.12 +import QtQuick.Dialogs 1.3 as D +import eu.ad5001.LogarithmPlotter.Popup 1.0 as Popup +import "../js/mathlib.js" as MathLib + + +/*! + \qmltype ExpressionEditor + \inqmlmodule eu.ad5001.LogarithmPlotter.Setting + \brief Setting to edit strings and numbers. + + \sa EditorDialog, Settings, Icon +*/ +Item { + id: control + height: 30 + + /*! + \qmlsignal ExpressionEditor::changed(var newValue) + + Emitted when the value of the expression has been changed. + The corresponding handler is \c onChanged. + */ + signal changed(var newValue) + + /*! + \qmlproperty string ExpressionEditor::defValue + Default editable expression value of the editor. + */ + property string defValue + /*! + \qmlproperty string ExpressionEditor::value + Value of the editor. + */ + property alias value: editor.text + /*! + \qmlproperty string ExpressionEditor::self + Object or context of the expression to be edited. + Used to prevent circular dependency. + */ + property string self: "" + /*! + \qmlproperty string ExpressionEditor::placeholderText + Value of the editor. + */ + property alias placeholderText: editor.placeholderText + /*! + \qmlproperty string ExpressionEditor::label + Label of the editor. + */ + property string label + /*! + \qmlproperty string ExpressionEditor::icon + Icon path of the editor. + */ + property string icon: "" + + /*! + \qmlproperty string ExpressionEditor::openAndCloseMatches + Characters that when pressed, should be immediately followed up by their closing character. + */ + readonly property var openAndCloseMatches: { + "(": ")", + "[": "]", + "'": "'", + '"': '"' + } + + Icon { + id: iconLabel + anchors.top: parent.top + anchors.topMargin: icon == "" ? 0 : 3 + source: control.visible && icon != "" ? "../icons/" + control.icon : "" + width: height + height: icon == "" || !visible ? 0 : 24 + color: sysPalette.windowText + } + + Label { + id: labelItem + anchors.left: iconLabel.right + anchors.leftMargin: icon == "" ? 0 : 5 + height: parent.height + anchors.top: parent.top + verticalAlignment: TextInput.AlignVCenter + //color: sysPalette.windowText + text: visible ? qsTranslate("control", "%1: ").arg(control.label) : "" + visible: control.label != "" + } + + D.MessageDialog { + id: parsingErrorDialog + title: qsTranslate("expression", "LogarithmPlotter - Parsing error") + text: "" + function showDialog(propName, propValue, error) { + text = qsTranslate("expression", "Error while parsing expression for property %1:\n%2\n\nEvaluated expression: %3").arg(propName).arg(error).arg(propValue) + open() + } + } + + TextField { + id: editor + anchors.top: parent.top + anchors.left: labelItem.right + anchors.leftMargin: 5 + width: control.width - (labelItem.visible ? labelItem.width + 5 : 0) - iconLabel.width - 5 + height: parent.height + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: control.label == "" ? TextInput.AlignLeft : TextInput.AlignHCenter + text: control.defValue + color: sysPalette.windowText + focus: true + selectByMouse: true + + Keys.priority: Keys.BeforeItem // Required for knowing which key the user presses. + + onEditingFinished: { + if(insertButton.focus || insertPopup.focus) return + let value = text + if(value != "" && value.toString() != defValue) { + let expr = parse(value) + if(expr != null) { + control.changed(expr) + defValue = expr.toEditableString() + } + } + } + + Keys.onPressed: function(event) { + if(event.text in openAndCloseMatches) { + let start = selectionStart + insert(selectionStart, event.text) + insert(selectionEnd, openAndCloseMatches[event.text]) + cursorPosition = start+1 + event.accepted = true + } + } + } + + Button { + id: insertButton + text: "α" + anchors.right: parent.right + anchors.rightMargin: 5 + anchors.verticalCenter: parent.verticalCenter + width: 20 + height: width + onClicked: { + insertPopup.open() + insertPopup.focus = true + } + } + + Popup.InsertCharacter { + id: insertPopup + + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + + onSelected: function(c) { + editor.insert(editor.cursorPosition, c) + insertPopup.close() + editor.focus = true + } + } + + /*! + \qmlmethod var ExpressionEditor::parse(string newExpression) + Parses the \c newExpression as an expression, checks for errors, shows them if any. + Returns the parsed expression if possible, null otherwise.. + */ + function parse(newExpression) { + let expr = null + try { + expr = new MathLib.Expression(value.toString()) + // Check if the expression is valid, throws error otherwise. + if(!expr.allRequirementsFullfilled()) { + let undefVars = expr.undefinedVariables() + console.log(JSON.stringify(undefVars), undefVars.join(', ')) + if(undefVars.length > 1) + throw new Error(qsTranslate('error', 'No object found with names %1.').arg(undefVars.join(', '))) + else + throw new Error(qsTranslate('error', 'No object found with name %1.').arg(undefVars.join(', '))) + } + if(expr.requiredObjects().includes(control.self)) + throw new Error(qsTranslate('error', 'Object cannot be dependent on itself.')) + // TODO: Check for recursive dependencies. + } catch(e) { + // Error in expression + parsingErrorDialog.showDialog(propertyName, newExpression, e.message) + } + return expr + } +} + diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/TextSetting.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/TextSetting.qml index e361250..a823645 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/TextSetting.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/TextSetting.qml @@ -18,6 +18,7 @@ import QtQuick.Controls 2.12 import QtQuick 2.12 +import eu.ad5001.LogarithmPlotter.Popup 1.0 as Popup /*! \qmltype TextSetting @@ -119,6 +120,7 @@ Item { text: control.defValue selectByMouse: true onEditingFinished: { + if(insertButton.focus || insertPopup.focus) return 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)) @@ -130,6 +132,7 @@ Item { } Button { + id: insertButton text: "α" anchors.right: parent.right anchors.rightMargin: 5 @@ -137,53 +140,22 @@ Item { width: 20 height: width visible: !isInt && !isDouble - onClicked: insertPopup.open() + onClicked: { + insertPopup.open() + insertPopup.focus = true + } } - Popup { + Popup.InsertCharacter { id: insertPopup + x: Math.round((parent.width - width) / 2) y: Math.round((parent.height - height) / 2) - width: 280 - height: insertGrid.insertChars.length/insertGrid.columns*(width/insertGrid.columns) - modal: true - focus: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent - Grid { - id: insertGrid - width: parent.width - columns: 7 - - property var insertChars: [ - "α","β","γ","δ","ε","ζ","η", - "π","θ","κ","λ","μ","ξ","ρ", - "ς","σ","τ","φ","χ","ψ","ω", - "Γ","Δ","Θ","Λ","Ξ","Π","Σ", - "Φ","Ψ","Ω","ₐ","ₑ","ₒ","ₓ", - "ₕ","ₖ","ₗ","ₘ","ₙ","ₚ","ₛ", - "ₜ","¹","²","³","⁴","⁵","⁶", - "⁷","⁸","⁹","⁰","₁","₂","₃", - "₄","₅","₆","₇","₈","₉","₀" - ] - Repeater { - model: parent.insertChars.length - - Button { - id: insertBtn - width: insertGrid.width/insertGrid.columns - height: width - text: insertGrid.insertChars[modelData] - flat: text == " " - font.pixelSize: 18 - - onClicked: { - input.insert(input.cursorPosition, insertGrid.insertChars[modelData]) - insertPopup.close() - input.focus = true - } - } - } + onSelected: function(c) { + input.insert(input.cursorPosition, c) + insertPopup.close() + input.focus = true } } } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/qmldir b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/qmldir index 22ad816..d6e55e5 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/qmldir +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/qmldir @@ -4,4 +4,4 @@ ComboBoxSetting 1.0 ComboBoxSetting.qml Icon 1.0 Icon.qml ListSetting 1.0 ListSetting.qml TextSetting 1.0 TextSetting.qml - +ExpressionEditor 1.0 ExpressionEditor.qml