Qt6 theming changes and editor search issue fixes (#2204)

This commit is contained in:
Veronica Berglyd Olsen
2025-01-22 22:52:59 +01:00
committed by GitHub
13 changed files with 73 additions and 110 deletions
+1 -6
View File
@@ -38,7 +38,7 @@ from urllib.parse import urljoin
from urllib.request import pathname2url
from PyQt6.QtCore import QCoreApplication, QMimeData, QUrl
from PyQt6.QtGui import QAction, QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo
from PyQt6.QtGui import QAction, QDesktopServices, QFont, QFontDatabase, QFontInfo
from PyQt6.QtWidgets import QMenu, QMenuBar, QWidget
from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst
@@ -417,11 +417,6 @@ def numberToRoman(value: int, toLower: bool = False) -> str:
# Qt Helpers
##
def cssCol(col: QColor, alpha: int | None = None) -> str:
"""Convert a QColor object to an rgba entry to use in CSS."""
return f"rgba({col.red()}, {col.green()}, {col.blue()}, {alpha or col.alpha()})"
def describeFont(font: QFont) -> str:
"""Describe a font in a way that can be displayed on the GUI."""
if isinstance(font, QFont):
+3 -3
View File
@@ -31,11 +31,11 @@ from PyQt6.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import cssCol, readTextFile
from novelwriter.common import readTextFile
from novelwriter.extensions.configlayout import NColorLabel
from novelwriter.extensions.modified import NDialog
from novelwriter.extensions.versioninfo import VersionInfoWidget
from novelwriter.types import QtAlignRightTop, QtDialogClose
from novelwriter.types import QtAlignRightTop, QtDialogClose, QtHexArgb
logger = logging.getLogger(__name__)
@@ -135,7 +135,7 @@ class GuiAbout(NDialog):
def _setStyleSheet(self) -> None:
"""Set stylesheet text document."""
baseCol = cssCol(self.palette().window().color())
baseCol = self.palette().window().color().name(QtHexArgb)
self.txtCredits.setStyleSheet(
f"QTextBrowser {{border: none; background: {baseCol};}} "
)
+20 -32
View File
@@ -42,9 +42,9 @@ from PyQt6.QtCore import (
pyqtSlot
)
from PyQt6.QtGui import (
QAction, QColor, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent,
QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent,
QShortcut, QTextBlock, QTextCursor, QTextDocument, QTextOption
QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent,
QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent, QShortcut,
QTextBlock, QTextCursor, QTextDocument, QTextOption
)
from PyQt6.QtWidgets import (
QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
@@ -100,6 +100,7 @@ class GuiDocEditor(QPlainTextEdit):
Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down,
Qt.Key.Key_PageUp, Qt.Key.Key_PageDown
)
ENTER_KEYS = (Qt.Key.Key_Return, Qt.Key.Key_Enter)
# Custom Signals
closeEditorRequest = pyqtSignal()
@@ -927,7 +928,7 @@ class GuiDocEditor(QPlainTextEdit):
* We also handle automatic scrolling here.
"""
self._lastActive = time()
if self.docSearch.anyFocus() and event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
if self.docSearch.anyFocus() and event.key() in self.ENTER_KEYS:
return
elif event == QKeySequence.StandardKey.Redo:
self.docAction(nwDocAction.REDO)
@@ -1325,6 +1326,7 @@ class GuiDocEditor(QPlainTextEdit):
self.beginSearch()
return
prevFocus = QApplication.focusWidget() or self
resS, resE = self.findAllOccurences()
if len(resS) == 0 and self._docHandle:
self.docSearch.setResultCount(0, 0)
@@ -1333,7 +1335,7 @@ class GuiDocEditor(QPlainTextEdit):
self.requestNextDocument.emit(self._docHandle, CONFIG.searchLoop)
QApplication.processEvents()
self.beginSearch()
self.setFocus()
prevFocus.setFocus()
return
cursor = self.textCursor()
@@ -1353,7 +1355,7 @@ class GuiDocEditor(QPlainTextEdit):
self.requestNextDocument.emit(self._docHandle, CONFIG.searchLoop)
QApplication.processEvents()
self.beginSearch()
self.setFocus()
prevFocus.setFocus()
return
else:
resIdx = 0 if doLoop else maxIdx
@@ -2662,10 +2664,11 @@ class GuiDocEditSearch(QFrame):
def updateTheme(self) -> None:
"""Update theme elements."""
qPalette = QApplication.palette()
self.setPalette(qPalette)
self.searchBox.setPalette(qPalette)
self.replaceBox.setPalette(qPalette)
palette = QApplication.palette()
self.setPalette(palette)
self.searchBox.setPalette(palette)
self.replaceBox.setPalette(palette)
# Set icons
self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
@@ -2682,24 +2685,6 @@ class GuiDocEditSearch(QFrame):
self.searchOpt.setStyleSheet("QToolBar {padding: 0;}")
self.showReplace.setStyleSheet("QToolButton {border: none; background: transparent;}")
# Construct Box Colours
qPalette = self.searchBox.palette()
baseCol = qPalette.base().color()
rCol = baseCol.redF() + 0.1
gCol = baseCol.greenF() - 0.1
bCol = baseCol.blueF() - 0.1
mCol = max(rCol, gCol, bCol, 1.0)
errCol = QColor()
errCol.setRedF(rCol/mCol)
errCol.setGreenF(gCol/mCol)
errCol.setBlueF(bCol/mCol)
self.rxCol = {
True: baseCol,
False: errCol
}
return
def cycleFocus(self) -> bool:
@@ -2716,7 +2701,7 @@ class GuiDocEditSearch(QFrame):
def anyFocus(self) -> bool:
"""Return True if any of the input boxes have focus."""
return self.searchBox.hasFocus() or self.replaceBox.hasFocus()
return self.hasFocus() or self.isAncestorOf(QApplication.focusWidget())
##
# Public Slots
@@ -2800,9 +2785,12 @@ class GuiDocEditSearch(QFrame):
"""Highlight the search box to indicate the search string is or
isn't valid. Take the colour from the replace box.
"""
qPalette = self.replaceBox.palette()
qPalette.setColor(QPalette.ColorRole.Base, self.rxCol[isValid])
self.searchBox.setPalette(qPalette)
palette = self.replaceBox.palette()
palette.setColor(
QPalette.ColorRole.Text,
palette.text().color() if isValid else SHARED.theme.errorText
)
self.searchBox.setPalette(palette)
return
+1 -4
View File
@@ -201,6 +201,7 @@ class GuiNovelToolBar(QWidget):
iSz = SHARED.theme.baseIconSize
self.setContentsMargins(0, 0, 0, 0)
self.setBackgroundRole(QPalette.ColorRole.Base)
self.setAutoFillBackground(True)
# Novel Selector
@@ -270,10 +271,6 @@ class GuiNovelToolBar(QWidget):
self.tbRefresh.setThemeIcon("refresh", "green")
self.tbMore.setThemeIcon("more_vertical")
qPalette = self.palette()
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
self.setPalette(qPalette)
# StyleSheets
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
self.tbNovel.setStyleSheet(buttonStyle)
+1 -4
View File
@@ -245,6 +245,7 @@ class GuiProjectToolBar(QWidget):
iSz = SHARED.theme.baseIconSize
self.setContentsMargins(0, 0, 0, 0)
self.setBackgroundRole(QPalette.ColorRole.Base)
self.setAutoFillBackground(True)
# Widget Label
@@ -351,10 +352,6 @@ class GuiProjectToolBar(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
qPalette = self.palette()
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
self.setPalette(qPalette)
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
self.tbQuick.setStyleSheet(buttonStyle)
self.tbMoveU.setStyleSheet(buttonStyle)
+9 -9
View File
@@ -35,12 +35,12 @@ from PyQt6.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import checkInt, cssCol, qtAddAction
from novelwriter.common import checkInt, qtAddAction
from novelwriter.core.coretools import DocSearch
from novelwriter.core.item import NWItem
from novelwriter.types import (
QtAlignMiddle, QtAlignRight, QtHeaderStretch, QtHeaderToContents,
QtUserRole
QtHexArgb, QtUserRole
)
logger = logging.getLogger(__name__)
@@ -71,6 +71,10 @@ class GuiProjectSearch(QWidget):
self._blocked = False
self._map: dict[str, tuple[int, float]] = {}
self.setContentsMargins(0, 0, 0, 0)
self.setBackgroundRole(QPalette.ColorRole.Base)
self.setAutoFillBackground(True)
# Header
self.viewLabel = QLabel(self.tr("Project Search"), self)
self.viewLabel.setFont(SHARED.theme.guiFontB)
@@ -138,7 +142,6 @@ class GuiProjectSearch(QWidget):
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox.setSpacing(2)
self.setAutoFillBackground(True)
self.setLayout(self.outerBox)
self.updateTheme()
@@ -152,12 +155,9 @@ class GuiProjectSearch(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
qPalette = self.palette()
colBase = cssCol(qPalette.base().color())
colFocus = cssCol(qPalette.highlight().color())
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
self.setPalette(qPalette)
palette = QApplication.palette()
colBase = palette.base().color().name(QtHexArgb)
colFocus = palette.highlight().color().name(QtHexArgb)
self.setStyleSheet(
"QToolBar {padding: 0; background: none;} "
-5
View File
@@ -28,7 +28,6 @@ import logging
from typing import TYPE_CHECKING
from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget
from novelwriter import SHARED
@@ -125,10 +124,6 @@ class GuiSideBar(QWidget):
def updateTheme(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
qPalette = self.palette()
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
self.setPalette(qPalette)
buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
self.tbProject.setStyleSheet(buttonStyle)
+9 -7
View File
@@ -37,12 +37,12 @@ from PyQt6.QtGui import (
from PyQt6.QtWidgets import QApplication
from novelwriter import CONFIG
from novelwriter.common import NWConfigParser, cssCol, minmax
from novelwriter.common import NWConfigParser, minmax
from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX
from novelwriter.constants import nwLabels
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
from novelwriter.types import QtBlack, QtPaintAntiAlias, QtTransparent
from novelwriter.types import QtBlack, QtHexArgb, QtPaintAntiAlias, QtTransparent
logger = logging.getLogger(__name__)
@@ -528,27 +528,29 @@ class GuiTheme:
"""Build default style sheets."""
self._styleSheets = {}
tCol = palette.text().color()
hCol = palette.highlight().color()
text = palette.text().color()
text.setAlpha(48)
tCol = text.name(QtHexArgb)
hCol = palette.highlight().color().name(QtHexArgb)
# Flat Tab Widget and Tab Bar:
self._styleSheets[STYLES_FLAT_TABS] = (
"QTabWidget::pane {border: 0;} "
"QTabWidget QTabBar::tab {border: 0; padding: 4px 8px;} "
f"QTabWidget QTabBar::tab:selected {{color: {cssCol(hCol)};}} "
f"QTabWidget QTabBar::tab:selected {{color: {hCol};}} "
)
# Minimal Tool Button
self._styleSheets[STYLES_MIN_TOOLBUTTON] = (
"QToolButton {padding: 2px; margin: 0; border: none; background: transparent;} "
f"QToolButton:hover {{border: none; background: {cssCol(tCol, 48)};}} "
f"QToolButton:hover {{border: none; background: {tCol};}} "
"QToolButton::menu-indicator {image: none;} "
)
# Big Tool Button
self._styleSheets[STYLES_BIG_TOOLBUTTON] = (
"QToolButton {padding: 6px; margin: 0; border: none; background: transparent;} "
f"QToolButton:hover {{border: none; background: {cssCol(tCol, 48)};}} "
f"QToolButton:hover {{border: none; background: {tCol};}} "
"QToolButton::menu-indicator {image: none;} "
)
-2
View File
@@ -1046,8 +1046,6 @@ class GuiMain(QMainWindow):
SHARED.project.tree.refreshAllItems()
if theme:
# 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()
+12 -18
View File
@@ -36,10 +36,10 @@ from PyQt6.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import cssCol, formatFileFilter, formatInt, getFileSize, openExternalPath
from novelwriter.common import formatFileFilter, formatInt, getFileSize, openExternalPath
from novelwriter.error import formatException
from novelwriter.extensions.modified import NIconToolButton, NNonBlockingDialog
from novelwriter.types import QtDialogClose
from novelwriter.types import QtDialogClose, QtHexArgb
logger = logging.getLogger(__name__)
@@ -101,7 +101,6 @@ class GuiDictionaries(NNonBlockingDialog):
# Info Box
self.infoBox = QPlainTextEdit(self)
self.infoBox.setReadOnly(True)
self.infoBox.setFixedHeight(4*SHARED.theme.fontPixelSize)
self.infoBox.setFrameStyle(QFrame.Shape.NoFrame)
# Buttons
@@ -109,21 +108,16 @@ class GuiDictionaries(NNonBlockingDialog):
self.buttonBox.rejected.connect(self.reject)
# Assemble
self.innerBox = QVBoxLayout()
self.innerBox.addWidget(self.huInfo)
self.innerBox.addLayout(self.huPathBox)
self.innerBox.addLayout(self.huAddBox)
self.innerBox.addSpacing(8)
self.innerBox.addWidget(self.inInfo)
self.innerBox.addLayout(self.inBox)
self.innerBox.addWidget(self.infoBox)
self.innerBox.setSpacing(4)
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.innerBox, 0)
self.outerBox.addStretch(1)
self.outerBox.addWidget(self.huInfo, 0)
self.outerBox.addLayout(self.huPathBox, 0)
self.outerBox.addLayout(self.huAddBox, 0)
self.outerBox.addSpacing(8)
self.outerBox.addWidget(self.inInfo, 0)
self.outerBox.addLayout(self.inBox, 0)
self.outerBox.addWidget(self.infoBox, 1)
self.outerBox.addSpacing(8)
self.outerBox.addWidget(self.buttonBox, 0)
self.outerBox.setSpacing(16)
self.setLayout(self.outerBox)
@@ -248,8 +242,8 @@ class GuiDictionaries(NNonBlockingDialog):
cursor.movePosition(QTextCursor.MoveOperation.End)
if cursor.position() > 0:
cursor.insertText("\n")
textCol = cssCol(SHARED.theme.errorText if err else self.palette().text().color())
cursor.insertHtml(f"<font style='color: {textCol}'>{text}</font>")
textCol = SHARED.theme.errorText if err else self.palette().text().color()
cursor.insertHtml(f"<font style='color: {textCol.name(QtHexArgb)}'>{text}</font>")
cursor.movePosition(QTextCursor.MoveOperation.End)
cursor.deleteChar()
self.infoBox.setTextCursor(cursor)
+8 -4
View File
@@ -40,7 +40,7 @@ from PyQt6.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import cssCol, formatInt, makeFileNameSafe, qtAddAction, qtLambda
from novelwriter.common import formatInt, makeFileNameSafe, qtAddAction, qtLambda
from novelwriter.constants import nwFiles
from novelwriter.core.coretools import ProjectBuilder
from novelwriter.enum import nwItemClass
@@ -48,7 +48,7 @@ from novelwriter.extensions.configlayout import NWrappedWidgetBox
from novelwriter.extensions.modified import NDialog, NIconToolButton, NSpinBox
from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.versioninfo import VersionInfoWidget
from novelwriter.types import QtAlignLeft, QtAlignRightTop, QtScrollAsNeeded, QtSelected
from novelwriter.types import QtAlignLeft, QtAlignRightTop, QtHexArgb, QtScrollAsNeeded, QtSelected
logger = logging.getLogger(__name__)
@@ -301,7 +301,9 @@ class _OpenProjectPage(QWidget):
self._selectFirstItem()
baseCol = cssCol(self.palette().base().color(), PANEL_ALPHA)
base = self.palette().base().color()
base.setAlpha(PANEL_ALPHA)
baseCol = base.name(QtHexArgb)
self.setStyleSheet(
f"QListView {{border: none; background: {baseCol};}} "
f"QLineEdit {{border: none; background: {baseCol}; padding: 4px;}} "
@@ -507,7 +509,9 @@ class _NewProjectPage(QWidget):
# Styles
# ======
baseCol = cssCol(self.palette().base().color(), PANEL_ALPHA)
base = self.palette().base().color()
base.setAlpha(PANEL_ALPHA)
baseCol = base.name(QtHexArgb)
self.setStyleSheet(
f"QScrollArea {{border: none; background: {baseCol};}} "
f"_NewProjectForm {{border: none; background: {baseCol};}} "
+1
View File
@@ -70,6 +70,7 @@ QtSelected = QStyle.StateFlag.State_Selected
# Qt Colour Types
QtHexRgb = QColor.NameFormat.HexRgb
QtHexArgb = QColor.NameFormat.HexArgb
# Qt Tree and Table Types
+8 -16
View File
@@ -28,18 +28,17 @@ from xml.etree import ElementTree as ET
import pytest
from PyQt6.QtCore import QMimeData, QUrl
from PyQt6.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo
from PyQt6.QtGui import QDesktopServices, QFont, QFontDatabase, QFontInfo
from novelwriter.common import (
NWConfigParser, checkBool, checkFloat, checkInt, checkIntTuple, checkPath,
checkString, checkStringNone, checkUuid, compact, cssCol,
decodeMimeHandles, describeFont, elide, encodeMimeHandles, firstFloat,
fontMatcher, formatFileFilter, formatInt, formatTime, formatTimeStamp,
formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
isItemLayout, isItemType, isListInstance, isTitleTag, jsonEncode,
makeFileNameSafe, minmax, numberToRoman, openExternalPath, readTextFile,
simplified, transferCase, uniqueCompact, xmlElement, xmlIndent, xmlSubElem,
yesNo
checkString, checkStringNone, checkUuid, compact, decodeMimeHandles,
describeFont, elide, encodeMimeHandles, firstFloat, fontMatcher,
formatFileFilter, formatInt, formatTime, formatTimeStamp, formatVersion,
fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout,
isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe,
minmax, numberToRoman, openExternalPath, readTextFile, simplified,
transferCase, uniqueCompact, xmlElement, xmlIndent, xmlSubElem, yesNo
)
from tests.mocked import causeOSError
@@ -512,13 +511,6 @@ def testBaseCommon_numberToRoman():
assert numberToRoman(999, True) == "cmxcix"
@pytest.mark.base
def testBaseCommon_cssCol():
"""Test the cssCol function."""
assert cssCol(QColor(0, 0, 0, 0)) == "rgba(0, 0, 0, 0)"
assert cssCol(QColor(10, 20, 30, 40)) == "rgba(10, 20, 30, 40)"
@pytest.mark.base
def testBaseCommon_describeFont():
"""Test the describeFont function."""