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 844e22a..93d76db 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() - } - - Connections { - target: Helper - function onChangelogFetched(chl) { - changelogNeedsFetching = false; - changelog.text = chl + Helper.fetchChangelog().then((fetchedText) => { + changelogNeedsFetching = false + changelog.text = fetchedText changelogView.contentItem.implicitHeight = changelog.height - // console.log(changelog.height, changelogView.contentItem.implicitHeight) - } + }, (error) => { + const e = qsTranslate("changelog", "Could not fetch update: {}.").replace('{}', error) + console.error(e) + changelogNeedsFetching = false + changelog.text = e + changelogView.contentItem.implicitHeight = changelog.height + }) } ScrollView { diff --git a/runtime-pyside6/LogarithmPlotter/util/helper.py b/runtime-pyside6/LogarithmPlotter/util/helper.py index 344e425..7f7afb9 100644 --- a/runtime-pyside6/LogarithmPlotter/util/helper.py +++ b/runtime-pyside6/LogarithmPlotter/util/helper.py @@ -29,13 +29,16 @@ 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 @@ -46,31 +49,30 @@ 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 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) +def read_changelog(): + f = open(CHANGELOG_CACHE_PATH, 'r', -1) + data = f.read().strip() + f.close() + return data class Helper(QObject): - changelogFetched = Signal(str) - def __init__(self, cwd: str, tmpfile: str): QObject.__init__(self) self.cwd = cwd @@ -150,15 +152,14 @@ 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() + @Slot(result=PyPromise) def fetchChangelog(self): - changelog_cache_path = path.join(path.dirname(path.realpath(__file__)), "CHANGELOG.md") - if path.exists(changelog_cache_path): + """ + Fetches the changelog and returns a Promise. + """ + if path.exists(CHANGELOG_CACHE_PATH): # We have a cached version of the changelog, for env that don't have access to the internet. - f = open(changelog_cache_path); - self.changelogFetched.emit("".join(f.readlines()).strip()) - f.close() + return PyPromise(read_changelog) else: # Fetch it from the internet. - runnable = ChangelogFetcher(self) - QThreadPool.globalInstance().start(runnable) + return PyPromise(fetch_changelog) diff --git a/runtime-pyside6/LogarithmPlotter/util/js.py b/runtime-pyside6/LogarithmPlotter/util/js.py index dbe60bc..05f30d5 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(), Function), + (lambda: self.qjs_value.isCallable(), Callable), (lambda: self.qjs_value.isDate(), QDateTime), (lambda: self.qjs_value.isError(), Exception), (lambda: self.qjs_value.isNull(), None), @@ -103,4 +103,6 @@ class PyJSValue: """ if self.type() not in [bool, float, str, None]: raise NotAPrimitiveException() - return self.qjs_value.toPrimitive().toVariant() \ No newline at end of file + return self.qjs_value.toPrimitive().toVariant() + + diff --git a/runtime-pyside6/LogarithmPlotter/util/latex.py b/runtime-pyside6/LogarithmPlotter/util/latex.py index df32d46..aac5c5b 100644 --- a/runtime-pyside6/LogarithmPlotter/util/latex.py +++ b/runtime-pyside6/LogarithmPlotter/util/latex.py @@ -16,18 +16,17 @@ * along with this program. If not, see . """ -from PySide6.QtCore import QObject, Slot, Property, QCoreApplication +from PySide6.QtCore import QObject, Slot, Property, QCoreApplication, Signal from PySide6.QtGui import QImage, QColor from PySide6.QtWidgets import QMessageBox -from os import path, remove, environ, makedirs +from os import path, remove, makedirs from string import Template from subprocess import Popen, TimeoutExpired, PIPE from hashlib import sha512 from shutil import which from sys import argv - """ Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/) installation and collects the binary path in the DVIPNG_PATH variable. @@ -76,6 +75,7 @@ class Latex(QObject): dvipng to be installed on the system. """ + def __init__(self, cache_path): QObject.__init__(self) self.tempdir = path.join(cache_path, "latex") diff --git a/runtime-pyside6/LogarithmPlotter/util/promise.py b/runtime-pyside6/LogarithmPlotter/util/promise.py new file mode 100644 index 0000000..1ccfe8e --- /dev/null +++ b/runtime-pyside6/LogarithmPlotter/util/promise.py @@ -0,0 +1,110 @@ +""" + * 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): + QRunnable.__init__(self) + self.runner = runner + self.promise = promise + print("Initialized", self.runner) + + def run(self): + try: + data = self.runner() + 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: + self.promise.errored.emit(repr(e)) + + +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): + QObject.__init__(self) + self._fulfills = [] + self._rejects = [] + self.finished.connect(self._fulfill) + self.errored.connect(self._reject) + self._runner = PyPromiseRunner(to_run, self) + 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) + + @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.