From f734e40ad9e986357c2e591a7ce50e35c97799f9 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 15 Oct 2024 01:22:25 +0200 Subject: [PATCH 1/4] Starting PyPromise --- .../LogarithmPlotter/Popup/Changelog.qml | 20 ++-- .../LogarithmPlotter/util/helper.py | 57 ++++----- runtime-pyside6/LogarithmPlotter/util/js.py | 8 +- .../LogarithmPlotter/util/latex.py | 6 +- .../LogarithmPlotter/util/promise.py | 110 ++++++++++++++++++ 5 files changed, 157 insertions(+), 44 deletions(-) create mode 100644 runtime-pyside6/LogarithmPlotter/util/promise.py 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. From cf73b35a9ab1494485006ec4dafcb9653a29e106 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 15 Oct 2024 03:01:27 +0200 Subject: [PATCH 2/4] Adding experimental async LaTeX renderer (speeds up rendering ridiculously, but causes instability) --- common/src/module/interface.mjs | 10 +++++++- common/src/module/latex.mjs | 7 +++++- common/src/preferences/general.mjs | 9 ++++++- .../LogarithmPlotter/util/config.py | 1 + .../LogarithmPlotter/util/latex.py | 24 +++++++++++++++---- .../LogarithmPlotter/util/promise.py | 11 +++++---- 6 files changed, 50 insertions(+), 12 deletions(-) diff --git a/common/src/module/interface.mjs b/common/src/module/interface.mjs index b6d462f..a96621c 100644 --- a/common/src/module/interface.mjs +++ b/common/src/module/interface.mjs @@ -84,13 +84,21 @@ 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) */ - 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} - Comma separated data of the image (source, width, height) + */ + renderAsync = 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 a6aa216..830f096 100644 --- a/common/src/module/latex.mjs +++ b/common/src/module/latex.mjs @@ -112,7 +112,12 @@ class LatexAPI extends Module { */ async requestAsyncRender(markup, fontSize, color) { 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) } diff --git a/common/src/preferences/general.mjs b/common/src/preferences/general.mjs index 81c5e3d..4a1098f 100644 --- a/common/src/preferences/general.mjs +++ b/common/src/preferences/general.mjs @@ -46,8 +46,15 @@ class EnableLatex extends BoolSetting { } } +const ENABLE_LATEX_ASYNC = new BoolSetting( + qsTranslate("general", "Enable asynchronous LaTeX renderer (experimental)"), + "enable_latex_async", + "new" +) + export default [ CHECK_FOR_UPDATES, RESET_REDO_STACK, - new EnableLatex() + new EnableLatex(), + ENABLE_LATEX_ASYNC ] diff --git a/runtime-pyside6/LogarithmPlotter/util/config.py b/runtime-pyside6/LogarithmPlotter/util/config.py index a5ee23c..ae10adb 100644 --- a/runtime-pyside6/LogarithmPlotter/util/config.py +++ b/runtime-pyside6/LogarithmPlotter/util/config.py @@ -26,6 +26,7 @@ DEFAULT_SETTINGS = { "reset_redo_stack": True, "last_install_greet": "0", "enable_latex": False, + "enable_latex_async": False, "expression_editor": { "autoclose": True, "colorize": True, diff --git a/runtime-pyside6/LogarithmPlotter/util/latex.py b/runtime-pyside6/LogarithmPlotter/util/latex.py index aac5c5b..0d720c3 100644 --- a/runtime-pyside6/LogarithmPlotter/util/latex.py +++ b/runtime-pyside6/LogarithmPlotter/util/latex.py @@ -15,6 +15,7 @@ * 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.QtGui import QImage, QColor @@ -27,6 +28,9 @@ 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/) installation and collects the binary path in the DVIPNG_PATH variable. @@ -75,7 +79,6 @@ 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") @@ -85,6 +88,10 @@ 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: """ @@ -105,13 +112,20 @@ class Latex(QObject): valid_install = False else: try: - self.render("", 14, QColor(0, 0, 0, 255)) + self.renderSync("", 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 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. """ @@ -124,12 +138,14 @@ 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 index 1ccfe8e..6868432 100644 --- a/runtime-pyside6/LogarithmPlotter/util/promise.py +++ b/runtime-pyside6/LogarithmPlotter/util/promise.py @@ -30,15 +30,15 @@ class PyPromiseRunner(QRunnable): """ QRunnable for running Promises in different threads. """ - def __init__(self, runner, promise): + def __init__(self, runner, promise, args): QRunnable.__init__(self) self.runner = runner self.promise = promise - print("Initialized", self.runner) + self.args = args def run(self): try: - data = self.runner() + data = self.runner(*self.args) if isinstance(data, QObject): data = data elif type(data) in [int, str, float, bool, bytes]: @@ -64,13 +64,13 @@ class PyPromise(QObject): finished = Signal((QJSValue,), (QObject,)) errored = Signal(Exception) - def __init__(self, to_run: Callable): + 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) + self._runner = PyPromiseRunner(to_run, self, args) QThreadPool.globalInstance().start(self._runner) @@ -88,6 +88,7 @@ class PyPromise(QObject): self._rejects.append(PyJSValue(on_reject)) elif isinstance(on_reject, Callable): self._rejects.append(on_reject) + return self @Slot(QJSValue) @Slot(QObject) From 531342825095e0703d251c38e382e293289b263b Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 15 Oct 2024 03:52:06 +0200 Subject: [PATCH 3/4] Improving stability of asynchronous LaTeX renderer. --- common/src/module/canvas.mjs | 16 +++++++++++----- common/src/preferences/general.mjs | 2 +- runtime-pyside6/LogarithmPlotter/util/promise.py | 7 ++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/common/src/module/canvas.mjs b/common/src/module/canvas.mjs index c46c9a1..62649cf 100644 --- a/common/src/module/canvas.mjs +++ b/common/src/module/canvas.mjs @@ -30,16 +30,18 @@ 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.} @@ -207,6 +209,7 @@ 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() @@ -519,15 +522,18 @@ 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(() => { - callback(imgData) + if(this.#redrawCount === currentRedrawCount) + callback(imgData) }) } else { // Callback directly - callback(imgData) + if(this.#redrawCount === currentRedrawCount) + callback(imgData) } } const prerendered = Latex.findPrerendered(ltxText, this.textsize, color) diff --git a/common/src/preferences/general.mjs b/common/src/preferences/general.mjs index 4a1098f..a6957c6 100644 --- a/common/src/preferences/general.mjs +++ b/common/src/preferences/general.mjs @@ -47,7 +47,7 @@ class EnableLatex extends BoolSetting { } const ENABLE_LATEX_ASYNC = new BoolSetting( - qsTranslate("general", "Enable asynchronous LaTeX renderer (experimental)"), + qsTranslate("general", "Enable asynchronous LaTeX renderer"), "enable_latex_async", "new" ) diff --git a/runtime-pyside6/LogarithmPlotter/util/promise.py b/runtime-pyside6/LogarithmPlotter/util/promise.py index 6868432..f129d41 100644 --- a/runtime-pyside6/LogarithmPlotter/util/promise.py +++ b/runtime-pyside6/LogarithmPlotter/util/promise.py @@ -53,7 +53,12 @@ class PyPromiseRunner(QRunnable): 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)) + 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): From 37ac400f230591edb0e14efaaff50de127b7e19a Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 15 Oct 2024 03:52:47 +0200 Subject: [PATCH 4/4] Enabling latex async generation by default, default LaTeX setting now depends on state of installation. --- runtime-pyside6/LogarithmPlotter/util/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/runtime-pyside6/LogarithmPlotter/util/config.py b/runtime-pyside6/LogarithmPlotter/util/config.py index ae10adb..e01deae 100644 --- a/runtime-pyside6/LogarithmPlotter/util/config.py +++ b/runtime-pyside6/LogarithmPlotter/util/config.py @@ -19,14 +19,16 @@ 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": False, - "enable_latex_async": False, + "enable_latex": which("latex") is not None and which("dvipng") is not None, + "enable_latex_async": True, "expression_editor": { "autoclose": True, "colorize": True,