Switch to Qt6 (#2184)

This commit is contained in:
Veronica Berglyd Olsen
2025-01-12 19:40:34 +01:00
committed by GitHub
122 changed files with 934 additions and 894 deletions
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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.
+3 -4
View File
@@ -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:
+4 -5
View File
@@ -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
+1 -1
View File
@@ -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
# ================
+4 -4
View File
@@ -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__))
+21 -21
View File
@@ -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
@@ -97,7 +97,6 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
"style=",
"config=",
"data=",
"testmode",
"meminfo"
]
@@ -117,7 +116,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"
)
@@ -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
@@ -206,14 +202,14 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
"At least Python 3.10 is required, found %s" % CONFIG.verPyString
)
errorCode |= 0x04
if CONFIG.verQtValue < 0x050f00:
if CONFIG.verQtValue < 0x060000:
errorData.append(
"At least Qt5 version 5.15.0 is required, found %s" % CONFIG.verQtString
"At least Qt6 version 6.0 is required, found %s" % CONFIG.verQtString
)
errorCode |= 0x08
if CONFIG.verPyQtValue < 0x050f00:
if CONFIG.verPyQtValue < 0x060000:
errorData.append(
"At least PyQt5 version 5.15.0 is required, found %s" % CONFIG.verPyQtString
"At least PyQt6 version 6.0 is required, found %s" % CONFIG.verPyQtString
)
errorCode |= 0x10
@@ -254,19 +250,11 @@ 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()
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
@@ -274,9 +262,21 @@ 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()
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
+2 -2
View File
@@ -54,8 +54,8 @@ more contributions are listed on the project's Members page.</p>
<p>The following libraries are dependencies of novelWriter:</p>
<ul>
<li><b>Qt5</b> by Qt Company</li>
<li><b>PyQt5</b> by Riverbank Computing</li>
<li><b>Qt6</b> by Qt Company</li>
<li><b>PyQt6</b> by Riverbank Computing</li>
<li><b>Enchant</b> by Dom Lachowicz</li>
<li><b>PyEnchant</b> by Dimitri Merejkowsky</li>
</ul>
+5 -6
View File
@@ -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
@@ -434,17 +434,16 @@ 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)
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:
+24 -31
View File
@@ -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")
@@ -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
@@ -217,7 +216,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
@@ -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:
@@ -385,13 +380,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 +402,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 +412,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
@@ -429,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."""
+1 -1
View File
@@ -23,7 +23,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 (
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 (
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2 -3
View File
@@ -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
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)))
+2 -2
View File
@@ -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
)
+2 -2
View File
@@ -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
)
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
)
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
)
+3 -3
View File
@@ -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
)
+5 -5
View File
@@ -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)
+2 -2
View File
@@ -27,8 +27,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
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
)
+3 -3
View File
@@ -24,9 +24,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
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):
+3 -3
View File
@@ -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
)
+2 -2
View File
@@ -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
+5 -5
View File
@@ -25,11 +25,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
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 (
+3 -3
View File
@@ -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,
+2 -2
View File
@@ -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
+9 -8
View File
@@ -23,9 +23,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
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 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
@@ -57,11 +57,11 @@ class NSwitch(QAbstractButton):
# Properties
##
@pyqtProperty(int) # type: ignore
@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()
@@ -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
@@ -128,7 +129,7 @@ class NSwitch(QAbstractButton):
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)
+3 -3
View File
@@ -23,9 +23,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
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 (
+3 -3
View File
@@ -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
+4 -1
View File
@@ -27,7 +27,9 @@ import re
from enum import Flag, IntEnum
from PyQt5.QtGui import QColor
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."""
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+8 -10
View File
@@ -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
+39 -31
View File
@@ -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
@@ -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)
+31 -30
View File
@@ -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
)
@@ -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))
+41 -37
View File
@@ -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
@@ -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
+2 -2
View File
@@ -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
)
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
+5 -4
View File
@@ -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
@@ -1020,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)
+5 -5
View File
@@ -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
+4 -4
View File
@@ -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
)
+4 -4
View File
@@ -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
)
+3 -3
View File
@@ -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
)
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
+235 -175
View File
@@ -29,19 +29,19 @@ 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
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,30 +50,75 @@ STYLES_MIN_TOOLBUTTON = "minimalToolButton"
STYLES_BIG_TOOLBUTTON = "bigToolButton"
@nwDataClass
class ThemeMeta:
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:
"""Gui Theme Class
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
# Theme
self.themeName = ""
self.themeDescription = ""
self.themeAuthor = ""
self.themeCredit = ""
self.themeUrl = ""
self.themeLicense = ""
self.themeLicenseUrl = ""
self.isLightTheme = True
# GUI
self.statNone = QColor(0, 0, 0)
self.statUnsaved = QColor(0, 0, 0)
self.statSaved = QColor(0, 0, 0)
@@ -81,42 +126,9 @@ class GuiTheme:
self.fadedText = QColor(0, 0, 0)
self.errorText = QColor(255, 0, 0)
# Loaded Syntax Settings
# ======================
# Main
self.syntaxName = ""
self.syntaxDescription = ""
self.syntaxAuthor = ""
self.syntaxCredit = ""
self.syntaxUrl = ""
self.syntaxLicense = ""
self.syntaxLicenseUrl = ""
# Colours
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
# ===========
# Syntax Theme
self.syntaxMeta = ThemeMeta()
self.syntaxTheme = SyntaxColors()
# Load Themes
self._guiPalette = QPalette()
@@ -143,13 +155,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()
@@ -233,20 +238,21 @@ class GuiTheme:
return False
# Reset Palette
self._guiPalette = QApplication.style().standardPalette()
self._resetGuiColors()
self.iconCache.clear()
self._resetTheme()
# 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"
@@ -301,30 +307,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()
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()
)
QtColActive = QPalette.ColorGroup.Active
QtColInactive = QPalette.ColorGroup.Inactive
QtColDisabled = QPalette.ColorGroup.Disabled
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)
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
@@ -344,49 +380,54 @@ 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", "")
# 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"
syntax = SyntaxColors()
if parser.has_section(sec):
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
@@ -395,12 +436,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)
@@ -411,12 +451,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)
@@ -430,14 +469,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:
@@ -459,7 +539,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:
@@ -507,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"),
@@ -519,6 +604,7 @@ class GuiIcons:
def __init__(self, mainTheme: GuiTheme) -> None:
self.mainTheme = mainTheme
self.themeMeta = ThemeMeta()
# Storage
self._svgData: dict[str, bytes] = {}
@@ -534,44 +620,16 @@ class GuiIcons:
# None Icon
self._noIcon = QIcon(str(self._iconPath / "none.svg"))
# Icon Theme Meta
self.themeName = ""
self.themeAuthor = ""
self.themeLicense = ""
return
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 = []
self.themeName = ""
self.themeAuthor = ""
self.themeLicense = ""
self.themeMeta = ThemeMeta()
return
##
@@ -586,6 +644,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("=")
@@ -595,11 +654,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()
@@ -633,7 +693,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)
+8 -8
View File
@@ -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__
@@ -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
@@ -92,8 +91,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)
@@ -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()
+18 -10
View File
@@ -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
@@ -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:
+3 -3
View File
@@ -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
)
+2 -2
View File
@@ -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
)
+3 -3
View File
@@ -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
+9 -9
View File
@@ -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
+6 -5
View File
@@ -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,
@@ -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:
+3 -3
View File
@@ -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
+12 -24
View File
@@ -28,20 +28,19 @@ import logging
from datetime import datetime
from pathlib import Path
from PyQt5.QtCore import (
QAbstractListModel, QEvent, QModelIndex, QObject, QPoint, QSize, Qt,
pyqtSignal, pyqtSlot
from PyQt6.QtCore import (
QAbstractListModel, 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
@@ -86,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)))
@@ -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"))
@@ -591,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"))
@@ -803,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)
+8 -10
View File
@@ -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
+33 -7
View File
@@ -23,12 +23,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
from PyQt5.QtCore import Qt
from PyQt5.QtGui import (
QColor, QFont, QPainter, QTextBlockFormat, QTextCharFormat, QTextCursor,
QTextFormat
)
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QHeaderView, QSizePolicy, QStyle
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
# Qt Alignment Flags
@@ -56,7 +55,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
@@ -149,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)
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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
-1
View File
@@ -3,4 +3,3 @@ flake8-pep585
flake8-pyproject
flake8-annotations
isort
pyqt5-stubs
+1 -1
View File
@@ -1,2 +1,2 @@
pyqt5>=5.15
pyqt6>=6.0
pyenchant>=3.0.0
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -18,12 +18,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
Dependencies
Qt5
Qt6
Copyright: Qt Company
Website: <https://www.qt.io/>
License: LGPL v3 <https://www.gnu.org/licenses/lgpl-3.0.html>
PyQt5 / PyQt5-sip
PyQt6 / PyQt6-sip
Copyright: Riverbank Computing
Website: <https://www.riverbankcomputing.com/software/pyqt/>
License: GPL v3 <https://www.gnu.org/licenses/gpl-3.0.html>
+7 -7
View File
@@ -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
+10 -12
View File
@@ -28,12 +28,12 @@ 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()))
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()
@@ -172,13 +176,7 @@ def nwGUI(qtbot, monkeypatch, functionFixture):
nwGUI.show()
qtbot.wait(20)
yield nwGUI
qtbot.wait(20)
nwGUI.closeMain()
qtbot.wait(20)
return
return nwGUI
##
+2 -2
View File
@@ -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):
+6 -8
View File
@@ -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:
-59
View File
@@ -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):
+8 -8
View File
@@ -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
+100 -97
View File
@@ -23,166 +23,169 @@ 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("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)
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("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("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)
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)
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
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]
+9 -6
View File
@@ -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
@@ -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 == ""
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
@@ -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
+5 -4
View File
@@ -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
+5 -4
View File
@@ -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,
)
+4 -4
View File
@@ -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
)
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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()
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More