From 40c4e74d4802562b7e997c00ce5daa3e0b28c63e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jan 2025 20:15:32 +0100 Subject: [PATCH 01/13] Switch app code to PyQt6 --- i18n/qtbase.py | 2 +- novelWriter.py | 8 +++---- novelwriter/__init__.py | 32 ++++++++++++------------- novelwriter/common.py | 9 ++++--- novelwriter/config.py | 18 +++++++------- novelwriter/constants.py | 2 +- novelwriter/core/buildsettings.py | 2 +- novelwriter/core/coretools.py | 2 +- novelwriter/core/docbuild.py | 2 +- novelwriter/core/item.py | 2 +- novelwriter/core/itemmodel.py | 4 ++-- novelwriter/core/project.py | 2 +- novelwriter/core/spellcheck.py | 2 +- novelwriter/core/status.py | 4 ++-- novelwriter/core/tree.py | 2 +- novelwriter/dialogs/about.py | 4 ++-- novelwriter/dialogs/docmerge.py | 4 ++-- novelwriter/dialogs/docsplit.py | 4 ++-- novelwriter/dialogs/editlabel.py | 2 +- novelwriter/dialogs/preferences.py | 6 ++--- novelwriter/dialogs/projectsettings.py | 6 ++--- novelwriter/dialogs/quotes.py | 6 ++--- novelwriter/dialogs/wordlist.py | 6 ++--- novelwriter/error.py | 10 ++++---- novelwriter/extensions/configlayout.py | 4 ++-- novelwriter/extensions/eventfilters.py | 6 ++--- novelwriter/extensions/modified.py | 6 ++--- novelwriter/extensions/novelselector.py | 4 ++-- novelwriter/extensions/pagedsidebar.py | 10 ++++---- novelwriter/extensions/progressbars.py | 6 ++--- novelwriter/extensions/statusled.py | 4 ++-- novelwriter/extensions/switch.py | 14 ++++++----- novelwriter/extensions/switchbox.py | 6 ++--- novelwriter/extensions/versioninfo.py | 6 ++--- novelwriter/formats/shared.py | 2 +- novelwriter/formats/todocx.py | 4 ++-- novelwriter/formats/tokenizer.py | 4 ++-- novelwriter/formats/toodt.py | 2 +- novelwriter/formats/toqdoc.py | 18 +++++++------- novelwriter/gui/doceditor.py | 16 ++++++------- novelwriter/gui/dochighlight.py | 4 ++-- novelwriter/gui/docviewer.py | 14 +++++------ novelwriter/gui/docviewerpanel.py | 4 ++-- novelwriter/gui/editordocument.py | 6 ++--- novelwriter/gui/itemdetails.py | 4 ++-- novelwriter/gui/mainmenu.py | 5 ++-- novelwriter/gui/noveltree.py | 10 ++++---- novelwriter/gui/outline.py | 8 +++---- novelwriter/gui/projtree.py | 8 +++---- novelwriter/gui/search.py | 6 ++--- novelwriter/gui/sidebar.py | 6 ++--- novelwriter/gui/statusbar.py | 4 ++-- novelwriter/gui/theme.py | 6 ++--- novelwriter/guimain.py | 8 +++---- novelwriter/shared.py | 6 ++--- novelwriter/tools/dictionaries.py | 6 ++--- novelwriter/tools/lipsum.py | 4 ++-- novelwriter/tools/manusbuild.py | 6 ++--- novelwriter/tools/manuscript.py | 18 +++++++------- novelwriter/tools/manussettings.py | 6 ++--- novelwriter/tools/noveldetails.py | 6 ++--- novelwriter/tools/welcome.py | 17 +++++++------ novelwriter/tools/writingstats.py | 18 +++++++------- novelwriter/types.py | 8 +++---- 64 files changed, 218 insertions(+), 223 deletions(-) diff --git a/i18n/qtbase.py b/i18n/qtbase.py index 90f6f9ae..3126bea3 100644 --- a/i18n/qtbase.py +++ b/i18n/qtbase.py @@ -10,7 +10,7 @@ If a qtbase_xx.qm file already exists, do not add a translation for the entries generated from this file. """ -from PyQt5.QtCore import QT_TRANSLATE_NOOP +from PyQt6.QtCore import QT_TRANSLATE_NOOP # QDialogButtonBox # ================ diff --git a/novelWriter.py b/novelWriter.py index 33ce3768..285cb5dd 100755 --- a/novelWriter.py +++ b/novelWriter.py @@ -7,11 +7,11 @@ import os import sys try: - import PyQt5.QtWidgets # noqa: F401 - import PyQt5.QtGui # noqa: F401 - import PyQt5.QtCore # noqa: F401 + import PyQt6.QtCore # noqa: F401 + import PyQt6.QtGui # noqa: F401 + import PyQt6.QtWidgets # noqa: F401 except Exception: - print("ERROR: Failed to load dependency PyQt5") + print("ERROR: Failed to load dependency PyQt6") sys.exit(1) os.curdir = os.path.abspath(os.path.dirname(__file__)) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 7e9f74f1..62305d6d 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -29,7 +29,7 @@ import sys from typing import TYPE_CHECKING -from PyQt5.QtWidgets import QApplication, QErrorMessage +from PyQt6.QtWidgets import QApplication, QErrorMessage from novelwriter.config import Config from novelwriter.error import exceptionHandler @@ -201,21 +201,21 @@ def main(sysArgs: list | None = None) -> GuiMain | None: # Check Packages and Versions errorData = [] errorCode = 0 - if sys.hexversion < 0x030a00f0: - errorData.append( - "At least Python 3.10 is required, found %s" % CONFIG.verPyString - ) - errorCode |= 0x04 - if CONFIG.verQtValue < 0x050f00: - errorData.append( - "At least Qt5 version 5.15.0 is required, found %s" % CONFIG.verQtString - ) - errorCode |= 0x08 - if CONFIG.verPyQtValue < 0x050f00: - errorData.append( - "At least PyQt5 version 5.15.0 is required, found %s" % CONFIG.verPyQtString - ) - errorCode |= 0x10 + # if sys.hexversion < 0x030a00f0: + # errorData.append( + # "At least Python 3.10 is required, found %s" % CONFIG.verPyString + # ) + # errorCode |= 0x04 + # if CONFIG.verQtValue < 0x060000: + # errorData.append( + # "At least Qt6 version 6.0 is required, found %s" % CONFIG.verQtString + # ) + # errorCode |= 0x08 + # if CONFIG.verPyQtValue < 0x060000: + # errorData.append( + # "At least PyQt6 version 6.0 is required, found %s" % CONFIG.verPyQtString + # ) + # errorCode |= 0x10 if errorData: errApp = QApplication([]) diff --git a/novelwriter/common.py b/novelwriter/common.py index 17a465ee..4731e82f 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -37,8 +37,8 @@ from typing import Any, Literal, TypeGuard, TypeVar from urllib.parse import urljoin from urllib.request import pathname2url -from PyQt5.QtCore import QCoreApplication, QMimeData, QUrl -from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo +from PyQt6.QtCore import QCoreApplication, QMimeData, QUrl +from PyQt6.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType @@ -440,11 +440,10 @@ def fontMatcher(font: QFont) -> QFont: info = QFontInfo(font) if (famRequest := font.family()) != (famActual := info.family()): logger.warning("Font mismatch: Requested '%s', but got '%s'", famRequest, famActual) - db = QFontDatabase() - if famRequest in db.families(): + if famRequest in QFontDatabase.families(): styleRequest, sizeRequest = font.styleName(), font.pointSize() logger.info("Lookup: %s, %s, %d pt", famRequest, styleRequest, sizeRequest) - temp = db.font(famRequest, styleRequest, sizeRequest) + temp = QFontDatabase.font(famRequest, styleRequest, sizeRequest) temp.setPointSize(sizeRequest) # Make sure it isn't changed famFound, styleFound, sizeFound = temp.family(), temp.styleName(), temp.pointSize() if famFound == famRequest: diff --git a/novelwriter/config.py b/novelwriter/config.py index cd377199..8e4cded1 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -33,12 +33,12 @@ from datetime import datetime from pathlib import Path from time import time -from PyQt5.QtCore import ( +from PyQt6.QtCore import ( PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo, QLocale, QStandardPaths, QSysInfo, QTranslator ) -from PyQt5.QtGui import QFont, QFontDatabase -from PyQt5.QtWidgets import QApplication +from PyQt6.QtGui import QFont, QFontDatabase +from PyQt6.QtWidgets import QApplication from novelwriter.common import ( NWConfigParser, checkInt, checkPath, describeFont, fontMatcher, @@ -91,7 +91,7 @@ class Config: # Localisation # Note that these paths must be strings self._nwLangPath = self._appPath / "assets" / "i18n" - self._qtLangPath = QLibraryInfo.location(QLibraryInfo.LibraryLocation.TranslationsPath) + self._qtLangPath = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath) hasLocale = (self._nwLangPath / f"nw_{QLocale.system().name()}.qm").exists() self._qLocale = QLocale.system() if hasLocale else QLocale("en_GB") @@ -385,13 +385,12 @@ class Config: self.guiFont = fontMatcher(font) else: font = QFont() - fontDB = QFontDatabase() - if self.osWindows and "Arial" in fontDB.families(): + if self.osWindows and "Arial" in QFontDatabase.families(): # On Windows we default to Arial if possible font.setFamily("Arial") font.setPointSize(10) else: - font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont) + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) self.guiFont = fontMatcher(font) logger.debug("GUI font set to: %s", describeFont(font)) QApplication.setFont(self.guiFont) @@ -408,8 +407,7 @@ class Config: font.fromString(value) self.textFont = fontMatcher(font) else: - fontDB = QFontDatabase() - fontFam = fontDB.families() + fontFam = QFontDatabase.families() if self.osWindows and "Arial" in fontFam: font = QFont() font.setFamily("Arial") @@ -419,7 +417,7 @@ class Config: font.setFamily("Helvetica") font.setPointSize(12) else: - font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont) + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) self.textFont = fontMatcher(font) logger.debug("Text font set to: %s", describeFont(self.textFont)) return diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 36099c38..daf4e9e8 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -23,7 +23,7 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt5.QtCore import QT_TRANSLATE_NOOP, QCoreApplication +from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication from novelwriter.enum import ( nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index 0d2d3b44..7c62c5b9 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -32,7 +32,7 @@ from collections.abc import Iterable from enum import Enum from pathlib import Path -from PyQt5.QtCore import QT_TRANSLATE_NOOP, QCoreApplication +from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication from novelwriter import CONFIG from novelwriter.common import checkUuid, isHandle, jsonEncode diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index d8432449..f118ac7a 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -35,7 +35,7 @@ from functools import partial from pathlib import Path from zipfile import ZipFile, is_zipfile -from PyQt5.QtCore import QCoreApplication +from PyQt6.QtCore import QCoreApplication from novelwriter import CONFIG, SHARED from novelwriter.common import isHandle, minmax, simplified diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index b2431567..2f775fa5 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -28,7 +28,7 @@ import logging from collections.abc import Iterable from pathlib import Path -from PyQt5.QtGui import QFont +from PyQt6.QtGui import QFont from novelwriter import CONFIG from novelwriter.constants import nwLabels diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 6cf0ec90..38a2dbcc 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -27,7 +27,7 @@ import logging from typing import TYPE_CHECKING, Any -from PyQt5.QtGui import QFont, QIcon +from PyQt6.QtGui import QFont, QIcon from novelwriter import CONFIG, SHARED from novelwriter.common import ( diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index 8910e2b4..82b16e3e 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -28,8 +28,8 @@ import logging from typing import TYPE_CHECKING -from PyQt5.QtCore import QAbstractItemModel, QMimeData, QModelIndex, Qt -from PyQt5.QtGui import QFont, QIcon +from PyQt6.QtCore import QAbstractItemModel, QMimeData, QModelIndex, Qt +from PyQt6.QtGui import QFont, QIcon from novelwriter.common import decodeMimeHandles, encodeMimeHandles, minmax from novelwriter.constants import nwConst diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 50bf4a94..3999371c 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -31,7 +31,7 @@ from functools import partial from pathlib import Path from time import time -from PyQt5.QtCore import QCoreApplication +from PyQt6.QtCore import QCoreApplication from novelwriter import CONFIG, SHARED, __hexversion__, __version__ from novelwriter.common import ( diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py index dea46048..87b701ba 100644 --- a/novelwriter/core/spellcheck.py +++ b/novelwriter/core/spellcheck.py @@ -31,7 +31,7 @@ from collections.abc import Iterator from pathlib import Path from typing import TYPE_CHECKING -from PyQt5.QtCore import QLocale +from PyQt6.QtCore import QLocale from novelwriter.constants import nwFiles from novelwriter.error import logException diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 24b57aa6..92c78d50 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -31,8 +31,8 @@ import random from collections.abc import Iterable from typing import Literal, TypeGuard -from PyQt5.QtCore import QPointF, Qt -from PyQt5.QtGui import QColor, QIcon, QPainter, QPainterPath, QPixmap, QPolygonF +from PyQt6.QtCore import QPointF, Qt +from PyQt6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPixmap, QPolygonF from novelwriter import SHARED from novelwriter.common import simplified diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index c217f942..002ff811 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -31,7 +31,7 @@ from collections.abc import Iterable, Iterator from pathlib import Path from typing import TYPE_CHECKING, Literal, overload -from PyQt5.QtCore import QModelIndex +from PyQt6.QtCore import QModelIndex from novelwriter import SHARED from novelwriter.constants import nwFiles, nwLabels, trConst diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index a7d921da..eaa91a8c 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -25,8 +25,8 @@ from __future__ import annotations import logging -from PyQt5.QtGui import QCloseEvent, QColor -from PyQt5.QtWidgets import ( +from PyQt6.QtGui import QCloseEvent, QColor +from PyQt6.QtWidgets import ( QDialogButtonBox, QHBoxLayout, QLabel, QTextBrowser, QVBoxLayout, QWidget ) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 8082e8d9..4d1d529f 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -26,8 +26,8 @@ from __future__ import annotations import logging -from PyQt5.QtCore import Qt, pyqtSlot -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import Qt, pyqtSlot +from PyQt6.QtWidgets import ( QAbstractItemView, QDialogButtonBox, QGridLayout, QLabel, QListWidget, QListWidgetItem, QVBoxLayout, QWidget ) diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 80fd6418..259125a6 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -26,8 +26,8 @@ from __future__ import annotations import logging -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtWidgets import ( QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QLabel, QListWidget, QListWidgetItem, QVBoxLayout, QWidget ) diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py index 529caf21..9bc4c2b4 100644 --- a/novelwriter/dialogs/editlabel.py +++ b/novelwriter/dialogs/editlabel.py @@ -25,7 +25,7 @@ from __future__ import annotations import logging -from PyQt5.QtWidgets import QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget +from PyQt6.QtWidgets import QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget from novelwriter import CONFIG from novelwriter.extensions.modified import NDialog diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index a272dab5..8e3d65f1 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -26,9 +26,9 @@ from __future__ import annotations import logging -from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QKeyEvent, QKeySequence -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QCloseEvent, QKeyEvent, QKeySequence +from PyQt6.QtWidgets import ( QCompleter, QDialogButtonBox, QFileDialog, QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget ) diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index e9572157..222a8c77 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -29,9 +29,9 @@ import logging from pathlib import Path -from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QColor -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QCloseEvent, QColor +from PyQt6.QtWidgets import ( QAbstractItemView, QApplication, QColorDialog, QDialogButtonBox, QFileDialog, QGridLayout, QHBoxLayout, QLineEdit, QMenu, QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index 2d5c489e..373f8ca5 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -25,9 +25,9 @@ from __future__ import annotations import logging -from PyQt5.QtCore import QSize, pyqtSlot -from PyQt5.QtGui import QFontMetrics -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import QSize, pyqtSlot +from PyQt6.QtGui import QFontMetrics +from PyQt6.QtWidgets import ( QDialogButtonBox, QFrame, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QVBoxLayout, QWidget ) diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 5ecd8c1d..1988b79b 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -27,9 +27,9 @@ import logging from pathlib import Path -from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QCloseEvent -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QCloseEvent +from PyQt6.QtWidgets import ( QAbstractItemView, QApplication, QDialogButtonBox, QFileDialog, QHBoxLayout, QLineEdit, QListWidget, QVBoxLayout, QWidget ) diff --git a/novelwriter/error.py b/novelwriter/error.py index bdca704b..6f272c0b 100644 --- a/novelwriter/error.py +++ b/novelwriter/error.py @@ -29,9 +29,9 @@ import sys from typing import TYPE_CHECKING -from PyQt5.QtCore import Qt, pyqtSlot -from PyQt5.QtGui import QFont, QFontDatabase -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import Qt, pyqtSlot +from PyQt6.QtGui import QFont, QFontDatabase +from PyQt6.QtWidgets import ( QApplication, QDialog, QDialogButtonBox, QGridLayout, QLabel, QPlainTextEdit, QStyle, QWidget ) @@ -118,7 +118,7 @@ class NWErrorMessage(QDialog): """ from traceback import format_tb - from PyQt5.QtCore import PYQT_VERSION_STR, QT_VERSION_STR, QSysInfo + from PyQt6.QtCore import PYQT_VERSION_STR, QT_VERSION_STR, QSysInfo from novelwriter import __version__ from novelwriter.constants import nwConst @@ -174,7 +174,7 @@ def exceptionHandler(exType: type, exValue: BaseException, exTrace: TracebackTyp """Function to catch unhandled global exceptions.""" from traceback import print_tb - from PyQt5.QtWidgets import QApplication + from PyQt6.QtWidgets import QApplication logger.critical("%s: %s", exType.__name__, str(exValue)) print_tb(exTrace) diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 6582612a..47b74175 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -27,8 +27,8 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt5.QtGui import QColor, QFont, QPalette, QPixmap -from PyQt5.QtWidgets import ( +from PyQt6.QtGui import QColor, QFont, QPalette, QPixmap +from PyQt6.QtWidgets import ( QAbstractButton, QFrame, QHBoxLayout, QLabel, QLayout, QScrollArea, QVBoxLayout, QWidget ) diff --git a/novelwriter/extensions/eventfilters.py b/novelwriter/extensions/eventfilters.py index 3c2e2aa8..f305db6c 100644 --- a/novelwriter/extensions/eventfilters.py +++ b/novelwriter/extensions/eventfilters.py @@ -24,9 +24,9 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt5.QtCore import QEvent, QObject -from PyQt5.QtGui import QStatusTipEvent, QWheelEvent -from PyQt5.QtWidgets import QWidget +from PyQt6.QtCore import QEvent, QObject +from PyQt6.QtGui import QStatusTipEvent, QWheelEvent +from PyQt6.QtWidgets import QWidget class WheelEventFilter(QObject): diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index 5471da29..67aa7453 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -30,9 +30,9 @@ from __future__ import annotations from enum import Enum from typing import TYPE_CHECKING -from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QMouseEvent, QWheelEvent -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import QSize, Qt, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QMouseEvent, QWheelEvent +from PyQt6.QtWidgets import ( QApplication, QComboBox, QDialog, QDoubleSpinBox, QLabel, QSpinBox, QToolButton, QWidget ) diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py index b604bd22..f568f4ea 100644 --- a/novelwriter/extensions/novelselector.py +++ b/novelwriter/extensions/novelselector.py @@ -25,8 +25,8 @@ from __future__ import annotations import logging -from PyQt5.QtCore import pyqtSignal, pyqtSlot -from PyQt5.QtWidgets import QComboBox, QWidget +from PyQt6.QtCore import pyqtSignal, pyqtSlot +from PyQt6.QtWidgets import QComboBox, QWidget from novelwriter import SHARED from novelwriter.constants import nwLabels diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index 319cfaaf..6dd835f3 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -25,11 +25,11 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt5.QtCore import QPoint, QRectF, QSize, Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QColor, QPainter, QPaintEvent, QPolygon -from PyQt5.QtWidgets import ( - QAbstractButton, QAction, QButtonGroup, QLabel, QStyle, - QStyleOptionToolButton, QToolBar, QToolButton, QWidget +from PyQt6.QtCore import QPoint, QRectF, QSize, Qt, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QAction, QColor, QPainter, QPaintEvent, QPolygon +from PyQt6.QtWidgets import ( + QAbstractButton, QButtonGroup, QLabel, QStyle, QStyleOptionToolButton, + QToolBar, QToolButton, QWidget ) from novelwriter.types import ( diff --git a/novelwriter/extensions/progressbars.py b/novelwriter/extensions/progressbars.py index 1c7684fb..358501ac 100644 --- a/novelwriter/extensions/progressbars.py +++ b/novelwriter/extensions/progressbars.py @@ -26,9 +26,9 @@ from __future__ import annotations from math import ceil -from PyQt5.QtCore import QRect -from PyQt5.QtGui import QBrush, QColor, QPainter, QPaintEvent, QPen -from PyQt5.QtWidgets import QProgressBar, QWidget +from PyQt6.QtCore import QRect +from PyQt6.QtGui import QBrush, QColor, QPainter, QPaintEvent, QPen +from PyQt6.QtWidgets import QProgressBar, QWidget from novelwriter.types import ( QtAlignCenter, QtPaintAntiAlias, QtRoundCap, QtSizeFixed, QtSolidLine, diff --git a/novelwriter/extensions/statusled.py b/novelwriter/extensions/statusled.py index e9a88104..f4377823 100644 --- a/novelwriter/extensions/statusled.py +++ b/novelwriter/extensions/statusled.py @@ -25,8 +25,8 @@ from __future__ import annotations import logging -from PyQt5.QtGui import QColor, QPainter, QPaintEvent -from PyQt5.QtWidgets import QAbstractButton, QWidget +from PyQt6.QtGui import QColor, QPainter, QPaintEvent +from PyQt6.QtWidgets import QAbstractButton, QWidget from novelwriter import CONFIG from novelwriter.enum import nwTrinary diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py index 43d60fc6..7b8c401d 100644 --- a/novelwriter/extensions/switch.py +++ b/novelwriter/extensions/switch.py @@ -23,13 +23,15 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt5.QtCore import QEvent, QPropertyAnimation, Qt, pyqtProperty -from PyQt5.QtGui import QMouseEvent, QPainter, QPaintEvent, QResizeEvent -from PyQt5.QtWidgets import QAbstractButton, QWidget +from PyQt6.QtCore import QByteArray, QPropertyAnimation, Qt +from PyQt6.QtGui import QEnterEvent, QMouseEvent, QPainter, QPaintEvent, QResizeEvent +from PyQt6.QtWidgets import QAbstractButton, QWidget from novelwriter import CONFIG, SHARED from novelwriter.types import QtMouseLeft, QtNoPen, QtPaintAntiAlias, QtSizeFixed +OFFSET = QByteArray(b"offset") # type: ignore + class NSwitch(QAbstractButton): @@ -57,7 +59,7 @@ class NSwitch(QAbstractButton): # Properties ## - @pyqtProperty(int) # type: ignore + @property def offset(self) -> int: # type: ignore return self._offset @@ -121,14 +123,14 @@ class NSwitch(QAbstractButton): """Animate the switch on mouse release.""" super().mouseReleaseEvent(event) if event.button() == QtMouseLeft: - anim = QPropertyAnimation(self, b"offset", self) + anim = QPropertyAnimation(self, OFFSET, self) anim.setDuration(120) anim.setStartValue(self._offset) anim.setEndValue((self._xW - self._xR) if self.isChecked() else self._xR) anim.start() return - def enterEvent(self, event: QEvent) -> None: + def enterEvent(self, event: QEnterEvent) -> None: """Change the cursor when hovering the button.""" self.setCursor(Qt.CursorShape.PointingHandCursor) super().enterEvent(event) diff --git a/novelwriter/extensions/switchbox.py b/novelwriter/extensions/switchbox.py index ac11d083..9acc0cc9 100644 --- a/novelwriter/extensions/switchbox.py +++ b/novelwriter/extensions/switchbox.py @@ -23,9 +23,9 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt5.QtCore import pyqtSignal -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QGridLayout, QLabel, QScrollArea, QWidget +from PyQt6.QtCore import pyqtSignal +from PyQt6.QtGui import QIcon +from PyQt6.QtWidgets import QGridLayout, QLabel, QScrollArea, QWidget from novelwriter.extensions.switch import NSwitch from novelwriter.types import ( diff --git a/novelwriter/extensions/versioninfo.py b/novelwriter/extensions/versioninfo.py index 22ecd251..c44ce820 100644 --- a/novelwriter/extensions/versioninfo.py +++ b/novelwriter/extensions/versioninfo.py @@ -31,9 +31,9 @@ from time import sleep from urllib.error import HTTPError from urllib.request import Request, urlopen -from PyQt5.QtCore import QObject, QRunnable, QUrl, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QDesktopServices -from PyQt5.QtWidgets import QLabel, QVBoxLayout, QWidget +from PyQt6.QtCore import QObject, QRunnable, QUrl, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QDesktopServices +from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget from novelwriter import CONFIG, SHARED, __date__, __domain__, __version__ from novelwriter.common import formatVersion diff --git a/novelwriter/formats/shared.py b/novelwriter/formats/shared.py index b1c0f288..549f6f19 100644 --- a/novelwriter/formats/shared.py +++ b/novelwriter/formats/shared.py @@ -27,7 +27,7 @@ import re from enum import Flag, IntEnum -from PyQt5.QtGui import QColor +from PyQt6.QtGui import QColor ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""} RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL) diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 694ffac0..f3df8a31 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -33,8 +33,8 @@ from pathlib import Path from typing import NamedTuple from zipfile import ZIP_DEFLATED, ZipFile -from PyQt5.QtCore import QMargins, QSize -from PyQt5.QtGui import QColor +from PyQt6.QtCore import QMargins, QSize +from PyQt6.QtGui import QColor from novelwriter import __version__ from novelwriter.common import firstFloat, xmlElement, xmlSubElem diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 70d1de0c..b254400b 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -31,8 +31,8 @@ from abc import ABC, abstractmethod from pathlib import Path from typing import NamedTuple -from PyQt5.QtCore import QLocale -from PyQt5.QtGui import QColor, QFont +from PyQt6.QtCore import QLocale +from PyQt6.QtGui import QColor, QFont from novelwriter import CONFIG from novelwriter.common import checkInt, fontMatcher, numberToRoman diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index 442b8284..347c1235 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -35,7 +35,7 @@ from hashlib import sha256 from pathlib import Path from zipfile import ZIP_DEFLATED, ZipFile -from PyQt5.QtGui import QColor, QFont +from PyQt6.QtGui import QColor, QFont from novelwriter import __version__ from novelwriter.common import xmlElement, xmlIndent, xmlSubElem diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index 8d9c8f6e..13647988 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -27,12 +27,12 @@ import logging from pathlib import Path -from PyQt5.QtCore import QMarginsF, QSizeF -from PyQt5.QtGui import ( - QColor, QFont, QFontDatabase, QPageSize, QTextBlockFormat, QTextCharFormat, - QTextCursor, QTextDocument, QTextFrameFormat +from PyQt6.QtCore import QMarginsF, QSizeF +from PyQt6.QtGui import ( + QColor, QFont, QFontDatabase, QPageLayout, QPageSize, QTextBlockFormat, + QTextCharFormat, QTextCursor, QTextDocument, QTextFrameFormat ) -from PyQt5.QtPrintSupport import QPrinter +from PyQt6.QtPrintSupport import QPrinter from novelwriter import __version__ from novelwriter.constants import nwStyles, nwUnicode @@ -129,10 +129,9 @@ class ToQTextDocument(Tokenizer): super().initDocument() if pdf: - fontDB = QFontDatabase() family = self._textFont.family() style = self._textFont.styleName() - self._dpi = 1200 if fontDB.isScalable(family, style) else 72 + self._dpi = 1200 if QFontDatabase.isScalable(family, style) else 72 self._document.setUndoRedoEnabled(False) self._document.blockSignals(True) @@ -268,7 +267,6 @@ class ToQTextDocument(Tokenizer): def saveDocument(self, path: Path) -> None: """Save the document as a PDF file.""" - m = self._pageMargins logger.info("Writing PDF at %d DPI", self._dpi) printer = QPrinter(QPrinter.PrinterMode.HighResolution) @@ -277,11 +275,11 @@ class ToQTextDocument(Tokenizer): printer.setResolution(self._dpi) printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat) printer.setPageSize(self._pageSize) - printer.setPageMargins(m.left(), m.top(), m.right(), m.bottom(), QPrinter.Unit.Millimeter) + printer.setPageMargins(self._pageMargins, QPageLayout.Unit.Millimeter) printer.setOutputFileName(str(path)) self._document.documentLayout().setPaintDevice(printer) - self._document.setPageSize(QSizeF(printer.pageRect().size())) + self._document.setPageSize(printer.pageRect(QPrinter.Unit.Millimeter).size()) self._document.print(printer) return diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index e7abcdb2..70ab48ec 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -37,18 +37,18 @@ import logging from enum import Enum from time import time -from PyQt5.QtCore import ( +from PyQt6.QtCore import ( QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal, pyqtSlot ) -from PyQt5.QtGui import ( - QColor, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent, - QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent, QTextBlock, - QTextCursor, QTextDocument, QTextOption +from PyQt6.QtGui import ( + QAction, QColor, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, + QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent, + QShortcut, QTextBlock, QTextCursor, QTextDocument, QTextOption ) -from PyQt5.QtWidgets import ( - QAction, QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, - QMenu, QPlainTextEdit, QShortcut, QToolBar, QVBoxLayout, QWidget +from PyQt6.QtWidgets import ( + QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu, + QPlainTextEdit, QToolBar, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index b6f01928..c20c3ecf 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -29,8 +29,8 @@ import re from time import time -from PyQt5.QtCore import Qt -from PyQt5.QtGui import ( +from PyQt6.QtCore import Qt +from PyQt6.QtGui import ( QBrush, QColor, QFont, QSyntaxHighlighter, QTextBlockUserData, QTextCharFormat, QTextDocument ) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 6bda315a..f20a9840 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -30,14 +30,14 @@ import logging from enum import Enum -from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot -from PyQt5.QtGui import ( - QCursor, QDesktopServices, QDragEnterEvent, QDragMoveEvent, QDropEvent, - QMouseEvent, QPalette, QResizeEvent, QTextCursor +from PyQt6.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot +from PyQt6.QtGui import ( + QAction, QCursor, QDesktopServices, QDragEnterEvent, QDragMoveEvent, + QDropEvent, QMouseEvent, QPalette, QResizeEvent, QTextCursor ) -from PyQt5.QtWidgets import ( - QAction, QApplication, QFrame, QHBoxLayout, QMenu, QTextBrowser, - QToolButton, QWidget +from PyQt6.QtWidgets import ( + QApplication, QFrame, QHBoxLayout, QMenu, QTextBrowser, QToolButton, + QWidget ) from novelwriter import CONFIG, SHARED diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py index 7bcb52b5..91723b91 100644 --- a/novelwriter/gui/docviewerpanel.py +++ b/novelwriter/gui/docviewerpanel.py @@ -27,8 +27,8 @@ import logging from enum import Enum -from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, pyqtSlot -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal, pyqtSlot +from PyQt6.QtWidgets import ( QAbstractItemView, QFrame, QMenu, QTabWidget, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) diff --git a/novelwriter/gui/editordocument.py b/novelwriter/gui/editordocument.py index e46c01b5..79754656 100644 --- a/novelwriter/gui/editordocument.py +++ b/novelwriter/gui/editordocument.py @@ -28,9 +28,9 @@ import logging from collections.abc import Iterable from time import time -from PyQt5.QtCore import QObject, pyqtSlot -from PyQt5.QtGui import QTextBlock, QTextCursor, QTextDocument -from PyQt5.QtWidgets import QApplication, QPlainTextDocumentLayout +from PyQt6.QtCore import QObject, pyqtSlot +from PyQt6.QtGui import QTextBlock, QTextCursor, QTextDocument +from PyQt6.QtWidgets import QApplication, QPlainTextDocumentLayout from novelwriter import SHARED from novelwriter.gui.dochighlight import GuiDocHighlighter, TextBlockData diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index d6435bdf..8cc264b7 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -27,8 +27,8 @@ import logging from enum import Enum -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtWidgets import QGridLayout, QLabel, QWidget +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtWidgets import QGridLayout, QLabel, QWidget from novelwriter import CONFIG, SHARED from novelwriter.common import elide diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 02527c2b..42d7048b 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -28,8 +28,9 @@ import logging from pathlib import Path from typing import TYPE_CHECKING -from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt5.QtWidgets import QAction, QMenuBar +from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QAction +from PyQt6.QtWidgets import QMenuBar from novelwriter import CONFIG, SHARED from novelwriter.common import openExternalPath, qtLambda diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 190bcafd..e2e321c5 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -30,11 +30,11 @@ import logging from enum import Enum from time import time -from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QFocusEvent, QFont, QMouseEvent, QPalette, QResizeEvent -from PyQt5.QtWidgets import ( - QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QInputDialog, QMenu, - QToolTip, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget +from PyQt6.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QActionGroup, QFocusEvent, QFont, QMouseEvent, QPalette, QResizeEvent +from PyQt6.QtWidgets import ( + QAbstractItemView, QFrame, QHBoxLayout, QInputDialog, QMenu, QToolTip, + QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 9250f06c..a237e5fd 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -33,10 +33,10 @@ import logging from enum import Enum from time import time -from PyQt5.QtCore import QT_TRANSLATE_NOOP, Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import ( - QAbstractItemView, QAction, QFileDialog, QFrame, QGridLayout, QGroupBox, +from PyQt6.QtCore import QT_TRANSLATE_NOOP, Qt, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QAction, QIcon +from PyQt6.QtWidgets import ( + QAbstractItemView, QFileDialog, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QMenu, QScrollArea, QSplitter, QToolBar, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index b45f8f24..6acaa264 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -32,10 +32,10 @@ import logging from enum import Enum -from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QIcon, QMouseEvent, QPainter, QPalette -from PyQt5.QtWidgets import ( - QAbstractItemView, QAction, QFrame, QHBoxLayout, QLabel, QMenu, QShortcut, +from PyQt6.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QAction, QIcon, QMouseEvent, QPainter, QPalette, QShortcut +from PyQt6.QtWidgets import ( + QAbstractItemView, QFrame, QHBoxLayout, QLabel, QMenu, QStyleOptionViewItem, QTreeView, QVBoxLayout, QWidget ) diff --git a/novelwriter/gui/search.py b/novelwriter/gui/search.py index 10184d29..78f06838 100644 --- a/novelwriter/gui/search.py +++ b/novelwriter/gui/search.py @@ -27,9 +27,9 @@ import logging from time import time -from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QCursor, QKeyEvent -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QCursor, QKeyEvent +from PyQt6.QtWidgets import ( QApplication, QFrame, QHBoxLayout, QLabel, QLineEdit, QToolBar, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index 8baaaa48..cf9d26bf 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -27,9 +27,9 @@ import logging from typing import TYPE_CHECKING -from PyQt5.QtCore import QEvent, QPoint, QSize, pyqtSignal -from PyQt5.QtGui import QPalette -from PyQt5.QtWidgets import QMenu, QVBoxLayout, QWidget +from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal +from PyQt6.QtGui import QPalette +from PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget from novelwriter import CONFIG, SHARED from novelwriter.common import qtLambda diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index a2fe5b9f..413b93df 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -28,8 +28,8 @@ import logging from datetime import datetime from time import time -from PyQt5.QtCore import QLocale, pyqtSlot -from PyQt5.QtWidgets import QApplication, QLabel, QStatusBar, QWidget +from PyQt6.QtCore import QLocale, pyqtSlot +from PyQt6.QtWidgets import QApplication, QLabel, QStatusBar, QWidget from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 6fd4f198..9093bb8f 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -29,12 +29,12 @@ import logging from math import ceil from pathlib import Path -from PyQt5.QtCore import QSize, Qt -from PyQt5.QtGui import ( +from PyQt6.QtCore import QSize, Qt +from PyQt6.QtGui import ( QColor, QFont, QFontDatabase, QFontMetrics, QIcon, QPainter, QPainterPath, QPalette, QPixmap ) -from PyQt5.QtWidgets import QApplication +from PyQt6.QtWidgets import QApplication from novelwriter import CONFIG from novelwriter.common import NWConfigParser, cssCol, minmax diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index f7aef529..12d8a971 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -30,11 +30,11 @@ from datetime import datetime from pathlib import Path from time import time -from PyQt5.QtCore import Qt, QTimer, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QCursor, QIcon -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import Qt, QTimer, pyqtSlot +from PyQt6.QtGui import QCloseEvent, QCursor, QIcon, QShortcut +from PyQt6.QtWidgets import ( QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, - QShortcut, QSplitter, QStackedWidget, QVBoxLayout, QWidget + QSplitter, QStackedWidget, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED, __hexversion__, __version__ diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 86ef6eca..2fd5359c 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -31,9 +31,9 @@ from pathlib import Path from time import time from typing import TYPE_CHECKING, TypeVar -from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QDesktopServices, QFont -from PyQt5.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget +from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QDesktopServices, QFont +from PyQt6.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget from novelwriter.common import formatFileFilter from novelwriter.constants import nwFiles diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py index 057637af..ddb371e1 100644 --- a/novelwriter/tools/dictionaries.py +++ b/novelwriter/tools/dictionaries.py @@ -28,9 +28,9 @@ import logging from pathlib import Path from zipfile import ZipFile -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtGui import QCloseEvent, QTextCursor -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtGui import QCloseEvent, QTextCursor +from PyQt6.QtWidgets import ( QApplication, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, QLabel, QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget ) diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index df02d82d..f9144d49 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -26,8 +26,8 @@ from __future__ import annotations import logging import random -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtWidgets import ( QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, QSpinBox, QVBoxLayout, QWidget ) diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index b636ac3c..40e6eefc 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -27,9 +27,9 @@ import logging from pathlib import Path -from PyQt5.QtCore import QTimer, pyqtSlot -from PyQt5.QtGui import QCloseEvent -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import QTimer, pyqtSlot +from PyQt6.QtGui import QCloseEvent +from PyQt6.QtWidgets import ( QAbstractButton, QAbstractItemView, QDialogButtonBox, QFileDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QPushButton, QSplitter, QVBoxLayout, QWidget diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 07501af1..7238ea84 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -28,13 +28,13 @@ import logging from time import time from typing import TYPE_CHECKING -from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal, pyqtSlot -from PyQt5.QtGui import ( - QCloseEvent, QColor, QCursor, QDesktopServices, QFont, QPalette, - QResizeEvent, QTextDocument +from PyQt6.QtCore import Qt, QTimer, QUrl, pyqtSignal, pyqtSlot +from PyQt6.QtGui import ( + QCloseEvent, QColor, QCursor, QDesktopServices, QFont, QPageLayout, + QPalette, QResizeEvent, QTextDocument ) -from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog -from PyQt5.QtWidgets import ( +from PyQt6.QtPrintSupport import QPrinter, QPrintPreviewDialog +from PyQt6.QtWidgets import ( QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, QSplitter, QStackedWidget, QTabWidget, QTextBrowser, QTreeWidget, QTreeWidgetItem, @@ -42,7 +42,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import fuzzyTime +from novelwriter.common import fuzzyTime, qtLambda from novelwriter.constants import nwLabels, nwStats, trConst from novelwriter.core.buildsettings import BuildCollection, BuildSettings from novelwriter.core.docbuild import NWBuildDocument @@ -185,7 +185,7 @@ class GuiManuscript(NToolDialog): self.btnBuild.clicked.connect(self._buildManuscript) self.btnClose = QPushButton(self.tr("Close"), self) - self.btnClose.clicked.connect(self.close) + self.btnClose.clicked.connect(qtLambda(self.close)) self.processBox = QGridLayout() self.processBox.addWidget(self.btnPreview, 0, 0) @@ -884,7 +884,7 @@ class _PreviewWidget(QTextBrowser): def printPreview(self, printer: QPrinter) -> None: """Connect the print preview painter to the document viewer.""" QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) - printer.setOrientation(QPrinter.Orientation.Portrait) + printer.setPageOrientation(QPageLayout.Orientation.Portrait) self.document().print(printer) QApplication.restoreOverrideCursor() return diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index c0202e2f..6e0738e3 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -27,9 +27,9 @@ import logging from typing import TYPE_CHECKING -from PyQt5.QtCore import QEvent, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import QEvent, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument +from PyQt6.QtWidgets import ( QAbstractButton, QAbstractItemView, QDialogButtonBox, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu, QPlainTextEdit, QPushButton, QSplitter, QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index 175462ab..a71539e5 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -26,9 +26,9 @@ from __future__ import annotations import logging import math -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtGui import QCloseEvent -from PyQt5.QtWidgets import ( +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtGui import QCloseEvent +from PyQt6.QtWidgets import ( QAbstractItemView, QDialogButtonBox, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QSpinBox, QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index 85482cfe..885a7da7 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -28,20 +28,19 @@ import logging from datetime import datetime from pathlib import Path -from PyQt5.QtCore import ( +from PyQt6.QtCore import ( QAbstractListModel, QEvent, QModelIndex, QObject, QPoint, QSize, Qt, pyqtSignal, pyqtSlot ) -from PyQt5.QtGui import QCloseEvent, QColor, QFont, QPainter, QPaintEvent, QPen -from PyQt5.QtWidgets import ( - QAction, QApplication, QFileDialog, QFormLayout, QHBoxLayout, QLabel, - QLineEdit, QListView, QMenu, QPushButton, QScrollArea, QShortcut, - QStackedWidget, QStyledItemDelegate, QStyleOptionViewItem, QVBoxLayout, - QWidget +from PyQt6.QtGui import QAction, QCloseEvent, QColor, QFont, QPainter, QPaintEvent, QPen, QShortcut +from PyQt6.QtWidgets import ( + QApplication, QFileDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit, + QListView, QMenu, QPushButton, QScrollArea, QStackedWidget, + QStyledItemDelegate, QStyleOptionViewItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED -from novelwriter.common import cssCol, formatInt, makeFileNameSafe +from novelwriter.common import cssCol, formatInt, makeFileNameSafe, qtLambda from novelwriter.constants import nwFiles from novelwriter.core.coretools import ProjectBuilder from novelwriter.enum import nwItemClass @@ -127,7 +126,7 @@ class GuiWelcome(NDialog): self.btnCancel = QPushButton(self.tr("Cancel"), self) self.btnCancel.setIcon(SHARED.theme.getIcon("cancel", "red")) self.btnCancel.setIconSize(btnIconSize) - self.btnCancel.clicked.connect(self.close) + self.btnCancel.clicked.connect(qtLambda(self.close)) self.btnCreate = QPushButton(self.tr("Create"), self) self.btnCreate.setIcon(SHARED.theme.getIcon("star", "yellow")) diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index 69fae080..bc59ab62 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -29,12 +29,11 @@ import logging from datetime import datetime from typing import TYPE_CHECKING -from PyQt5.QtCore import Qt, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QCursor, QPixmap -from PyQt5.QtWidgets import ( - QAction, QApplication, QDialogButtonBox, QFileDialog, QGridLayout, - QGroupBox, QHBoxLayout, QLabel, QMenu, QSpinBox, QTreeWidget, - QTreeWidgetItem +from PyQt6.QtCore import Qt, pyqtSlot +from PyQt6.QtGui import QAction, QCloseEvent, QCursor, QPixmap +from PyQt6.QtWidgets import ( + QApplication, QDialogButtonBox, QFileDialog, QGridLayout, QGroupBox, + QHBoxLayout, QLabel, QMenu, QSpinBox, QTreeWidget, QTreeWidgetItem ) from novelwriter import CONFIG, SHARED @@ -124,13 +123,12 @@ class GuiWritingStats(NToolDialog): hHeader.setTextAlignment(self.C_IDLE, QtAlignRight) hHeader.setTextAlignment(self.C_COUNT, QtAlignRight) - sDec = Qt.SortOrder.DescendingOrder - sAsc = Qt.SortOrder.AscendingOrder sortCol = minmax(pOptions.getInt("GuiWritingStats", "sortCol", 0), 0, 2) sortOrder = checkIntTuple( - pOptions.getInt("GuiWritingStats", "sortOrder", sDec), (sAsc, sDec), sDec + pOptions.getInt("GuiWritingStats", "sortOrder", 1), (0, 1), 1 ) - self.listBox.sortByColumn(sortCol, sortOrder) # type: ignore + sortOrders = (Qt.SortOrder.AscendingOrder, Qt.SortOrder.DescendingOrder) + self.listBox.sortByColumn(sortCol, sortOrders[sortOrder]) self.listBox.setSortingEnabled(True) # Word Bar diff --git a/novelwriter/types.py b/novelwriter/types.py index 0732f5a8..23455b97 100644 --- a/novelwriter/types.py +++ b/novelwriter/types.py @@ -23,12 +23,12 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt5.QtCore import Qt -from PyQt5.QtGui import ( +from PyQt6.QtCore import Qt +from PyQt6.QtGui import ( QColor, QFont, QPainter, QTextBlockFormat, QTextCharFormat, QTextCursor, QTextFormat ) -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QHeaderView, QSizePolicy, QStyle +from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QHeaderView, QSizePolicy, QStyle # Qt Alignment Flags @@ -56,7 +56,7 @@ QtVAlignSuper = QTextCharFormat.VerticalAlignment.AlignSuperScript QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore QtPageBreakAfter = QTextFormat.PageBreakFlag.PageBreak_AlwaysAfter -QtPropLineHeight = QTextBlockFormat.LineHeightTypes.ProportionalHeight +QtPropLineHeight = 1 # QTextBlockFormat.LineHeightTypes.ProportionalHeight # Qt Painter Types From da036efdcf1b03a219ac89ea787f0c33c2124aa7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jan 2025 20:36:42 +0100 Subject: [PATCH 02/13] Switch test code to PyQt6 --- novelwriter/__init__.py | 30 +++++++++---------- pytest.ini | 4 +-- tests/conftest.py | 4 +-- tests/mocked.py | 4 +-- tests/test_base/test_base_common.py | 14 ++++----- tests/test_base/test_base_error.py | 16 +++++----- tests/test_base/test_base_init.py | 27 ++++++++--------- tests/test_base/test_base_shared.py | 6 ++-- tests/test_core/test_core_item.py | 2 +- tests/test_core/test_core_itemmodel.py | 2 +- tests/test_core/test_core_project.py | 2 +- tests/test_core/test_core_projectxml.py | 2 +- tests/test_core/test_core_status.py | 2 +- tests/test_dialogs/test_dlg_about.py | 3 +- tests/test_dialogs/test_dlg_dialogs.py | 4 +-- tests/test_dialogs/test_dlg_docmerge.py | 2 +- tests/test_dialogs/test_dlg_preferences.py | 6 ++-- .../test_dialogs/test_dlg_projectsettings.py | 4 +-- tests/test_dialogs/test_dlg_wordlist.py | 9 +++--- tests/test_ext/test_ext_eventfilters.py | 9 +++--- tests/test_ext/test_ext_modified.py | 8 ++--- tests/test_ext/test_ext_progressbars.py | 2 +- tests/test_ext/test_ext_switch.py | 8 ++--- tests/test_ext/test_ext_versioninfo.py | 2 +- tests/test_formats/test_fmt_tokenizer.py | 2 +- tests/test_formats/test_fmt_toodt.py | 2 +- tests/test_formats/test_fmt_toqdoc.py | 2 +- tests/test_gui/test_gui_doceditor.py | 23 +++++++------- tests/test_gui/test_gui_docviewer.py | 30 ++++++++++--------- tests/test_gui/test_gui_docviewerpanel.py | 2 +- tests/test_gui/test_gui_guimain.py | 13 ++++---- tests/test_gui/test_gui_i18n.py | 2 +- tests/test_gui/test_gui_mainmenu.py | 8 ++--- tests/test_gui/test_gui_noveltree.py | 8 ++--- tests/test_gui/test_gui_outline.py | 3 +- tests/test_gui/test_gui_projtree.py | 10 +++---- tests/test_gui/test_gui_search.py | 4 +-- tests/test_gui/test_gui_theme.py | 4 +-- tests/test_tools/test_tools_dictionaries.py | 4 +-- tests/test_tools/test_tools_lipsum.py | 2 +- tests/test_tools/test_tools_manusbuild.py | 6 ++-- tests/test_tools/test_tools_manuscript.py | 8 ++--- tests/test_tools/test_tools_manussettings.py | 6 ++-- tests/test_tools/test_tools_noveldetails.py | 2 +- tests/test_tools/test_tools_welcome.py | 6 ++-- tests/test_tools/test_tools_writingstats.py | 8 +++-- tests/tools.py | 2 +- 47 files changed, 169 insertions(+), 160 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 62305d6d..a7fd61f5 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -201,21 +201,21 @@ def main(sysArgs: list | None = None) -> GuiMain | None: # Check Packages and Versions errorData = [] errorCode = 0 - # if sys.hexversion < 0x030a00f0: - # errorData.append( - # "At least Python 3.10 is required, found %s" % CONFIG.verPyString - # ) - # errorCode |= 0x04 - # if CONFIG.verQtValue < 0x060000: - # errorData.append( - # "At least Qt6 version 6.0 is required, found %s" % CONFIG.verQtString - # ) - # errorCode |= 0x08 - # if CONFIG.verPyQtValue < 0x060000: - # errorData.append( - # "At least PyQt6 version 6.0 is required, found %s" % CONFIG.verPyQtString - # ) - # errorCode |= 0x10 + if sys.hexversion < 0x030a00f0: + errorData.append( + "At least Python 3.10 is required, found %s" % CONFIG.verPyString + ) + errorCode |= 0x04 + if CONFIG.verQtValue < 0x060000: + errorData.append( + "At least Qt6 version 6.0 is required, found %s" % CONFIG.verQtString + ) + errorCode |= 0x08 + if CONFIG.verPyQtValue < 0x060000: + errorData.append( + "At least PyQt6 version 6.0 is required, found %s" % CONFIG.verPyQtString + ) + errorCode |= 0x10 if errorData: errApp = QApplication([]) diff --git a/pytest.ini b/pytest.ini index 641984a8..dbc7a006 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,8 +1,8 @@ [pytest] log_level = DEBUG -qt_api = pyqt5 +qt_api = pyqt6 markers = base: Base classes tests core: Core classes tests - gui: Qt5 GUI tests + gui: GUI classes tests serial diff --git a/tests/conftest.py b/tests/conftest.py index 8209f8b6..1097ae6c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,8 +28,8 @@ from pathlib import Path import pytest -from PyQt5.QtCore import QLocale -from PyQt5.QtWidgets import QMessageBox +from PyQt6.QtCore import QLocale +from PyQt6.QtWidgets import QMessageBox sys.path.insert(1, str(Path(__file__).parent.parent.absolute())) diff --git a/tests/mocked.py b/tests/mocked.py index 8778f0cb..b7edeaf9 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -22,8 +22,8 @@ from __future__ import annotations from unittest.mock import MagicMock -from PyQt5.QtGui import QFont, QIcon, QPixmap -from PyQt5.QtWidgets import QWidget +from PyQt6.QtGui import QFont, QIcon, QPixmap +from PyQt6.QtWidgets import QWidget class MockGuiMain(QWidget): diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index fea36070..5bfe2723 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -27,8 +27,8 @@ from xml.etree import ElementTree as ET import pytest -from PyQt5.QtCore import QMimeData, QUrl -from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo +from PyQt6.QtCore import QMimeData, QUrl +from PyQt6.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo from novelwriter.common import ( NWConfigParser, checkBool, checkFloat, checkInt, checkIntTuple, checkPath, @@ -522,8 +522,7 @@ def testBaseCommon_cssCol(): @pytest.mark.base def testBaseCommon_describeFont(): """Test the describeFont function.""" - fontDB = QFontDatabase() - font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont) + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) font.setPointSize(12) assert describeFont(font).startswith("12 pt") assert describeFont(None) == "Error" # type: ignore @@ -537,10 +536,9 @@ def testBaseCommon_fontMatcher(monkeypatch): assert fontMatcher(nonsense) is nonsense # General font - fontDB = QFontDatabase() - if len(fontDB.families()) > 1: - fontOne = QFont(fontDB.families()[0]) - fontTwo = QFont(fontDB.families()[1]) + if len(QFontDatabase.families()) > 1: + fontOne = QFont(QFontDatabase.families()[0]) + fontTwo = QFont(QFontDatabase.families()[1]) check = QFont(fontOne) check.setFamily(fontTwo.family()) with monkeypatch.context() as mp: diff --git a/tests/test_base/test_base_error.py b/tests/test_base/test_base_error.py index 97e4f714..65499666 100644 --- a/tests/test_base/test_base_error.py +++ b/tests/test_base/test_base_error.py @@ -42,7 +42,7 @@ def testBaseError_Dialog(qtbot, monkeypatch, nwGUI): # Valid Error Message with monkeypatch.context() as mp: - mp.setattr("PyQt5.QtCore.QSysInfo.kernelVersion", lambda: "1.2.3") + mp.setattr("PyQt6.QtCore.QSysInfo.kernelVersion", lambda: "1.2.3") nwErr.setMessage(Exception, "Fine Error", None) # type: ignore message = nwErr.msgBody.toPlainText() assert message != "" @@ -52,7 +52,7 @@ def testBaseError_Dialog(qtbot, monkeypatch, nwGUI): # No kernel version retrieved with monkeypatch.context() as mp: - mp.setattr("PyQt5.QtCore.QSysInfo.kernelVersion", causeException) + mp.setattr("PyQt6.QtCore.QSysInfo.kernelVersion", causeException) nwErr.setMessage(Exception, "Almost Fine Error", None) # type: ignore message = nwErr.msgBody.toPlainText() assert message != "" @@ -80,27 +80,27 @@ def testBaseError_Handler(qtbot, monkeypatch, nwGUI): # Normal shutdown with monkeypatch.context() as mp: mp.setattr(NWErrorMessage, "exec", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.exit", lambda *a: None) + mp.setattr("PyQt6.QtWidgets.QApplication.exit", lambda *a: None) exceptionHandler(Exception, "Error Message", None) # type: ignore # Should not crash when no GUI is found with monkeypatch.context() as mp: mp.setattr(NWErrorMessage, "exec", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.exit", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.topLevelWidgets", lambda: []) + mp.setattr("PyQt6.QtWidgets.QApplication.exit", lambda *a: None) + mp.setattr("PyQt6.QtWidgets.QApplication.topLevelWidgets", lambda: []) exceptionHandler(Exception, "Error Message", None) # type: ignore # Should handle QApplication failing with monkeypatch.context() as mp: mp.setattr(NWErrorMessage, "exec", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.exit", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.topLevelWidgets", causeException) + mp.setattr("PyQt6.QtWidgets.QApplication.exit", lambda *a: None) + mp.setattr("PyQt6.QtWidgets.QApplication.topLevelWidgets", causeException) exceptionHandler(Exception, "Error Message", None) # type: ignore # Should handle failing to close main GUI with monkeypatch.context() as mp: mp.setattr(NWErrorMessage, "exec", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.exit", lambda *a: None) + mp.setattr("PyQt6.QtWidgets.QApplication.exit", lambda *a: None) mp.setattr("novelwriter.guimain.GuiMain.closeMain", causeException) exceptionHandler(Exception, "Error Message", None) # type: ignore diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py index 868c0278..8bc9bfb4 100644 --- a/tests/test_base/test_base_init.py +++ b/tests/test_base/test_base_init.py @@ -63,13 +63,12 @@ def testBaseInit_Launch(caplog, monkeypatch, fncPath): # Normal Launch with monkeypatch.context() as mp: - mp.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.setApplicationName", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.setApplicationVersion", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.setWindowIcon", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.setOrganizationDomain", lambda *a: None) - mp.setattr("PyQt5.QtWidgets.QApplication.exec", lambda *a: 0) - # mp.setattr("PyQt5.QtWidgets.QApplication.focusChange.connect", lambda *a: None) + mp.setattr("PyQt6.QtWidgets.QApplication.__init__", lambda *a: None) + mp.setattr("PyQt6.QtWidgets.QApplication.setApplicationName", lambda *a: None) + mp.setattr("PyQt6.QtWidgets.QApplication.setApplicationVersion", lambda *a: None) + mp.setattr("PyQt6.QtWidgets.QApplication.setWindowIcon", lambda *a: None) + mp.setattr("PyQt6.QtWidgets.QApplication.setOrganizationDomain", lambda *a: None) + mp.setattr("PyQt6.QtWidgets.QApplication.exec", lambda *a: 0) with pytest.raises(SystemExit) as ex: main([f"--config={fncPath}", f"--data={fncPath}"]) assert ex.value.code == 0 @@ -165,11 +164,11 @@ def testBaseInit_Options(monkeypatch, fncPath): def testBaseInit_Imports(caplog, monkeypatch, fncPath): """Check import error handling.""" monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) - monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None) - monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec", lambda *a: 0) - monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.__init__", lambda *a: None) - monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.resize", lambda *a: None) - monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.showMessage", lambda *a: None) + monkeypatch.setattr("PyQt6.QtWidgets.QApplication.__init__", lambda *a: None) + monkeypatch.setattr("PyQt6.QtWidgets.QApplication.exec", lambda *a: 0) + monkeypatch.setattr("PyQt6.QtWidgets.QErrorMessage.__init__", lambda *a: None) + monkeypatch.setattr("PyQt6.QtWidgets.QErrorMessage.resize", lambda *a: None) + monkeypatch.setattr("PyQt6.QtWidgets.QErrorMessage.showMessage", lambda *a: None) monkeypatch.setattr("sys.hexversion", 0x0) monkeypatch.setattr("novelwriter.CONFIG.verQtValue", 0x050000) monkeypatch.setattr("novelwriter.CONFIG.verPyQtValue", 0x050000) @@ -184,5 +183,5 @@ def testBaseInit_Imports(caplog, monkeypatch, fncPath): assert ex.value.code & 16 == 16 # PyQt version not satisfied # type: ignore assert "At least Python" in caplog.messages[0] - assert "At least Qt5" in caplog.messages[1] - assert "At least PyQt5" in caplog.messages[2] + assert "At least Qt6" in caplog.messages[1] + assert "At least PyQt6" in caplog.messages[2] diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py index 81713b1d..85a8200a 100644 --- a/tests/test_base/test_base_shared.py +++ b/tests/test_base/test_base_shared.py @@ -24,9 +24,9 @@ from unittest.mock import MagicMock import pytest -from PyQt5.QtCore import QUrl -from PyQt5.QtGui import QDesktopServices -from PyQt5.QtWidgets import QFileDialog, QMessageBox, QWidget +from PyQt6.QtCore import QUrl +from PyQt6.QtGui import QDesktopServices +from PyQt6.QtWidgets import QFileDialog, QMessageBox, QWidget from novelwriter.core.project import NWProject from novelwriter.shared import SharedData diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index b6946896..395db6da 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -24,7 +24,7 @@ import copy import pytest -from PyQt5.QtGui import QIcon +from PyQt6.QtGui import QIcon from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject diff --git a/tests/test_core/test_core_itemmodel.py b/tests/test_core/test_core_itemmodel.py index 6c60d9ed..e2033f5e 100644 --- a/tests/test_core/test_core_itemmodel.py +++ b/tests/test_core/test_core_itemmodel.py @@ -22,7 +22,7 @@ from __future__ import annotations import pytest -from PyQt5.QtCore import QMimeData, QModelIndex, Qt +from PyQt6.QtCore import QMimeData, QModelIndex, Qt from novelwriter.common import decodeMimeHandles from novelwriter.constants import nwConst diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 89b18b75..4c8618f3 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -25,7 +25,7 @@ from zipfile import ZipFile import pytest -from PyQt5.QtWidgets import QMessageBox +from PyQt6.QtWidgets import QMessageBox from novelwriter import CONFIG, SHARED from novelwriter.constants import nwFiles diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py index 746a14a1..20f6965d 100644 --- a/tests/test_core/test_core_projectxml.py +++ b/tests/test_core/test_core_projectxml.py @@ -27,7 +27,7 @@ from shutil import copyfile import pytest -from PyQt5.QtGui import QColor +from PyQt6.QtGui import QColor from novelwriter.constants import nwFiles from novelwriter.core.item import NWItem diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index afc05ba4..dd6b18a5 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -22,7 +22,7 @@ from __future__ import annotations import pytest -from PyQt5.QtGui import QColor, QIcon +from PyQt6.QtGui import QColor, QIcon from novelwriter.core.status import NWStatus, StatusEntry, _ShapeCache from novelwriter.enum import nwStatusShape diff --git a/tests/test_dialogs/test_dlg_about.py b/tests/test_dialogs/test_dlg_about.py index 2cfcb065..0f659035 100644 --- a/tests/test_dialogs/test_dlg_about.py +++ b/tests/test_dialogs/test_dlg_about.py @@ -24,7 +24,8 @@ from pathlib import Path import pytest -from PyQt5.QtWidgets import QAction, QMessageBox +from PyQt6.QtGui import QAction +from PyQt6.QtWidgets import QMessageBox from novelwriter import SHARED from novelwriter.dialogs.about import GuiAbout diff --git a/tests/test_dialogs/test_dlg_dialogs.py b/tests/test_dialogs/test_dlg_dialogs.py index b8a1d8df..ce3d6bc3 100644 --- a/tests/test_dialogs/test_dlg_dialogs.py +++ b/tests/test_dialogs/test_dlg_dialogs.py @@ -22,8 +22,8 @@ from __future__ import annotations import pytest -from PyQt5.QtCore import QItemSelectionModel -from PyQt5.QtWidgets import QListWidgetItem +from PyQt6.QtCore import QItemSelectionModel +from PyQt6.QtWidgets import QListWidgetItem from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.quotes import GuiQuoteSelect diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index 2c1f5e11..7431b3d7 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -22,7 +22,7 @@ from __future__ import annotations import pytest -from PyQt5.QtCore import Qt +from PyQt6.QtCore import Qt from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.types import QtAccepted, QtRejected, QtUserRole diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index df0a0ff8..bdc4db27 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -22,9 +22,9 @@ from __future__ import annotations import pytest -from PyQt5.QtCore import QEvent, Qt -from PyQt5.QtGui import QFont, QFontDatabase, QKeyEvent -from PyQt5.QtWidgets import QAction, QFileDialog, QFontDialog +from PyQt6.QtCore import QEvent, Qt +from PyQt6.QtGui import QAction, QFont, QFontDatabase, QKeyEvent +from PyQt6.QtWidgets import QFileDialog, QFontDialog from novelwriter import CONFIG, SHARED from novelwriter.constants import nwUnicode diff --git a/tests/test_dialogs/test_dlg_projectsettings.py b/tests/test_dialogs/test_dlg_projectsettings.py index 53a8db7c..54b5cd1d 100644 --- a/tests/test_dialogs/test_dlg_projectsettings.py +++ b/tests/test_dialogs/test_dlg_projectsettings.py @@ -22,8 +22,8 @@ from __future__ import annotations import pytest -from PyQt5.QtGui import QColor -from PyQt5.QtWidgets import QAction, QColorDialog, QFileDialog +from PyQt6.QtGui import QAction, QColor +from PyQt6.QtWidgets import QColorDialog, QFileDialog from novelwriter import CONFIG, SHARED from novelwriter.dialogs.editlabel import GuiEditLabel diff --git a/tests/test_dialogs/test_dlg_wordlist.py b/tests/test_dialogs/test_dlg_wordlist.py index 84fb3500..9b42d5a7 100644 --- a/tests/test_dialogs/test_dlg_wordlist.py +++ b/tests/test_dialogs/test_dlg_wordlist.py @@ -22,8 +22,9 @@ from __future__ import annotations import pytest -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QAction, QFileDialog +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QAction +from PyQt6.QtWidgets import QFileDialog from novelwriter import SHARED from novelwriter.core.spellcheck import UserDictionary @@ -104,11 +105,11 @@ def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, fncPath, projPath): wList._doAdd() assert wList.listBox.item(0).text() == "delete_me" # type: ignore - delItem = wList.listBox.findItems("delete_me", Qt.MatchExactly)[0] + delItem = wList.listBox.findItems("delete_me", Qt.MatchFlag.MatchExactly)[0] assert delItem.text() == "delete_me" delItem.setSelected(True) wList._doDelete() - assert wList.listBox.findItems("delete_me", Qt.MatchExactly) == [] + assert wList.listBox.findItems("delete_me", Qt.MatchFlag.MatchExactly) == [] assert wList.listBox.item(0).text() == "word_a" # type: ignore # Import/Export diff --git a/tests/test_ext/test_ext_eventfilters.py b/tests/test_ext/test_ext_eventfilters.py index afc92ebc..628932f1 100644 --- a/tests/test_ext/test_ext_eventfilters.py +++ b/tests/test_ext/test_ext_eventfilters.py @@ -22,9 +22,9 @@ from __future__ import annotations import pytest -from PyQt5.QtCore import QEvent, QObject, QPoint, Qt -from PyQt5.QtGui import QKeyEvent, QWheelEvent -from PyQt5.QtWidgets import QWidget +from PyQt6.QtCore import QEvent, QObject, QPoint, QPointF, Qt +from PyQt6.QtGui import QKeyEvent, QWheelEvent +from PyQt6.QtWidgets import QWidget from novelwriter.extensions.eventfilters import WheelEventFilter from novelwriter.types import QtModNone, QtModShift @@ -57,8 +57,9 @@ def testExtEventFilters_WheelEventFilter(): # Sending a mouse wheel event forwards it pos = QPoint(0, 0) + posF = QPointF(0.0, 0.0) event = QWheelEvent( - pos, pos, pos, pos, + posF, posF, pos, pos, Qt.MouseButton.NoButton, QtModNone, Qt.ScrollPhase.NoScrollPhase, False, ) diff --git a/tests/test_ext/test_ext_modified.py b/tests/test_ext/test_ext_modified.py index 69d4d717..161531da 100644 --- a/tests/test_ext/test_ext_modified.py +++ b/tests/test_ext/test_ext_modified.py @@ -22,9 +22,9 @@ from __future__ import annotations import pytest -from PyQt5.QtCore import QEvent, QPoint, QPointF, Qt -from PyQt5.QtGui import QKeyEvent, QMouseEvent, QWheelEvent -from PyQt5.QtWidgets import QWidget +from PyQt6.QtCore import QEvent, QPoint, QPointF, Qt +from PyQt6.QtGui import QKeyEvent, QMouseEvent, QWheelEvent +from PyQt6.QtWidgets import QWidget from novelwriter.extensions.modified import ( NClickableLabel, NComboBox, NDialog, NDoubleSpinBox, NSpinBox @@ -150,7 +150,7 @@ def testExtModified_NClickableLabel(qtbot, monkeypatch): dialog = SimpleDialog(widget) dialog.show() - position = widget.rect().center() + position = QPointF(widget.rect().center()) event = QMouseEvent( QEvent.Type.MouseButtonPress, position, QtMouseLeft, QtMouseLeft, QtModNone ) diff --git a/tests/test_ext/test_ext_progressbars.py b/tests/test_ext/test_ext_progressbars.py index bbe08161..f3872735 100644 --- a/tests/test_ext/test_ext_progressbars.py +++ b/tests/test_ext/test_ext_progressbars.py @@ -24,7 +24,7 @@ from time import sleep import pytest -from PyQt5.QtGui import QColor +from PyQt6.QtGui import QColor from novelwriter.extensions.progressbars import NProgressCircle, NProgressSimple diff --git a/tests/test_ext/test_ext_switch.py b/tests/test_ext/test_ext_switch.py index 95dbf2bd..734a2257 100644 --- a/tests/test_ext/test_ext_switch.py +++ b/tests/test_ext/test_ext_switch.py @@ -22,8 +22,8 @@ from __future__ import annotations import pytest -from PyQt5.QtCore import QEvent, QPoint -from PyQt5.QtGui import QMouseEvent +from PyQt6.QtCore import QEvent, QPointF +from PyQt6.QtGui import QEnterEvent, QMouseEvent from novelwriter.extensions.switch import NSwitch from novelwriter.types import QtModNone, QtMouseLeft @@ -65,10 +65,10 @@ def testExtSwitch_Main(qtbot): button = QtMouseLeft modifier = QtModNone - event = QMouseEvent(QEvent.Type.MouseButtonRelease, QPoint(), button, button, modifier) + event = QMouseEvent(QEvent.Type.MouseButtonRelease, QPointF(), button, button, modifier) switch.mouseReleaseEvent(event) - event = QEvent(QEvent.Type.Enter) + event = QEnterEvent(QPointF(), QPointF(), QPointF()) switch.enterEvent(event) # qtbot.stop() diff --git a/tests/test_ext/test_ext_versioninfo.py b/tests/test_ext/test_ext_versioninfo.py index a5566db2..cbf1428c 100644 --- a/tests/test_ext/test_ext_versioninfo.py +++ b/tests/test_ext/test_ext_versioninfo.py @@ -24,7 +24,7 @@ from urllib.error import HTTPError import pytest -from PyQt5.QtCore import QUrl +from PyQt6.QtCore import QUrl from novelwriter import SHARED from novelwriter.constants import nwConst diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index 706bcdd6..3716657c 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -22,7 +22,7 @@ from __future__ import annotations import pytest -from PyQt5.QtGui import QFont +from PyQt6.QtGui import QFont from novelwriter import CONFIG from novelwriter.constants import nwHeadFmt, nwStyles diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py index e2796c3d..a6936598 100644 --- a/tests/test_formats/test_fmt_toodt.py +++ b/tests/test_formats/test_fmt_toodt.py @@ -27,7 +27,7 @@ from shutil import copyfile import pytest -from PyQt5.QtGui import QColor +from PyQt6.QtGui import QColor from novelwriter.common import xmlIndent from novelwriter.constants import nwHeadFmt diff --git a/tests/test_formats/test_fmt_toqdoc.py b/tests/test_formats/test_fmt_toqdoc.py index c53a34ba..97a16d9a 100644 --- a/tests/test_formats/test_fmt_toqdoc.py +++ b/tests/test_formats/test_fmt_toqdoc.py @@ -22,7 +22,7 @@ from __future__ import annotations import pytest -from PyQt5.QtGui import QFont, QTextBlock, QTextCharFormat, QTextCursor +from PyQt6.QtGui import QFont, QTextBlock, QTextCharFormat, QTextCursor from novelwriter import CONFIG from novelwriter.constants import nwUnicode diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 611f0145..32198bec 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -24,12 +24,12 @@ from unittest.mock import MagicMock import pytest -from PyQt5.QtCore import QEvent, QMimeData, Qt, QThreadPool, QUrl -from PyQt5.QtGui import ( - QClipboard, QDesktopServices, QDragEnterEvent, QDragMoveEvent, QDropEvent, - QFont, QMouseEvent, QTextBlock, QTextCursor, QTextOption +from PyQt6.QtCore import QEvent, QMimeData, QPointF, Qt, QThreadPool, QUrl +from PyQt6.QtGui import ( + QAction, QClipboard, QDesktopServices, QDragEnterEvent, QDragMoveEvent, + QDropEvent, QFont, QMouseEvent, QTextBlock, QTextCursor, QTextOption ) -from PyQt5.QtWidgets import QAction, QApplication, QMenu, QPlainTextEdit +from PyQt6.QtWidgets import QApplication, QMenu, QPlainTextEdit from novelwriter import CONFIG, SHARED from novelwriter.common import decodeMimeHandles @@ -102,8 +102,8 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd): qDoc = docEditor.document() assert CONFIG.textFont == qDoc.defaultFont() assert qDoc.defaultTextOption().alignment() == QtAlignJustify - assert qDoc.defaultTextOption().flags() & QTextOption.ShowTabsAndSpaces - assert qDoc.defaultTextOption().flags() & QTextOption.ShowLineAndParagraphSeparators + assert qDoc.defaultTextOption().flags() & QTextOption.Flag.ShowTabsAndSpaces + assert qDoc.defaultTextOption().flags() & QTextOption.Flag.ShowLineAndParagraphSeparators assert docEditor.verticalScrollBarPolicy() == QtScrollAlwaysOff assert docEditor.horizontalScrollBarPolicy() == QtScrollAlwaysOff assert docEditor._autoReplace._padChar == nwUnicode.U_THNBSP @@ -262,6 +262,7 @@ def testGuiEditor_DragAndDrop(qtbot, monkeypatch, nwGUI, projPath, mockRnd): assert mockMove.call_count == 1 # Drop + middle = QPointF(docEditor.viewport().rect().center()) mockDrop = MagicMock() docEvent = QDropEvent(middle, action, docMime, mouse, QtModNone) noneEvent = QDropEvent(middle, action, noneMime, mouse, QtModNone) @@ -1708,7 +1709,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd): # On Known Tag, Follow docEditor.setCursorPosition(22) - position = docEditor.cursorRect().center() + position = QPointF(docEditor.cursorRect().center()) event = QMouseEvent( QEvent.Type.MouseButtonPress, position, QtMouseLeft, QtMouseLeft, QtModCtrl ) @@ -1750,7 +1751,7 @@ def testGuiEditor_Links(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd) docEditor.replaceText("### Scene\n\nFoo http://www.example.com bar.\n\n") docEditor.setCursorPosition(20) - position = docEditor.cursorRect().center() + position = QPointF(docEditor.cursorRect().center()) event = QMouseEvent( QEvent.Type.MouseButtonPress, position, QtMouseLeft, QtMouseLeft, QtModCtrl ) @@ -1988,7 +1989,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): # Select the Word "est" docEditor.setCursorPosition(645) - docEditor._makeSelection(QTextCursor.WordUnderCursor) + docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor) cursor = docEditor.textCursor() assert cursor.selectedText() == "est" @@ -2118,7 +2119,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): # Close search and select "est" again docSearch.cancelSearch.activate(QAction.ActionEvent.Trigger) docEditor.setCursorPosition(645) - docEditor._makeSelection(QTextCursor.WordUnderCursor) + docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor) cursor = docEditor.textCursor() assert cursor.selectedText() == "est" diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index b2e3d26f..2eb8cb59 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -24,18 +24,18 @@ from unittest.mock import MagicMock import pytest -from PyQt5.QtCore import QEvent, QMimeData, QPoint, Qt, QUrl -from PyQt5.QtGui import ( - QDesktopServices, QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent, - QTextCursor +from PyQt6.QtCore import QEvent, QMimeData, QPointF, Qt, QUrl +from PyQt6.QtGui import ( + QAction, QDesktopServices, QDragEnterEvent, QDragMoveEvent, QDropEvent, + QMouseEvent, QTextCursor ) -from PyQt5.QtWidgets import QAction, QApplication, QMenu, QTextBrowser +from PyQt6.QtWidgets import QApplication, QMenu, QTextBrowser from novelwriter import CONFIG, SHARED from novelwriter.common import decodeMimeHandles from novelwriter.enum import nwChange, nwDocAction from novelwriter.formats.toqdoc import ToQTextDocument -from novelwriter.types import QtModNone, QtMouseLeft +from novelwriter.types import QtModNone, QtMouseLeft, QtMouseMiddle from tests.mocked import causeException from tests.tools import C, buildTestProject @@ -59,7 +59,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): # Middle-click the selected item index = SHARED.project.tree.model.indexFromHandle("88243afbe5ed8") rect = nwGUI.projView.projTree.visualRect(index) - qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=rect.center()) + qtbot.mouseClick(nwGUI.projView.projTree.viewport(), QtMouseMiddle, pos=rect.center()) assert docViewer.docHandle == "88243afbe5ed8" # Clear selection @@ -69,7 +69,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): # Re-select via header click button = QtMouseLeft modifier = QtModNone - event = QMouseEvent(QEvent.Type.MouseButtonPress, QPoint(), button, button, modifier) + event = QMouseEvent(QEvent.Type.MouseButtonPress, QPointF(), button, button, modifier) docViewer.docHeader.mousePressEvent(event) assert nwGUI.projView.projTree.getSelectedHandle() == "88243afbe5ed8" @@ -89,7 +89,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): cursor = docViewer.textCursor() cursor.setPosition(100) docViewer.setTextCursor(cursor) - docViewer._makeSelection(QTextCursor.WordUnderCursor) + docViewer._makeSelection(QTextCursor.SelectionType.WordUnderCursor) qClip = QApplication.clipboard() qClip.clear() @@ -158,7 +158,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): cursor = docViewer.textCursor() cursor.setPosition(27) docViewer.setTextCursor(cursor) - docViewer._makeSelection(QTextCursor.WordUnderCursor) + docViewer._makeSelection(QTextCursor.SelectionType.WordUnderCursor) with monkeypatch.context() as mp: mp.setattr(QMenu, "exec", mockExec) docViewer._openContextMenu(docViewer.cursorRect().center()) @@ -168,7 +168,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): cursor = docViewer.textCursor() cursor.setPosition(27) docViewer.setTextCursor(cursor) - docViewer._makeSelection(QTextCursor.WordUnderCursor) + docViewer._makeSelection(QTextCursor.SelectionType.WordUnderCursor) rect = docViewer.cursorRect() docViewer._linkClicked(QUrl("#tag_bod")) assert docViewer.docHandle == "4c4f28287af27" @@ -187,11 +187,12 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): assert openUrl.call_args[0][0] == QUrl("http://www.example.com") # Click mouse nav buttons - qtbot.mouseClick(docViewer.viewport(), Qt.BackButton, pos=rect.center(), delay=100) + viewport = docViewer.viewport() + qtbot.mouseClick(viewport, Qt.MouseButton.BackButton, pos=rect.center(), delay=100) assert docViewer.docHandle == "88243afbe5ed8" - qtbot.mouseClick(docViewer.viewport(), Qt.ForwardButton, pos=rect.center(), delay=100) + qtbot.mouseClick(viewport, Qt.MouseButton.ForwardButton, pos=rect.center(), delay=100) assert docViewer.docHandle == "4c4f28287af27" - qtbot.mouseClick(docViewer.viewport(), QtMouseLeft, pos=rect.center(), delay=100) + qtbot.mouseClick(viewport, QtMouseLeft, pos=rect.center(), delay=100) assert docViewer.docHandle == "4c4f28287af27" # Scroll bar default on empty document @@ -299,6 +300,7 @@ def testGuiViewer_DragAndDrop(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Drop mockDrop = MagicMock() + middle = QPointF(docViewer.viewport().rect().center()) docEvent = QDropEvent(middle, action, docMime, mouse, QtModNone) noneEvent = QDropEvent(middle, action, noneMime, mouse, QtModNone) with monkeypatch.context() as mp: diff --git a/tests/test_gui/test_gui_docviewerpanel.py b/tests/test_gui/test_gui_docviewerpanel.py index 0daaaad0..b0d02493 100644 --- a/tests/test_gui/test_gui_docviewerpanel.py +++ b/tests/test_gui/test_gui_docviewerpanel.py @@ -22,7 +22,7 @@ from __future__ import annotations import pytest -from PyQt5.QtGui import QIcon +from PyQt6.QtGui import QIcon from novelwriter import SHARED from novelwriter.constants import nwLists diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index ff472363..fd3914ca 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -28,9 +28,9 @@ from shutil import copyfile import pytest -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPalette -from PyQt5.QtWidgets import QInputDialog, QMenu, QMessageBox +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QPalette +from PyQt6.QtWidgets import QInputDialog, QMenu, QMessageBox from novelwriter import CONFIG, SHARED from novelwriter.constants import nwFiles @@ -782,11 +782,12 @@ def testGuiMain_Features(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Full Screen Mode # ================ - assert nwGUI.windowState() & Qt.WindowFullScreen != Qt.WindowFullScreen + fullScreen = Qt.WindowState.WindowFullScreen + assert nwGUI.windowState() & fullScreen != fullScreen nwGUI.toggleFullScreenMode() - assert nwGUI.windowState() & Qt.WindowFullScreen == Qt.WindowFullScreen + assert nwGUI.windowState() & fullScreen == fullScreen nwGUI.toggleFullScreenMode() - assert nwGUI.windowState() & Qt.WindowFullScreen != Qt.WindowFullScreen + assert nwGUI.windowState() & fullScreen != fullScreen # SideBar Menu # ============ diff --git a/tests/test_gui/test_gui_i18n.py b/tests/test_gui/test_gui_i18n.py index e9caa806..aab521ce 100644 --- a/tests/test_gui/test_gui_i18n.py +++ b/tests/test_gui/test_gui_i18n.py @@ -24,7 +24,7 @@ import sys import pytest -from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox +from PyQt6.QtWidgets import QApplication, QDialog, QMessageBox from novelwriter import CONFIG, SHARED from novelwriter.dialogs.about import GuiAbout diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 66b1d94e..b2bc3fd9 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -24,8 +24,8 @@ from unittest.mock import MagicMock import pytest -from PyQt5.QtGui import QDesktopServices, QTextBlock, QTextCursor -from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox +from PyQt6.QtGui import QAction, QDesktopServices, QTextBlock, QTextCursor +from PyQt6.QtWidgets import QFileDialog, QMessageBox from novelwriter import CONFIG, SHARED from novelwriter.constants import nwKeyWords, nwShortcode, nwStats, nwUnicode @@ -187,7 +187,7 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): # Cut, Copy and Paste docEditor.setCursorPosition(54) - docEditor._makeSelection(QTextCursor.WordUnderCursor) + docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor) mainMenu.aEditCut.activate(QAction.ActionEvent.Trigger) assert docEditor.getText()[54:104] == ( @@ -200,7 +200,7 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): ) docEditor.setCursorPosition(54) - docEditor._makeSelection(QTextCursor.WordUnderCursor) + docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor) mainMenu.aEditCopy.activate(QAction.ActionEvent.Trigger) assert docEditor.getText()[54:104] == ( diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 3974d432..9ec5f352 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -24,9 +24,9 @@ from pathlib import Path import pytest -from PyQt5.QtCore import QEvent, QPoint, Qt -from PyQt5.QtGui import QFocusEvent -from PyQt5.QtWidgets import QInputDialog, QToolTip +from PyQt6.QtCore import QEvent, QPoint, Qt +from PyQt6.QtGui import QFocusEvent +from PyQt6.QtWidgets import QInputDialog, QToolTip from novelwriter import CONFIG, SHARED from novelwriter.dialogs.editlabel import GuiEditLabel @@ -225,7 +225,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): scItem = novelTree.topLevelItem(2) scItem.setSelected(True) assert scItem.isSelected() - novelTree.focusOutEvent(QFocusEvent(QEvent.Type.None_, Qt.MouseFocusReason)) + novelTree.focusOutEvent(QFocusEvent(QEvent.Type.None_, Qt.FocusReason.MouseFocusReason)) assert not scItem.isSelected() # Close diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 9ddc87b8..8ee650df 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -26,7 +26,8 @@ from shutil import copyfile import pytest -from PyQt5.QtWidgets import QAction, QFileDialog, QWidget +from PyQt6.QtGui import QAction +from PyQt6.QtWidgets import QFileDialog, QWidget from novelwriter import CONFIG, SHARED from novelwriter.constants import nwKeyWords diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 3642ef4b..de3163d2 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -24,9 +24,9 @@ from unittest.mock import MagicMock import pytest -from PyQt5.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPoint -from PyQt5.QtGui import QMouseEvent -from PyQt5.QtWidgets import QMenu, QMessageBox +from PyQt6.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPointF +from PyQt6.QtGui import QMouseEvent +from PyQt6.QtWidgets import QMenu, QMessageBox from novelwriter import CONFIG, SHARED from novelwriter.dialogs.docmerge import GuiDocMerge @@ -516,14 +516,14 @@ def testGuiProjTree_MouseClicks(qtbot, monkeypatch, nwGUI, projPath, mockRnd): modifier = QtModNone # Trigger the viewer - pos = projTree.visualRect(model.indexFromHandle(C.hChapterDoc)).center() + pos = QPointF(projTree.visualRect(model.indexFromHandle(C.hChapterDoc)).center()) button = QtMouseMiddle event = QMouseEvent(eType, pos, button, button, modifier) projTree.mousePressEvent(event) assert nwGUI.docViewer.docHandle == C.hChapterDoc # Trigger the left click clear - pos = QPoint(5000, 5000) + pos = QPointF(5000.0, 5000.0) button = QtMouseLeft event = QMouseEvent(eType, pos, button, button, modifier) diff --git a/tests/test_gui/test_gui_search.py b/tests/test_gui/test_gui_search.py index 426591fd..843c1ae9 100644 --- a/tests/test_gui/test_gui_search.py +++ b/tests/test_gui/test_gui_search.py @@ -24,8 +24,8 @@ from time import time import pytest -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QAction +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QAction from novelwriter.enum import nwView from novelwriter.gui.search import GuiProjectSearch diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index e8cc42ee..29c96720 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -24,8 +24,8 @@ from pathlib import Path import pytest -from PyQt5.QtGui import QColor, QIcon, QPalette, QPixmap -from PyQt5.QtWidgets import QApplication +from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap +from PyQt6.QtWidgets import QApplication from novelwriter import CONFIG, SHARED from novelwriter.common import NWConfigParser diff --git a/tests/test_tools/test_tools_dictionaries.py b/tests/test_tools/test_tools_dictionaries.py index e225f973..e8a23c7b 100644 --- a/tests/test_tools/test_tools_dictionaries.py +++ b/tests/test_tools/test_tools_dictionaries.py @@ -25,8 +25,8 @@ from zipfile import ZipFile import enchant import pytest -from PyQt5.QtGui import QDesktopServices -from PyQt5.QtWidgets import QFileDialog +from PyQt6.QtGui import QDesktopServices +from PyQt6.QtWidgets import QFileDialog from novelwriter import SHARED from novelwriter.tools.dictionaries import GuiDictionaries diff --git a/tests/test_tools/test_tools_lipsum.py b/tests/test_tools/test_tools_lipsum.py index 889c47bd..27098b5c 100644 --- a/tests/test_tools/test_tools_lipsum.py +++ b/tests/test_tools/test_tools_lipsum.py @@ -22,7 +22,7 @@ from __future__ import annotations import pytest -from PyQt5.QtWidgets import QAction +from PyQt6.QtGui import QAction from novelwriter import SHARED from novelwriter.enum import nwDocInsert diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py index 898f120c..07b0c1c5 100644 --- a/tests/test_tools/test_tools_manusbuild.py +++ b/tests/test_tools/test_tools_manusbuild.py @@ -24,9 +24,9 @@ from pathlib import Path import pytest -from PyQt5.QtCore import QUrl -from PyQt5.QtGui import QDesktopServices -from PyQt5.QtWidgets import QFileDialog, QListWidgetItem, QMessageBox +from PyQt6.QtCore import QUrl +from PyQt6.QtGui import QDesktopServices +from PyQt6.QtWidgets import QFileDialog, QListWidgetItem, QMessageBox from pytestqt.qtbot import QtBot from novelwriter.constants import nwLabels diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py index 5db4f933..bdd5b900 100644 --- a/tests/test_tools/test_tools_manuscript.py +++ b/tests/test_tools/test_tools_manuscript.py @@ -26,10 +26,10 @@ from unittest.mock import MagicMock import pytest -from PyQt5.QtCore import QUrl, pyqtSlot -from PyQt5.QtGui import QDesktopServices -from PyQt5.QtPrintSupport import QPrintPreviewDialog -from PyQt5.QtWidgets import QAction, QListWidgetItem +from PyQt6.QtCore import QUrl, pyqtSlot +from PyQt6.QtGui import QAction, QDesktopServices +from PyQt6.QtPrintSupport import QPrintPreviewDialog +from PyQt6.QtWidgets import QListWidgetItem from novelwriter import SHARED from novelwriter.constants import nwHeadFmt diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index b41d50a8..256feee7 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -22,9 +22,9 @@ from __future__ import annotations import pytest -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import QFontDialog +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtGui import QFont +from PyQt6.QtWidgets import QFontDialog from novelwriter import SHARED from novelwriter.common import describeFont diff --git a/tests/test_tools/test_tools_noveldetails.py b/tests/test_tools/test_tools_noveldetails.py index 73bb6063..067cbc08 100644 --- a/tests/test_tools/test_tools_noveldetails.py +++ b/tests/test_tools/test_tools_noveldetails.py @@ -22,7 +22,7 @@ from __future__ import annotations import pytest -from PyQt5.QtWidgets import QAction +from PyQt6.QtGui import QAction from novelwriter import SHARED from novelwriter.enum import nwItemClass diff --git a/tests/test_tools/test_tools_welcome.py b/tests/test_tools/test_tools_welcome.py index eb30fcb7..82b84a87 100644 --- a/tests/test_tools/test_tools_welcome.py +++ b/tests/test_tools/test_tools_welcome.py @@ -25,8 +25,9 @@ from pathlib import Path import pytest -from PyQt5.QtCore import QPoint -from PyQt5.QtWidgets import QAction, QFileDialog, QMenu +from PyQt6.QtCore import QPoint +from PyQt6.QtGui import QAction +from PyQt6.QtWidgets import QFileDialog, QMenu from pytestqt.qtbot import QtBot from novelwriter import CONFIG, SHARED @@ -162,6 +163,7 @@ def testToolWelcome_Open(qtbot: QtBot, monkeypatch, nwGUI, fncPath): welcome.close() +@pytest.mark.skip @pytest.mark.gui def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath): """Test the new project tab in the Welcome window.""" diff --git a/tests/test_tools/test_tools_writingstats.py b/tests/test_tools/test_tools_writingstats.py index c56f9731..4902c3fd 100644 --- a/tests/test_tools/test_tools_writingstats.py +++ b/tests/test_tools/test_tools_writingstats.py @@ -24,7 +24,9 @@ import json import pytest -from PyQt5.QtWidgets import QAction, QFileDialog +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QAction +from PyQt6.QtWidgets import QFileDialog from novelwriter import SHARED from novelwriter.constants import nwFiles @@ -117,7 +119,7 @@ def testToolWritingStats_Export(qtbot, monkeypatch, nwGUI, projPath, tstPaths): assert not sessLog._saveData(None) # type: ignore # Sort by time - sessLog.listBox.sortByColumn(sessLog.C_TIME, 0) # type: ignore + sessLog.listBox.sortByColumn(sessLog.C_TIME, Qt.SortOrder.AscendingOrder) assert sessLog.novelWords.text() == "{:n}".format(600) assert sessLog.notesWords.text() == "{:n}".format(275) @@ -201,7 +203,7 @@ def testToolWritingStats_Filters(qtbot, monkeypatch, nwGUI, projPath, tstPaths): ] sessFile.write_text("".join(data), encoding="utf-8") sessLog.populateGUI() - sessLog.listBox.sortByColumn(sessLog.C_TIME, 0) # type: ignore + sessLog.listBox.sortByColumn(sessLog.C_TIME, Qt.SortOrder.AscendingOrder) assert sessLog.listBox.topLevelItem(0).text(sessLog.C_COUNT) == "{:n}".format(1) assert sessLog.listBox.topLevelItem(1).text(sessLog.C_COUNT) == "{:n}".format(-200) diff --git a/tests/tools.py b/tests/tools.py index 28f4acce..e3d7100e 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -26,7 +26,7 @@ import xml.etree.ElementTree as ET from datetime import datetime from pathlib import Path -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QWidget +from PyQt6.QtWidgets import QDialog, QVBoxLayout, QWidget XML_IGNORE = (" Date: Sat, 11 Jan 2025 20:55:09 +0100 Subject: [PATCH 03/13] Fix duplicate tests issue --- novelwriter/types.py | 5 +---- tests/conftest.py | 8 +------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/novelwriter/types.py b/novelwriter/types.py index 23455b97..e1b05ead 100644 --- a/novelwriter/types.py +++ b/novelwriter/types.py @@ -24,10 +24,7 @@ along with this program. If not, see . from __future__ import annotations from PyQt6.QtCore import Qt -from PyQt6.QtGui import ( - QColor, QFont, QPainter, QTextBlockFormat, QTextCharFormat, QTextCursor, - QTextFormat -) +from PyQt6.QtGui import QColor, QFont, QPainter, QTextCharFormat, QTextCursor, QTextFormat from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QHeaderView, QSizePolicy, QStyle # Qt Alignment Flags diff --git a/tests/conftest.py b/tests/conftest.py index 1097ae6c..5ffebaa7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -172,13 +172,7 @@ def nwGUI(qtbot, monkeypatch, functionFixture): nwGUI.show() qtbot.wait(20) - yield nwGUI - - qtbot.wait(20) - nwGUI.closeMain() - qtbot.wait(20) - - return + return nwGUI ## From 1307fccc593918e5fdca74e7625da3223a65cadb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jan 2025 21:42:37 +0100 Subject: [PATCH 04/13] Fix some minor GUI issues --- novelwriter/extensions/switch.py | 10 ++++------ novelwriter/tools/welcome.py | 17 +++-------------- tests/test_tools/test_tools_welcome.py | 12 ++++-------- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py index 7b8c401d..9ee155a7 100644 --- a/novelwriter/extensions/switch.py +++ b/novelwriter/extensions/switch.py @@ -23,15 +23,13 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt6.QtCore import QByteArray, QPropertyAnimation, Qt +from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty from PyQt6.QtGui import QEnterEvent, QMouseEvent, QPainter, QPaintEvent, QResizeEvent from PyQt6.QtWidgets import QAbstractButton, QWidget from novelwriter import CONFIG, SHARED from novelwriter.types import QtMouseLeft, QtNoPen, QtPaintAntiAlias, QtSizeFixed -OFFSET = QByteArray(b"offset") # type: ignore - class NSwitch(QAbstractButton): @@ -59,11 +57,11 @@ class NSwitch(QAbstractButton): # Properties ## - @property + @pyqtProperty(int) def offset(self) -> int: # type: ignore return self._offset - @offset.setter # type: ignore + @offset.setter def offset(self, offset: int) -> None: self._offset = offset self.update() @@ -123,7 +121,7 @@ class NSwitch(QAbstractButton): """Animate the switch on mouse release.""" super().mouseReleaseEvent(event) if event.button() == QtMouseLeft: - anim = QPropertyAnimation(self, OFFSET, self) + anim = QPropertyAnimation(self, b"offset", self) anim.setDuration(120) anim.setStartValue(self._offset) anim.setEndValue((self._xW - self._xR) if self.isChecked() else self._xR) diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index 885a7da7..7dc2298e 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -29,8 +29,8 @@ from datetime import datetime from pathlib import Path from PyQt6.QtCore import ( - QAbstractListModel, QEvent, QModelIndex, QObject, QPoint, QSize, Qt, - pyqtSignal, pyqtSlot + QAbstractListModel, QModelIndex, QObject, QPoint, QSize, Qt, pyqtSignal, + pyqtSlot ) from PyQt6.QtGui import QAction, QCloseEvent, QColor, QFont, QPainter, QPaintEvent, QPen, QShortcut from PyQt6.QtWidgets import ( @@ -590,7 +590,7 @@ class _NewProjectForm(QWidget): self.browseFill = NIconToolButton(self, iSz, "document_add", "blue") - self.fillMenu = _PopLeftDirectionMenu(self.browseFill) + self.fillMenu = QMenu(self.browseFill) self.fillBlank = self.fillMenu.addAction(self.tr("Create a fresh project")) self.fillBlank.setIcon(SHARED.theme.getIcon("document")) @@ -802,14 +802,3 @@ class _NewProjectForm(QWidget): self.extraWidget.setVisible(self._fillMode == self.FILL_BLANK) return - - -class _PopLeftDirectionMenu(QMenu): - - def event(self, event: QEvent) -> bool: - """Overload the show event and move the menu popup location.""" - if event.type() == QEvent.Type.Show: - if isinstance(parent := self.parent(), QWidget): - offset = QPoint(parent.width() - self.width(), parent.height()) - self.move(parent.mapToGlobal(offset)) - return super(_PopLeftDirectionMenu, self).event(event) diff --git a/tests/test_tools/test_tools_welcome.py b/tests/test_tools/test_tools_welcome.py index 82b84a87..228b162f 100644 --- a/tests/test_tools/test_tools_welcome.py +++ b/tests/test_tools/test_tools_welcome.py @@ -28,7 +28,6 @@ import pytest from PyQt6.QtCore import QPoint from PyQt6.QtGui import QAction from PyQt6.QtWidgets import QFileDialog, QMenu -from pytestqt.qtbot import QtBot from novelwriter import CONFIG, SHARED from novelwriter.constants import nwFiles @@ -38,7 +37,7 @@ from novelwriter.types import QtMouseLeft @pytest.mark.gui -def testToolWelcome_Main(qtbot: QtBot, monkeypatch, nwGUI, fncPath): +def testToolWelcome_Main(qtbot, monkeypatch, nwGUI, fncPath): """Test the main Welcome window.""" welcome = GuiWelcome(nwGUI) with qtbot.waitExposed(welcome): @@ -68,7 +67,7 @@ def testToolWelcome_Main(qtbot: QtBot, monkeypatch, nwGUI, fncPath): @pytest.mark.gui -def testToolWelcome_Open(qtbot: QtBot, monkeypatch, nwGUI, fncPath): +def testToolWelcome_Open(qtbot, monkeypatch, nwGUI, fncPath): """Test the open tab in the Welcome window.""" monkeypatch.setattr(QMenu, "exec", lambda *a: None) @@ -163,9 +162,9 @@ def testToolWelcome_Open(qtbot: QtBot, monkeypatch, nwGUI, fncPath): welcome.close() -@pytest.mark.skip +# @pytest.mark.skip @pytest.mark.gui -def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath): +def testToolWelcome_New(qtbot, caplog, monkeypatch, nwGUI, fncPath): """Test the new project tab in the Welcome window.""" welcome = GuiWelcome(nwGUI) with qtbot.waitExposed(welcome): @@ -205,10 +204,7 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath): assert newForm.extraWidget.isVisible() is False # Change back to fill blank using the menu - newForm.browseFill.click() - assert newForm.fillMenu.isVisible() is True newForm.fillMenu.actions()[0].activate(QAction.ActionEvent.Trigger) - newForm.fillMenu.close() assert newForm._fillMode == newForm.FILL_BLANK assert newForm.projFill.text() == "Fresh Project" assert newForm.extraWidget.isVisible() is True From ff7446a2ef41b461847df412ff487fae3f7cdd07 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jan 2025 21:55:26 +0100 Subject: [PATCH 05/13] Update references to Qt5 and PyQt5 --- CREDITS.md | 4 ++-- README.md | 2 +- docs/source/tech_source.rst | 7 +++---- i18n/README.md | 9 ++++----- novelwriter/__init__.py | 2 +- novelwriter/assets/text/credits_en.htm | 4 ++-- novelwriter/common.py | 2 +- novelwriter/config.py | 2 +- novelwriter/gui/mainmenu.py | 4 ++-- novelwriter/guimain.py | 4 ++-- pkgutils.py | 4 ++-- requirements-dev.txt | 1 - requirements.txt | 2 +- setup/description_pypi.md | 2 +- setup/iss_license.txt | 4 ++-- setup/macos/build.sh | 14 +++++++------- 16 files changed, 32 insertions(+), 35 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 51c45f6c..1f51a83d 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -45,8 +45,8 @@ contributions are listed on the project's Members page. The following libraries are dependencies of novelWriter: -* **Qt5** by Qt Company -* **PyQt5** by Riverbank Computing +* **Qt6** by Qt Company +* **PyQt6** by Riverbank Computing * **Enchant** by Dom Lachowicz * **PyEnchant** by Dimitri Merejkowsky diff --git a/README.md b/README.md index 3ac3c385..4f3e65f9 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ documentation. ## Implementation -novelWriter is written with Python 3 (3.10+) using Qt5 and PyQt5 (5.15 only), and is released on +novelWriter is written with Python 3 (3.10+) using Qt6 and PyQt6 (6.0+), and is released on Linux, Windows and macOS. It can in principle run on any Operating System that also supports Qt, PyQt and Python. diff --git a/docs/source/tech_source.rst b/docs/source/tech_source.rst index 9b100ebd..ad3d07c9 100644 --- a/docs/source/tech_source.rst +++ b/docs/source/tech_source.rst @@ -36,12 +36,11 @@ Everything else is handled with standard Python libraries. The following Python packages are needed to run all features of novelWriter: -* ``PyQt5`` – needed for connecting with the Qt5 libraries. +* ``PyQt6`` – needed for connecting with the Qt6 libraries. * ``PyEnchant`` – needed for spell checking (optional). -PyQt/Qt must be at least 5.15.0. If you want spell checking, you must install the ``PyEnchant`` -package. The spell check library must be at least 3.0 to work with Windows. On Linux, 2.0 also -works fine. +If you want spell checking, you must install the ``PyEnchant`` package. The spell check library +must be at least 3.0 to work with Windows. On Linux, 2.0 also works fine. If you install from PyPi, these dependencies should be installed automatically. If you install from source, dependencies can still be installed from PyPi with: diff --git a/i18n/README.md b/i18n/README.md index 1c5303a1..b45439dd 100644 --- a/i18n/README.md +++ b/i18n/README.md @@ -18,7 +18,7 @@ When contributing translations, keep the following things in mind. * For descriptive labels and dialog boxes, make sure you do _not_ change the meaning of the text when you translate it from English. The user must receive the same instructions or information - regardless of language. This is improtant, otherwise the documentation will be inconsistent with + regardless of language. This is important, otherwise the documentation will be inconsistent with the user interface and it will become a lot more difficult to handle user issues and questions. * If you think a label or description is misleading or incomplete, please file an issue report. The correct way to handle such changes is to change the text in the code first, which will then be @@ -35,7 +35,7 @@ Linguist, or updating the translations for an already existing supported languag There are two areas relevant to localisation: -* The Qt5 GUI translation files, which consists of `nw_XX.ts` files. This is the bulk of the +* The Qt GUI translation files, which consists of `nw_XX.ts` files. This is the bulk of the translation work. * The `project_XX.json` files. See [Project Localisation](#project-localisation) below. @@ -43,7 +43,7 @@ The `XX` in the file name corresponds to the language and country code for the t instance `en_GB` for British English. -## Qt5 GUI Localisation +## Qt GUI Localisation You will need the translation tool Qt 5 Linguist on your system. @@ -89,8 +89,7 @@ For instance, the French translation uses the language code `fr_FR`, so its tran be `nw_fr_FR.ts` Note: The `qtlupdate` command needs the `lupdate` tool provided by PyQt6, which uses the latest -TS file format. The tool in PyQt5 generates an older file format. On Debian/Ubuntu it is provided -by the package `pyqt6-dev-tools`. +TS file format. On Debian/Ubuntu it is provided by the package `pyqt6-dev-tools`. ### Edit the Translation File in Qt Linguist diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index a7fd61f5..6f6fc63e 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -117,7 +117,7 @@ def main(sysArgs: list | None = None) -> GuiMain | None: " --debug Print debug output. Includes --info.\n" " --color Add ANSI colors to log output.\n" " --meminfo Show memory usage information in the status bar.\n" - " --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n" + " --style= Sets Qt style flag. Defaults to 'Fusion'.\n" " --config= Alternative config file.\n" " --data= Alternative user data path.\n" ) diff --git a/novelwriter/assets/text/credits_en.htm b/novelwriter/assets/text/credits_en.htm index 30e6b9e0..60f5c41f 100644 --- a/novelwriter/assets/text/credits_en.htm +++ b/novelwriter/assets/text/credits_en.htm @@ -54,8 +54,8 @@ more contributions are listed on the project's Members page.

The following libraries are dependencies of novelWriter:

    -
  • Qt5 by Qt Company
  • -
  • PyQt5 by Riverbank Computing
  • +
  • Qt6 by Qt Company
  • +
  • PyQt6 by Riverbank Computing
  • Enchant by Dom Lachowicz
  • PyEnchant by Dimitri Merejkowsky
diff --git a/novelwriter/common.py b/novelwriter/common.py index 4731e82f..193f7c81 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -434,7 +434,7 @@ def describeFont(font: QFont) -> str: def fontMatcher(font: QFont) -> QFont: """Make sure the font is the correct family, if possible. This ensures that Qt doesn't re-use another font under the hood. The - default Qt5 font matching algorithm doesn't handle well changing + default Qt font matching algorithm doesn't handle well changing application fonts at runtime. """ info = QFontInfo(font) diff --git a/novelwriter/config.py b/novelwriter/config.py index 8e4cded1..232fcdf7 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -217,7 +217,7 @@ class Config: # System and App Information # ========================== - # Check Qt5 Versions + # Check Qt Versions self.verQtString = QT_VERSION_STR self.verQtValue = QT_VERSION self.verPyQtString = PYQT_VERSION_STR diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 42d7048b..f477217e 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -1021,8 +1021,8 @@ class GuiMainMenu(QMenuBar): self.aAboutNW.setMenuRole(QAction.MenuRole.AboutRole) self.aAboutNW.triggered.connect(self.mainGui.showAboutNWDialog) - # Help > About Qt5 - self.aAboutQt = self.helpMenu.addAction(self.tr("About Qt5")) + # Help > About Qt + self.aAboutQt = self.helpMenu.addAction(self.tr("About Qt")) self.aAboutQt.setMenuRole(QAction.MenuRole.AboutQtRole) self.aAboutQt.triggered.connect(self.mainGui.showAboutQtDialog) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 12d8a971..9e10e3f3 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -92,8 +92,8 @@ class GuiMain(QMainWindow): logger.info("OS: %s", CONFIG.osType) logger.info("Kernel: %s", CONFIG.kernelVer) logger.info("Host: %s", CONFIG.hostName) - logger.info("Qt5: %s (0x%06x)", CONFIG.verQtString, CONFIG.verQtValue) - logger.info("PyQt5: %s (0x%06x)", CONFIG.verPyQtString, CONFIG.verPyQtValue) + logger.info("Qt: %s (0x%06x)", CONFIG.verQtString, CONFIG.verQtValue) + logger.info("PyQt: %s (0x%06x)", CONFIG.verPyQtString, CONFIG.verPyQtValue) logger.info("Python: %s (0x%08x)", CONFIG.verPyString, sys.hexversion) logger.info("GUI Language: %s", CONFIG.guiLocale) diff --git a/pkgutils.py b/pkgutils.py index 8e0d5cbf..100db556 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -543,8 +543,8 @@ def buildTranslationAssets(args: argparse.Namespace | None = None) -> None: try: subprocess.call(["lrelease", "-verbose", *srcList]) except Exception as exc: - print("Qt5 Linguist tools seem to be missing") - print("On Debian/Ubuntu, install: qttools5-dev-tools pyqt5-dev-tools") + print("Qt Linguist tools seem to be missing") + print("On Debian/Ubuntu, install: qttools5-dev-tools") print(str(exc)) sys.exit(1) diff --git a/requirements-dev.txt b/requirements-dev.txt index e2e3b1d0..8499c6e8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,4 +3,3 @@ flake8-pep585 flake8-pyproject flake8-annotations isort -pyqt5-stubs diff --git a/requirements.txt b/requirements.txt index b6d30310..7b7c4d39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -pyqt5>=5.15 +pyqt6>=6.0 pyenchant>=3.0.0 diff --git a/setup/description_pypi.md b/setup/description_pypi.md index 336a2e1c..0221db8d 100644 --- a/setup/description_pypi.md +++ b/setup/description_pypi.md @@ -10,7 +10,7 @@ synchronisation tools. All text is saved as plain text files with a meta data he project structure is stored in a single project XML file, and other meta data is primarily saved as JSON files. -The application is written with Python 3 (3.10+) using Qt5 and PyQt5 (5.10+). It is developed on +The application is written with Python 3 (3.10+) using Qt6 and PyQt6 (5.10+). It is developed on Linux, but should in principle work fine on other operating systems as well as long as dependencies are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and MacOS. diff --git a/setup/iss_license.txt b/setup/iss_license.txt index 068dbc70..fe023cb4 100644 --- a/setup/iss_license.txt +++ b/setup/iss_license.txt @@ -18,12 +18,12 @@ along with this program. If not, see . Dependencies -Qt5 +Qt6 Copyright: Qt Company Website: License: LGPL v3 -PyQt5 / PyQt5-sip +PyQt6 / PyQt6-sip Copyright: Riverbank Computing Website: License: GPL v3 diff --git a/setup/macos/build.sh b/setup/macos/build.sh index 8cd47ce9..dc23601d 100755 --- a/setup/macos/build.sh +++ b/setup/macos/build.sh @@ -165,13 +165,13 @@ rm -rf share/{gtk-,}doc rm -rf lib/python3.1 # Remove web engine -rm lib/python3.*/site-packages/PyQt5/QtWebEngine* || true -rm -r lib/python3.*/site-packages/PyQt5/Qt/translations/qtwebengine* || true -rm lib/python3.*/site-packages/PyQt5/Qt/resources/qtwebengine* || true -rm -r lib/python3.*/site-packages/PyQt5/Qt/qml/QtWebEngine* || true -rm -r lib/python3.*/site-packages/PyQt5/Qt/plugins/webview/libqtwebview* || true -rm lib/python3.*/site-packages/PyQt5/Qt/libexec/QtWebEngineProcess* || true -rm lib/python3.*/site-packages/PyQt5/Qt/lib/libQt5WebEngine* || true +rm lib/python3.*/site-packages/PyQt6/QtWebEngine* || true +rm -r lib/python3.*/site-packages/PyQt6/Qt/translations/qtwebengine* || true +rm lib/python3.*/site-packages/PyQt6/Qt/resources/qtwebengine* || true +rm -r lib/python3.*/site-packages/PyQt6/Qt/qml/QtWebEngine* || true +rm -r lib/python3.*/site-packages/PyQt6/Qt/plugins/webview/libqtwebview* || true +rm lib/python3.*/site-packages/PyQt6/Qt/libexec/QtWebEngineProcess* || true +rm lib/python3.*/site-packages/PyQt6/Qt/lib/libQt5WebEngine* || true popd || exit 1 popd || exit 1 From 27a0a47aaacb08a803b6187bd4edadd7ff7719fc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jan 2025 23:55:08 +0100 Subject: [PATCH 06/13] Fix custom theme handling for Qt6 --- novelwriter/__init__.py | 3 +++ novelwriter/extensions/switch.py | 5 +++-- novelwriter/gui/theme.py | 14 ++++++++++++++ novelwriter/guimain.py | 4 ++-- novelwriter/shared.py | 22 +++++++++++++++------- tests/conftest.py | 10 +++++++--- tests/test_base/test_base_shared.py | 9 ++++++--- 7 files changed, 50 insertions(+), 17 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 6f6fc63e..2eb80148 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -254,11 +254,13 @@ def main(sysArgs: list | None = None) -> GuiMain | None: pass # Quietly ignore error # Import GUI (after dependency checks), and launch + from novelwriter.gui.theme import GuiTheme from novelwriter.guimain import GuiMain if testMode: # Only used for testing where the test framework creates the app CONFIG.loadConfig() + SHARED.initTheme(GuiTheme()) return GuiMain() app = QApplication([CONFIG.appName, (f"-style={qtStyle}")]) @@ -274,6 +276,7 @@ def main(sysArgs: list | None = None) -> GuiMain | None: # Run Config steps that require the QApplication CONFIG.loadConfig() CONFIG.initLocalisation(app) + SHARED.initTheme(GuiTheme()) # Launch main GUI nwGUI = GuiMain() diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py index 9ee155a7..2b320506 100644 --- a/novelwriter/extensions/switch.py +++ b/novelwriter/extensions/switch.py @@ -98,14 +98,14 @@ class NSwitch(QAbstractButton): trackBrush = palette.highlight() thumbBrush = palette.highlightedText() else: - trackBrush = palette.dark() + trackBrush = palette.mid() thumbBrush = palette.light() if self.isEnabled(): trackOpacity = 1.0 else: trackOpacity = 0.6 - trackBrush = palette.shadow() + trackBrush = palette.dark() thumbBrush = palette.mid() painter.setBrush(trackBrush) @@ -114,6 +114,7 @@ class NSwitch(QAbstractButton): painter.setBrush(thumbBrush) painter.drawEllipse(self._offset - self._rR, self._rB, self._rH, self._rH) + painter.end() return diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 9093bb8f..98efd50b 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -304,6 +304,20 @@ class GuiTheme: backCol = self._guiPalette.window().color() textCol = self._guiPalette.windowText().color() + # Calculate Based on Qt Fusion + light = backCol.lighter(150) + mid = backCol.darker(130) + midLight = mid.lighter(110) + dark = backCol.darker(150) + shadow = dark.darker(135) + + self._guiPalette.setColor(QPalette.ColorRole.Light, light) + self._guiPalette.setColor(QPalette.ColorRole.Mid, mid) + self._guiPalette.setColor(QPalette.ColorRole.Midlight, midLight) + self._guiPalette.setColor(QPalette.ColorRole.Dark, dark) + self._guiPalette.setColor(QPalette.ColorRole.Shadow, shadow) + + # Calculate Help Text backLNess = backCol.lightnessF() textLNess = textCol.lightnessF() self.isLightTheme = backLNess > textLNess diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 9e10e3f3..56e17517 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -56,7 +56,6 @@ from novelwriter.gui.projtree import GuiProjectView from novelwriter.gui.search import GuiProjectSearch from novelwriter.gui.sidebar import GuiSideBar from novelwriter.gui.statusbar import GuiMainStatus -from novelwriter.gui.theme import GuiTheme from novelwriter.tools.dictionaries import GuiDictionaries from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.noveldetails import GuiNovelDetails @@ -101,7 +100,7 @@ class GuiMain(QMainWindow): # ============ # Initialise UserData Instance - SHARED.initSharedData(self, GuiTheme()) + SHARED.initSharedData(self) # Prepare Main Window self.resize(*CONFIG.mainWinSize) @@ -1054,6 +1053,7 @@ class GuiMain(QMainWindow): # We are doing this manually instead of connecting to # paletteChanged since the processing order matters SHARED.theme.loadTheme() + self.setPalette(QApplication.palette()) self.docEditor.updateTheme() self.docViewer.updateTheme() self.docViewerPanel.updateTheme() diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 2fd5359c..588252d9 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -164,17 +164,24 @@ class SharedData(QObject): # Methods ## - def initSharedData(self, gui: GuiMain, theme: GuiTheme) -> None: + def initTheme(self, theme: GuiTheme) -> None: + """Initialise the GUI theme. This must be called before the GUI + is created. + """ + self._theme = theme + return + + def initSharedData(self, gui: GuiMain) -> None: """Initialise the SharedData instance. This must be called as soon as the Main GUI is created to ensure the SHARED singleton has the properties needed for operation. """ self._clock.start() self._gui = gui - self._theme = theme self._resetProject() logger.debug("Ready: SharedData") - logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount()) + if pool := QThreadPool.globalInstance(): + logger.debug("Thread Pool Max Count: %d", pool.maxThreadCount()) return def closeDocument(self, tHandle: str | None = None) -> None: @@ -266,7 +273,8 @@ class SharedData(QObject): def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None: """Queue a runnable in the application thread pool.""" - QThreadPool.globalInstance().start(runnable, priority=priority) + if pool := QThreadPool.globalInstance(): + pool.start(runnable, priority=priority) return def getProjectPath( @@ -278,13 +286,13 @@ class SharedData(QObject): label = (self.tr("novelWriter Project File or Zip File") if allowZip else self.tr("novelWriter Project File")) ext = f"{nwFiles.PROJ_FILE} *.zip" if allowZip else nwFiles.PROJ_FILE - ffilter = formatFileFilter([(label, ext), "*"]) + fFilter = formatFileFilter([(label, ext), "*"]) selected, _ = QFileDialog.getOpenFileName( - parent, self.tr("Open Project"), str(path or ""), filter=ffilter + parent, self.tr("Open Project"), str(path or ""), filter=fFilter ) return Path(selected) if selected else None - def getFont(self, current: QFont, native: bool) -> tuple[QFont, bool]: + def getFont(self, current: QFont, native: bool) -> tuple[QFont, bool | None]: """Open the font dialog and select a font.""" kwargs = {} if not native: diff --git a/tests/conftest.py b/tests/conftest.py index 5ffebaa7..0c7541d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,7 +33,7 @@ from PyQt6.QtWidgets import QMessageBox sys.path.insert(1, str(Path(__file__).parent.parent.absolute())) -from novelwriter import CONFIG, SHARED, main # noqa: E402 +from novelwriter import CONFIG, SHARED # noqa: E402 from tests.mocked import MockGuiMain, MockTheme # noqa: E402 from tests.tools import cleanProject # noqa: E402 @@ -160,11 +160,15 @@ def mockGUI(qtbot, monkeypatch): @pytest.fixture(scope="function") def nwGUI(qtbot, monkeypatch, functionFixture): """Create an instance of the novelWriter GUI.""" + from novelwriter.gui.theme import GuiTheme + from novelwriter.guimain import GuiMain + monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) - nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"]) - assert nwGUI is not None + CONFIG.loadConfig() + SHARED.initTheme(GuiTheme()) + nwGUI = GuiMain() qtbot.addWidget(nwGUI) resetConfigVars() nwGUI.docEditor.initEditor() diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py index 85a8200a..4f147b2b 100644 --- a/tests/test_base/test_base_shared.py +++ b/tests/test_base/test_base_shared.py @@ -56,7 +56,8 @@ def testBaseSharedData_Init(): assert mockGui is not mockTheme # Properly initialise the class - shared.initSharedData(mockGui, mockTheme) # type: ignore + shared.initTheme(mockTheme) # type: ignore + shared.initSharedData(mockGui) # type: ignore assert shared.mainGui is mockGui assert shared.theme is mockTheme @@ -94,7 +95,8 @@ def testBaseSharedData_Projects(monkeypatch, caplog, fncPath): # Initialise the instance, should create an empty project mockGui = MockGuiMain() mockTheme = MockTheme() - shared.initSharedData(mockGui, mockTheme) # type: ignore + shared.initTheme(mockTheme) # type: ignore + shared.initSharedData(mockGui) # type: ignore assert isinstance(shared.project, NWProject) assert shared.hasProject is False @@ -150,7 +152,8 @@ def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog): mockGui = MockGuiMain() mockTheme = MockTheme() - shared.initSharedData(mockGui, mockTheme) # type: ignore + shared.initTheme(mockTheme) # type: ignore + shared.initSharedData(mockGui) # type: ignore assert shared.lastAlert == "" From a1d94d63a34227f4fee10fa87761b1f53b52a834 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jan 2025 03:16:52 +0100 Subject: [PATCH 07/13] Remove main app testmode flag --- novelwriter/__init__.py | 29 +++-- tests/test_base/test_base_init.py | 182 +++++++++++++++--------------- tests/tools.py | 8 ++ 3 files changed, 114 insertions(+), 105 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 2eb80148..ab5b3c7a 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -97,7 +97,6 @@ def main(sysArgs: list | None = None) -> GuiMain | None: "style=", "config=", "data=", - "testmode", "meminfo" ] @@ -127,7 +126,6 @@ def main(sysArgs: list | None = None) -> GuiMain | None: fmtFlags = 0b00 confPath = None dataPath = None - testMode = False qtStyle = "Fusion" cmdOpen = None @@ -163,8 +161,6 @@ def main(sysArgs: list | None = None) -> GuiMain | None: confPath = inArg elif inOpt == "--data": dataPath = inArg - elif inOpt == "--testmode": - testMode = True elif inOpt == "--meminfo": CONFIG.memInfo = True @@ -257,18 +253,8 @@ def main(sysArgs: list | None = None) -> GuiMain | None: from novelwriter.gui.theme import GuiTheme from novelwriter.guimain import GuiMain - if testMode: - # Only used for testing where the test framework creates the app - CONFIG.loadConfig() - SHARED.initTheme(GuiTheme()) - return GuiMain() - - app = QApplication([CONFIG.appName, (f"-style={qtStyle}")]) - app.setApplicationName(CONFIG.appName) - app.setApplicationVersion(__version__) - app.setOrganizationDomain(__domain__) - app.setOrganizationName(__domain__) - app.setDesktopFileName(CONFIG.appName) + # Create App + app = _createApp(qtStyle) # Connect the exception handler before making the main GUI sys.excepthook = exceptionHandler @@ -283,3 +269,14 @@ def main(sysArgs: list | None = None) -> GuiMain | None: nwGUI.postLaunchTasks(cmdOpen) sys.exit(app.exec()) + + +def _createApp(style: str) -> QApplication: + """Create the app.""" + app = QApplication([CONFIG.appName, (f"-style={style}")]) + app.setApplicationName(CONFIG.appName) + app.setApplicationVersion(__version__) + app.setOrganizationDomain(__domain__) + app.setOrganizationName(__domain__) + app.setDesktopFileName(CONFIG.appName) + return app diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py index 8bc9bfb4..664007eb 100644 --- a/tests/test_base/test_base_init.py +++ b/tests/test_base/test_base_init.py @@ -23,147 +23,153 @@ from __future__ import annotations import logging import sys +from unittest.mock import Mock + import pytest -from novelwriter import CONFIG, logger, main +from PyQt6.QtWidgets import QApplication -from tests.mocked import MockGuiMain +from novelwriter import ( + C_BLUE, C_END, C_WHITE, CONFIG, L_FILE, L_LINE, L_LVLC, L_LVLP, L_TEXT, + L_TIME, _createApp, logger, main +) + +from tests.tools import clearLogHandlers @pytest.mark.base def testBaseInit_Launch(caplog, monkeypatch, fncPath): - """Check launching the main GUI.""" - monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) + """Check launching the main GUI. This test """ + monkeypatch.setattr("novelwriter._createApp", lambda *a: Mock()) + monkeypatch.setattr("novelwriter.guimain.GuiMain", Mock()) + monkeypatch.setattr(sys, "exit", Mock()) - # TestMode Launch - nwGUI = main(["--testmode", f"--config={fncPath}", f"--data={fncPath}"]) - assert isinstance(nwGUI, MockGuiMain) + # Default Launch + main([f"--config={fncPath}", f"--data={fncPath}"]) + assert CONFIG._confPath == fncPath + assert CONFIG._dataPath == fncPath - # Darwin Launch - caplog.clear() - osDarwin = CONFIG.osDarwin - CONFIG.osDarwin = True + # Darwin Launch Error Handling with monkeypatch.context() as mp: mp.setitem(sys.modules, "Foundation", None) - nwGUI = main(["--testmode", f"--config={fncPath}", f"--data={fncPath}"]) - assert isinstance(nwGUI, MockGuiMain) + main([f"--config={fncPath}", f"--data={fncPath}"]) - CONFIG.osDarwin = osDarwin - - # Windows Launch - caplog.clear() - osWindows = CONFIG.osWindows - CONFIG.osWindows = True + # Windows Launch Error Handling with monkeypatch.context() as mp: mp.setitem(sys.modules, "ctypes", None) - nwGUI = main(["--testmode", f"--config={fncPath}", f"--data={fncPath}"]) - assert isinstance(nwGUI, MockGuiMain) + main([f"--config={fncPath}", f"--data={fncPath}"]) - CONFIG.osWindows = osWindows - # Normal Launch - with monkeypatch.context() as mp: - mp.setattr("PyQt6.QtWidgets.QApplication.__init__", lambda *a: None) - mp.setattr("PyQt6.QtWidgets.QApplication.setApplicationName", lambda *a: None) - mp.setattr("PyQt6.QtWidgets.QApplication.setApplicationVersion", lambda *a: None) - mp.setattr("PyQt6.QtWidgets.QApplication.setWindowIcon", lambda *a: None) - mp.setattr("PyQt6.QtWidgets.QApplication.setOrganizationDomain", lambda *a: None) - mp.setattr("PyQt6.QtWidgets.QApplication.exec", lambda *a: 0) - with pytest.raises(SystemExit) as ex: - main([f"--config={fncPath}", f"--data={fncPath}"]) - assert ex.value.code == 0 +@pytest.mark.base +def testBaseInit_CreateApp(caplog, monkeypatch, fncPath): + """Check creating the Qt app.""" + monkeypatch.setattr("PyQt6.QtWidgets.QApplication.__init__", lambda *a: None) + monkeypatch.setattr("PyQt6.QtWidgets.QApplication.setApplicationName", lambda *a: None) + monkeypatch.setattr("PyQt6.QtWidgets.QApplication.setApplicationVersion", lambda *a: None) + monkeypatch.setattr("PyQt6.QtWidgets.QApplication.setWindowIcon", lambda *a: None) + monkeypatch.setattr("PyQt6.QtWidgets.QApplication.setOrganizationDomain", lambda *a: None) + monkeypatch.setattr("PyQt6.QtWidgets.QApplication.exec", lambda *a: 0) + + app = _createApp("Fusion") + assert isinstance(app, QApplication) @pytest.mark.base def testBaseInit_Options(monkeypatch, fncPath): """Test command line options for logging level.""" - monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) + gui = Mock() + app = Mock() + app.exec = Mock(return_value=0) + + monkeypatch.setattr("novelwriter._createApp", lambda *a: app) + monkeypatch.setattr("novelwriter.guimain.GuiMain", lambda *a: gui) monkeypatch.setattr(sys, "argv", [ - "novelWriter.py", "--testmode", "--meminfo", f"--config={fncPath}", f"--data={fncPath}" + "novelWriter.py", f"--config={fncPath}", f"--data={fncPath}" ]) - # Defaults w/None Args - nwGUI = main() - assert nwGUI is not None + # Defaults wo/Args + gui.reset_mock() + with pytest.raises(SystemExit) as ex: + main() + assert ex.value.code == 0 assert logger.getEffectiveLevel() == logging.WARNING - assert nwGUI.closeMain() == "closeMain" + gui.postLaunchTasks.assert_called_once() + gui.postLaunchTasks.assert_called_with(None) # Defaults - nwGUI = main( - ["--testmode", f"--config={fncPath}", f"--data={fncPath}", "--style=Fusion"] - ) - assert nwGUI is not None - assert logger.getEffectiveLevel() == logging.WARNING - assert nwGUI.closeMain() == "closeMain" + with pytest.raises(SystemExit) as ex: + main([f"--config={fncPath}", f"--data={fncPath}", "--style=Fusion", "--meminfo"]) + assert ex.value.code == 0 + assert CONFIG.memInfo is True + + def getFormat() -> str: + formatter = logger.handlers[0].formatter + assert formatter is not None + fmt = formatter._fmt + assert fmt is not None + return fmt # Log Levels w/Color - nwGUI = main( - ["--testmode", "--info", "--color", f"--config={fncPath}", f"--data={fncPath}"] - ) - assert nwGUI is not None + clearLogHandlers() + with pytest.raises(SystemExit) as ex: + main(["--info", "--color", f"--config={fncPath}", f"--data={fncPath}"]) + assert ex.value.code == 0 assert logger.getEffectiveLevel() == logging.INFO - assert nwGUI.closeMain() == "closeMain" + assert getFormat() == f"{L_LVLC} {L_TEXT}" - nwGUI = main( - ["--testmode", "--debug", "--color", f"--config={fncPath}", f"--data={fncPath}"] - ) - assert nwGUI is not None + clearLogHandlers() + with pytest.raises(SystemExit) as ex: + main(["--debug", "--color", f"--config={fncPath}", f"--data={fncPath}"]) + assert ex.value.code == 0 assert logger.getEffectiveLevel() == logging.DEBUG - assert nwGUI.closeMain() == "closeMain" + assert getFormat() == ( + f"{L_TIME} {C_BLUE}{L_FILE}{C_END}:{C_WHITE}{L_LINE}{C_END} {L_LVLC} {L_TEXT}" + ) # Log Levels wo/Color - nwGUI = main( - ["--testmode", "--info", f"--config={fncPath}", f"--data={fncPath}"] - ) - assert nwGUI is not None + clearLogHandlers() + with pytest.raises(SystemExit) as ex: + main(["--info", f"--config={fncPath}", f"--data={fncPath}"]) + assert ex.value.code == 0 assert logger.getEffectiveLevel() == logging.INFO - assert nwGUI.closeMain() == "closeMain" + assert getFormat() == f"{L_LVLP} {L_TEXT}" - nwGUI = main( - ["--testmode", "--debug", f"--config={fncPath}", f"--data={fncPath}"] - ) - assert nwGUI is not None + clearLogHandlers() + with pytest.raises(SystemExit) as ex: + main(["--debug", f"--config={fncPath}", f"--data={fncPath}"]) + assert ex.value.code == 0 assert logger.getEffectiveLevel() == logging.DEBUG - assert nwGUI.closeMain() == "closeMain" + assert getFormat() == f"{L_TIME} {L_FILE}:{L_LINE} {L_LVLP} {L_TEXT}" # Help and Version with pytest.raises(SystemExit) as ex: - nwGUI = main( - ["--testmode", "--help", f"--config={fncPath}", f"--data={fncPath}"] - ) - assert nwGUI is not None - assert nwGUI.closeMain() == "closeMain" + main(["--help", f"--config={fncPath}", f"--data={fncPath}"]) assert ex.value.code == 0 with pytest.raises(SystemExit) as ex: - nwGUI = main( - ["--testmode", "--version", f"--config={fncPath}", f"--data={fncPath}"] - ) - assert nwGUI is not None - assert nwGUI.closeMain() == "closeMain" + main(["--version", f"--config={fncPath}", f"--data={fncPath}"]) assert ex.value.code == 0 # Invalid options with pytest.raises(SystemExit) as ex: - nwGUI = main( - ["--testmode", "--invalid", f"--config={fncPath}", f"--data={fncPath}"] - ) - assert nwGUI is not None - assert nwGUI.closeMain() == "closeMain" + main(["--invalid", f"--config={fncPath}", f"--data={fncPath}"]) assert ex.value.code == 2 # Project Path - nwGUI = main( - ["--testmode", f"--config={fncPath}", f"--data={fncPath}", "sample/"] - ) - assert nwGUI is not None - assert nwGUI.closeMain() == "closeMain" + gui.reset_mock() + with pytest.raises(SystemExit) as ex: + main([f"--config={fncPath}", f"--data={fncPath}", "sample/"]) + assert ex.value.code == 0 + gui.postLaunchTasks.assert_called_once() + gui.postLaunchTasks.assert_called_with("sample/") @pytest.mark.base def testBaseInit_Imports(caplog, monkeypatch, fncPath): """Check import error handling.""" - monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) + monkeypatch.setattr("novelwriter._createApp", lambda *a: Mock()) + monkeypatch.setattr("novelwriter.guimain.GuiMain", lambda *a: Mock()) + monkeypatch.setattr("PyQt6.QtWidgets.QApplication.__init__", lambda *a: None) monkeypatch.setattr("PyQt6.QtWidgets.QApplication.exec", lambda *a: 0) monkeypatch.setattr("PyQt6.QtWidgets.QErrorMessage.__init__", lambda *a: None) @@ -174,9 +180,7 @@ def testBaseInit_Imports(caplog, monkeypatch, fncPath): monkeypatch.setattr("novelwriter.CONFIG.verPyQtValue", 0x050000) with pytest.raises(SystemExit) as ex: - _ = main( - ["--testmode", f"--config={fncPath}", f"--data={fncPath}"] - ) + main([f"--config={fncPath}", f"--data={fncPath}"]) assert ex.value.code & 4 == 4 # Python version not satisfied # type: ignore assert ex.value.code & 8 == 8 # Qt version not satisfied # type: ignore diff --git a/tests/tools.py b/tests/tools.py index e3d7100e..a21ac432 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -28,6 +28,8 @@ from pathlib import Path from PyQt6.QtWidgets import QDialog, QVBoxLayout, QWidget +from novelwriter import logger + XML_IGNORE = (" None: """Build a standard test project in projPath using the project object as the parent. From 60130f067b0f7dfb81d475a6fc3a7d4a3fa0f7d2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:39:10 +0100 Subject: [PATCH 08/13] Remove computed GUI scaling --- novelwriter/config.py | 35 ++++++++--------- novelwriter/gui/theme.py | 7 ---- tests/test_base/test_base_config.py | 59 ----------------------------- 3 files changed, 15 insertions(+), 86 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index 232fcdf7..f1389253 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -115,7 +115,6 @@ class Config: self.guiTheme = "default" # GUI theme self.guiSyntax = "default_light" # Syntax theme self.guiFont = QFont() # Main GUI font - self.guiScale = 1.0 # Set automatically by Theme class self.hideVScroll = False # Hide vertical scroll bars on main widgets self.hideHScroll = False # Hide horizontal scroll bars on main widgets self.lastNotes = "0x0" # The latest release notes that have been shown @@ -272,27 +271,27 @@ class Config: @property def mainWinSize(self) -> list[int]: - return [int(x*self.guiScale) for x in self._mainWinSize] + return self._mainWinSize @property def welcomeWinSize(self) -> list[int]: - return [int(x*self.guiScale) for x in self._welcomeSize] + return self._welcomeSize @property def preferencesWinSize(self) -> list[int]: - return [int(x*self.guiScale) for x in self._prefsWinSize] + return self._prefsWinSize @property def mainPanePos(self) -> list[int]: - return [int(x*self.guiScale) for x in self._mainPanePos] + return self._mainPanePos @property def viewPanePos(self) -> list[int]: - return [int(x*self.guiScale) for x in self._viewPanePos] + return self._viewPanePos @property def outlinePanePos(self) -> list[int]: - return [int(x*self.guiScale) for x in self._outlnPanePos] + return self._outlnPanePos ## # Getters @@ -323,8 +322,6 @@ class Config: adjust it a bit, and we don't want the main window to shrink or grow each time the app is opened. """ - width = int(width/self.guiScale) - height = int(height/self.guiScale) if abs(self._mainWinSize[0] - width) > 5: self._mainWinSize[0] = width if abs(self._mainWinSize[1] - height) > 5: @@ -333,29 +330,27 @@ class Config: def setWelcomeWinSize(self, width: int, height: int) -> None: """Set the size of the Preferences dialog window.""" - self._welcomeSize[0] = int(width/self.guiScale) - self._welcomeSize[1] = int(height/self.guiScale) + self._welcomeSize = [width, height] return def setPreferencesWinSize(self, width: int, height: int) -> None: """Set the size of the Preferences dialog window.""" - self._prefsWinSize[0] = int(width/self.guiScale) - self._prefsWinSize[1] = int(height/self.guiScale) + self._prefsWinSize = [width, height] return def setMainPanePos(self, pos: list[int]) -> None: """Set the position of the main GUI splitter.""" - self._mainPanePos = [int(x/self.guiScale) for x in pos] + self._mainPanePos = pos return def setViewPanePos(self, pos: list[int]) -> None: """Set the position of the viewer meta data splitter.""" - self._viewPanePos = [int(x/self.guiScale) for x in pos] + self._viewPanePos = pos return def setOutlinePanePos(self, pos: list[int]) -> None: """Set the position of the outline details splitter.""" - self._outlnPanePos = [int(x/self.guiScale) for x in pos] + self._outlnPanePos = pos return def setLastPath(self, key: str, path: str | Path) -> None: @@ -427,12 +422,12 @@ class Config: ## def pxInt(self, value: int) -> int: - """Scale fixed gui sizes by the screen scale factor.""" - return int(value*self.guiScale) + """Deprecated. Do not use.""" + return value def rpxInt(self, value: int) -> int: - """Un-scale fixed gui sizes by the screen scale factor.""" - return int(value/self.guiScale) + """Deprecated. Do not use.""" + return value def homePath(self) -> Path: """The user's home folder.""" diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 98efd50b..c0a01c19 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -143,13 +143,6 @@ class GuiTheme: self.getHeaderDecoration = self.iconCache.getHeaderDecoration self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow - # Extract Other Info - self.guiDPI = QApplication.primaryScreen().logicalDotsPerInchX() - self.guiScale = QApplication.primaryScreen().logicalDotsPerInchX()/96.0 - CONFIG.guiScale = self.guiScale - logger.debug("GUI DPI: %.1f", self.guiDPI) - logger.debug("GUI Scale: %.2f", self.guiScale) - # Fonts self.guiFont = QApplication.font() self.guiFontB = QApplication.font() diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 864f9910..34bac678 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -245,35 +245,13 @@ def testBaseConfig_SettersGetters(fncPath): tstConf = Config() tstConf.initConfig(confPath=fncPath, dataPath=fncPath) - # GUI Scaling - # =========== - - tstConf.guiScale = 1.0 - assert tstConf.pxInt(10) == 10 - assert tstConf.pxInt(13) == 13 - assert tstConf.rpxInt(10) == 10 - assert tstConf.rpxInt(13) == 13 - - tstConf.guiScale = 2.0 - assert tstConf.pxInt(10) == 20 - assert tstConf.pxInt(13) == 26 - assert tstConf.rpxInt(10) == 5 - assert tstConf.rpxInt(13) == 6 - # Setter + Getter Combos # ====================== # Window Size - tstConf.guiScale = 1.0 tstConf.setMainWinSize(1205, 655) assert tstConf.mainWinSize == [1200, 650] - tstConf.guiScale = 2.0 - tstConf.setMainWinSize(70, 70) - assert tstConf.mainWinSize == [70, 70] - assert tstConf._mainWinSize == [35, 35] - - tstConf.guiScale = 1.0 tstConf.setMainWinSize(70, 70) assert tstConf.mainWinSize == [70, 70] assert tstConf._mainWinSize == [70, 70] @@ -281,12 +259,6 @@ def testBaseConfig_SettersGetters(fncPath): tstConf.setMainWinSize(1200, 650) # Welcome Window Size - tstConf.guiScale = 2.0 - tstConf.setWelcomeWinSize(70, 70) - assert tstConf.welcomeWinSize == [70, 70] - assert tstConf._welcomeSize == [35, 35] - - tstConf.guiScale = 1.0 tstConf.setWelcomeWinSize(70, 70) assert tstConf.welcomeWinSize == [70, 70] assert tstConf._welcomeSize == [70, 70] @@ -294,12 +266,6 @@ def testBaseConfig_SettersGetters(fncPath): tstConf.setWelcomeWinSize(800, 500) # Preferences Size - tstConf.guiScale = 2.0 - tstConf.setPreferencesWinSize(70, 70) - assert tstConf.preferencesWinSize == [70, 70] - assert tstConf._prefsWinSize == [35, 35] - - tstConf.guiScale = 1.0 tstConf.setPreferencesWinSize(70, 70) assert tstConf.preferencesWinSize == [70, 70] assert tstConf._prefsWinSize == [70, 70] @@ -307,12 +273,6 @@ def testBaseConfig_SettersGetters(fncPath): tstConf.setPreferencesWinSize(700, 615) # Main Pane Splitter - tstConf.guiScale = 2.0 - tstConf.setMainPanePos([200, 700]) - assert tstConf.mainPanePos == [200, 700] - assert tstConf._mainPanePos == [100, 350] - - tstConf.guiScale = 1.0 tstConf.setMainPanePos([200, 700]) assert tstConf.mainPanePos == [200, 700] assert tstConf._mainPanePos == [200, 700] @@ -320,12 +280,6 @@ def testBaseConfig_SettersGetters(fncPath): tstConf.setMainPanePos([300, 800]) # View Pane Splitter - tstConf.guiScale = 2.0 - tstConf.setViewPanePos([400, 250]) - assert tstConf.viewPanePos == [400, 250] - assert tstConf._viewPanePos == [200, 125] - - tstConf.guiScale = 1.0 tstConf.setViewPanePos([400, 250]) assert tstConf.viewPanePos == [400, 250] assert tstConf._viewPanePos == [400, 250] @@ -333,12 +287,6 @@ def testBaseConfig_SettersGetters(fncPath): tstConf.setViewPanePos([500, 150]) # Outline Pane Splitter - tstConf.guiScale = 2.0 - tstConf.setOutlinePanePos([400, 250]) - assert tstConf.outlinePanePos == [400, 250] - assert tstConf._outlnPanePos == [200, 125] - - tstConf.guiScale = 1.0 tstConf.setOutlinePanePos([400, 250]) assert tstConf.outlinePanePos == [400, 250] assert tstConf._outlnPanePos == [400, 250] @@ -348,18 +296,11 @@ def testBaseConfig_SettersGetters(fncPath): # Getters Only # ============ - tstConf.guiScale = 1.0 assert tstConf.getTextWidth(False) == 700 assert tstConf.getTextWidth(True) == 800 assert tstConf.getTextMargin() == 40 assert tstConf.getTabWidth() == 40 - tstConf.guiScale = 2.0 - assert tstConf.getTextWidth(False) == 1400 - assert tstConf.getTextWidth(True) == 1600 - assert tstConf.getTextMargin() == 80 - assert tstConf.getTabWidth() == 80 - @pytest.mark.base def testBaseConfig_Internal(monkeypatch, fncPath): From 029ac58a757e1c9bf5f654ecf6aa182c1468a5b3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:30:46 +0100 Subject: [PATCH 09/13] Complete GUI theme colour handling to match Qt's own --- novelwriter/dialogs/about.py | 3 +- novelwriter/gui/theme.py | 164 +++++++++++++++++++------------ novelwriter/tools/welcome.py | 2 +- tests/test_gui/test_gui_theme.py | 7 +- 4 files changed, 103 insertions(+), 73 deletions(-) diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index eaa91a8c..efc61388 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -25,7 +25,7 @@ from __future__ import annotations import logging -from PyQt6.QtGui import QCloseEvent, QColor +from PyQt6.QtGui import QCloseEvent from PyQt6.QtWidgets import ( QDialogButtonBox, QHBoxLayout, QLabel, QTextBrowser, QVBoxLayout, QWidget ) @@ -58,7 +58,6 @@ class GuiAbout(NDialog): # Logo and Banner self.nwImage = SHARED.theme.loadDecoration("nw-text", h=nwH) - self.bgColor = QColor(255, 255, 255) if SHARED.theme.isLightTheme else QColor(54, 54, 54) self.nwLogo = QLabel(self) self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (nwPx, nwPx))) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index c0a01c19..5e568253 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -71,7 +71,6 @@ class GuiTheme: self.themeUrl = "" self.themeLicense = "" self.themeLicenseUrl = "" - self.isLightTheme = True # GUI self.statNone = QColor(0, 0, 0) @@ -80,6 +79,7 @@ class GuiTheme: self.helpText = QColor(0, 0, 0) self.fadedText = QColor(0, 0, 0) self.errorText = QColor(255, 0, 0) + self.isDarkTheme = False # Loaded Syntax Settings # ====================== @@ -226,9 +226,7 @@ class GuiTheme: return False # Reset Palette - self._guiPalette = QApplication.style().standardPalette() - self._resetGuiColors() - self.iconCache.clear() + self._resetTheme() # Main sec = "Main" @@ -294,44 +292,60 @@ class GuiTheme: self.statSaved = self._parseColour(parser, sec, "statussaved") # Update Dependant Colours - backCol = self._guiPalette.window().color() - textCol = self._guiPalette.windowText().color() + # Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp + text = self._guiPalette.text().color() + window = self._guiPalette.window().color() + highlight = self._guiPalette.highlight().color() + isDark = text.lightnessF() > window.lightnessF() - # Calculate Based on Qt Fusion - light = backCol.lighter(150) - mid = backCol.darker(130) - midLight = mid.lighter(110) - dark = backCol.darker(150) - shadow = dark.darker(135) + QtColActive = QPalette.ColorGroup.Active + QtColInactive = QPalette.ColorGroup.Inactive + QtColDisabled = QPalette.ColorGroup.Disabled - self._guiPalette.setColor(QPalette.ColorRole.Light, light) - self._guiPalette.setColor(QPalette.ColorRole.Mid, mid) - self._guiPalette.setColor(QPalette.ColorRole.Midlight, midLight) - self._guiPalette.setColor(QPalette.ColorRole.Dark, dark) - self._guiPalette.setColor(QPalette.ColorRole.Shadow, shadow) + light = window.lighter(150) + mid = window.darker(130) + midLight = mid.lighter(110) + dark = window.darker(150) + shadow = dark.darker(135) + darkOff = dark.darker(150) + shadowOff = shadow.darker(150) - # Calculate Help Text - backLNess = backCol.lightnessF() - textLNess = textCol.lightnessF() - self.isLightTheme = backLNess > textLNess - if self.helpText == QColor(0, 0, 0): - if self.isLightTheme: - helpLCol = textLNess + 0.35*(backLNess - textLNess) - else: - helpLCol = backLNess + 0.65*(textLNess - backLNess) - self.helpText = QColor.fromHsl(0, 0, int(255*helpLCol)) - logger.debug( - "Computed help text colour: rgb(%d, %d, %d)", - self.helpText.red(), self.helpText.green(), self.helpText.blue() - ) + grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140) + dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) + + placeholder = text + placeholder.setAlpha(128) + + self._guiPalette.setBrush(QPalette.ColorRole.Light, light) + self._guiPalette.setBrush(QPalette.ColorRole.Mid, mid) + self._guiPalette.setBrush(QPalette.ColorRole.Midlight, midLight) + self._guiPalette.setBrush(QPalette.ColorRole.Dark, dark) + self._guiPalette.setBrush(QPalette.ColorRole.Shadow, shadow) + + self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Text, dimmed) + self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.WindowText, dimmed) + self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.ButtonText, dimmed) + self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Base, window) + self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Dark, darkOff) + self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Shadow, shadowOff) + + self._guiPalette.setBrush(QPalette.ColorRole.PlaceholderText, placeholder) + + self._guiPalette.setBrush(QtColActive, QPalette.ColorRole.Highlight, highlight) + self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Highlight, highlight) + self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Highlight, grey) + + if CONFIG.verQtValue >= 0x060600: + self._guiPalette.setBrush(QtColActive, QPalette.ColorRole.Accent, highlight) + self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight) + self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey) # Load icons after theme is parsed self.iconCache.loadTheme(CONFIG.iconTheme) - # Apply styles + # Finalise + self.isDarkTheme = isDark QApplication.setPalette(self._guiPalette) - - # Reset stylesheets so that they are regenerated self._buildStyleSheets(self._guiPalette) return True @@ -437,14 +451,55 @@ class GuiTheme: # Internal Functions ## - def _resetGuiColors(self) -> None: + def _resetTheme(self) -> None: """Reset GUI colours to default values.""" - self.statNone = QColor(120, 120, 120) - self.statUnsaved = QColor(200, 15, 39) - self.statSaved = QColor(2, 133, 37) - self.helpText = QColor(0, 0, 0) - self.fadedText = QColor(128, 128, 128) - self.errorText = QColor(255, 0, 0) + palette = QPalette() + + text = palette.color(QPalette.ColorRole.Text) + window = palette.color(QPalette.ColorRole.Window) + isDark = text.lightnessF() > window.lightnessF() + + # Reset GUI Palette + faded = QColor(128, 128, 128) + dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) + grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140) + red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41) + orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31) + yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0) + green = QColor(153, 204, 153) if isDark else QColor(113, 140, 0) + aqua = QColor(102, 204, 204) if isDark else QColor(62, 153, 159) + blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174) + purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168) + + self.statNone = grey + self.statUnsaved = red + self.statSaved = green + self.helpText = dimmed + self.fadedText = faded + self.errorText = red + + self._guiPalette = palette + + # Reset Icons + icons = self.iconCache + icons.clear() + icons.setIconColor("default", text) + icons.setIconColor("faded", faded) + icons.setIconColor("red", red) + icons.setIconColor("orange", orange) + icons.setIconColor("yellow", yellow) + icons.setIconColor("green", green) + icons.setIconColor("aqua", aqua) + icons.setIconColor("blue", blue) + icons.setIconColor("purple", purple) + icons.setIconColor("root", blue) + icons.setIconColor("folder", yellow) + icons.setIconColor("file", text) + icons.setIconColor("title", green) + icons.setIconColor("chapter", red) + icons.setIconColor("scene", blue) + icons.setIconColor("note", yellow) + return def _listConf(self, targetDict: dict, checkDir: Path) -> bool: @@ -466,7 +521,7 @@ class GuiTheme: self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole ) -> None: """Set a palette colour value from a config string.""" - self._guiPalette.setColor(value, self._parseColour(parser, section, name)) + self._guiPalette.setBrush(value, self._parseColour(parser, section, name)) return def _buildStyleSheets(self, palette: QPalette) -> None: @@ -550,29 +605,8 @@ class GuiIcons: def clear(self) -> None: """Clear the icon cache.""" - text = QApplication.palette().windowText().color() - default = text.name(QColor.NameFormat.HexRgb).encode("utf-8") - faded = self.mainTheme.fadedText.name(QColor.NameFormat.HexRgb).encode("utf-8") - self._svgData = {} - self._svgColours = { - "default": default, - "faded": faded, - "red": b"#ff0000", - "orange": b"#ff7f00", - "yellow": b"#ffff00", - "green": b"#00ff00", - "aqua": b"#00ffff", - "blue": b"#0000ff", - "purple": b"#ff00ff", - "root": b"#0000ff", - "folder": b"#ffff00", - "file": default, - "title": b"#00ff00", - "chapter": b"#ff0000", - "scene": b"#0000ff", - "note": b"#ffff00", - } + self._svgColours = {} self._qIcons = {} self._headerDec = [] self._headerDecNarrow = [] @@ -640,7 +674,7 @@ class GuiIcons: map or the icon map. This function always returns a QPixmap. """ if name in self.IMAGE_MAP: - idx = 0 if self.mainTheme.isLightTheme else 1 + idx = int(self.mainTheme.isDarkTheme) imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx] else: logger.error("Decoration with name '%s' does not exist", name) diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index 7dc2298e..88126797 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -85,7 +85,7 @@ class GuiWelcome(NDialog): self.bgImage = SHARED.theme.loadDecoration("welcome") self.nwImage = SHARED.theme.loadDecoration("nw-text", h=hD) - self.bgColor = QColor(255, 255, 255) if SHARED.theme.isLightTheme else QColor(54, 54, 54) + self.bgColor = QColor(54, 54, 54) if SHARED.theme.isDarkTheme else QColor(255, 255, 255) self.nwLogo = QLabel(self) self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (hF, hF))) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 29c96720..a3f3495b 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -166,16 +166,13 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): "[Palette]\n" "window = 0, 0, 0\n" "windowtext = 255, 255, 255\n" - "\n" - "[GUI]\n" - "helptext = 0, 0, 0\n" ) mainTheme._availThemes["test"] = mockTheme CONFIG.guiTheme = "test" assert mainTheme.loadTheme() is True - assert mainTheme.isLightTheme is False - assert mainTheme.helpText.getRgb() == (165, 165, 165, 255) + assert mainTheme.isDarkTheme is False + assert mainTheme.helpText.getRgb() == (190, 190, 190, 255) # Load Default Light Theme # ======================== From fe064c9d9918ab66dbb8928061db57e2e7cfe12b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:31:11 +0100 Subject: [PATCH 10/13] Move handling of theme meta data --- novelwriter/gui/theme.py | 157 +++++++++++++++---------------- tests/test_gui/test_gui_theme.py | 6 +- 2 files changed, 80 insertions(+), 83 deletions(-) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 5e568253..edb7824a 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -50,6 +50,17 @@ STYLES_MIN_TOOLBUTTON = "minimalToolButton" STYLES_BIG_TOOLBUTTON = "bigToolButton" +class ThemeMeta: + + name = "" + description = "" + author = "" + credit = "" + url = "" + license = "" + licenseUrl = "" + + class GuiTheme: """Gui Theme Class @@ -63,37 +74,21 @@ class GuiTheme: # Loaded Theme Settings # ===================== - # Theme - self.themeName = "" - self.themeDescription = "" - self.themeAuthor = "" - self.themeCredit = "" - self.themeUrl = "" - self.themeLicense = "" - self.themeLicenseUrl = "" + self.themeMeta = ThemeMeta() + self.isDarkTheme = False - # GUI self.statNone = QColor(0, 0, 0) self.statUnsaved = QColor(0, 0, 0) self.statSaved = QColor(0, 0, 0) self.helpText = QColor(0, 0, 0) self.fadedText = QColor(0, 0, 0) self.errorText = QColor(255, 0, 0) - self.isDarkTheme = False # Loaded Syntax Settings # ====================== - # Main - self.syntaxName = "" - self.syntaxDescription = "" - self.syntaxAuthor = "" - self.syntaxCredit = "" - self.syntaxUrl = "" - self.syntaxLicense = "" - self.syntaxLicenseUrl = "" + self.syntaxMeta = ThemeMeta() - # Colours self.colBack = QColor(255, 255, 255) self.colText = QColor(0, 0, 0) self.colLink = QColor(0, 0, 0) @@ -230,14 +225,17 @@ class GuiTheme: # Main sec = "Main" + meta = ThemeMeta() if parser.has_section(sec): - self.themeName = parser.rdStr(sec, "name", "") - self.themeDescription = parser.rdStr(sec, "description", "N/A") - self.themeAuthor = parser.rdStr(sec, "author", "N/A") - self.themeCredit = parser.rdStr(sec, "credit", "N/A") - self.themeUrl = parser.rdStr(sec, "url", "") - self.themeLicense = parser.rdStr(sec, "license", "N/A") - self.themeLicenseUrl = parser.rdStr(sec, "licenseurl", "") + meta.name = parser.rdStr(sec, "name", "") + meta.description = parser.rdStr(sec, "description", "N/A") + meta.author = parser.rdStr(sec, "author", "N/A") + meta.credit = parser.rdStr(sec, "credit", "N/A") + meta.url = parser.rdStr(sec, "url", "") + meta.license = parser.rdStr(sec, "license", "N/A") + meta.licenseUrl = parser.rdStr(sec, "licenseurl", "") + + self.themeMeta = meta # Icons sec = "Icons" @@ -365,49 +363,52 @@ class GuiTheme: logger.info("Loading syntax theme '%s'", guiSyntax) - confParser = NWConfigParser() + parser = NWConfigParser() try: with open(syntaxFile, mode="r", encoding="utf-8") as inFile: - confParser.read_file(inFile) + parser.read_file(inFile) except Exception: logger.error("Could not load syntax colours from: %s", syntaxFile) logException() return False # Main - cnfSec = "Main" - if confParser.has_section(cnfSec): - self.syntaxName = confParser.rdStr(cnfSec, "name", "") - self.syntaxDescription = confParser.rdStr(cnfSec, "description", "N/A") - self.syntaxAuthor = confParser.rdStr(cnfSec, "author", "N/A") - self.syntaxCredit = confParser.rdStr(cnfSec, "credit", "N/A") - self.syntaxUrl = confParser.rdStr(cnfSec, "url", "") - self.syntaxLicense = confParser.rdStr(cnfSec, "license", "N/A") - self.syntaxLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "") + sec = "Main" + meta = ThemeMeta() + if parser.has_section(sec): + meta.name = parser.rdStr(sec, "name", "") + meta.description = parser.rdStr(sec, "description", "N/A") + meta.author = parser.rdStr(sec, "author", "N/A") + meta.credit = parser.rdStr(sec, "credit", "N/A") + meta.url = parser.rdStr(sec, "url", "") + meta.license = parser.rdStr(sec, "license", "N/A") + meta.licenseUrl = parser.rdStr(sec, "licenseurl", "") + + self.syntaxMeta = meta # Syntax - cnfSec = "Syntax" - if confParser.has_section(cnfSec): - self.colBack = self._parseColour(confParser, cnfSec, "background") - self.colText = self._parseColour(confParser, cnfSec, "text") - self.colLink = self._parseColour(confParser, cnfSec, "link") - self.colHead = self._parseColour(confParser, cnfSec, "headertext") - self.colHeadH = self._parseColour(confParser, cnfSec, "headertag") - self.colEmph = self._parseColour(confParser, cnfSec, "emphasis") - self.colDialN = self._parseColour(confParser, cnfSec, "dialog") - self.colDialA = self._parseColour(confParser, cnfSec, "altdialog") - self.colHidden = self._parseColour(confParser, cnfSec, "hidden") - self.colNote = self._parseColour(confParser, cnfSec, "note") - self.colCode = self._parseColour(confParser, cnfSec, "shortcode") - self.colKey = self._parseColour(confParser, cnfSec, "keyword") - self.colTag = self._parseColour(confParser, cnfSec, "tag") - self.colVal = self._parseColour(confParser, cnfSec, "value") - self.colOpt = self._parseColour(confParser, cnfSec, "optional") - self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline") - self.colError = self._parseColour(confParser, cnfSec, "errorline") - self.colRepTag = self._parseColour(confParser, cnfSec, "replacetag") - self.colMod = self._parseColour(confParser, cnfSec, "modifier") - self.colMark = self._parseColour(confParser, cnfSec, "texthighlight") + sec = "Syntax" + if parser.has_section(sec): + self.colBack = self._parseColour(parser, sec, "background") + self.colText = self._parseColour(parser, sec, "text") + self.colLink = self._parseColour(parser, sec, "link") + self.colHead = self._parseColour(parser, sec, "headertext") + self.colHeadH = self._parseColour(parser, sec, "headertag") + self.colEmph = self._parseColour(parser, sec, "emphasis") + self.colDialN = self._parseColour(parser, sec, "dialog") + self.colDialA = self._parseColour(parser, sec, "altdialog") + self.colHidden = self._parseColour(parser, sec, "hidden") + self.colNote = self._parseColour(parser, sec, "note") + self.colCode = self._parseColour(parser, sec, "shortcode") + self.colKey = self._parseColour(parser, sec, "keyword") + self.colTag = self._parseColour(parser, sec, "tag") + self.colVal = self._parseColour(parser, sec, "value") + self.colOpt = self._parseColour(parser, sec, "optional") + self.colSpell = self._parseColour(parser, sec, "spellcheckline") + self.colError = self._parseColour(parser, sec, "errorline") + self.colRepTag = self._parseColour(parser, sec, "replacetag") + self.colMod = self._parseColour(parser, sec, "modifier") + self.colMark = self._parseColour(parser, sec, "texthighlight") return True @@ -416,12 +417,11 @@ class GuiTheme: if self._themeList: return self._themeList - confParser = NWConfigParser() - for themeKey, themePath in self._availThemes.items(): - logger.debug("Checking theme config for '%s'", themeKey) - themeName = _loadInternalName(confParser, themePath) - if themeName: - self._themeList.append((themeKey, themeName)) + parser = NWConfigParser() + for key, path in self._availThemes.items(): + logger.debug("Checking theme config for '%s'", key) + if name := _loadInternalName(parser, path): + self._themeList.append((key, name)) self._themeList = sorted(self._themeList, key=_sortTheme) @@ -432,12 +432,11 @@ class GuiTheme: if self._syntaxList: return self._syntaxList - confParser = NWConfigParser() - for syntaxKey, syntaxPath in self._availSyntax.items(): - logger.debug("Checking theme syntax for '%s'", syntaxKey) - syntaxName = _loadInternalName(confParser, syntaxPath) - if syntaxName: - self._syntaxList.append((syntaxKey, syntaxName)) + parser = NWConfigParser() + for key, path in self._availSyntax.items(): + logger.debug("Checking theme syntax for '%s'", key) + if name := _loadInternalName(parser, path): + self._syntaxList.append((key, name)) self._syntaxList = sorted(self._syntaxList, key=_sortTheme) @@ -597,9 +596,7 @@ class GuiIcons: self._noIcon = QIcon(str(self._iconPath / "none.svg")) # Icon Theme Meta - self.themeName = "" - self.themeAuthor = "" - self.themeLicense = "" + self.themeMeta = ThemeMeta() return @@ -610,9 +607,7 @@ class GuiIcons: self._qIcons = {} self._headerDec = [] self._headerDecNarrow = [] - self.themeName = "" - self.themeAuthor = "" - self.themeLicense = "" + self.themeMeta = ThemeMeta() return ## @@ -627,6 +622,7 @@ class GuiIcons: logger.info("Loading icon theme '%s'", iconTheme) themePath = self._iconPath / f"{iconTheme}.icons" try: + meta = ThemeMeta() with open(themePath, mode="r", encoding="utf-8") as icons: for icon in icons: bits = icon.partition("=") @@ -636,11 +632,12 @@ class GuiIcons: if key.startswith("icon:"): self._svgData[key[5:]] = value.encode("utf-8") elif key == "meta:name": - self.themeName = value + meta.name = value elif key == "meta:author": - self.themeAuthor = value + meta.author = value elif key == "meta:license": - self.themeLicense = value + meta.license = value + self.themeMeta = meta except Exception: logger.error("Could not load icon theme from: %s", themePath) logException() diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index a3f3495b..894aaefe 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -251,7 +251,7 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): assert mainTheme.loadSyntax() is True # Check some values - assert mainTheme.syntaxName == "Default Light" + assert mainTheme.syntaxMeta.name == "Default Light" assert mainTheme.colBack == QColor(255, 255, 255) assert mainTheme.colText == QColor(0, 0, 0) assert mainTheme.colLink == QColor(0, 0, 200) @@ -264,7 +264,7 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): assert mainTheme.loadSyntax() is True # Check some values - assert mainTheme.syntaxName == "Default Dark" + assert mainTheme.syntaxMeta.name == "Default Dark" assert mainTheme.colBack == QColor(42, 42, 42) assert mainTheme.colText == QColor(204, 204, 204) assert mainTheme.colLink == QColor(102, 153, 204) @@ -290,7 +290,7 @@ def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths): # Load working theme file assert iconCache.loadTheme("material_rounded_normal") is True - assert iconCache.themeName == "Material Symbols - Rounded Medium" + assert iconCache.themeMeta.name == "Material Symbols - Rounded Medium" # Load with project colour override purple = iconCache._svgColours["purple"] From 4c2185fa1e60cc1ea0abc209e4cf77136d5990d5 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jan 2025 19:30:11 +0100 Subject: [PATCH 11/13] Add custom data class decorator that uses slots --- novelwriter/types.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/novelwriter/types.py b/novelwriter/types.py index e1b05ead..e06177b7 100644 --- a/novelwriter/types.py +++ b/novelwriter/types.py @@ -23,6 +23,8 @@ along with this program. If not, see . """ from __future__ import annotations +from typing import Any, TypeVar + from PyQt6.QtCore import Qt from PyQt6.QtGui import QColor, QFont, QPainter, QTextCharFormat, QTextCursor, QTextFormat from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QHeaderView, QSizePolicy, QStyle @@ -146,3 +148,30 @@ FONT_STYLE: dict[QFont.Style, str] = { QFont.Style.StyleItalic: "italic", QFont.Style.StyleOblique: "oblique", } + +## +# Decorators and MetaClasses +## + +T_ = TypeVar("T_", bound=object) + + +def nwDataClass(cls: T_) -> T_: + """A simple data class decorator that generates slots automatically + and creates an init function to match. + """ + + def wrap(cls: T_) -> T_: + fields = tuple(a for a in dir(cls) if not a.startswith("__")) + values = {a: getattr(cls, a) for a in fields} + + def init(self: Any) -> None: + nonlocal values + for a, v in values.items(): + self.__setattr__(a, v) + + return type(cls.__class__.__name__, (object,), { # type: ignore + "__slots__": fields, "__init__": init + }) + + return wrap(cls) From 1878adf78cd24f49a7483eedcf9cc8535947126f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jan 2025 19:31:30 +0100 Subject: [PATCH 12/13] Use data classes for theme values --- novelwriter/formats/shared.py | 3 + novelwriter/gui/doceditor.py | 54 ++++++----- novelwriter/gui/dochighlight.py | 57 +++++------ novelwriter/gui/docviewer.py | 64 +++++++------ novelwriter/gui/theme.py | 148 +++++++++++++++++------------ novelwriter/tools/manussettings.py | 5 +- tests/test_gui/test_gui_guimain.py | 10 +- tests/test_gui/test_gui_theme.py | 12 +-- 8 files changed, 196 insertions(+), 157 deletions(-) diff --git a/novelwriter/formats/shared.py b/novelwriter/formats/shared.py index 549f6f19..fecafea9 100644 --- a/novelwriter/formats/shared.py +++ b/novelwriter/formats/shared.py @@ -29,6 +29,8 @@ from enum import Flag, IntEnum from PyQt6.QtGui import QColor +from novelwriter.types import nwDataClass + ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""} RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL) @@ -40,6 +42,7 @@ def stripEscape(text: str) -> str: return text +@nwDataClass class TextDocumentTheme: """Default document theme.""" diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 70ab48ec..1a4f7c4d 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -287,16 +287,18 @@ class GuiDocEditor(QPlainTextEdit): def updateSyntaxColours(self) -> None: """Update the syntax highlighting theme.""" - mainPalette = self.palette() - mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) - mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack) - mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) - self.setPalette(mainPalette) + syntax = SHARED.theme.syntaxTheme - docPalette = self.viewport().palette() - docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack) - docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) - self.viewport().setPalette(docPalette) + palette = self.palette() + palette.setColor(QPalette.ColorRole.Window, syntax.back) + palette.setColor(QPalette.ColorRole.Base, syntax.back) + palette.setColor(QPalette.ColorRole.Text, syntax.text) + self.setPalette(palette) + + palette = self.viewport().palette() + palette.setColor(QPalette.ColorRole.Base, syntax.back) + palette.setColor(QPalette.ColorRole.Text, syntax.text) + self.viewport().setPalette(palette) self.docHeader.matchColours() self.docFooter.matchColours() @@ -2032,8 +2034,9 @@ class GuiDocEditor(QPlainTextEdit): return cursor - def _makeSelection(self, mode: QTextCursor.SelectionType, - cursor: QTextCursor | None = None) -> None: + def _makeSelection( + self, mode: QTextCursor.SelectionType, cursor: QTextCursor | None = None + ) -> None: """Select text based on selection mode.""" if cursor is None: cursor = self.textCursor() @@ -2432,10 +2435,12 @@ class GuiDocToolBar(QWidget): def updateTheme(self) -> None: """Initialise GUI elements that depend on specific settings.""" - palette = QPalette() - palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) - palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) - palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) + syntax = SHARED.theme.syntaxTheme + + palette = self.palette() + palette.setColor(QPalette.ColorRole.Window, syntax.back) + palette.setColor(QPalette.ColorRole.WindowText, syntax.text) + palette.setColor(QPalette.ColorRole.Text, syntax.text) self.setPalette(palette) self.tbBoldMD.setThemeIcon("fmt_bold", "orange") @@ -2975,10 +2980,11 @@ class GuiDocEditHeader(QWidget): """Update the colours of the widget to match those of the syntax theme rather than the main GUI. """ - palette = QPalette() - palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) - palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) - palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) + syntax = SHARED.theme.syntaxTheme + palette = self.palette() + palette.setColor(QPalette.ColorRole.Window, syntax.back) + palette.setColor(QPalette.ColorRole.WindowText, syntax.text) + palette.setColor(QPalette.ColorRole.Text, syntax.text) self.setPalette(palette) self.itemTitle.setTextColors( color=palette.windowText().color(), faded=SHARED.theme.fadedText @@ -3174,10 +3180,12 @@ class GuiDocEditFooter(QWidget): """Update the colours of the widget to match those of the syntax theme rather than the main GUI. """ - palette = QPalette() - palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) - palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) - palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) + syntax = SHARED.theme.syntaxTheme + + palette = self.palette() + palette.setColor(QPalette.ColorRole.Window, syntax.back) + palette.setColor(QPalette.ColorRole.WindowText, syntax.text) + palette.setColor(QPalette.ColorRole.Text, syntax.text) self.setPalette(palette) self.statusText.setPalette(palette) diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index c20c3ecf..de9d9f27 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -91,44 +91,45 @@ class GuiDocHighlighter(QSyntaxHighlighter): rules and building the RegExes. """ logger.debug("Setting up highlighting rules") + syntax = SHARED.theme.syntaxTheme - colEmph = SHARED.theme.colEmph if CONFIG.highlightEmph else None - colBreak = QColor(SHARED.theme.colEmph) + colEmph = syntax.emph if CONFIG.highlightEmph else None + colBreak = QColor(syntax.emph) colBreak.setAlpha(64) # Create Character Formats - self._addCharFormat("text", SHARED.theme.colText) - self._addCharFormat("header1", SHARED.theme.colHead, "b", nwStyles.H_SIZES[1]) - self._addCharFormat("header2", SHARED.theme.colHead, "b", nwStyles.H_SIZES[2]) - self._addCharFormat("header3", SHARED.theme.colHead, "b", nwStyles.H_SIZES[3]) - self._addCharFormat("header4", SHARED.theme.colHead, "b", nwStyles.H_SIZES[4]) - self._addCharFormat("head1h", SHARED.theme.colHeadH, "b", nwStyles.H_SIZES[1]) - self._addCharFormat("head2h", SHARED.theme.colHeadH, "b", nwStyles.H_SIZES[2]) - self._addCharFormat("head3h", SHARED.theme.colHeadH, "b", nwStyles.H_SIZES[3]) - self._addCharFormat("head4h", SHARED.theme.colHeadH, "b", nwStyles.H_SIZES[4]) + self._addCharFormat("text", syntax.text) + self._addCharFormat("header1", syntax.head, "b", nwStyles.H_SIZES[1]) + self._addCharFormat("header2", syntax.head, "b", nwStyles.H_SIZES[2]) + self._addCharFormat("header3", syntax.head, "b", nwStyles.H_SIZES[3]) + self._addCharFormat("header4", syntax.head, "b", nwStyles.H_SIZES[4]) + self._addCharFormat("head1h", syntax.headH, "b", nwStyles.H_SIZES[1]) + self._addCharFormat("head2h", syntax.headH, "b", nwStyles.H_SIZES[2]) + self._addCharFormat("head3h", syntax.headH, "b", nwStyles.H_SIZES[3]) + self._addCharFormat("head4h", syntax.headH, "b", nwStyles.H_SIZES[4]) self._addCharFormat("bold", colEmph, "b") self._addCharFormat("italic", colEmph, "i") - self._addCharFormat("strike", SHARED.theme.colHidden, "s") - self._addCharFormat("mspaces", SHARED.theme.colError, "err") + self._addCharFormat("strike", syntax.hidden, "s") + self._addCharFormat("mspaces", syntax.error, "err") self._addCharFormat("nobreak", colBreak, "bg") - self._addCharFormat("altdialog", SHARED.theme.colDialA) - self._addCharFormat("dialog", SHARED.theme.colDialN) - self._addCharFormat("replace", SHARED.theme.colRepTag) - self._addCharFormat("hidden", SHARED.theme.colHidden) - self._addCharFormat("markup", SHARED.theme.colHidden) - self._addCharFormat("link", SHARED.theme.colLink, "u") - self._addCharFormat("note", SHARED.theme.colNote) - self._addCharFormat("code", SHARED.theme.colCode) - self._addCharFormat("keyword", SHARED.theme.colKey) - self._addCharFormat("tag", SHARED.theme.colTag, "u") - self._addCharFormat("modifier", SHARED.theme.colMod) - self._addCharFormat("value", SHARED.theme.colVal) - self._addCharFormat("optional", SHARED.theme.colOpt) + self._addCharFormat("altdialog", syntax.dialA) + self._addCharFormat("dialog", syntax.dialN) + self._addCharFormat("replace", syntax.repTag) + self._addCharFormat("hidden", syntax.hidden) + self._addCharFormat("markup", syntax.hidden) + self._addCharFormat("link", syntax.link, "u") + self._addCharFormat("note", syntax.note) + self._addCharFormat("code", syntax.code) + self._addCharFormat("keyword", syntax.key) + self._addCharFormat("tag", syntax.tag, "u") + self._addCharFormat("modifier", syntax.mod) + self._addCharFormat("value", syntax.val) + self._addCharFormat("optional", syntax.opt) self._addCharFormat("invalid", None, "err") # Cache Spell Error Format self._spellErr = QTextCharFormat() - self._spellErr.setUnderlineColor(SHARED.theme.colSpell) + self._spellErr.setUnderlineColor(syntax.spell) self._spellErr.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SpellCheckUnderline) self._txtRules.clear() @@ -450,7 +451,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): if "s" in styles: charFormat.setFontStrikeOut(True) if "err" in styles: - charFormat.setUnderlineColor(SHARED.theme.colError) + charFormat.setUnderlineColor(SHARED.theme.syntaxTheme.error) charFormat.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SpellCheckUnderline) if "bg" in styles and color is not None: charFormat.setBackground(QBrush(color, Qt.BrushStyle.SolidPattern)) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index f20a9840..bc545b6f 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -155,33 +155,35 @@ class GuiDocViewer(QTextBrowser): self.docFooter.updateFont() # Set the widget colours to match syntax theme - mainPalette = self.palette() - mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) - mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack) - mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) - self.setPalette(mainPalette) + syntax = SHARED.theme.syntaxTheme - docPalette = self.viewport().palette() - docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack) - docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) - self.viewport().setPalette(docPalette) + palette = self.palette() + palette.setColor(QPalette.ColorRole.Window, syntax.back) + palette.setColor(QPalette.ColorRole.Base, syntax.back) + palette.setColor(QPalette.ColorRole.Text, syntax.text) + self.setPalette(palette) + + palette = self.viewport().palette() + palette.setColor(QPalette.ColorRole.Base, syntax.back) + palette.setColor(QPalette.ColorRole.Text, syntax.text) + self.viewport().setPalette(palette) self.docHeader.matchColours() self.docFooter.matchColours() # Update theme colours - self._docTheme.text = SHARED.theme.colText - self._docTheme.highlight = SHARED.theme.colMark - self._docTheme.head = SHARED.theme.colHead - self._docTheme.link = SHARED.theme.colLink - self._docTheme.comment = SHARED.theme.colHidden - self._docTheme.note = SHARED.theme.colNote - self._docTheme.code = SHARED.theme.colCode - self._docTheme.modifier = SHARED.theme.colMod - self._docTheme.keyword = SHARED.theme.colKey - self._docTheme.tag = SHARED.theme.colTag - self._docTheme.optional = SHARED.theme.colOpt - self._docTheme.dialog = SHARED.theme.colDialN - self._docTheme.altdialog = SHARED.theme.colDialA + self._docTheme.text = syntax.text + self._docTheme.highlight = syntax.mark + self._docTheme.head = syntax.head + self._docTheme.link = syntax.link + self._docTheme.comment = syntax.hidden + self._docTheme.note = syntax.note + self._docTheme.code = syntax.code + self._docTheme.modifier = syntax.mod + self._docTheme.keyword = syntax.key + self._docTheme.tag = syntax.tag + self._docTheme.optional = syntax.opt + self._docTheme.dialog = syntax.dialN + self._docTheme.altdialog = syntax.dialA # Set default text margins self.document().setDocumentMargin(0) @@ -783,10 +785,11 @@ class GuiDocViewHeader(QWidget): """Update the colours of the widget to match those of the syntax theme rather than the main GUI. """ - palette = QPalette() - palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) - palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) - palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) + syntax = SHARED.theme.syntaxTheme + palette = self.palette() + palette.setColor(QPalette.ColorRole.Window, syntax.back) + palette.setColor(QPalette.ColorRole.WindowText, syntax.text) + palette.setColor(QPalette.ColorRole.Text, syntax.text) self.setPalette(palette) self.itemTitle.setTextColors( color=palette.windowText().color(), faded=SHARED.theme.fadedText @@ -970,10 +973,11 @@ class GuiDocViewFooter(QWidget): """Update the colours of the widget to match those of the syntax theme rather than the main GUI. """ - palette = QPalette() - palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) - palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) - palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) + syntax = SHARED.theme.syntaxTheme + palette = self.palette() + palette.setColor(QPalette.ColorRole.Window, syntax.back) + palette.setColor(QPalette.ColorRole.WindowText, syntax.text) + palette.setColor(QPalette.ColorRole.Text, syntax.text) self.setPalette(palette) return diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index edb7824a..9c583a80 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -41,7 +41,7 @@ from novelwriter.common import NWConfigParser, cssCol, minmax from novelwriter.constants import nwLabels from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.error import logException -from novelwriter.types import QtPaintAntiAlias, QtTransparent +from novelwriter.types import QtPaintAntiAlias, QtTransparent, nwDataClass logger = logging.getLogger(__name__) @@ -50,15 +50,41 @@ STYLES_MIN_TOOLBUTTON = "minimalToolButton" STYLES_BIG_TOOLBUTTON = "bigToolButton" +@nwDataClass class ThemeMeta: - name = "" - description = "" - author = "" - credit = "" - url = "" - license = "" - licenseUrl = "" + name: str = "" + description: str = "" + author: str = "" + credit: str = "" + url: str = "" + license: str = "" + licenseUrl: str = "" + + +@nwDataClass +class SyntaxColors: + + back: QColor = QColor(255, 255, 255) + text: QColor = QColor(0, 0, 0) + link: QColor = QColor(0, 0, 0) + head: QColor = QColor(0, 0, 0) + headH: QColor = QColor(0, 0, 0) + emph: QColor = QColor(0, 0, 0) + dialN: QColor = QColor(0, 0, 0) + dialA: QColor = QColor(0, 0, 0) + hidden: QColor = QColor(0, 0, 0) + note: QColor = QColor(0, 0, 0) + code: QColor = QColor(0, 0, 0) + key: QColor = QColor(0, 0, 0) + tag: QColor = QColor(0, 0, 0) + val: QColor = QColor(0, 0, 0) + opt: QColor = QColor(0, 0, 0) + spell: QColor = QColor(0, 0, 0) + error: QColor = QColor(0, 0, 0) + repTag: QColor = QColor(0, 0, 0) + mod: QColor = QColor(0, 0, 0) + mark: QColor = QColor(255, 255, 255, 128) class GuiTheme: @@ -67,13 +93,29 @@ class GuiTheme: Handles the look and feel of novelWriter. """ + __slots__ = ( + # Attributes + "iconCache", "themeMeta", "isDarkTheme", "statNone", "statUnsaved", + "statSaved", "helpText", "fadedText", "errorText", "syntaxMeta", + "syntaxTheme", "guiFont", "guiFontB", "guiFontBU", "guiFontSmall", + "fontPointSize", "fontPixelSize", "baseIconHeight", "baseButtonHeight", + "textNHeight", "textNWidth", "baseIconSize", "buttonIconSize", + "guiFontFixed", + + # Functions + "getIcon", "getPixmap", "getItemIcon", "getToggleIcon", + "loadDecoration", "getHeaderDecoration", "getHeaderDecorationNarrow", + + # Internal + "_guiPalette", "_themeList", "_syntaxList", "_availThemes", + "_availSyntax", "_styleSheets", + ) + def __init__(self) -> None: self.iconCache = GuiIcons(self) - # Loaded Theme Settings - # ===================== - + # GUI Theme self.themeMeta = ThemeMeta() self.isDarkTheme = False @@ -84,34 +126,9 @@ class GuiTheme: self.fadedText = QColor(0, 0, 0) self.errorText = QColor(255, 0, 0) - # Loaded Syntax Settings - # ====================== - + # Syntax Theme self.syntaxMeta = ThemeMeta() - - self.colBack = QColor(255, 255, 255) - self.colText = QColor(0, 0, 0) - self.colLink = QColor(0, 0, 0) - self.colHead = QColor(0, 0, 0) - self.colHeadH = QColor(0, 0, 0) - self.colEmph = QColor(0, 0, 0) - self.colDialN = QColor(0, 0, 0) - self.colDialA = QColor(0, 0, 0) - self.colHidden = QColor(0, 0, 0) - self.colNote = QColor(0, 0, 0) - self.colCode = QColor(0, 0, 0) - self.colKey = QColor(0, 0, 0) - self.colTag = QColor(0, 0, 0) - self.colVal = QColor(0, 0, 0) - self.colOpt = QColor(0, 0, 0) - self.colSpell = QColor(0, 0, 0) - self.colError = QColor(0, 0, 0) - self.colRepTag = QColor(0, 0, 0) - self.colMod = QColor(0, 0, 0) - self.colMark = QColor(255, 255, 255, 128) - - # Class Setup - # =========== + self.syntaxTheme = SyntaxColors() # Load Themes self._guiPalette = QPalette() @@ -384,31 +401,33 @@ class GuiTheme: meta.license = parser.rdStr(sec, "license", "N/A") meta.licenseUrl = parser.rdStr(sec, "licenseurl", "") - self.syntaxMeta = meta - # Syntax sec = "Syntax" + syntax = SyntaxColors() if parser.has_section(sec): - self.colBack = self._parseColour(parser, sec, "background") - self.colText = self._parseColour(parser, sec, "text") - self.colLink = self._parseColour(parser, sec, "link") - self.colHead = self._parseColour(parser, sec, "headertext") - self.colHeadH = self._parseColour(parser, sec, "headertag") - self.colEmph = self._parseColour(parser, sec, "emphasis") - self.colDialN = self._parseColour(parser, sec, "dialog") - self.colDialA = self._parseColour(parser, sec, "altdialog") - self.colHidden = self._parseColour(parser, sec, "hidden") - self.colNote = self._parseColour(parser, sec, "note") - self.colCode = self._parseColour(parser, sec, "shortcode") - self.colKey = self._parseColour(parser, sec, "keyword") - self.colTag = self._parseColour(parser, sec, "tag") - self.colVal = self._parseColour(parser, sec, "value") - self.colOpt = self._parseColour(parser, sec, "optional") - self.colSpell = self._parseColour(parser, sec, "spellcheckline") - self.colError = self._parseColour(parser, sec, "errorline") - self.colRepTag = self._parseColour(parser, sec, "replacetag") - self.colMod = self._parseColour(parser, sec, "modifier") - self.colMark = self._parseColour(parser, sec, "texthighlight") + syntax.back = self._parseColour(parser, sec, "background") + syntax.text = self._parseColour(parser, sec, "text") + syntax.link = self._parseColour(parser, sec, "link") + syntax.head = self._parseColour(parser, sec, "headertext") + syntax.headH = self._parseColour(parser, sec, "headertag") + syntax.emph = self._parseColour(parser, sec, "emphasis") + syntax.dialN = self._parseColour(parser, sec, "dialog") + syntax.dialA = self._parseColour(parser, sec, "altdialog") + syntax.hidden = self._parseColour(parser, sec, "hidden") + syntax.note = self._parseColour(parser, sec, "note") + syntax.code = self._parseColour(parser, sec, "shortcode") + syntax.key = self._parseColour(parser, sec, "keyword") + syntax.tag = self._parseColour(parser, sec, "tag") + syntax.val = self._parseColour(parser, sec, "value") + syntax.opt = self._parseColour(parser, sec, "optional") + syntax.spell = self._parseColour(parser, sec, "spellcheckline") + syntax.error = self._parseColour(parser, sec, "errorline") + syntax.repTag = self._parseColour(parser, sec, "replacetag") + syntax.mod = self._parseColour(parser, sec, "modifier") + syntax.mark = self._parseColour(parser, sec, "texthighlight") + + self.syntaxMeta = meta + self.syntaxTheme = syntax return True @@ -568,6 +587,11 @@ class GuiIcons: returned instead. """ + __slots__ = ( + "mainTheme", "themeMeta", "_svgData", "_svgColours", "_qIcons", + "_headerDec", "_headerDecNarrow", "_themeList", "_iconPath", "_noIcon", + ) + TOGGLE_ICON_KEYS: dict[str, tuple[str, str]] = { "bullet": ("bullet-on", "bullet-off"), "unfold": ("unfold-show", "unfold-hide"), @@ -580,6 +604,7 @@ class GuiIcons: def __init__(self, mainTheme: GuiTheme) -> None: self.mainTheme = mainTheme + self.themeMeta = ThemeMeta() # Storage self._svgData: dict[str, bytes] = {} @@ -595,9 +620,6 @@ class GuiIcons: # None Icon self._noIcon = QIcon(str(self._iconPath / "none.svg")) - # Icon Theme Meta - self.themeMeta = ThemeMeta() - return def clear(self) -> None: diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 6e0738e3..549d39d1 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -906,10 +906,11 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter): def __init__(self, document: QTextDocument) -> None: super().__init__(document) + syntax = SHARED.theme.syntaxTheme self._fmtSymbol = QTextCharFormat() - self._fmtSymbol.setForeground(SHARED.theme.colHead) + self._fmtSymbol.setForeground(syntax.head) self._fmtFormat = QTextCharFormat() - self._fmtFormat.setForeground(SHARED.theme.colEmph) + self._fmtFormat.setForeground(syntax.emph) return def highlightBlock(self, text: str) -> None: diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index fd3914ca..42cf1cb1 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -186,12 +186,12 @@ def testGuiMain_UpdateTheme(qtbot, nwGUI): mainTheme.loadSyntax() nwGUI._processConfigChanges(True, True, True, True) - syntaxBack = SHARED.theme.colBack + syntax = SHARED.theme.syntaxTheme - assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntaxBack - assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntaxBack - assert nwGUI.docViewer.palette().color(QPalette.ColorRole.Window) == syntaxBack - assert nwGUI.docViewer.docHeader.palette().color(QPalette.ColorRole.Window) == syntaxBack + assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntax.back + assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back + assert nwGUI.docViewer.palette().color(QPalette.ColorRole.Window) == syntax.back + assert nwGUI.docViewer.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back # qtbot.stop() diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 894aaefe..50acebfc 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -252,9 +252,9 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): # Check some values assert mainTheme.syntaxMeta.name == "Default Light" - assert mainTheme.colBack == QColor(255, 255, 255) - assert mainTheme.colText == QColor(0, 0, 0) - assert mainTheme.colLink == QColor(0, 0, 200) + assert mainTheme.syntaxTheme.back == QColor(255, 255, 255) + assert mainTheme.syntaxTheme.text == QColor(0, 0, 0) + assert mainTheme.syntaxTheme.link == QColor(0, 0, 200) # Load Default Dark Theme # ======================= @@ -265,9 +265,9 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): # Check some values assert mainTheme.syntaxMeta.name == "Default Dark" - assert mainTheme.colBack == QColor(42, 42, 42) - assert mainTheme.colText == QColor(204, 204, 204) - assert mainTheme.colLink == QColor(102, 153, 204) + assert mainTheme.syntaxTheme.back == QColor(42, 42, 42) + assert mainTheme.syntaxTheme.text == QColor(204, 204, 204) + assert mainTheme.syntaxTheme.link == QColor(102, 153, 204) # qtbot.stop() From b83d7ca2cce25fbde8cedef08a57c6db02ddf7f5 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Jan 2025 19:36:47 +0100 Subject: [PATCH 13/13] Fix theme test --- tests/test_gui/test_gui_theme.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 50acebfc..9c368f11 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -25,7 +25,6 @@ from pathlib import Path import pytest from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap -from PyQt6.QtWidgets import QApplication from novelwriter import CONFIG, SHARED from novelwriter.common import NWConfigParser @@ -152,7 +151,7 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): assert mainTheme.loadTheme() is True # This should load a standard palette - wCol = QApplication.style().standardPalette().color(QPalette.ColorRole.Window).getRgb() + wCol = QPalette().color(QPalette.ColorRole.Window).getRgb() assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol # Mock Dark Theme