Fix a few minor issues with the new icon themes, and Qt6 theme support (#2190)

This commit is contained in:
Veronica Berglyd Olsen
2025-01-15 23:20:16 +01:00
committed by GitHub
15 changed files with 255 additions and 256 deletions
@@ -30,11 +30,11 @@ note = 255, 255, 0
window = 0, 0, 0 window = 0, 0, 0
windowtext = 150, 150, 150 windowtext = 150, 150, 150
base = 0, 0, 0 base = 0, 0, 0
alternatebase = 30, 20, 45 alternatebase = 40, 40, 40
text = 150, 150, 150 text = 150, 150, 150
tooltipbase = 40, 20, 70 tooltipbase = 40, 20, 70
tooltiptext = 255, 255, 255 tooltiptext = 255, 255, 255
button = 5, 0, 10 button = 5, 0, 10
buttontext = 150, 150, 150 buttontext = 150, 150, 150
brighttext = 255, 255, 255 brighttext = 255, 255, 255
highlight = 50, 30, 80 highlight = 50, 30, 80
@@ -45,7 +45,4 @@ linkvisited = 50, 0, 80
[GUI] [GUI]
helptext = 97, 97, 97 helptext = 97, 97, 97
fadedtext = 97, 97, 97 fadedtext = 97, 97, 97
errortext = 255, 77, 77 errortext = 242, 72, 23
statusnone = 50, 50, 50
statussaved = 77, 255, 77
statusunsaved = 255, 77, 77
@@ -47,6 +47,3 @@ linkvisited = 102, 153, 204
helptext = 164, 164, 164 helptext = 164, 164, 164
fadedtext = 148, 148, 148 fadedtext = 148, 148, 148
errortext = 255, 164, 164 errortext = 255, 164, 164
statusnone = 150, 152, 150
statussaved = 39, 135, 78
statusunsaved = 138, 32, 32
+1 -4
View File
@@ -31,7 +31,7 @@ note = 234, 183, 0
window = 239, 239, 239 window = 239, 239, 239
windowtext = 0, 0, 0 windowtext = 0, 0, 0
base = 255, 255, 255 base = 255, 255, 255
alternatebase = 239, 239, 239 alternatebase = 224, 224, 224
text = 0, 0, 0 text = 0, 0, 0
tooltipbase = 255, 255, 220 tooltipbase = 255, 255, 220
tooltiptext = 0, 0, 0 tooltiptext = 0, 0, 0
@@ -47,6 +47,3 @@ linkvisited = 66, 113, 174
helptext = 92, 92, 92 helptext = 92, 92, 92
fadedtext = 108, 108, 108 fadedtext = 108, 108, 108
errortext = 255, 92, 92 errortext = 255, 92, 92
statusnone = 120, 120, 120
statussaved = 2, 133, 37
statusunsaved = 200, 15, 39
-3
View File
@@ -62,6 +62,3 @@ linkvisited = 139, 233, 253
helptext = 204, 172, 249 helptext = 204, 172, 249
fadedtext = 98, 114, 164 fadedtext = 98, 114, 164
errortext = 255, 85, 85 errortext = 255, 85, 85
statusnone = 98, 114, 164
statussaved = 80, 250, 123
statusunsaved = 255, 85, 85
@@ -15,7 +15,7 @@ yellow = 181, 137, 0
green = 133, 153, 0 green = 133, 153, 0
aqua = 42, 161, 152 aqua = 42, 161, 152
blue = 38, 139, 210 blue = 38, 139, 210
purple = 108, 113, 196 purple = 211, 54, 130
[Project] [Project]
root = 42, 161, 152 root = 42, 161, 152
@@ -30,7 +30,7 @@ note = 181, 137, 0
window = 0, 43, 54 window = 0, 43, 54
windowtext = 253, 246, 227 windowtext = 253, 246, 227
base = 7, 54, 66 base = 7, 54, 66
alternatebase = 0, 43, 54 alternatebase = 88, 110, 117
text = 253, 246, 227 text = 253, 246, 227
tooltipbase = 133, 153, 0 tooltipbase = 133, 153, 0
tooltiptext = 0, 43, 54 tooltiptext = 0, 43, 54
@@ -43,9 +43,6 @@ link = 38, 139, 210
linkvisited = 38, 139, 210 linkvisited = 38, 139, 210
[GUI] [GUI]
helptext = 166, 161, 149 helptext = 101, 123, 131
fadedtext = 166, 161, 149 fadedtext = 101, 123, 131
errortext = 255, 161, 149 errortext = 220, 50, 47
statusnone = 88, 110, 117
statussaved = 42, 161, 152
statusunsaved = 203, 75, 22
@@ -15,7 +15,7 @@ yellow = 181, 137, 0
green = 133, 153, 0 green = 133, 153, 0
aqua = 42, 161, 152 aqua = 42, 161, 152
blue = 38, 139, 210 blue = 38, 139, 210
purple = 108, 113, 196 purple = 211, 54, 130
[Project] [Project]
root = 42, 161, 152 root = 42, 161, 152
@@ -30,7 +30,7 @@ note = 181, 137, 0
window = 238, 232, 213 window = 238, 232, 213
windowtext = 0, 43, 54 windowtext = 0, 43, 54
base = 253, 246, 227 base = 253, 246, 227
alternatebase = 238, 232, 213 alternatebase = 147, 161, 161
text = 0, 43, 54 text = 0, 43, 54
tooltipbase = 133, 153, 0 tooltipbase = 133, 153, 0
tooltiptext = 0, 43, 54 tooltiptext = 0, 43, 54
@@ -45,7 +45,4 @@ linkvisited = 38, 139, 210
[GUI] [GUI]
helptext = 78, 91, 95 helptext = 78, 91, 95
fadedtext = 78, 91, 95 fadedtext = 78, 91, 95
errortext = 255, 91, 95 errortext = 220, 50, 47
statusnone = 88, 110, 117
statussaved = 42, 161, 152
statusunsaved = 203, 75, 22
+18 -12
View File
@@ -49,6 +49,11 @@ from novelwriter.error import formatException, logException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DEF_GUI = "default"
DEF_SYNTAX = "default_light"
DEF_ICONS = "material_rounded_normal"
DEF_TREECOL = "theme"
class Config: class Config:
@@ -137,18 +142,18 @@ class Config:
# General GUI Settings # General GUI Settings
self.guiLocale = self._qLocale.name() self.guiLocale = self._qLocale.name()
self.guiTheme = "default" # GUI theme self.guiTheme = DEF_GUI # GUI theme
self.guiSyntax = "default_light" # Syntax theme self.guiSyntax = DEF_SYNTAX # Syntax theme
self.guiFont = QFont() # Main GUI font self.guiFont = QFont() # Main GUI font
self.hideVScroll = False # Hide vertical scroll bars on main widgets self.hideVScroll = False # Hide vertical scroll bars on main widgets
self.hideHScroll = False # Hide horizontal 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 self.lastNotes = "0x0" # The latest release notes that have been shown
self.nativeFont = True # Use native font dialog self.nativeFont = True # Use native font dialog
# Icons # Icons
self.iconTheme = "material_rounded_normal" # Icons theme self.iconTheme = DEF_ICONS # Icons theme
self.iconColTree = "theme" # Project tree icon colours self.iconColTree = DEF_TREECOL # Project tree icon colours
self.iconColDocs = False # Keep theme colours on documents self.iconColDocs = False # Keep theme colours on documents
# Size Settings # Size Settings
self.mainWinSize = [1200, 650] # Last size of the main GUI window self.mainWinSize = [1200, 650] # Last size of the main GUI window
@@ -477,8 +482,9 @@ class Config:
# Config Actions # Config Actions
## ##
def initConfig(self, confPath: str | Path | None = None, def initConfig(
dataPath: str | Path | None = None) -> None: self, confPath: str | Path | None = None, dataPath: str | Path | None = None
) -> None:
"""Initialise the config class. The manual setting of confPath """Initialise the config class. The manual setting of confPath
and dataPath is mainly intended for the test suite. and dataPath is mainly intended for the test suite.
""" """
+1 -1
View File
@@ -52,7 +52,7 @@ class GuiAbout(NDialog):
self.resize(700, 500) self.resize(700, 500)
# Logo and Banner # Logo and Banner
self.nwImage = SHARED.theme.loadDecoration("nw-text", h=36) self.nwImage = SHARED.theme.getDecoration("nw-text", h=36)
self.nwLogo = QLabel(self) self.nwLogo = QLabel(self)
self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (128, 128))) self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (128, 128)))
+5 -4
View File
@@ -35,6 +35,7 @@ from PyQt6.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import compact, describeFont, uniqueCompact from novelwriter.common import compact, describeFont, uniqueCompact
from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL
from novelwriter.constants import nwLabels, nwUnicode, trConst from novelwriter.constants import nwLabels, nwUnicode, trConst
from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm
@@ -167,7 +168,7 @@ class GuiPreferences(NDialog):
self.guiTheme.setMinimumWidth(200) self.guiTheme.setMinimumWidth(200)
for theme, name in SHARED.theme.listThemes(): for theme, name in SHARED.theme.listThemes():
self.guiTheme.addItem(name, theme) self.guiTheme.addItem(name, theme)
self.guiTheme.setCurrentData(CONFIG.guiTheme, "default") self.guiTheme.setCurrentData(CONFIG.guiTheme, DEF_GUI)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Colour theme"), self.guiTheme, self.tr("Colour theme"), self.guiTheme,
@@ -179,7 +180,7 @@ class GuiPreferences(NDialog):
self.iconTheme.setMinimumWidth(200) self.iconTheme.setMinimumWidth(200)
for theme, name in SHARED.theme.iconCache.listThemes(): for theme, name in SHARED.theme.iconCache.listThemes():
self.iconTheme.addItem(name, theme) self.iconTheme.addItem(name, theme)
self.iconTheme.setCurrentData(CONFIG.iconTheme, "material_rounded_bold") self.iconTheme.setCurrentData(CONFIG.iconTheme, DEF_ICONS)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Icon theme"), self.iconTheme, self.tr("Icon theme"), self.iconTheme,
@@ -191,7 +192,7 @@ class GuiPreferences(NDialog):
self.iconColTree.setMinimumWidth(200) self.iconColTree.setMinimumWidth(200)
for key, label in nwLabels.THEME_COLORS.items(): for key, label in nwLabels.THEME_COLORS.items():
self.iconColTree.addItem(trConst(label), key) self.iconColTree.addItem(trConst(label), key)
self.iconColTree.setCurrentData(CONFIG.iconColTree, "theme") self.iconColTree.setCurrentData(CONFIG.iconColTree, DEF_TREECOL)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Project tree icon colours"), self.iconColTree, self.tr("Project tree icon colours"), self.iconColTree,
@@ -257,7 +258,7 @@ class GuiPreferences(NDialog):
self.guiSyntax.setMinimumWidth(200) self.guiSyntax.setMinimumWidth(200)
for syntax, name in SHARED.theme.listSyntax(): for syntax, name in SHARED.theme.listSyntax():
self.guiSyntax.addItem(name, syntax) self.guiSyntax.addItem(name, syntax)
self.guiSyntax.setCurrentData(CONFIG.guiSyntax, "default_light") self.guiSyntax.setCurrentData(CONFIG.guiSyntax, DEF_SYNTAX)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Document colour theme"), self.guiSyntax, self.tr("Document colour theme"), self.guiSyntax,
+8 -19
View File
@@ -88,31 +88,20 @@ class NSwitch(QAbstractButton):
def paintEvent(self, event: QPaintEvent) -> None: def paintEvent(self, event: QPaintEvent) -> None:
"""Drawing the switch itself.""" """Drawing the switch itself."""
palette = self.palette()
painter = QPainter(self) painter = QPainter(self)
painter.setRenderHint(QtPaintAntiAlias, True) painter.setRenderHint(QtPaintAntiAlias, True)
painter.setPen(QtNoPen) painter.setOpacity(1.0 if self.isEnabled() else 0.5)
palette = self.palette() painter.setPen(palette.mid().color())
if self.isChecked(): painter.setBrush(palette.highlight() if self.isChecked() else palette.alternateBase())
trackBrush = palette.highlight()
thumbBrush = palette.highlightedText()
else:
trackBrush = palette.midlight()
thumbBrush = palette.light()
if self.isEnabled():
trackOpacity = 1.0
else:
trackOpacity = 0.6
trackBrush = palette.mid()
thumbBrush = palette.mid()
painter.setBrush(trackBrush)
painter.setOpacity(trackOpacity)
painter.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR) painter.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR)
painter.setBrush(thumbBrush) painter.setPen(QtNoPen)
painter.setBrush(palette.highlightedText())
painter.drawEllipse(self._offset - self._rR, 2, self._rH, self._rH) painter.drawEllipse(self._offset - self._rR, 2, self._rH, self._rH)
painter.end() painter.end()
return return
+3 -3
View File
@@ -133,9 +133,9 @@ class GuiMainStatus(QStatusBar):
self.idlePixmap = SHARED.theme.getPixmap("timer_off", (iPx, iPx)) self.idlePixmap = SHARED.theme.getPixmap("timer_off", (iPx, iPx))
self.timeIcon.setPixmap(self.timePixmap) self.timeIcon.setPixmap(self.timePixmap)
colNone = SHARED.theme.statNone colNone = SHARED.theme.getIconColor("default").darker(150)
colSaved = SHARED.theme.statSaved colSaved = SHARED.theme.getIconColor("green").darker(150)
colUnsaved = SHARED.theme.statUnsaved colUnsaved = SHARED.theme.getIconColor("red").darker(150)
self.docIcon.setColors(colNone, colSaved, colUnsaved) self.docIcon.setColors(colNone, colSaved, colUnsaved)
self.projIcon.setColors(colNone, colSaved, colUnsaved) self.projIcon.setColors(colNone, colSaved, colUnsaved)
+162 -152
View File
@@ -38,10 +38,11 @@ from PyQt6.QtWidgets import QApplication
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.common import NWConfigParser, cssCol, minmax from novelwriter.common import NWConfigParser, cssCol, minmax
from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.types import QtPaintAntiAlias, QtTransparent from novelwriter.types import QtBlack, QtPaintAntiAlias, QtTransparent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -93,20 +94,17 @@ class GuiTheme:
__slots__ = ( __slots__ = (
# Attributes # Attributes
"iconCache", "themeMeta", "isDarkTheme", "statNone", "statUnsaved", "iconCache", "themeMeta", "isDarkTheme", "helpText", "fadedText", "errorText",
"statSaved", "helpText", "fadedText", "errorText", "syntaxMeta", "syntaxMeta", "syntaxTheme", "guiFont", "guiFontB", "guiFontBU", "guiFontSmall",
"syntaxTheme", "guiFont", "guiFontB", "guiFontBU", "guiFontSmall", "fontPointSize", "fontPixelSize", "baseIconHeight", "baseButtonHeight", "textNHeight",
"fontPointSize", "fontPixelSize", "baseIconHeight", "baseButtonHeight", "textNWidth", "baseIconSize", "buttonIconSize", "guiFontFixed",
"textNHeight", "textNWidth", "baseIconSize", "buttonIconSize",
"guiFontFixed",
# Functions # Functions
"getIcon", "getPixmap", "getItemIcon", "getToggleIcon", "getIcon", "getPixmap", "getItemIcon", "getIconColor", "getToggleIcon", "getDecoration",
"loadDecoration", "getHeaderDecoration", "getHeaderDecorationNarrow", "getHeaderDecoration", "getHeaderDecorationNarrow",
# Internal # Internal
"_guiPalette", "_themeList", "_syntaxList", "_availThemes", "_guiPalette", "_themeList", "_syntaxList", "_availThemes", "_availSyntax", "_styleSheets",
"_availSyntax", "_styleSheets",
) )
def __init__(self) -> None: def __init__(self) -> None:
@@ -117,12 +115,10 @@ class GuiTheme:
self.themeMeta = ThemeMeta() self.themeMeta = ThemeMeta()
self.isDarkTheme = False self.isDarkTheme = False
self.statNone = QColor(0, 0, 0) # Special Text Colours
self.statUnsaved = QColor(0, 0, 0) self.helpText = QColor(0, 0, 0)
self.statSaved = QColor(0, 0, 0) self.fadedText = QColor(0, 0, 0)
self.helpText = QColor(0, 0, 0) self.errorText = QColor(255, 0, 0)
self.fadedText = QColor(0, 0, 0)
self.errorText = QColor(255, 0, 0)
# Syntax Theme # Syntax Theme
self.syntaxMeta = ThemeMeta() self.syntaxMeta = ThemeMeta()
@@ -136,10 +132,10 @@ class GuiTheme:
self._availSyntax: dict[str, Path] = {} self._availSyntax: dict[str, Path] = {}
self._styleSheets: dict[str, str] = {} self._styleSheets: dict[str, str] = {}
self._listConf(self._availSyntax, CONFIG.assetPath("syntax")) _listConf(self._availSyntax, CONFIG.assetPath("syntax"), ".conf")
self._listConf(self._availThemes, CONFIG.assetPath("themes")) _listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf")
self._listConf(self._availSyntax, CONFIG.dataPath("syntax")) _listConf(self._availSyntax, CONFIG.dataPath("syntax"), ".conf")
self._listConf(self._availThemes, CONFIG.dataPath("themes")) _listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf")
self.loadTheme() self.loadTheme()
self.loadSyntax() self.loadSyntax()
@@ -148,8 +144,9 @@ class GuiTheme:
self.getIcon = self.iconCache.getIcon self.getIcon = self.iconCache.getIcon
self.getPixmap = self.iconCache.getPixmap self.getPixmap = self.iconCache.getPixmap
self.getItemIcon = self.iconCache.getItemIcon self.getItemIcon = self.iconCache.getItemIcon
self.getIconColor = self.iconCache.getIconColor
self.getToggleIcon = self.iconCache.getToggleIcon self.getToggleIcon = self.iconCache.getToggleIcon
self.loadDecoration = self.iconCache.loadDecoration self.getDecoration = self.iconCache.getDecoration
self.getHeaderDecoration = self.iconCache.getHeaderDecoration self.getHeaderDecoration = self.iconCache.getHeaderDecoration
self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow
@@ -213,25 +210,23 @@ class GuiTheme:
def loadTheme(self) -> bool: def loadTheme(self) -> bool:
"""Load the currently specified GUI theme.""" """Load the currently specified GUI theme."""
guiTheme = CONFIG.guiTheme theme = CONFIG.guiTheme
if guiTheme not in self._availThemes: if theme not in self._availThemes:
logger.error("Could not find GUI theme '%s'", guiTheme) logger.error("Could not find GUI theme '%s'", theme)
guiTheme = "default" theme = DEF_GUI
CONFIG.guiTheme = guiTheme CONFIG.guiTheme = theme
themeFile = self._availThemes.get(guiTheme, None) if not (file := self._availThemes.get(theme)):
if themeFile is None:
logger.error("Could not load GUI theme") logger.error("Could not load GUI theme")
return False return False
# Config File logger.info("Loading GUI theme '%s'", theme)
logger.info("Loading GUI theme '%s'", guiTheme)
parser = NWConfigParser() parser = NWConfigParser()
try: try:
with open(themeFile, mode="r", encoding="utf-8") as inFile: with open(file, mode="r", encoding="utf-8") as fo:
parser.read_file(inFile) parser.read_file(fo)
except Exception: except Exception:
logger.error("Could not load theme settings from: %s", themeFile) logger.error("Could not read file: %s", file)
logException() logException()
return False return False
@@ -297,12 +292,9 @@ class GuiTheme:
# GUI # GUI
sec = "GUI" sec = "GUI"
if parser.has_section(sec): if parser.has_section(sec):
self.helpText = self._parseColour(parser, sec, "helptext") self.helpText = self._parseColour(parser, sec, "helptext")
self.fadedText = self._parseColour(parser, sec, "fadedtext") self.fadedText = self._parseColour(parser, sec, "fadedtext")
self.errorText = self._parseColour(parser, sec, "errortext") self.errorText = self._parseColour(parser, sec, "errortext")
self.statNone = self._parseColour(parser, sec, "statusnone")
self.statUnsaved = self._parseColour(parser, sec, "statusunsaved")
self.statSaved = self._parseColour(parser, sec, "statussaved")
# Update Dependant Colours # Update Dependant Colours
# Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp # Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp
@@ -315,18 +307,24 @@ class GuiTheme:
QtColInactive = QPalette.ColorGroup.Inactive QtColInactive = QPalette.ColorGroup.Inactive
QtColDisabled = QPalette.ColorGroup.Disabled QtColDisabled = QPalette.ColorGroup.Disabled
light = window.lighter(150) if window.lightnessF() < 0.15:
mid = window.darker(130) # If window is too dark, we need a lighter ref colour for shades
ref = QColor.fromHslF(window.hueF(), window.saturationF(), 0.15, window.alphaF())
else:
ref = window
light = ref.lighter(150)
mid = ref.darker(130)
midLight = mid.lighter(110) midLight = mid.lighter(110)
dark = window.darker(150) dark = ref.darker(150)
shadow = dark.darker(135) shadow = dark.darker(135)
darkOff = dark.darker(150) darkOff = dark.darker(150)
shadowOff = shadow.darker(150) shadowOff = ref.darker(150)
grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140) grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140)
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
placeholder = text placeholder = QColor(text)
placeholder.setAlpha(128) placeholder.setAlpha(128)
self._guiPalette.setBrush(QPalette.ColorRole.Light, light) self._guiPalette.setBrush(QPalette.ColorRole.Light, light)
@@ -353,7 +351,7 @@ class GuiTheme:
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight) self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight)
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey) self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey)
# Load icons after theme is parsed # Load icons after the theme is parsed
self.iconCache.loadTheme(CONFIG.iconTheme) self.iconCache.loadTheme(CONFIG.iconTheme)
# Finalise # Finalise
@@ -365,25 +363,23 @@ class GuiTheme:
def loadSyntax(self) -> bool: def loadSyntax(self) -> bool:
"""Load the currently specified syntax highlighter theme.""" """Load the currently specified syntax highlighter theme."""
guiSyntax = CONFIG.guiSyntax theme = CONFIG.guiSyntax
if guiSyntax not in self._availSyntax: if theme not in self._availSyntax:
logger.error("Could not find syntax theme '%s'", guiSyntax) logger.error("Could not find syntax theme '%s'", theme)
guiSyntax = "default_light" theme = DEF_SYNTAX
CONFIG.guiSyntax = guiSyntax CONFIG.guiSyntax = theme
syntaxFile = self._availSyntax.get(guiSyntax, None) if not (file := self._availSyntax.get(theme)):
if syntaxFile is None:
logger.error("Could not load syntax theme") logger.error("Could not load syntax theme")
return False return False
logger.info("Loading syntax theme '%s'", guiSyntax) logger.info("Loading syntax theme '%s'", theme)
parser = NWConfigParser() parser = NWConfigParser()
try: try:
with open(syntaxFile, mode="r", encoding="utf-8") as inFile: with open(file, mode="r", encoding="utf-8") as fo:
parser.read_file(inFile) parser.read_file(fo)
except Exception: except Exception:
logger.error("Could not load syntax colours from: %s", syntaxFile) logger.error("Could not read file: %s", file)
logException() logException()
return False return False
@@ -434,13 +430,14 @@ class GuiTheme:
if self._themeList: if self._themeList:
return self._themeList return self._themeList
themes = []
parser = NWConfigParser() parser = NWConfigParser()
for key, path in self._availThemes.items(): for key, path in self._availThemes.items():
logger.debug("Checking theme config for '%s'", key) logger.debug("Checking theme config '%s'", key)
if name := _loadInternalName(parser, path): if name := _loadInternalName(parser, path):
self._themeList.append((key, name)) themes.append((key, name))
self._themeList = sorted(self._themeList, key=_sortTheme) self._themeList = sorted(themes, key=_sortTheme)
return self._themeList return self._themeList
@@ -449,13 +446,14 @@ class GuiTheme:
if self._syntaxList: if self._syntaxList:
return self._syntaxList return self._syntaxList
themes = []
parser = NWConfigParser() parser = NWConfigParser()
for key, path in self._availSyntax.items(): for key, path in self._availSyntax.items():
logger.debug("Checking theme syntax for '%s'", key) logger.debug("Checking theme syntax '%s'", key)
if name := _loadInternalName(parser, path): if name := _loadInternalName(parser, path):
self._syntaxList.append((key, name)) themes.append((key, name))
self._syntaxList = sorted(self._syntaxList, key=_sortTheme) self._syntaxList = sorted(themes, key=_sortTheme)
return self._syntaxList return self._syntaxList
@@ -478,7 +476,6 @@ class GuiTheme:
# Reset GUI Palette # Reset GUI Palette
faded = QColor(128, 128, 128) faded = QColor(128, 128, 128)
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) 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) red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41)
orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31) orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31)
yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0) yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0)
@@ -487,12 +484,10 @@ class GuiTheme:
blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174) blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174)
purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168) purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168)
self.statNone = grey # Text Colours
self.statUnsaved = red self.helpText = dimmed
self.statSaved = green self.fadedText = faded
self.helpText = dimmed self.errorText = red
self.fadedText = faded
self.errorText = red
self._guiPalette = palette self._guiPalette = palette
@@ -518,17 +513,6 @@ class GuiTheme:
return return
def _listConf(self, targetDict: dict, checkDir: Path) -> bool:
"""Scan for theme config files and populate the dictionary."""
if not checkDir.is_dir():
return False
for checkFile in checkDir.iterdir():
if checkFile.is_file() and checkFile.name.endswith(".conf"):
targetDict[checkFile.name[:-5]] = checkFile
return True
def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> QColor: def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> QColor:
"""Parse a colour value from a config string.""" """Parse a colour value from a config string."""
return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255])) return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255]))
@@ -581,8 +565,9 @@ class GuiIcons:
""" """
__slots__ = ( __slots__ = (
"mainTheme", "themeMeta", "_svgData", "_svgColours", "_qIcons", "mainTheme", "themeMeta", "_svgData", "_svgColors", "_qColors",
"_headerDec", "_headerDecNarrow", "_themeList", "_iconPath", "_noIcon", "_qIcons", "_headerDec", "_headerDecNarrow", "_availThemes",
"_themeList", "_noIcon",
) )
TOGGLE_ICON_KEYS: dict[str, tuple[str, str]] = { TOGGLE_ICON_KEYS: dict[str, tuple[str, str]] = {
@@ -601,24 +586,29 @@ class GuiIcons:
# Storage # Storage
self._svgData: dict[str, bytes] = {} self._svgData: dict[str, bytes] = {}
self._svgColours: dict[str, bytes] = {} self._svgColors: dict[str, bytes] = {}
self._qColors: dict[str, QColor] = {}
self._qIcons: dict[str, QIcon] = {} self._qIcons: dict[str, QIcon] = {}
self._headerDec: list[QPixmap] = [] self._headerDec: list[QPixmap] = []
self._headerDecNarrow: list[QPixmap] = [] self._headerDecNarrow: list[QPixmap] = []
# Icon Theme Path # Icon Theme Path
self._availThemes: dict[str, Path] = {}
self._themeList: list[tuple[str, str]] = [] self._themeList: list[tuple[str, str]] = []
self._iconPath = CONFIG.assetPath("icons")
# None Icon # None Icon
self._noIcon = QIcon(str(self._iconPath / "none.svg")) self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg"))
_listConf(self._availThemes, CONFIG.assetPath("icons"), ".icons")
_listConf(self._availThemes, CONFIG.dataPath("icons"), ".icons")
return return
def clear(self) -> None: def clear(self) -> None:
"""Clear the icon cache.""" """Clear the icon cache."""
self._svgData = {} self._svgData = {}
self._svgColours = {} self._svgColors = {}
self._qColors = {}
self._qIcons = {} self._qIcons = {}
self._headerDec = [] self._headerDec = []
self._headerDecNarrow = [] self._headerDecNarrow = []
@@ -629,16 +619,24 @@ class GuiIcons:
# Actions # Actions
## ##
def loadTheme(self, iconTheme: str) -> bool: def loadTheme(self, theme: str) -> bool:
"""Update the theme map. This is more of an init, since many of """Update the theme map. This is more of an init, since many of
the GUI icons cannot really be replaced without writing specific the GUI icons cannot really be replaced without writing specific
update functions for the classes where they're used. update functions for the classes where they're used.
""" """
logger.info("Loading icon theme '%s'", iconTheme) if theme not in self._availThemes:
themePath = self._iconPath / f"{iconTheme}.icons" logger.error("Could not find icon theme '%s'", theme)
theme = DEF_ICONS
CONFIG.iconTheme = theme
if not (file := self._availThemes.get(theme)):
logger.error("Could not load icon theme")
return False
logger.info("Loading icon theme '%s'", theme)
try: try:
meta = ThemeMeta() meta = ThemeMeta()
with open(themePath, mode="r", encoding="utf-8") as icons: with open(file, mode="r", encoding="utf-8") as icons:
for icon in icons: for icon in icons:
bits = icon.partition("=") bits = icon.partition("=")
key = bits[0].strip() key = bits[0].strip()
@@ -654,58 +652,37 @@ class GuiIcons:
meta.license = value meta.license = value
self.themeMeta = meta self.themeMeta = meta
except Exception: except Exception:
logger.error("Could not load icon theme from: %s", themePath) logger.error("Could not read file: %s", file)
logException() logException()
return False return False
# Set colour overrides for project item icons # Set colour overrides for project item icons
if (override := CONFIG.iconColTree) != "theme": if (override := CONFIG.iconColTree) != "theme":
color = self._svgColours.get(override, b"#000000") color = self._svgColors.get(override, b"#000000")
self._svgColours["root"] = color self._svgColors["root"] = color
self._svgColours["folder"] = color self._svgColors["folder"] = color
if not CONFIG.iconColDocs: if not CONFIG.iconColDocs:
self._svgColours["file"] = color self._svgColors["file"] = color
self._svgColours["title"] = color self._svgColors["title"] = color
self._svgColours["chapter"] = color self._svgColors["chapter"] = color
self._svgColours["scene"] = color self._svgColors["scene"] = color
self._svgColours["note"] = color self._svgColors["note"] = color
return True return True
def setIconColor(self, key: str, color: QColor) -> None: def setIconColor(self, key: str, color: QColor) -> None:
"""Set an icon colour for a named colour.""" """Set an icon colour for a named colour."""
self._svgColours[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8") self._qColors[key] = QColor(color)
self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
return return
## ##
# Access Functions # Access Functions
## ##
def loadDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap: def getIconColor(self, name: str) -> QColor:
"""Load graphical decoration element based on the decoration """Return an icon color."""
map or the icon map. This function always returns a QPixmap. return QColor(self._qColors.get(name) or QtBlack)
"""
if name in self.IMAGE_MAP:
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)
return QPixmap()
if not imgPath.is_file():
logger.error("Asset not found: %s", imgPath)
return QPixmap()
pixmap = QPixmap(str(imgPath))
tMode = Qt.TransformationMode.SmoothTransformation
if w is not None and h is not None:
return pixmap.scaled(w, h, Qt.AspectRatioMode.IgnoreAspectRatio, tMode)
elif w is None and h is not None:
return pixmap.scaledToHeight(h, tMode)
elif w is not None and h is None:
return pixmap.scaledToWidth(w, tMode)
return pixmap
def getIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon: def getIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
"""Return an icon from the icon buffer, or load it.""" """Return an icon from the icon buffer, or load it."""
@@ -729,13 +706,6 @@ class GuiIcons:
return icon return icon
return self._noIcon return self._noIcon
def getPixmap(self, name: str, size: tuple[int, int], color: str | None = None) -> QPixmap:
"""Return an icon from the icon buffer as a QPixmap. If it
doesn't exist, return an empty QPixmap.
"""
w, h = size
return self.getIcon(name, color, w, h).pixmap(w, h, QIcon.Mode.Normal)
def getItemIcon( def getItemIcon(
self, tType: nwItemType, tClass: nwItemClass, tLayout: nwItemLayout, hLevel: str = "H0" self, tType: nwItemType, tClass: nwItemClass, tLayout: nwItemLayout, hLevel: str = "H0"
) -> QIcon: ) -> QIcon:
@@ -772,6 +742,39 @@ class GuiIcons:
return self.getIcon(name, color) return self.getIcon(name, color)
def getPixmap(self, name: str, size: tuple[int, int], color: str | None = None) -> QPixmap:
"""Return an icon from the icon buffer as a QPixmap. If it
doesn't exist, return an empty QPixmap.
"""
w, h = size
return self.getIcon(name, color, w, h).pixmap(w, h, QIcon.Mode.Normal)
def getDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap:
"""Load graphical decoration element based on the decoration
map or the icon map. This function always returns a QPixmap.
"""
if name in self.IMAGE_MAP:
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)
return QPixmap()
if not imgPath.is_file():
logger.error("Asset not found: %s", imgPath)
return QPixmap()
pixmap = QPixmap(str(imgPath))
tMode = Qt.TransformationMode.SmoothTransformation
if w is not None and h is not None:
return pixmap.scaled(w, h, Qt.AspectRatioMode.IgnoreAspectRatio, tMode)
elif w is None and h is not None:
return pixmap.scaledToHeight(h, tMode)
elif w is not None and h is None:
return pixmap.scaledToWidth(w, tMode)
return pixmap
def getHeaderDecoration(self, hLevel: int) -> QPixmap: def getHeaderDecoration(self, hLevel: int) -> QPixmap:
"""Get the decoration for a specific heading level.""" """Get the decoration for a specific heading level."""
if not self._headerDec: if not self._headerDec:
@@ -804,12 +807,13 @@ class GuiIcons:
if self._themeList: if self._themeList:
return self._themeList return self._themeList
for item in self._iconPath.iterdir(): themes = []
if item.is_file() and item.suffix == ".icons": for key, path in self._availThemes.items():
if name := _loadIconName(item): logger.debug("Checking icon theme '%s'", key)
self._themeList.append((item.stem, name)) if name := _loadIconName(path):
themes.append((key, name))
self._themeList = sorted(self._themeList, key=_sortTheme) self._themeList = sorted(themes, key=_sortTheme)
return self._themeList return self._themeList
@@ -823,12 +827,12 @@ class GuiIcons:
""" """
# If we just want the app icons, return right away # If we just want the app icons, return right away
if name == "novelwriter": if name == "novelwriter":
return QIcon(str(self._iconPath / "novelwriter.svg")) return QIcon(str(CONFIG.assetPath("icons") / "novelwriter.svg"))
elif name == "proj_nwx": elif name == "proj_nwx":
return QIcon(str(self._iconPath / "x-novelwriter-project.svg")) return QIcon(str(CONFIG.assetPath("icons") / "x-novelwriter-project.svg"))
if svg := self._svgData.get(name, b""): if svg := self._svgData.get(name, b""):
if fill := self._svgColours.get(color or "default"): if fill := self._svgColors.get(color or "default"):
svg = svg.replace(b"#000000", fill) svg = svg.replace(b"#000000", fill)
pixmap = QPixmap(w, h) pixmap = QPixmap(w, h)
pixmap.fill(QtTransparent) pixmap.fill(QtTransparent)
@@ -850,7 +854,7 @@ class GuiIcons:
painter = QPainter(pixmap) painter = QPainter(pixmap)
painter.setRenderHint(QtPaintAntiAlias) painter.setRenderHint(QtPaintAntiAlias)
if fill := self._svgColours.get(color or "default"): if fill := self._svgColors.get(color or "default"):
painter.fillPath(path, QColor(fill.decode(encoding="utf-8"))) painter.fillPath(path, QColor(fill.decode(encoding="utf-8")))
painter.end() painter.end()
@@ -861,6 +865,14 @@ class GuiIcons:
# Module Functions # Module Functions
# ================ # ================
def _listConf(target: dict, path: Path, extension: str) -> None:
"""Scan for theme files and populate the dictionary."""
if path.is_dir():
for item in path.iterdir():
if item.is_file() and item.name.endswith(extension):
target[item.stem] = item
return
def _sortTheme(data: tuple[str, str]) -> str: def _sortTheme(data: tuple[str, str]) -> str:
"""Key function for theme sorting.""" """Key function for theme sorting."""
@@ -868,17 +880,16 @@ def _sortTheme(data: tuple[str, str]) -> str:
return f"*{name}" if key.startswith("default_") else name return f"*{name}" if key.startswith("default_") else name
def _loadInternalName(confParser: NWConfigParser, confFile: str | Path) -> str: def _loadInternalName(parser: NWConfigParser, path: str | Path) -> str:
"""Open a conf file and read the 'name' setting.""" """Open a conf file and read the 'name' setting."""
try: try:
with open(confFile, mode="r", encoding="utf-8") as inFile: with open(path, mode="r", encoding="utf-8") as inFile:
confParser.read_file(inFile) parser.read_file(inFile)
return parser.rdStr("Main", "name", "")
except Exception: except Exception:
logger.error("Could not load file: %s", confFile) logger.error("Could not read file: %s", path)
logException() logException()
return "" return ""
return confParser.rdStr("Main", "name", "")
def _loadIconName(path: Path) -> str: def _loadIconName(path: Path) -> str:
@@ -890,7 +901,6 @@ def _loadIconName(path: Path) -> str:
if key.strip() == "meta:name": if key.strip() == "meta:name":
return value.strip() return value.strip()
except Exception: except Exception:
logger.error("Could not load file: %s", path) logger.error("Could not read file: %s", path)
logException() logException()
return "" return ""
+2 -2
View File
@@ -75,8 +75,8 @@ class GuiWelcome(NDialog):
# Elements # Elements
# ======== # ========
self.bgImage = SHARED.theme.loadDecoration("welcome") self.bgImage = SHARED.theme.getDecoration("welcome")
self.nwImage = SHARED.theme.loadDecoration("nw-text", h=36) self.nwImage = SHARED.theme.getDecoration("nw-text", h=36)
self.bgColor = QColor(54, 54, 54) if SHARED.theme.isDarkTheme else QColor(255, 255, 255) self.bgColor = QColor(54, 54, 54) if SHARED.theme.isDarkTheme else QColor(255, 255, 255)
self.nwLogo = QLabel(self) self.nwLogo = QLabel(self)
+2 -1
View File
@@ -27,6 +27,7 @@ from PyQt6.QtGui import QAction, QFont, QFontDatabase, QKeyEvent
from PyQt6.QtWidgets import QFileDialog, QFontDialog from PyQt6.QtWidgets import QFileDialog, QFontDialog
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.config import DEF_GUI
from novelwriter.constants import nwUnicode from novelwriter.constants import nwUnicode
from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.dialogs.quotes import GuiQuoteSelect
@@ -56,7 +57,7 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
# Check GUI Themes # Check GUI Themes
themes = [prefs.guiTheme.itemData(i) for i in range(prefs.guiTheme.count())] themes = [prefs.guiTheme.itemData(i) for i in range(prefs.guiTheme.count())]
assert len(themes) >= 5 assert len(themes) >= 5
assert "default" in themes assert DEF_GUI in themes
# Check GUI Syntax # Check GUI Syntax
syntax = [prefs.guiSyntax.itemData(i) for i in range(prefs.guiSyntax.count())] syntax = [prefs.guiSyntax.itemData(i) for i in range(prefs.guiSyntax.count())]
+42 -32
View File
@@ -28,8 +28,10 @@ from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import NWConfigParser from novelwriter.common import NWConfigParser
from novelwriter.config import DEF_GUI
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.gui.theme import _listConf
from tests.mocked import causeOSError from tests.mocked import causeOSError
from tests.tools import writeFile from tests.tools import writeFile
@@ -50,15 +52,16 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
# Scan for Themes # Scan for Themes
# =============== # ===============
assert mainTheme._listConf({}, Path("not_a_path")) is False result = {}
_listConf({}, Path("not_a_path"), ".conf")
assert result == {}
themeOne = tstPaths.cnfDir / "themes" / "themeone.conf" themeOne = tstPaths.cnfDir / "themes" / "themeone.conf"
themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf" themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf"
writeFile(themeOne, "# Stuff") writeFile(themeOne, "# Stuff")
writeFile(themeTwo, "# Stuff") writeFile(themeTwo, "# Stuff")
result = {} _listConf(result, tstPaths.cnfDir / "themes", ".conf")
assert mainTheme._listConf(result, tstPaths.cnfDir / "themes") is True
assert result["themeone"] == themeOne assert result["themeone"] == themeOne
assert result["themetwo"] == themeTwo assert result["themetwo"] == themeTwo
@@ -135,7 +138,7 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
mainTheme._availThemes = availThemes mainTheme._availThemes = availThemes
# Check handling of unreadable file # Check handling of unreadable file
CONFIG.guiTheme = "default" CONFIG.guiTheme = DEF_GUI
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError) mp.setattr("builtins.open", causeOSError)
assert mainTheme.loadTheme() is False assert mainTheme.loadTheme() is False
@@ -147,7 +150,7 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0) mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0)
# Load the default theme # Load the default theme
CONFIG.guiTheme = "default" CONFIG.guiTheme = DEF_GUI
assert mainTheme.loadTheme() is True assert mainTheme.loadTheme() is True
# This should load a standard palette # This should load a standard palette
@@ -181,13 +184,17 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
# Check a few values # Check a few values
assert mainTheme._guiPalette.color( assert mainTheme._guiPalette.color(
QPalette.ColorRole.Window).getRgb() == (239, 239, 239, 255) QPalette.ColorRole.Window
).getRgb() == (239, 239, 239, 255)
assert mainTheme._guiPalette.color( assert mainTheme._guiPalette.color(
QPalette.ColorRole.WindowText).getRgb() == (0, 0, 0, 255) QPalette.ColorRole.WindowText
).getRgb() == (0, 0, 0, 255)
assert mainTheme._guiPalette.color( assert mainTheme._guiPalette.color(
QPalette.ColorRole.Base).getRgb() == (255, 255, 255, 255) QPalette.ColorRole.Base
).getRgb() == (255, 255, 255, 255)
assert mainTheme._guiPalette.color( assert mainTheme._guiPalette.color(
QPalette.ColorRole.AlternateBase).getRgb() == (239, 239, 239, 255) QPalette.ColorRole.AlternateBase
).getRgb() == (224, 224, 224, 255)
# Load Default Dark Theme # Load Default Dark Theme
# ======================= # =======================
@@ -280,7 +287,10 @@ def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
# ========== # ==========
# Invalid theme name # Invalid theme name
availThemes = iconCache._availThemes
iconCache._availThemes = {}
assert iconCache.loadTheme("not_a_theme") is False assert iconCache.loadTheme("not_a_theme") is False
iconCache._availThemes = availThemes
# Check handling of unreadable file # Check handling of unreadable file
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -292,29 +302,29 @@ def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
assert iconCache.themeMeta.name == "Material Symbols - Rounded Medium" assert iconCache.themeMeta.name == "Material Symbols - Rounded Medium"
# Load with project colour override # Load with project colour override
purple = iconCache._svgColours["purple"] purple = iconCache._svgColors["purple"]
assert iconCache._svgColours["root"] != purple assert iconCache._svgColors["root"] != purple
assert iconCache._svgColours["folder"] != purple assert iconCache._svgColors["folder"] != purple
assert iconCache._svgColours["file"] != purple assert iconCache._svgColors["file"] != purple
assert iconCache._svgColours["title"] != purple assert iconCache._svgColors["title"] != purple
assert iconCache._svgColours["chapter"] != purple assert iconCache._svgColors["chapter"] != purple
assert iconCache._svgColours["scene"] != purple assert iconCache._svgColors["scene"] != purple
assert iconCache._svgColours["note"] != purple assert iconCache._svgColors["note"] != purple
CONFIG.iconColTree = "purple" CONFIG.iconColTree = "purple"
assert iconCache.loadTheme("material_rounded_normal") is True assert iconCache.loadTheme("material_rounded_normal") is True
assert iconCache._svgColours["root"] == purple assert iconCache._svgColors["root"] == purple
assert iconCache._svgColours["folder"] == purple assert iconCache._svgColors["folder"] == purple
assert iconCache._svgColours["file"] == purple assert iconCache._svgColors["file"] == purple
assert iconCache._svgColours["title"] == purple assert iconCache._svgColors["title"] == purple
assert iconCache._svgColours["chapter"] == purple assert iconCache._svgColors["chapter"] == purple
assert iconCache._svgColours["scene"] == purple assert iconCache._svgColors["scene"] == purple
assert iconCache._svgColours["note"] == purple assert iconCache._svgColors["note"] == purple
# Change some colours # Change some colours
iconCache.setIconColor("root", QColor(255, 255, 255)) iconCache.setIconColor("root", QColor(255, 255, 255))
assert iconCache._svgColours["root"] != purple assert iconCache._svgColors["root"] != purple
assert iconCache._svgColours["root"] == b"#ffffff" assert iconCache._svgColors["root"] == b"#ffffff"
# List Themes # List Themes
# =========== # ===========
@@ -453,31 +463,31 @@ def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI):
# ================ # ================
# Invalid name should return empty pixmap # Invalid name should return empty pixmap
qPix = iconCache.loadDecoration("stuff") qPix = iconCache.getDecoration("stuff")
assert qPix.isNull() is True assert qPix.isNull() is True
# Load an image # Load an image
qPix = iconCache.loadDecoration("welcome") qPix = iconCache.getDecoration("welcome")
assert qPix.isNull() is False assert qPix.isNull() is False
# Fail finding the file # Fail finding the file
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.is_file", lambda *a: False) mp.setattr("pathlib.Path.is_file", lambda *a: False)
qPix = iconCache.loadDecoration("welcome") qPix = iconCache.getDecoration("welcome")
assert qPix.isNull() is True assert qPix.isNull() is True
# Test image sizes # Test image sizes
qPix = iconCache.loadDecoration("welcome", w=100, h=None) qPix = iconCache.getDecoration("welcome", w=100, h=None)
assert qPix.isNull() is False assert qPix.isNull() is False
assert qPix.width() == 100 assert qPix.width() == 100
assert qPix.height() > 50 assert qPix.height() > 50
qPix = iconCache.loadDecoration("welcome", w=None, h=100) qPix = iconCache.getDecoration("welcome", w=None, h=100)
assert qPix.isNull() is False assert qPix.isNull() is False
assert qPix.width() > 100 assert qPix.width() > 100
assert qPix.height() == 100 assert qPix.height() == 100
qPix = iconCache.loadDecoration("welcome", w=100, h=100) qPix = iconCache.getDecoration("welcome", w=100, h=100)
assert qPix.isNull() is False assert qPix.isNull() is False
assert qPix.width() == 100 assert qPix.width() == 100
assert qPix.height() == 100 assert qPix.height() == 100