diff --git a/LogarithmPlotter/__init__.py b/LogarithmPlotter/__init__.py index 0853e1f..9f4a4cd 100644 --- a/LogarithmPlotter/__init__.py +++ b/LogarithmPlotter/__init__.py @@ -134,8 +134,10 @@ def run(): engine.addImportPath(os.path.realpath(os.path.join(os.getcwd(), "qml"))) engine.load(os.path.realpath(os.path.join(os.getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "LogarithmPlotter.qml"))) + os.chdir(pwd) if len(argv) > 0 and os.path.exists(argv[-1]) and argv[-1].split('.')[-1] in ['json', 'lgg', 'lpf']: engine.rootObjects()[0].loadDiagram(argv[-1]) + os.chdir(os.path.dirname(os.path.realpath(__file__))) if not engine.rootObjects(): print("No root object") diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js new file mode 100644 index 0000000..ab2313b --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js @@ -0,0 +1,504 @@ +/** + * LogarithmPlotter - Create graphs with logarithm scales. + * Copyright (C) 2021 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 . + */ + +.pragma library + +import "reference.js" as Reference + +const OPERATION_PRIORITY = { + "+": 10, "-": 10, + "*": 20, "/": 20 +} + +enum ASEType { + UNKNOWN, + VARIABLE, + NUMBER, + STRING, + FUNCTION, + CONSTANT, + OPERATION +} + +class AbstractSyntaxElement { + type = ASEType.UNKNOWN; + + execute(variables) { + return null; + } + simplify() { + return this; + } + derivative(variable) { + return this; + } + integrate(variable) { + return this; + } + toEditableString() { + return ""; + } + toLatex() { + return ""; + } + isConstant() { + return true; + } +} + +class Variable extends AbstractSyntaxElement { + type = ASEType.VARIABLE; + + constructor(variableName) { + this.varName = variableName; + } + + execute(variables) { + if(variables.includes(this.varName)) { + return variables[this.varName]; + } else { + throw new EvalError("Unknown variable " + this.varName + "."); + } + } + + derivative(variable) { + if(variable == this.varName) + return new NumberElement(1); + return this; + } + + integrate(variable) { + if(variable == this.varName) + // ^2/2 + return new Operation(new Operation(this, '^', new NumberElement(2)), '/', new NumberElement(2)); + return this; + } + + toEditableString() { + return this.varName; + } + + toLatex() { + return this.varName; + } + + isConstant() { + return false; + } +} + +class ArrayVariable extends Variable { + constructor(arrayName, astIndex) { + super(arrayName + "[" + astIndex.toEditableString() + "]") + this.arrayName = arrayName; + this.astIndex = astIndex; + } + + execute(variables) { + if(variables.includes(this.arrayName)) { + let index = this.astIndex.execute(variables) + if(index % 1 != 0 || index < 0) { // Float index. + throw new EvalError("Non-integer array index " + index + " used as array index for " + this.varName + "."); + } else if(variables[this.arrayName].length <= index) { + throw new EvalError("Out-of-range index " + index + " used as array index for " + this.varName + "."); + } else { + return variables[this.arrayName][index]; + } + } else { + throw new EvalError("Unknown variable " + this.varName + "."); + } + + toLatex() { + return this.varName; + } + } + + simplify() { + return new ArrayVariable(this.arrayName, this.astIndex.simplify()); + } + + toLatex() { + return this.arrayName + '\\left[' + this.astIndex.toLatex() + '\\right]'; + } + + isConstant() { + return false; + } +} + + +class Constant extends Variable { + type = ASEType.CONSTANT; + + constructor(constant) { + super(constant) + } + + execute(variables) { + if(Reference.CONSTANTS_LIST.includes(this.varName)) { + return Reference.CONSTANTS[this.varName]; + } else { + throw new EvalError("Unknown constant " + this.varName + "."); + } + } + + derivative(variable) { + if(variable == this.varName) + return new NumberElement(0); + return this; + } + + integrate(variable) { + return new Operation(new Variable(variable), '^', this); + } + + toEditableString() { + return this.varName; + } + + toLatex() { + return this.varName; + } + + isConstant() { + return true; + } +} + +class NumberElement extends AbstractSyntaxElement { + type = ASEType.NUMBER; + + constructor(number) { + this.value = parseFloat(number); + } + + derivative(variable) { + return new NumberElement(0); + } + + integrate(variable) { + return new Variable(variable); + } + + toEditableString() { + return this.value.toString(); + } + + toLatex() { + return this.value.toString(); + } + + isConstant() { + return true; + } +} + +class StringElement extends AbstractSyntaxElement { + type = ASEType.STRING; + + constructor(str) { + this.str = str; + } + + execute(variables) { + return this.str + } + + derivative(variable) { + return this; + } + + integrate(variable) { + return this; + } + + toEditableString() { + return '"' + this.str + '"'; + } + + toLatex() { + return this.str; + } + + isConstant() { + return true; + } +} + +class FunctionElement extends AbstractSyntaxElement { + type = ASEType.STRING; + + constructor(functionName, astArguments) { + this.function = functionName; + this.args = astArguments; + } + + execute(variables) { + if(Reference.FUNCTIONS_LIST.includes(this.function)) { + let args = this.args.map(arg => arg.execute(variables)); + return Reference.FUNCTIONS[this.function](...args); + } else { + throw new EvalError("Unknown function " + this.function + "."); + } + } + + simplify() { + let args = this.args.map(arg => arg.simplify(variables)); + let newFunc = new FunctionElement(this.function, args); + let result; + if(newFunc.isConstant() && (result = newFunc.execute({})) % 1 == 0) { // Simplification (e.g. cos(0), sin(π/2)...) + return new NumberElement(result); + } else { + return newFunc; + } + } + + derivative(variable) { + //TODO: Use DERIVATIVES elements in reference. + return new FunctionElement("derivative", this.toEditableString()); + } + + integrate(variable) { + //TODO: Use INTEGRALS elements in reference. + return new FunctionElement("integrate", this.toEditableString()); + } + + toEditableString() { + return this.function + '(' + this.args.map(arg => arg.toEditableString()).join(', ') + ')'; + } + + toLatex() { + switch(this.function) { + case "sqrt": + return '\\sqrt{' + this.args.map(arg => arg.toLatex()).join(', ') + '}'; + case "abs": + return '\\left|' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right|'; + case "floor": + return '\\left\\lfloor' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right\\rfloor'; + case "ceil": + return '\\left\\lceil' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right\\rceil'; + default: + return '\\mathrm{' + this.function + '}\\left(' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right)'; + } + } + + isConstant() { + return this.args.every(x => x.isConstant()); + } +} + +class Operation extends AbstractSyntaxElement { + type = ASEType.OPERATION; + + constructor(leftHand, operation, rightHand) { + this.leftHand = leftHand; + this.ope = operation; + this.rightHand = rightHand; + } + + evaluate(variables) { + switch(this.ope) { + case '+': + return this.leftHand.evaluate(variables) + this.rightHand.evaluate(variables); + case '-': + return this.leftHand.evaluate(variables) - this.rightHand.evaluate(variables); + case '*': + return this.leftHand.evaluate(variables) * this.rightHand.evaluate(variables); + case '/': + return this.leftHand.evaluate(variables) / this.rightHand.evaluate(variables); + case '%': + return this.leftHand.evaluate(variables) % this.rightHand.evaluate(variables); + case '^': + return Math.pow(this.leftHand.evaluate(variables), this.rightHand.evaluate(variables)); + default: + throw new EvalError("Unknown operator " + ope + "."); + } + } + + simplify() { + let leftHand = this.leftHand.simplify(); + let rightHand = this.rightHand.simplify(); + let newOpe = new Operation(leftHand, this.ope, rightHand); + if(leftHand.type == ASEType.NUMBER && rightHand.type == ASEType.NUMBER && Math.abs(newOpe.value) < 1000000) { + // Do not simplify to too big numbers + switch(this.ope) { + case '+': + case '-': + case '*': + case '^': + case '%': + return new NumberElement(newOpe.value); + case '/': + if(result % 1 == 0) + return new NumberElement(newOpe.value); + else { + let simplified = simplifyFraction(leftHand.number, rightHand.number) + return new Operation(new NumberElement(simplified[0]), '/', new NumberElement(simplified[1])) + } + return this.leftHand.evaluate(variables) / this.rightHand.evaluate(variables); + return Math.pow(this.leftHand.evaluate(variables), this.rightHand.evaluate(variables)); + default: + throw new EvalError("Unknown operator " + ope + "."); + } + } else { + // Simplifications of +- 0 or *1 + switch(this.ope) { + case '+': + case '-': + if(leftHand.type == ASEType.NUMBER && leftHand.value == 0) + return rightHand; + else if(rightHand.type == ASEType.NUMBER && rightHand.value == 0) { + if(ope == '-') leftHand.value = -leftHand.value; + return leftHand; + } else + return newOpe + case '*': + if((leftHand.type == ASEType.NUMBER && leftHand.value == 0) || (rightHand.type == ASEType.NUMBER && rightHand.value == 0)) + return new NumberElement(0); + else if(leftHand.type == ASEType.NUMBER && leftHand.value == 0) + return rightHand; + else if(rightHand.type == ASEType.NUMBER && rightHand.value == 1) + return leftHand; + else + return newOpe + case '^': + if(rightHand.type == ASEType.NUMBER && rightHand.value == 0) + return new NumberElement(1); + else if(rightHand.type == ASEType.NUMBER && rightHand.value == 1) + return new NumberElement(leftHand.value); + else + return newOpe; + case '/': + if(rightHand.type == ASEType.NUMBER && rightHand.value == 1) + return new NumberElement(leftHand.value); + else + return newOpe; + case '%': + return newOpe; + default: + throw new EvalError("Unknown operator " + ope + "."); + } + } + } + + derivative(variable) { + switch(this.ope) { + case '-': + case '+': + return new Operation(this.leftHand.derivative(variable), this.ope, this.rightHand.derivative(variable)); + case '*': + return new Operation( + new Operation(this.leftHand.derivative(variable), '*', this.rightHand), + '+', + new Operation(this.leftHand, '*', this.rightHand.derivative(variable)) + ); + case '/': + return new Operation( + new Operation(this.leftHand.derivative(variable), '*', this.rightHand), + '+', + new Operation(this.leftHand, '*', this.rightHand.derivative(variable)) + ); + case '^': + case '%': + return new FunctionElement("integrate", this.toEditableString()); + default: + throw new EvalError("Unknown operator " + ope + "."); + } + } + + integrate(variable) { + switch(this.ope) { + case '-': + case '+': + return new Operation(this.leftHand.integrate(variable), this.ope, this.rightHand.integrate(variable)); + case '*': + return new Operation( + new Operation(this.leftHand.derivative(variable), '*', this.rightHand), + '+', + new Operation(this.leftHand, '*', this.rightHand.derivative(variable)) + ); + case '/': + return new Operation( + new Operation(this.leftHand.derivative(variable), '*', this.rightHand), + '+', + new Operation(this.leftHand, '*', this.rightHand.derivative(variable)) + ); + case '^': + case '%': + return new FunctionElement("integrate", this.toEditableString()); + default: + throw new EvalError("Unknown operator " + ope + "."); + } + } + + toEditableString() { + let leftString = this.leftHand.toEditableString(); + let rightString = this.rightHand.toEditableString(); + if(this.leftHand.type == ASEType.OPERATION && OPERATION_PRIORITY[this.ope] > OPERATION_PRIORITY[this.leftHand.ope]) + leftString = "(" + leftString + ")" + if(this.rightHand.type == ASEType.OPERATION && OPERATION_PRIORITY[this.ope] > OPERATION_PRIORITY[this.rightHand.ope]) + rightString = "(" + rightString + ")" + return leftString + " " + this.ope + " " + rightString; + } + + + toLatex() { + switch(this.ope) { + case '-': + case '+': + return this.leftHand.toLatex() + this.ope + this.rightHand.toLatex(); + case '*': + return this.leftHand.toLatex() + " \\times " + this.rightHand.toLatex(); + case '%': + return this.leftHand.toLatex() + " \\mathrm{mod} " + this.rightHand.toLatex(); + case '/': + return "\\frac{" + this.leftHand.toLatex() + "}{" + this.rightHand.toLatex() + "}" + case '^': + return this.leftHand.toLatex() + "^{" + this.rightHand.toLatex() + "}"; + default: + throw new EvalError("Unknown operator " + ope + "."); + } + return this.leftHand.toLatex() + ope + this.rightHand.toLatex(); + } + + isConstant() { + return this.leftHand.isConstant() && this.rightHand.isConstant(); + } +} + +function simplifyFraction(num,den) { + // More than gcd because it allows decimals fractions. + let mult = 1; + if(num%1 != 0) + mult = Math.max(mult,Math.pow(10,num.toString().split('.')[1].length)) + else if(den%1 != 0) + mult = Math.max(mult,Math.pow(10,den.toString().split('.')[1].length)) + let a = Math.abs(num*mult); + let b = Math.abs(den*mult); + let gcd = 0 + if (b > a) {let temp = a; a = b; b = temp;} + while (gcd == 0) { + if (b == 0) gcd = a; + a %= b; + if (a == 0) gcd = b; + b %= a; + } + return [num*mult/gcd, den*mult/gcd] +} + + diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js new file mode 100644 index 0000000..d0ac2c6 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js @@ -0,0 +1,20 @@ +/** + * LogarithmPlotter - Create graphs with logarithm scales. + * Copyright (C) 2021 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 . + */ + +.pragma library + diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/common.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/common.js new file mode 100644 index 0000000..ffc00b1 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/common.js @@ -0,0 +1,51 @@ +/** + * LogarithmPlotter - Create graphs with logarithm scales. + * Copyright (C) 2021 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 . + */ + +.pragma library + +class InputExpression { + constructor(expression) { + this.position = 0; + this.input = expression; + } + + next() { + return this.input[this.position++]; + } + + peek() { + return this.input[this.position]; + } + + skip(char) { + if(!atEnd() && peek() == char) { + this.position++; + } else { + this.raise("Unexpected character " + peek() + ". Expected character " + char); + } + } + + atEnd() { + return this.position >= this.input.length; + } + + raise(message) { + throw new SyntaxError(message + " at " + this.position ".") + } +} + diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js new file mode 100644 index 0000000..26c5013 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js @@ -0,0 +1,88 @@ +/** + * LogarithmPlotter - Create graphs with logarithm scales. + * Copyright (C) 2021 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 . + */ + +.pragma library + +const CONSTANTS = { + "π": Math.PI, + "pi": Math.PI, + "inf": Infinity, + "infinity": Infinity, + "∞": Infinity, + "e": Infinity +}; +const CONSTANTS_LIST = Object.keys(CONSTANTS); + +const FUNCTIONS = { + "abs": Math.abs, + "acos": Math.acos, + "acosh": Math.acosh, + "asin": Math.asin, + "asinh": Math.asinh, + "atan": Math.atan, + "atan2": Math.atan2, + "atanh": Math.atanh, + "cbrt": Math.cbrt, + "ceil": Math.ceil, + "clz32": Math.clz32, + "cos": Math.cos, + "cosh": Math.cosh, + "exp": Math.exp, + "expm1": Math.expm1, + "floor": Math.floor, + "fround": Math.fround, + "hypot": Math.hypot, + "imul": Math.imul, + "log": Math.log, + "log10": Math.log10, + "log1p": Math.log1p, + "log2": Math.log2, + "max": Math.max, + "min": Math.min, + "pow": Math.log2, + "random": Math.random, + "round": Math.round, + "sign": Math.sign, + "sin": Math.sin, + "sinh": Math.sinh, + "sqrt": Math.sqrt, + "tan": Math.tan, + "tanh": Math.tanh, + "trunc": Math.trunc +} +const FUNCTIONS_LIST = Object.keys(FUNCTIONS); +// TODO: Complete +const DERIVATIVES = { + "abs": "abs(<1>)/<1>", + "acos": "-derivate(<1>)/sqrt(1-(<1>)^2)", + "acosh": "derivate(<1>)/sqrt((<1>)^2-1)", + "asin": "derivate(<1>)/sqrt(1-(<1>)^2)", + "asinh": "derivate(<1>)/sqrt((<1>)^2+1)", + "atan": "derivate(<1>)/(1+(<1>)^2)", + "atan2": "", +} +const INTEGRALS = { + "abs": "integrate(<1>)*sign(<1>)", + "acos": "", + "acosh": "", + "asin": "", + "asinh": "", + "atan": "", + "atan2": "", +} + diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js new file mode 100644 index 0000000..e4eb533 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js @@ -0,0 +1,141 @@ +/** + * LogarithmPlotter - Create graphs with logarithm scales. + * Copyright (C) 2021 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 . + */ + +.pragma library + +import "reference.js" as Reference + +const WHITESPACES = " \t\n\r" +const STRING_LIMITORS = '"\'`'; +const OPERATORS = "+-*/^%"; +const PUNCTUTATION = "()[]{},"; +const NUMBER_CHARS = "0123456789." +const IDENTIFIER_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_₀₁₂₃₄₅₆₇₈₉αβγδεζηθκλμξρςστφχψωₐₑₒₓₔₕₖₗₘₙₚₛₜ" + +enum TokenType { + // Expression type + VARIABLE, + CONSTANT, + FUNCTION, + OPERATOR, + PUNCT, + NUMBER, + STRING +} + +class Token { + constructor(type, value) { + this.type = type; + this.value = value; + } +} + +class ExpressionTokenizer { + constructor(input) { + this.input = input; + this.currentToken = null; + } + + skipWhitespaces() { + while(!this.input.atEnd() && WHITESPACES.includes(this.input.peek())) + this.input.next(); + } + + readString() { + let delimitation = this.input.peek(); + if(STRING_LIMITORS.includes(delimitation)) { + this.input.skip(delimitation) + let included = ""; + let justEscaped = false; + while(!this.input.atEnd() && (!STRING_LIMITORS.includes(this.input.peek()) || justEscaped)) { + justEscaped = this.input.peek() == "\\" + if(!justEscaped) + included += this.input.next(); + } + this.input.skip(delimitation) + return new Token(TokenType.STRING, included); + } else { + this.input.raise("Unexpected " + delimitation + ". Expected string delimitator") + } + } + + readNumber() { + let included = ""; + let hasDot = false; + while(!this.input.atEnd() && NUMBER_CHARS.includes(this.input.peek())) { + if(this.input.peek() == ".") { + if(hasDot) this.input.raise("Unexpected '.'. Expected digit") + hasDot = true; + } + included += this.input.next(); + } + } + + readIdentifier() { + let identifier = ""; + let hasDot = false; + while(!this.input.atEnd() && IDENTIFIER_CHARS.includes(this.input.peek())) { + identifier += this.input.next(); + } + if(Reference.CONSTANTS_LIST.includes(identifier.toLowerCase())) { + return new Token(TokenType.CONSTANT, identifier.toLowerCase()) + } else if(Reference.FUNCTIONS_LIST.includes(identifier.toLowerCase())) { + return new Token(TokenType.FUNCTION, identifier.toLowerCase()) + } else { + return new Token(TokenType.VARIABLE, identifier) + } + } + + readNextToken() { + this.skipWhitespaces() + if(input.atEnd()) return null; + let c = input.peek(); + if(STRING_LIMITORS.includes(c)) return this.readString(); + if(NUMBER_CHARS.includes(c)) return this.readNumber(); + if(IDENTIFIER_CHARS.includes(c)) return this.readIdentifier(); + if(Reference.CONSTANTS_LIST.includes(c)) return new Token(TokenType.CONSTANT, c); + if(OPERATORS.includes(c)) return new Token(TokenType.OPERATOR, c); + if(PUNCTUTATION.includes(c)) return new Token(TokenType.PUNCT, c); + this.input.throw("Unknown token character " + c) + } + + peek() { + if(this.currentToken == null) this.currentToken = this.readNextToken(); + return this.currentToken; + } + + next() { + let tmp; + if(this.currentToken == null) + tmp = this.readNextToken(); + else + tmp = this.currentToken; + this.currentToken = null; + return tmp; + } + + atEnd() { + return this.peek() == null; + } + + skip(type) { + Token next = Next(); + if(next.type != type) + input.raise("Unexpected token " + next.type.oLowerCase() + ' "' + next.value + '". Expected ' + type.toLowerCase()); + } +} diff --git a/LogarithmPlotter/qml/eu/ad5001/MixedMenu b/LogarithmPlotter/qml/eu/ad5001/MixedMenu index bcae0ab..092a101 160000 --- a/LogarithmPlotter/qml/eu/ad5001/MixedMenu +++ b/LogarithmPlotter/qml/eu/ad5001/MixedMenu @@ -1 +1 @@ -Subproject commit bcae0ab4563f20bfaf9e6e1e19421d724ec7ba50 +Subproject commit 092a101d2d8690a9d101783e97ea4fdc54d9d0f7