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.