diff --git a/common/src/module/canvas.mjs b/common/src/module/canvas.mjs index 62649cf..c46c9a1 100644 --- a/common/src/module/canvas.mjs +++ b/common/src/module/canvas.mjs @@ -30,18 +30,16 @@ class CanvasAPI extends Module { #canvas = null /** @type {CanvasRenderingContext2D} */ #ctx = null - /** Lock to prevent asynchronous stuff from printing stuff that is outdated. */ - #redrawCount = 0 /** @type {{show(string, string, string)}} */ #drawingErrorDialog = null - - + + constructor() { super("Canvas", { canvas: CanvasInterface, drawingErrorDialog: DialogInterface }) - + /** * * @type {Object.} @@ -209,7 +207,6 @@ class CanvasAPI extends Module { */ redraw() { if(!this.initialized) throw new Error("Attempting redraw before initialize!") - this.#redrawCount = (this.#redrawCount + 1) % 10000 this.#ctx = this.#canvas.getContext("2d") this._computeAxes() this._reset() @@ -522,18 +519,15 @@ class CanvasAPI extends Module { * @param {function(LatexRenderResult|{width: number, height: number, source: string})} callback */ renderLatexImage(ltxText, color, callback) { - const currentRedrawCount = this.#redrawCount const onRendered = (imgData) => { if(!this.#canvas.isImageLoaded(imgData.source) && !this.#canvas.isImageLoading(imgData.source)) { // Wait until the image is loaded to callback. this.#canvas.loadImageAsync(imgData.source).then(() => { - if(this.#redrawCount === currentRedrawCount) - callback(imgData) + callback(imgData) }) } else { // Callback directly - if(this.#redrawCount === currentRedrawCount) - callback(imgData) + callback(imgData) } } const prerendered = Latex.findPrerendered(ltxText, this.textsize, color) diff --git a/common/src/module/interface.mjs b/common/src/module/interface.mjs index a96621c..b6d462f 100644 --- a/common/src/module/interface.mjs +++ b/common/src/module/interface.mjs @@ -84,21 +84,13 @@ export class DialogInterface extends Interface { } export class LatexInterface extends Interface { - supportsAsyncRender = BOOLEAN /** * @param {string} markup - LaTeX markup to render * @param {number} fontSize - Font size (in pt) to render * @param {string} color - Color of the text to render * @returns {string} - Comma separated data of the image (source, width, height) */ - renderSync = FUNCTION - /** - * @param {string} markup - LaTeX markup to render - * @param {number} fontSize - Font size (in pt) to render - * @param {string} color - Color of the text to render - * @returns {Promise} - Comma separated data of the image (source, width, height) - */ - renderAsync = FUNCTION + render = FUNCTION /** * @param {string} markup - LaTeX markup to render * @param {number} fontSize - Font size (in pt) to render diff --git a/common/src/module/latex.mjs b/common/src/module/latex.mjs index 830f096..a6aa216 100644 --- a/common/src/module/latex.mjs +++ b/common/src/module/latex.mjs @@ -112,12 +112,7 @@ class LatexAPI extends Module { */ async requestAsyncRender(markup, fontSize, color) { if(!this.initialized) throw new Error("Attempting requestAsyncRender before initialize!") - let render - if(this.#latex.supportsAsyncRender) - render = await this.#latex.renderAsync(markup, fontSize, color) - else - render = this.#latex.renderSync(markup, fontSize, color) - const args = render.split(",") + let args = this.#latex.render(markup, fontSize, color).split(",") return new LatexRenderResult(...args) } diff --git a/common/src/preferences/general.mjs b/common/src/preferences/general.mjs index a6957c6..81c5e3d 100644 --- a/common/src/preferences/general.mjs +++ b/common/src/preferences/general.mjs @@ -46,15 +46,8 @@ class EnableLatex extends BoolSetting { } } -const ENABLE_LATEX_ASYNC = new BoolSetting( - qsTranslate("general", "Enable asynchronous LaTeX renderer"), - "enable_latex_async", - "new" -) - export default [ CHECK_FOR_UPDATES, RESET_REDO_STACK, - new EnableLatex(), - ENABLE_LATEX_ASYNC + new EnableLatex() ] diff --git a/runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/Changelog.qml b/runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/Changelog.qml index 93d76db..844e22a 100644 --- a/runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/Changelog.qml +++ b/runtime-pyside6/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Popup/Changelog.qml @@ -45,17 +45,17 @@ Popup { property bool changelogNeedsFetching: true onAboutToShow: if(changelogNeedsFetching) { - Helper.fetchChangelog().then((fetchedText) => { - changelogNeedsFetching = false - changelog.text = fetchedText + Helper.fetchChangelog() + } + + Connections { + target: Helper + function onChangelogFetched(chl) { + changelogNeedsFetching = false; + changelog.text = chl changelogView.contentItem.implicitHeight = changelog.height - }, (error) => { - const e = qsTranslate("changelog", "Could not fetch update: {}.").replace('{}', error) - console.error(e) - changelogNeedsFetching = false - changelog.text = e - changelogView.contentItem.implicitHeight = changelog.height - }) + // console.log(changelog.height, changelogView.contentItem.implicitHeight) + } } ScrollView { diff --git a/runtime-pyside6/LogarithmPlotter/util/config.py b/runtime-pyside6/LogarithmPlotter/util/config.py index e01deae..a5ee23c 100644 --- a/runtime-pyside6/LogarithmPlotter/util/config.py +++ b/runtime-pyside6/LogarithmPlotter/util/config.py @@ -19,16 +19,13 @@ from os import path, environ, makedirs from platform import system from json import load, dumps -from shutil import which - from PySide6.QtCore import QLocale, QTranslator DEFAULT_SETTINGS = { "check_for_updates": True, "reset_redo_stack": True, "last_install_greet": "0", - "enable_latex": which("latex") is not None and which("dvipng") is not None, - "enable_latex_async": True, + "enable_latex": False, "expression_editor": { "autoclose": True, "colorize": True, diff --git a/runtime-pyside6/LogarithmPlotter/util/helper.py b/runtime-pyside6/LogarithmPlotter/util/helper.py index 7f7afb9..344e425 100644 --- a/runtime-pyside6/LogarithmPlotter/util/helper.py +++ b/runtime-pyside6/LogarithmPlotter/util/helper.py @@ -29,16 +29,13 @@ from urllib.error import HTTPError, URLError from LogarithmPlotter import __VERSION__ from LogarithmPlotter.util import config -from LogarithmPlotter.util.promise import PyPromise SHOW_GUI_MESSAGES = "--test-build" not in argv CHANGELOG_VERSION = __VERSION__ -CHANGELOG_CACHE_PATH = path.join(path.dirname(path.realpath(__file__)), "CHANGELOG.md") class InvalidFileException(Exception): pass - def show_message(msg: str) -> None: """ Shows a GUI message if GUI messages are enabled @@ -49,30 +46,31 @@ def show_message(msg: str) -> None: raise InvalidFileException(msg) -def fetch_changelog(): - msg_text = "Unknown changelog error." - try: - # Fetching version - r = urlopen("https://api.ad5001.eu/changelog/logarithmplotter/?version=" + CHANGELOG_VERSION) - lines = r.readlines() - r.close() - msg_text = "".join(map(lambda x: x.decode('utf-8'), lines)).strip() - except HTTPError as e: - msg_text = QCoreApplication.translate("changelog", "Could not fetch changelog: Server error {}.").format( - str(e.code)) - except URLError as e: - msg_text = QCoreApplication.translate("changelog", "Could not fetch update: {}.").format(str(e.reason)) - return msg_text +class ChangelogFetcher(QRunnable): + def __init__(self, helper): + QRunnable.__init__(self) + self.helper = helper -def read_changelog(): - f = open(CHANGELOG_CACHE_PATH, 'r', -1) - data = f.read().strip() - f.close() - return data + def run(self): + msg_text = "Unknown changelog error." + try: + # Fetching version + r = urlopen("https://api.ad5001.eu/changelog/logarithmplotter/?version=" + CHANGELOG_VERSION) + lines = r.readlines() + r.close() + msg_text = "".join(map(lambda x: x.decode('utf-8'), lines)).strip() + except HTTPError as e: + msg_text = QCoreApplication.translate("changelog", "Could not fetch changelog: Server error {}.").format( + str(e.code)) + except URLError as e: + msg_text = QCoreApplication.translate("changelog", "Could not fetch update: {}.").format(str(e.reason)) + self.helper.changelogFetched.emit(msg_text) class Helper(QObject): + changelogFetched = Signal(str) + def __init__(self, cwd: str, tmpfile: str): QObject.__init__(self) self.cwd = cwd @@ -152,14 +150,15 @@ class Helper(QObject): msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}") return msg.format(PySide6_version, sys_version.split("\n")[0]) - @Slot(result=PyPromise) + @Slot() def fetchChangelog(self): - """ - Fetches the changelog and returns a Promise. - """ - if path.exists(CHANGELOG_CACHE_PATH): + changelog_cache_path = path.join(path.dirname(path.realpath(__file__)), "CHANGELOG.md") + if path.exists(changelog_cache_path): # We have a cached version of the changelog, for env that don't have access to the internet. - return PyPromise(read_changelog) + f = open(changelog_cache_path); + self.changelogFetched.emit("".join(f.readlines()).strip()) + f.close() else: # Fetch it from the internet. - return PyPromise(fetch_changelog) + runnable = ChangelogFetcher(self) + QThreadPool.globalInstance().start(runnable) diff --git a/runtime-pyside6/LogarithmPlotter/util/js.py b/runtime-pyside6/LogarithmPlotter/util/js.py index 05f30d5..dbe60bc 100644 --- a/runtime-pyside6/LogarithmPlotter/util/js.py +++ b/runtime-pyside6/LogarithmPlotter/util/js.py @@ -16,13 +16,13 @@ * along with this program. If not, see . """ from re import Pattern -from typing import Callable from PySide6.QtCore import QMetaObject, QObject, QDateTime from PySide6.QtQml import QJSValue class InvalidAttributeValueException(Exception): pass class NotAPrimitiveException(Exception): pass +class Function: pass class URL: pass class PyJSValue: @@ -78,7 +78,7 @@ class PyJSValue: matcher = [ (lambda: self.qjs_value.isArray(), list), (lambda: self.qjs_value.isBool(), bool), - (lambda: self.qjs_value.isCallable(), Callable), + (lambda: self.qjs_value.isCallable(), Function), (lambda: self.qjs_value.isDate(), QDateTime), (lambda: self.qjs_value.isError(), Exception), (lambda: self.qjs_value.isNull(), None), @@ -103,6 +103,4 @@ class PyJSValue: """ if self.type() not in [bool, float, str, None]: raise NotAPrimitiveException() - return self.qjs_value.toPrimitive().toVariant() - - + return self.qjs_value.toPrimitive().toVariant() \ No newline at end of file diff --git a/runtime-pyside6/LogarithmPlotter/util/latex.py b/runtime-pyside6/LogarithmPlotter/util/latex.py index 0d720c3..df32d46 100644 --- a/runtime-pyside6/LogarithmPlotter/util/latex.py +++ b/runtime-pyside6/LogarithmPlotter/util/latex.py @@ -15,21 +15,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . """ -from time import sleep -from PySide6.QtCore import QObject, Slot, Property, QCoreApplication, Signal +from PySide6.QtCore import QObject, Slot, Property, QCoreApplication from PySide6.QtGui import QImage, QColor from PySide6.QtWidgets import QMessageBox -from os import path, remove, makedirs +from os import path, remove, environ, makedirs from string import Template from subprocess import Popen, TimeoutExpired, PIPE from hashlib import sha512 from shutil import which from sys import argv -from LogarithmPlotter.util import config -from LogarithmPlotter.util.promise import PyPromise """ Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/) @@ -88,10 +85,6 @@ class Latex(QObject): def latexSupported(self) -> bool: return LATEX_PATH is not None and DVIPNG_PATH is not None - @Property(bool) - def supportsAsyncRender(self) -> bool: - return config.getSetting("enable_latex_async") - @Slot(result=bool) def checkLatexInstallation(self) -> bool: """ @@ -112,20 +105,13 @@ class Latex(QObject): valid_install = False else: try: - self.renderSync("", 14, QColor(0, 0, 0, 255)) + self.render("", 14, QColor(0, 0, 0, 255)) except MissingPackageException: valid_install = False # Should have sent an error message if failed to render return valid_install - @Slot(str, float, QColor, result=PyPromise) - def renderAsync(self, latex_markup: str, font_size: float, color: QColor) -> PyPromise: - """ - Prepares and renders a latex string into a png file asynchronously. - """ - return PyPromise(self.renderSync, [latex_markup, font_size, color]) - @Slot(str, float, QColor, result=str) - def renderSync(self, latex_markup: str, font_size: float, color: QColor) -> str: + def render(self, latex_markup: str, font_size: float, color: QColor) -> str: """ Prepares and renders a latex string into a png file. """ @@ -138,14 +124,12 @@ class Latex(QObject): if not path.exists(latex_path + ".dvi"): self.create_latex_doc(latex_path, latex_markup) self.convert_latex_to_dvi(latex_path) - # self.cleanup(latex_path) + self.cleanup(latex_path) # Creating four pictures of different sizes to better handle dpi. self.convert_dvi_to_png(latex_path, export_path, font_size, color) # self.convert_dvi_to_png(latex_path, export_path+"@2", font_size*2, color) # self.convert_dvi_to_png(latex_path, export_path+"@3", font_size*3, color) # self.convert_dvi_to_png(latex_path, export_path+"@4", font_size*4, color) - else: - sleep(0) img = QImage(export_path) # Small hack, not very optimized since we load the image twice, but you can't pass a QImage to QML and expect it to be loaded return f'{export_path}.png,{img.width()},{img.height()}' diff --git a/runtime-pyside6/LogarithmPlotter/util/promise.py b/runtime-pyside6/LogarithmPlotter/util/promise.py deleted file mode 100644 index f129d41..0000000 --- a/runtime-pyside6/LogarithmPlotter/util/promise.py +++ /dev/null @@ -1,116 +0,0 @@ -""" - * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions. - * Copyright (C) 2021-2024 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 . -""" -from typing import Callable - -from PySide6.QtCore import QRunnable, Signal, QObject, Slot, QThreadPool -from PySide6.QtQml import QJSValue - -from LogarithmPlotter.util.js import PyJSValue - - -class InvalidReturnValue(Exception): pass - - -class PyPromiseRunner(QRunnable): - """ - QRunnable for running Promises in different threads. - """ - def __init__(self, runner, promise, args): - QRunnable.__init__(self) - self.runner = runner - self.promise = promise - self.args = args - - def run(self): - try: - data = self.runner(*self.args) - if isinstance(data, QObject): - data = data - elif type(data) in [int, str, float, bool, bytes]: - data = QJSValue(data) - elif data is None: - data = QJSValue.SpecialValue.UndefinedValue - elif isinstance(data, QJSValue): - data = data - elif isinstance(data, PyJSValue): - data = data.qjs_value - else: - raise InvalidReturnValue("Must return either a primitive, a valid QObject, JS Value, or None.") - self.promise.finished.emit(data) - except Exception as e: - try: - self.promise.errored.emit(repr(e)) - except RuntimeError as e2: - # Happens when the PyPromise has already been garbage collected. - # In other words, nothing to report to nowhere. - pass - - -class PyPromise(QObject): - """ - Asynchronous Promise-like object meant to interface between Python and Javascript easily. - Runs to_run in another thread, and calls fulfilled (populated by then) with its return value. - """ - finished = Signal((QJSValue,), (QObject,)) - errored = Signal(Exception) - - def __init__(self, to_run: Callable, args): - QObject.__init__(self) - self._fulfills = [] - self._rejects = [] - self.finished.connect(self._fulfill) - self.errored.connect(self._reject) - self._runner = PyPromiseRunner(to_run, self, args) - QThreadPool.globalInstance().start(self._runner) - - - @Slot(QJSValue, result=QObject) - @Slot(QJSValue, QJSValue, result=QObject) - def then(self, on_fulfill: QJSValue | Callable, on_reject: QJSValue | Callable = None): - """ - Adds listeners for both fulfilment and catching errors of the Promise. - """ - if isinstance(on_fulfill, QJSValue): - self._fulfills.append(PyJSValue(on_fulfill)) - elif isinstance(on_fulfill, Callable): - self._fulfills.append(on_fulfill) - if isinstance(on_reject, QJSValue): - self._rejects.append(PyJSValue(on_reject)) - elif isinstance(on_reject, Callable): - self._rejects.append(on_reject) - return self - - @Slot(QJSValue) - @Slot(QObject) - def _fulfill(self, data): - no_return = [None, QJSValue.SpecialValue.UndefinedValue] - for on_fulfill in self._fulfills: - try: - result = on_fulfill(data) - data = result if result not in no_return else data # Forward data. - except Exception as e: - self._reject(repr(e)) - break - - @Slot(QJSValue) - @Slot(str) - def _reject(self, error): - no_return = [None, QJSValue.SpecialValue.UndefinedValue] - for on_reject in self._rejects: - result = on_reject(error) - error = result if result not in no_return else error # Forward data.