Compare commits
4 commits
b33e1329db
...
37ac400f23
Author | SHA1 | Date | |
---|---|---|---|
37ac400f23 | |||
5313428250 | |||
cf73b35a9a | |||
f734e40ad9 |
10 changed files with 219 additions and 55 deletions
|
@ -30,16 +30,18 @@ class CanvasAPI extends Module {
|
||||||
#canvas = null
|
#canvas = null
|
||||||
/** @type {CanvasRenderingContext2D} */
|
/** @type {CanvasRenderingContext2D} */
|
||||||
#ctx = null
|
#ctx = null
|
||||||
|
/** Lock to prevent asynchronous stuff from printing stuff that is outdated. */
|
||||||
|
#redrawCount = 0
|
||||||
/** @type {{show(string, string, string)}} */
|
/** @type {{show(string, string, string)}} */
|
||||||
#drawingErrorDialog = null
|
#drawingErrorDialog = null
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("Canvas", {
|
super("Canvas", {
|
||||||
canvas: CanvasInterface,
|
canvas: CanvasInterface,
|
||||||
drawingErrorDialog: DialogInterface
|
drawingErrorDialog: DialogInterface
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Object.<string, {expression: Expression, value: number, maxDraw: number}>}
|
* @type {Object.<string, {expression: Expression, value: number, maxDraw: number}>}
|
||||||
|
@ -207,6 +209,7 @@ class CanvasAPI extends Module {
|
||||||
*/
|
*/
|
||||||
redraw() {
|
redraw() {
|
||||||
if(!this.initialized) throw new Error("Attempting redraw before initialize!")
|
if(!this.initialized) throw new Error("Attempting redraw before initialize!")
|
||||||
|
this.#redrawCount = (this.#redrawCount + 1) % 10000
|
||||||
this.#ctx = this.#canvas.getContext("2d")
|
this.#ctx = this.#canvas.getContext("2d")
|
||||||
this._computeAxes()
|
this._computeAxes()
|
||||||
this._reset()
|
this._reset()
|
||||||
|
@ -519,15 +522,18 @@ class CanvasAPI extends Module {
|
||||||
* @param {function(LatexRenderResult|{width: number, height: number, source: string})} callback
|
* @param {function(LatexRenderResult|{width: number, height: number, source: string})} callback
|
||||||
*/
|
*/
|
||||||
renderLatexImage(ltxText, color, callback) {
|
renderLatexImage(ltxText, color, callback) {
|
||||||
|
const currentRedrawCount = this.#redrawCount
|
||||||
const onRendered = (imgData) => {
|
const onRendered = (imgData) => {
|
||||||
if(!this.#canvas.isImageLoaded(imgData.source) && !this.#canvas.isImageLoading(imgData.source)) {
|
if(!this.#canvas.isImageLoaded(imgData.source) && !this.#canvas.isImageLoading(imgData.source)) {
|
||||||
// Wait until the image is loaded to callback.
|
// Wait until the image is loaded to callback.
|
||||||
this.#canvas.loadImageAsync(imgData.source).then(() => {
|
this.#canvas.loadImageAsync(imgData.source).then(() => {
|
||||||
callback(imgData)
|
if(this.#redrawCount === currentRedrawCount)
|
||||||
|
callback(imgData)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Callback directly
|
// Callback directly
|
||||||
callback(imgData)
|
if(this.#redrawCount === currentRedrawCount)
|
||||||
|
callback(imgData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const prerendered = Latex.findPrerendered(ltxText, this.textsize, color)
|
const prerendered = Latex.findPrerendered(ltxText, this.textsize, color)
|
||||||
|
|
|
@ -84,13 +84,21 @@ export class DialogInterface extends Interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LatexInterface extends Interface {
|
export class LatexInterface extends Interface {
|
||||||
|
supportsAsyncRender = BOOLEAN
|
||||||
/**
|
/**
|
||||||
* @param {string} markup - LaTeX markup to render
|
* @param {string} markup - LaTeX markup to render
|
||||||
* @param {number} fontSize - Font size (in pt) to render
|
* @param {number} fontSize - Font size (in pt) to render
|
||||||
* @param {string} color - Color of the text to render
|
* @param {string} color - Color of the text to render
|
||||||
* @returns {string} - Comma separated data of the image (source, width, height)
|
* @returns {string} - Comma separated data of the image (source, width, height)
|
||||||
*/
|
*/
|
||||||
render = FUNCTION
|
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<string>} - Comma separated data of the image (source, width, height)
|
||||||
|
*/
|
||||||
|
renderAsync = FUNCTION
|
||||||
/**
|
/**
|
||||||
* @param {string} markup - LaTeX markup to render
|
* @param {string} markup - LaTeX markup to render
|
||||||
* @param {number} fontSize - Font size (in pt) to render
|
* @param {number} fontSize - Font size (in pt) to render
|
||||||
|
|
|
@ -112,7 +112,12 @@ class LatexAPI extends Module {
|
||||||
*/
|
*/
|
||||||
async requestAsyncRender(markup, fontSize, color) {
|
async requestAsyncRender(markup, fontSize, color) {
|
||||||
if(!this.initialized) throw new Error("Attempting requestAsyncRender before initialize!")
|
if(!this.initialized) throw new Error("Attempting requestAsyncRender before initialize!")
|
||||||
let args = this.#latex.render(markup, fontSize, color).split(",")
|
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(",")
|
||||||
return new LatexRenderResult(...args)
|
return new LatexRenderResult(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,8 +46,15 @@ class EnableLatex extends BoolSetting {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ENABLE_LATEX_ASYNC = new BoolSetting(
|
||||||
|
qsTranslate("general", "Enable asynchronous LaTeX renderer"),
|
||||||
|
"enable_latex_async",
|
||||||
|
"new"
|
||||||
|
)
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
CHECK_FOR_UPDATES,
|
CHECK_FOR_UPDATES,
|
||||||
RESET_REDO_STACK,
|
RESET_REDO_STACK,
|
||||||
new EnableLatex()
|
new EnableLatex(),
|
||||||
|
ENABLE_LATEX_ASYNC
|
||||||
]
|
]
|
||||||
|
|
|
@ -45,17 +45,17 @@ Popup {
|
||||||
property bool changelogNeedsFetching: true
|
property bool changelogNeedsFetching: true
|
||||||
|
|
||||||
onAboutToShow: if(changelogNeedsFetching) {
|
onAboutToShow: if(changelogNeedsFetching) {
|
||||||
Helper.fetchChangelog()
|
Helper.fetchChangelog().then((fetchedText) => {
|
||||||
}
|
changelogNeedsFetching = false
|
||||||
|
changelog.text = fetchedText
|
||||||
Connections {
|
|
||||||
target: Helper
|
|
||||||
function onChangelogFetched(chl) {
|
|
||||||
changelogNeedsFetching = false;
|
|
||||||
changelog.text = chl
|
|
||||||
changelogView.contentItem.implicitHeight = changelog.height
|
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 {
|
ScrollView {
|
||||||
|
|
|
@ -19,13 +19,16 @@
|
||||||
from os import path, environ, makedirs
|
from os import path, environ, makedirs
|
||||||
from platform import system
|
from platform import system
|
||||||
from json import load, dumps
|
from json import load, dumps
|
||||||
|
from shutil import which
|
||||||
|
|
||||||
from PySide6.QtCore import QLocale, QTranslator
|
from PySide6.QtCore import QLocale, QTranslator
|
||||||
|
|
||||||
DEFAULT_SETTINGS = {
|
DEFAULT_SETTINGS = {
|
||||||
"check_for_updates": True,
|
"check_for_updates": True,
|
||||||
"reset_redo_stack": True,
|
"reset_redo_stack": True,
|
||||||
"last_install_greet": "0",
|
"last_install_greet": "0",
|
||||||
"enable_latex": False,
|
"enable_latex": which("latex") is not None and which("dvipng") is not None,
|
||||||
|
"enable_latex_async": True,
|
||||||
"expression_editor": {
|
"expression_editor": {
|
||||||
"autoclose": True,
|
"autoclose": True,
|
||||||
"colorize": True,
|
"colorize": True,
|
||||||
|
|
|
@ -29,13 +29,16 @@ from urllib.error import HTTPError, URLError
|
||||||
|
|
||||||
from LogarithmPlotter import __VERSION__
|
from LogarithmPlotter import __VERSION__
|
||||||
from LogarithmPlotter.util import config
|
from LogarithmPlotter.util import config
|
||||||
|
from LogarithmPlotter.util.promise import PyPromise
|
||||||
|
|
||||||
SHOW_GUI_MESSAGES = "--test-build" not in argv
|
SHOW_GUI_MESSAGES = "--test-build" not in argv
|
||||||
CHANGELOG_VERSION = __VERSION__
|
CHANGELOG_VERSION = __VERSION__
|
||||||
|
CHANGELOG_CACHE_PATH = path.join(path.dirname(path.realpath(__file__)), "CHANGELOG.md")
|
||||||
|
|
||||||
|
|
||||||
class InvalidFileException(Exception): pass
|
class InvalidFileException(Exception): pass
|
||||||
|
|
||||||
|
|
||||||
def show_message(msg: str) -> None:
|
def show_message(msg: str) -> None:
|
||||||
"""
|
"""
|
||||||
Shows a GUI message if GUI messages are enabled
|
Shows a GUI message if GUI messages are enabled
|
||||||
|
@ -46,31 +49,30 @@ def show_message(msg: str) -> None:
|
||||||
raise InvalidFileException(msg)
|
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):
|
def read_changelog():
|
||||||
msg_text = "Unknown changelog error."
|
f = open(CHANGELOG_CACHE_PATH, 'r', -1)
|
||||||
try:
|
data = f.read().strip()
|
||||||
# Fetching version
|
f.close()
|
||||||
r = urlopen("https://api.ad5001.eu/changelog/logarithmplotter/?version=" + CHANGELOG_VERSION)
|
return data
|
||||||
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):
|
class Helper(QObject):
|
||||||
changelogFetched = Signal(str)
|
|
||||||
|
|
||||||
def __init__(self, cwd: str, tmpfile: str):
|
def __init__(self, cwd: str, tmpfile: str):
|
||||||
QObject.__init__(self)
|
QObject.__init__(self)
|
||||||
self.cwd = cwd
|
self.cwd = cwd
|
||||||
|
@ -150,15 +152,14 @@ class Helper(QObject):
|
||||||
msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}")
|
msg = QCoreApplication.translate('main', "Built with PySide6 (Qt) v{} and python v{}")
|
||||||
return msg.format(PySide6_version, sys_version.split("\n")[0])
|
return msg.format(PySide6_version, sys_version.split("\n")[0])
|
||||||
|
|
||||||
@Slot()
|
@Slot(result=PyPromise)
|
||||||
def fetchChangelog(self):
|
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.
|
# We have a cached version of the changelog, for env that don't have access to the internet.
|
||||||
f = open(changelog_cache_path);
|
return PyPromise(read_changelog)
|
||||||
self.changelogFetched.emit("".join(f.readlines()).strip())
|
|
||||||
f.close()
|
|
||||||
else:
|
else:
|
||||||
# Fetch it from the internet.
|
# Fetch it from the internet.
|
||||||
runnable = ChangelogFetcher(self)
|
return PyPromise(fetch_changelog)
|
||||||
QThreadPool.globalInstance().start(runnable)
|
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
from re import Pattern
|
from re import Pattern
|
||||||
|
from typing import Callable
|
||||||
from PySide6.QtCore import QMetaObject, QObject, QDateTime
|
from PySide6.QtCore import QMetaObject, QObject, QDateTime
|
||||||
from PySide6.QtQml import QJSValue
|
from PySide6.QtQml import QJSValue
|
||||||
|
|
||||||
class InvalidAttributeValueException(Exception): pass
|
class InvalidAttributeValueException(Exception): pass
|
||||||
class NotAPrimitiveException(Exception): pass
|
class NotAPrimitiveException(Exception): pass
|
||||||
|
|
||||||
class Function: pass
|
|
||||||
class URL: pass
|
class URL: pass
|
||||||
|
|
||||||
class PyJSValue:
|
class PyJSValue:
|
||||||
|
@ -78,7 +78,7 @@ class PyJSValue:
|
||||||
matcher = [
|
matcher = [
|
||||||
(lambda: self.qjs_value.isArray(), list),
|
(lambda: self.qjs_value.isArray(), list),
|
||||||
(lambda: self.qjs_value.isBool(), bool),
|
(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.isDate(), QDateTime),
|
||||||
(lambda: self.qjs_value.isError(), Exception),
|
(lambda: self.qjs_value.isError(), Exception),
|
||||||
(lambda: self.qjs_value.isNull(), None),
|
(lambda: self.qjs_value.isNull(), None),
|
||||||
|
@ -103,4 +103,6 @@ class PyJSValue:
|
||||||
"""
|
"""
|
||||||
if self.type() not in [bool, float, str, None]:
|
if self.type() not in [bool, float, str, None]:
|
||||||
raise NotAPrimitiveException()
|
raise NotAPrimitiveException()
|
||||||
return self.qjs_value.toPrimitive().toVariant()
|
return self.qjs_value.toPrimitive().toVariant()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,18 +15,21 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
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.QtGui import QImage, QColor
|
||||||
from PySide6.QtWidgets import QMessageBox
|
from PySide6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from os import path, remove, environ, makedirs
|
from os import path, remove, makedirs
|
||||||
from string import Template
|
from string import Template
|
||||||
from subprocess import Popen, TimeoutExpired, PIPE
|
from subprocess import Popen, TimeoutExpired, PIPE
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from sys import argv
|
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/)
|
Searches for a valid Latex and DVIPNG (http://savannah.nongnu.org/projects/dvipng/)
|
||||||
|
@ -85,6 +88,10 @@ class Latex(QObject):
|
||||||
def latexSupported(self) -> bool:
|
def latexSupported(self) -> bool:
|
||||||
return LATEX_PATH is not None and DVIPNG_PATH is not None
|
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)
|
@Slot(result=bool)
|
||||||
def checkLatexInstallation(self) -> bool:
|
def checkLatexInstallation(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -105,13 +112,20 @@ class Latex(QObject):
|
||||||
valid_install = False
|
valid_install = False
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.render("", 14, QColor(0, 0, 0, 255))
|
self.renderSync("", 14, QColor(0, 0, 0, 255))
|
||||||
except MissingPackageException:
|
except MissingPackageException:
|
||||||
valid_install = False # Should have sent an error message if failed to render
|
valid_install = False # Should have sent an error message if failed to render
|
||||||
return valid_install
|
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)
|
@Slot(str, float, QColor, result=str)
|
||||||
def render(self, latex_markup: str, font_size: float, color: QColor) -> str:
|
def renderSync(self, latex_markup: str, font_size: float, color: QColor) -> str:
|
||||||
"""
|
"""
|
||||||
Prepares and renders a latex string into a png file.
|
Prepares and renders a latex string into a png file.
|
||||||
"""
|
"""
|
||||||
|
@ -124,12 +138,14 @@ class Latex(QObject):
|
||||||
if not path.exists(latex_path + ".dvi"):
|
if not path.exists(latex_path + ".dvi"):
|
||||||
self.create_latex_doc(latex_path, latex_markup)
|
self.create_latex_doc(latex_path, latex_markup)
|
||||||
self.convert_latex_to_dvi(latex_path)
|
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.
|
# 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, 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+"@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+"@3", font_size*3, color)
|
||||||
# self.convert_dvi_to_png(latex_path, export_path+"@4", font_size*4, color)
|
# self.convert_dvi_to_png(latex_path, export_path+"@4", font_size*4, color)
|
||||||
|
else:
|
||||||
|
sleep(0)
|
||||||
img = QImage(export_path)
|
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
|
# 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()}'
|
return f'{export_path}.png,{img.width()},{img.height()}'
|
||||||
|
|
116
runtime-pyside6/LogarithmPlotter/util/promise.py
Normal file
116
runtime-pyside6/LogarithmPlotter/util/promise.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
"""
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
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.
|
Loading…
Reference in a new issue