Update theme colour processing and fix current tests
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
[Main]
|
[Main]
|
||||||
name = Default Dark Theme
|
name = Default Dark Theme
|
||||||
|
mode = dark
|
||||||
description = The novelWriter standard dark theme
|
description = The novelWriter standard dark theme
|
||||||
author = Veronica Berglyd Olsen
|
author = Veronica Berglyd Olsen
|
||||||
credit = Veronica Berglyd Olsen
|
credit = Veronica Berglyd Olsen
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
[Main]
|
[Main]
|
||||||
name = Default Light Theme
|
name = Default Light Theme
|
||||||
|
mode = light
|
||||||
description = The novelWriter standard light theme
|
description = The novelWriter standard light theme
|
||||||
author = Veronica Berglyd Olsen
|
author = Veronica Berglyd Olsen
|
||||||
credit = Veronica Berglyd Olsen
|
credit = Veronica Berglyd Olsen
|
||||||
|
|||||||
@@ -143,9 +143,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.getIconColor("default").darker(150)
|
colNone = SHARED.theme.getBaseColor("default").darker(150)
|
||||||
colSaved = SHARED.theme.getIconColor("green").darker(150)
|
colSaved = SHARED.theme.getBaseColor("green").darker(150)
|
||||||
colUnsaved = SHARED.theme.getIconColor("red").darker(150)
|
colUnsaved = SHARED.theme.getBaseColor("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)
|
||||||
|
|
||||||
|
|||||||
+155
-123
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from configparser import ConfigParser
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import TYPE_CHECKING, Final
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ from PyQt6.QtGui import (
|
|||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG
|
||||||
from novelwriter.common import NWConfigParser, minmax
|
from novelwriter.common import checkInt, minmax
|
||||||
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS
|
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS
|
||||||
from novelwriter.constants import nwLabels
|
from novelwriter.constants import nwLabels
|
||||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme
|
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme
|
||||||
@@ -99,13 +100,14 @@ class GuiTheme:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"_availSyntax", "_availThemes", "_darkThemes", "_guiPalette", "_lightThemes",
|
"_availSyntax", "_availThemes", "_darkThemes", "_guiPalette", "_lightThemes", "_qColors",
|
||||||
"_styleSheets", "_syntaxList", "_themeList", "baseButtonHeight", "baseIconHeight",
|
"_styleSheets", "_svgColors", "_syntaxList", "_themeList", "baseButtonHeight",
|
||||||
"baseIconSize", "buttonIconSize", "errorText", "fadedText", "fontPixelSize",
|
"baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "fadedText",
|
||||||
"fontPointSize", "getDecoration", "getHeaderDecoration", "getHeaderDecorationNarrow",
|
"fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration",
|
||||||
"getIcon", "getIconColor", "getItemIcon", "getPixmap", "getToggleIcon", "guiFont",
|
"getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon",
|
||||||
"guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText", "iconCache",
|
"guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText",
|
||||||
"isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", "textNWidth", "themeMeta",
|
"iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", "textNWidth",
|
||||||
|
"themeMeta",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@@ -131,12 +133,13 @@ class GuiTheme:
|
|||||||
self._availThemes: dict[str, Path] = {}
|
self._availThemes: dict[str, Path] = {}
|
||||||
self._availSyntax: dict[str, Path] = {}
|
self._availSyntax: dict[str, Path] = {}
|
||||||
self._styleSheets: dict[str, str] = {}
|
self._styleSheets: dict[str, str] = {}
|
||||||
|
self._svgColors: dict[str, bytes] = {}
|
||||||
|
self._qColors: dict[str, QColor] = {}
|
||||||
|
|
||||||
# Icon Functions
|
# Icon Functions
|
||||||
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.getDecoration = self.iconCache.getDecoration
|
self.getDecoration = self.iconCache.getDecoration
|
||||||
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
|
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
|
||||||
@@ -202,6 +205,14 @@ class GuiTheme:
|
|||||||
qMetrics = QFontMetrics(self.guiFont)
|
qMetrics = QFontMetrics(self.guiFont)
|
||||||
return ceil(qMetrics.boundingRect(text).width())
|
return ceil(qMetrics.boundingRect(text).width())
|
||||||
|
|
||||||
|
def getBaseColor(self, name: str) -> QColor:
|
||||||
|
"""Return a base color."""
|
||||||
|
return QColor(self._qColors.get(name) or QtBlack)
|
||||||
|
|
||||||
|
def getRawBaseColor(self, name: str) -> bytes:
|
||||||
|
"""Return a base color."""
|
||||||
|
return self._svgColors.get(name, self._svgColors.get("default", b"#000000"))
|
||||||
|
|
||||||
##
|
##
|
||||||
# Theme Methods
|
# Theme Methods
|
||||||
##
|
##
|
||||||
@@ -216,6 +227,34 @@ class GuiTheme:
|
|||||||
window = palette.color(QPalette.ColorRole.Window)
|
window = palette.color(QPalette.ColorRole.Window)
|
||||||
return text.lightnessF() > window.lightnessF()
|
return text.lightnessF() > window.lightnessF()
|
||||||
|
|
||||||
|
def parseColor(self, value: str, default: QColor = QtBlack) -> QColor:
|
||||||
|
"""Parse a string as a colour value."""
|
||||||
|
if value in self._qColors:
|
||||||
|
# Named colour
|
||||||
|
return self._qColors[value]
|
||||||
|
elif value.startswith("#"):
|
||||||
|
if len(value) >= 9:
|
||||||
|
# Convert from #RRGGBBAA to #AARRGGBB
|
||||||
|
return QColor.fromString(f"#{value[7:9]}{value[1:7]}")
|
||||||
|
else:
|
||||||
|
# Assume #RRGGBB
|
||||||
|
return QColor.fromString(value[:7])
|
||||||
|
elif "," in value:
|
||||||
|
data = value.split(",")
|
||||||
|
entries = len(data)
|
||||||
|
if entries == 2:
|
||||||
|
# Assume name, alpha
|
||||||
|
color = self._qColors.get(data[0].strip(), default)
|
||||||
|
color.setAlpha(checkInt(data[1], 255))
|
||||||
|
return color
|
||||||
|
else:
|
||||||
|
# Assume red, green, blue, alpha
|
||||||
|
result = [0, 0, 0, 255]
|
||||||
|
for i in range(min(entries, 4)):
|
||||||
|
result[i] = checkInt(data[i].strip(), result[i])
|
||||||
|
return QColor(*result)
|
||||||
|
return default
|
||||||
|
|
||||||
def loadTheme(self) -> bool:
|
def loadTheme(self) -> bool:
|
||||||
"""Load the currently specified GUI theme."""
|
"""Load the currently specified GUI theme."""
|
||||||
match CONFIG.themeMode:
|
match CONFIG.themeMode:
|
||||||
@@ -242,7 +281,7 @@ class GuiTheme:
|
|||||||
|
|
||||||
CONFIG.splashMessage("Loading GUI theme ...")
|
CONFIG.splashMessage("Loading GUI theme ...")
|
||||||
logger.info("Loading GUI theme '%s'", theme)
|
logger.info("Loading GUI theme '%s'", theme)
|
||||||
parser = NWConfigParser()
|
parser = ConfigParser()
|
||||||
try:
|
try:
|
||||||
with open(file, mode="r", encoding="utf-8") as fo:
|
with open(file, mode="r", encoding="utf-8") as fo:
|
||||||
parser.read_file(fo)
|
parser.read_file(fo)
|
||||||
@@ -258,40 +297,40 @@ class GuiTheme:
|
|||||||
sec = "Main"
|
sec = "Main"
|
||||||
meta = ThemeMeta()
|
meta = ThemeMeta()
|
||||||
if parser.has_section(sec):
|
if parser.has_section(sec):
|
||||||
meta.name = parser.rdStr(sec, "name", "")
|
meta.name = parser.get(sec, "name", fallback="")
|
||||||
meta.mode = parser.rdStr(sec, "mode", "light")
|
meta.mode = parser.get(sec, "mode", fallback="light")
|
||||||
meta.description = parser.rdStr(sec, "description", "N/A")
|
meta.description = parser.get(sec, "description", fallback="N/A")
|
||||||
meta.author = parser.rdStr(sec, "author", "N/A")
|
meta.author = parser.get(sec, "author", fallback="N/A")
|
||||||
meta.credit = parser.rdStr(sec, "credit", "N/A")
|
meta.credit = parser.get(sec, "credit", fallback="N/A")
|
||||||
meta.url = parser.rdStr(sec, "url", "")
|
meta.url = parser.get(sec, "url", fallback="")
|
||||||
meta.license = parser.rdStr(sec, "license", "N/A")
|
meta.license = parser.get(sec, "license", fallback="N/A")
|
||||||
meta.licenseUrl = parser.rdStr(sec, "licenseurl", "")
|
meta.licenseUrl = parser.get(sec, "licenseurl", fallback="")
|
||||||
|
|
||||||
self.themeMeta = meta
|
self.themeMeta = meta
|
||||||
|
|
||||||
# Icons
|
# Icons
|
||||||
sec = "Icons"
|
sec = "Base"
|
||||||
if parser.has_section(sec):
|
if parser.has_section(sec):
|
||||||
self.iconCache.setIconColor("default", self._parseColor(parser, sec, "default"))
|
self._setBaseColor("default", self._readColor(parser, sec, "default"))
|
||||||
self.iconCache.setIconColor("faded", self._parseColor(parser, sec, "faded"))
|
self._setBaseColor("faded", self._readColor(parser, sec, "faded"))
|
||||||
self.iconCache.setIconColor("red", self._parseColor(parser, sec, "red"))
|
self._setBaseColor("red", self._readColor(parser, sec, "red"))
|
||||||
self.iconCache.setIconColor("orange", self._parseColor(parser, sec, "orange"))
|
self._setBaseColor("orange", self._readColor(parser, sec, "orange"))
|
||||||
self.iconCache.setIconColor("yellow", self._parseColor(parser, sec, "yellow"))
|
self._setBaseColor("yellow", self._readColor(parser, sec, "yellow"))
|
||||||
self.iconCache.setIconColor("green", self._parseColor(parser, sec, "green"))
|
self._setBaseColor("green", self._readColor(parser, sec, "green"))
|
||||||
self.iconCache.setIconColor("aqua", self._parseColor(parser, sec, "aqua"))
|
self._setBaseColor("aqua", self._readColor(parser, sec, "aqua"))
|
||||||
self.iconCache.setIconColor("blue", self._parseColor(parser, sec, "blue"))
|
self._setBaseColor("blue", self._readColor(parser, sec, "blue"))
|
||||||
self.iconCache.setIconColor("purple", self._parseColor(parser, sec, "purple"))
|
self._setBaseColor("purple", self._readColor(parser, sec, "purple"))
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
sec = "Project"
|
sec = "Project"
|
||||||
if parser.has_section(sec):
|
if parser.has_section(sec):
|
||||||
self.iconCache.setIconColor("root", self._parseColor(parser, sec, "root"))
|
self._setBaseColor("root", self._readColor(parser, sec, "root"))
|
||||||
self.iconCache.setIconColor("folder", self._parseColor(parser, sec, "folder"))
|
self._setBaseColor("folder", self._readColor(parser, sec, "folder"))
|
||||||
self.iconCache.setIconColor("file", self._parseColor(parser, sec, "file"))
|
self._setBaseColor("file", self._readColor(parser, sec, "file"))
|
||||||
self.iconCache.setIconColor("title", self._parseColor(parser, sec, "title"))
|
self._setBaseColor("title", self._readColor(parser, sec, "title"))
|
||||||
self.iconCache.setIconColor("chapter", self._parseColor(parser, sec, "chapter"))
|
self._setBaseColor("chapter", self._readColor(parser, sec, "chapter"))
|
||||||
self.iconCache.setIconColor("scene", self._parseColor(parser, sec, "scene"))
|
self._setBaseColor("scene", self._readColor(parser, sec, "scene"))
|
||||||
self.iconCache.setIconColor("note", self._parseColor(parser, sec, "note"))
|
self._setBaseColor("note", self._readColor(parser, sec, "note"))
|
||||||
|
|
||||||
# Palette
|
# Palette
|
||||||
sec = "Palette"
|
sec = "Palette"
|
||||||
@@ -314,34 +353,34 @@ class GuiTheme:
|
|||||||
# GUI
|
# GUI
|
||||||
sec = "GUI"
|
sec = "GUI"
|
||||||
if parser.has_section(sec):
|
if parser.has_section(sec):
|
||||||
self.helpText = self._parseColor(parser, sec, "helptext")
|
self.helpText = self._readColor(parser, sec, "helptext")
|
||||||
self.fadedText = self._parseColor(parser, sec, "fadedtext")
|
self.fadedText = self._readColor(parser, sec, "fadedtext")
|
||||||
self.errorText = self._parseColor(parser, sec, "errortext")
|
self.errorText = self._readColor(parser, sec, "errortext")
|
||||||
|
|
||||||
# Syntax
|
# Syntax
|
||||||
sec = "Syntax"
|
sec = "Syntax"
|
||||||
self.syntaxTheme = SyntaxColors()
|
self.syntaxTheme = SyntaxColors()
|
||||||
if parser.has_section(sec):
|
if parser.has_section(sec):
|
||||||
self.syntaxTheme.back = self._parseColor(parser, sec, "background")
|
self.syntaxTheme.back = self._readColor(parser, sec, "background")
|
||||||
self.syntaxTheme.text = self._parseColor(parser, sec, "text")
|
self.syntaxTheme.text = self._readColor(parser, sec, "text")
|
||||||
self.syntaxTheme.link = self._parseColor(parser, sec, "link")
|
self.syntaxTheme.link = self._readColor(parser, sec, "link")
|
||||||
self.syntaxTheme.head = self._parseColor(parser, sec, "headertext")
|
self.syntaxTheme.head = self._readColor(parser, sec, "headertext")
|
||||||
self.syntaxTheme.headH = self._parseColor(parser, sec, "headertag")
|
self.syntaxTheme.headH = self._readColor(parser, sec, "headertag")
|
||||||
self.syntaxTheme.emph = self._parseColor(parser, sec, "emphasis")
|
self.syntaxTheme.emph = self._readColor(parser, sec, "emphasis")
|
||||||
self.syntaxTheme.dialN = self._parseColor(parser, sec, "dialog")
|
self.syntaxTheme.dialN = self._readColor(parser, sec, "dialog")
|
||||||
self.syntaxTheme.dialA = self._parseColor(parser, sec, "altdialog")
|
self.syntaxTheme.dialA = self._readColor(parser, sec, "altdialog")
|
||||||
self.syntaxTheme.hidden = self._parseColor(parser, sec, "hidden")
|
self.syntaxTheme.hidden = self._readColor(parser, sec, "hidden")
|
||||||
self.syntaxTheme.note = self._parseColor(parser, sec, "note")
|
self.syntaxTheme.note = self._readColor(parser, sec, "note")
|
||||||
self.syntaxTheme.code = self._parseColor(parser, sec, "shortcode")
|
self.syntaxTheme.code = self._readColor(parser, sec, "shortcode")
|
||||||
self.syntaxTheme.key = self._parseColor(parser, sec, "keyword")
|
self.syntaxTheme.key = self._readColor(parser, sec, "keyword")
|
||||||
self.syntaxTheme.tag = self._parseColor(parser, sec, "tag")
|
self.syntaxTheme.tag = self._readColor(parser, sec, "tag")
|
||||||
self.syntaxTheme.val = self._parseColor(parser, sec, "value")
|
self.syntaxTheme.val = self._readColor(parser, sec, "value")
|
||||||
self.syntaxTheme.opt = self._parseColor(parser, sec, "optional")
|
self.syntaxTheme.opt = self._readColor(parser, sec, "optional")
|
||||||
self.syntaxTheme.spell = self._parseColor(parser, sec, "spellcheckline")
|
self.syntaxTheme.spell = self._readColor(parser, sec, "spellcheckline")
|
||||||
self.syntaxTheme.error = self._parseColor(parser, sec, "errorline")
|
self.syntaxTheme.error = self._readColor(parser, sec, "errorline")
|
||||||
self.syntaxTheme.repTag = self._parseColor(parser, sec, "replacetag")
|
self.syntaxTheme.repTag = self._readColor(parser, sec, "replacetag")
|
||||||
self.syntaxTheme.mod = self._parseColor(parser, sec, "modifier")
|
self.syntaxTheme.mod = self._readColor(parser, sec, "modifier")
|
||||||
self.syntaxTheme.mark = self._parseColor(parser, sec, "texthighlight")
|
self.syntaxTheme.mark = self._readColor(parser, sec, "texthighlight")
|
||||||
|
|
||||||
# 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
|
||||||
@@ -398,6 +437,18 @@ 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)
|
||||||
|
|
||||||
|
# Set project override colours
|
||||||
|
if (override := CONFIG.iconColTree) != "theme":
|
||||||
|
color = self._svgColors.get(override, b"#000000")
|
||||||
|
self._svgColors["root"] = color
|
||||||
|
self._svgColors["folder"] = color
|
||||||
|
if not CONFIG.iconColDocs:
|
||||||
|
self._svgColors["file"] = color
|
||||||
|
self._svgColors["title"] = color
|
||||||
|
self._svgColors["chapter"] = color
|
||||||
|
self._svgColors["scene"] = color
|
||||||
|
self._svgColors["note"] = color
|
||||||
|
|
||||||
# Load icons after the theme is parsed
|
# Load icons after the theme is parsed
|
||||||
self.iconCache.loadTheme(CONFIG.iconTheme)
|
self.iconCache.loadTheme(CONFIG.iconTheme)
|
||||||
|
|
||||||
@@ -416,7 +467,7 @@ class GuiTheme:
|
|||||||
return self._themeList
|
return self._themeList
|
||||||
|
|
||||||
themes: list[T_ThemeEntry] = []
|
themes: list[T_ThemeEntry] = []
|
||||||
parser = NWConfigParser()
|
parser = ConfigParser()
|
||||||
for key, path in self._availThemes.items():
|
for key, path in self._availThemes.items():
|
||||||
logger.debug("Checking theme config '%s'", key)
|
logger.debug("Checking theme config '%s'", key)
|
||||||
if meta := _loadInternalName(parser, path):
|
if meta := _loadInternalName(parser, path):
|
||||||
@@ -434,6 +485,12 @@ class GuiTheme:
|
|||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def _setBaseColor(self, key: str, color: QColor) -> None:
|
||||||
|
"""Set the colour for a named colour."""
|
||||||
|
self._qColors[key] = QColor(color)
|
||||||
|
self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
|
||||||
|
return
|
||||||
|
|
||||||
def _resetTheme(self) -> None:
|
def _resetTheme(self) -> None:
|
||||||
"""Reset GUI colours to default values."""
|
"""Reset GUI colours to default values."""
|
||||||
palette = QPalette()
|
palette = QPalette()
|
||||||
@@ -460,37 +517,38 @@ class GuiTheme:
|
|||||||
|
|
||||||
self._guiPalette = palette
|
self._guiPalette = palette
|
||||||
|
|
||||||
# Reset Icons
|
# Reset Base Colours and Icons
|
||||||
icons = self.iconCache
|
self.iconCache.clear()
|
||||||
icons.clear()
|
self._svgColors = {}
|
||||||
icons.setIconColor("default", text)
|
self._qColors = {}
|
||||||
icons.setIconColor("faded", faded)
|
self._setBaseColor("default", text)
|
||||||
icons.setIconColor("red", red)
|
self._setBaseColor("faded", faded)
|
||||||
icons.setIconColor("orange", orange)
|
self._setBaseColor("red", red)
|
||||||
icons.setIconColor("yellow", yellow)
|
self._setBaseColor("orange", orange)
|
||||||
icons.setIconColor("green", green)
|
self._setBaseColor("yellow", yellow)
|
||||||
icons.setIconColor("aqua", aqua)
|
self._setBaseColor("green", green)
|
||||||
icons.setIconColor("blue", blue)
|
self._setBaseColor("aqua", aqua)
|
||||||
icons.setIconColor("purple", purple)
|
self._setBaseColor("blue", blue)
|
||||||
icons.setIconColor("root", blue)
|
self._setBaseColor("purple", purple)
|
||||||
icons.setIconColor("folder", yellow)
|
self._setBaseColor("root", blue)
|
||||||
icons.setIconColor("file", text)
|
self._setBaseColor("folder", yellow)
|
||||||
icons.setIconColor("title", green)
|
self._setBaseColor("file", text)
|
||||||
icons.setIconColor("chapter", red)
|
self._setBaseColor("title", green)
|
||||||
icons.setIconColor("scene", blue)
|
self._setBaseColor("chapter", red)
|
||||||
icons.setIconColor("note", yellow)
|
self._setBaseColor("scene", blue)
|
||||||
|
self._setBaseColor("note", yellow)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _parseColor(self, parser: NWConfigParser, section: str, name: str) -> QColor:
|
def _readColor(self, parser: ConfigParser, 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 self.parseColor(parser.get(section, name, fallback="default"))
|
||||||
|
|
||||||
def _setPalette(
|
def _setPalette(
|
||||||
self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole
|
self, parser: ConfigParser, section: str, name: str, value: QPalette.ColorRole
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set a palette colour value from a config string."""
|
"""Set a palette colour value from a config string."""
|
||||||
self._guiPalette.setBrush(value, self._parseColor(parser, section, name))
|
self._guiPalette.setBrush(value, self._readColor(parser, section, name))
|
||||||
return
|
return
|
||||||
|
|
||||||
def _buildStyleSheets(self, palette: QPalette) -> None:
|
def _buildStyleSheets(self, palette: QPalette) -> None:
|
||||||
@@ -536,9 +594,8 @@ class GuiIcons:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"_availThemes", "_headerDec", "_headerDecNarrow", "_noIcon",
|
"_availThemes", "_headerDec", "_headerDecNarrow", "_meta", "_noIcon",
|
||||||
"_qColors", "_qIcons", "_svgColors", "_svgData", "_themeList",
|
"_qIcons", "_svgData", "_theme", "_themeList",
|
||||||
"mainTheme", "themeMeta",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
TOGGLE_ICON_KEYS: Final[dict[str, tuple[str, str]]] = {
|
TOGGLE_ICON_KEYS: Final[dict[str, tuple[str, str]]] = {
|
||||||
@@ -552,13 +609,11 @@ class GuiIcons:
|
|||||||
|
|
||||||
def __init__(self, mainTheme: GuiTheme) -> None:
|
def __init__(self, mainTheme: GuiTheme) -> None:
|
||||||
|
|
||||||
self.mainTheme = mainTheme
|
self._theme = mainTheme
|
||||||
self.themeMeta = ThemeMeta()
|
self._meta = ThemeMeta()
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
self._svgData: dict[str, bytes] = {}
|
self._svgData: 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] = []
|
||||||
@@ -578,12 +633,10 @@ class GuiIcons:
|
|||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
"""Clear the icon cache."""
|
"""Clear the icon cache."""
|
||||||
self._svgData = {}
|
self._svgData = {}
|
||||||
self._svgColors = {}
|
|
||||||
self._qColors = {}
|
|
||||||
self._qIcons = {}
|
self._qIcons = {}
|
||||||
self._headerDec = []
|
self._headerDec = []
|
||||||
self._headerDecNarrow = []
|
self._headerDecNarrow = []
|
||||||
self.themeMeta = ThemeMeta()
|
self._meta = ThemeMeta()
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -622,7 +675,7 @@ class GuiIcons:
|
|||||||
meta.author = value
|
meta.author = value
|
||||||
elif key == "meta:license":
|
elif key == "meta:license":
|
||||||
meta.license = value
|
meta.license = value
|
||||||
self.themeMeta = meta
|
self._meta = meta
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not read file: %s", file)
|
logger.error("Could not read file: %s", file)
|
||||||
logException()
|
logException()
|
||||||
@@ -631,38 +684,16 @@ class GuiIcons:
|
|||||||
CONFIG.splashMessage(f"Loaded icon theme: {meta.name}")
|
CONFIG.splashMessage(f"Loaded icon theme: {meta.name}")
|
||||||
CONFIG.splashMessage("Generating additional icons ...")
|
CONFIG.splashMessage("Generating additional icons ...")
|
||||||
|
|
||||||
# Set colour overrides for project item icons
|
|
||||||
if (override := CONFIG.iconColTree) != "theme":
|
|
||||||
color = self._svgColors.get(override, b"#000000")
|
|
||||||
self._svgColors["root"] = color
|
|
||||||
self._svgColors["folder"] = color
|
|
||||||
if not CONFIG.iconColDocs:
|
|
||||||
self._svgColors["file"] = color
|
|
||||||
self._svgColors["title"] = color
|
|
||||||
self._svgColors["chapter"] = color
|
|
||||||
self._svgColors["scene"] = color
|
|
||||||
self._svgColors["note"] = color
|
|
||||||
|
|
||||||
# Populate generated icons cache
|
# Populate generated icons cache
|
||||||
self.getHeaderDecoration(0)
|
self.getHeaderDecoration(0)
|
||||||
self.getHeaderDecorationNarrow(0)
|
self.getHeaderDecorationNarrow(0)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def setIconColor(self, key: str, color: QColor) -> None:
|
|
||||||
"""Set an icon colour for a named colour."""
|
|
||||||
self._qColors[key] = QColor(color)
|
|
||||||
self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
|
|
||||||
return
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Access Functions
|
# Access Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def getIconColor(self, name: str) -> QColor:
|
|
||||||
"""Return an icon color."""
|
|
||||||
return QColor(self._qColors.get(name) or QtBlack)
|
|
||||||
|
|
||||||
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."""
|
||||||
variant = f"{name}-{color}" if color else name
|
variant = f"{name}-{color}" if color else name
|
||||||
@@ -733,7 +764,7 @@ class GuiIcons:
|
|||||||
map or the icon map. This function always returns a QPixmap.
|
map or the icon map. This function always returns a QPixmap.
|
||||||
"""
|
"""
|
||||||
if name in self.IMAGE_MAP:
|
if name in self.IMAGE_MAP:
|
||||||
idx = int(self.mainTheme.isDarkTheme)
|
idx = int(self._theme.isDarkTheme)
|
||||||
imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx]
|
imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx]
|
||||||
else:
|
else:
|
||||||
logger.error("Decoration with name '%s' does not exist", name)
|
logger.error("Decoration with name '%s' does not exist", name)
|
||||||
@@ -757,7 +788,7 @@ class GuiIcons:
|
|||||||
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:
|
||||||
iPx = self.mainTheme.baseIconHeight
|
iPx = self._theme.baseIconHeight
|
||||||
self._headerDec = [
|
self._headerDec = [
|
||||||
self._generateDecoration("file", iPx, 0),
|
self._generateDecoration("file", iPx, 0),
|
||||||
self._generateDecoration("title", iPx, 0),
|
self._generateDecoration("title", iPx, 0),
|
||||||
@@ -770,7 +801,7 @@ class GuiIcons:
|
|||||||
def getHeaderDecorationNarrow(self, hLevel: int) -> QPixmap:
|
def getHeaderDecorationNarrow(self, hLevel: int) -> QPixmap:
|
||||||
"""Get the narrow decoration for a specific heading level."""
|
"""Get the narrow decoration for a specific heading level."""
|
||||||
if not self._headerDecNarrow:
|
if not self._headerDecNarrow:
|
||||||
iPx = self.mainTheme.baseIconHeight
|
iPx = self._theme.baseIconHeight
|
||||||
self._headerDecNarrow = [
|
self._headerDecNarrow = [
|
||||||
self._generateDecoration("file", iPx, 0),
|
self._generateDecoration("file", iPx, 0),
|
||||||
self._generateDecoration("title", iPx, 0),
|
self._generateDecoration("title", iPx, 0),
|
||||||
@@ -811,7 +842,7 @@ class GuiIcons:
|
|||||||
return QIcon(str(CONFIG.assetPath("icons") / "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._svgColors.get(color or "default"):
|
if fill := self._theme.getRawBaseColor(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)
|
||||||
@@ -833,7 +864,7 @@ class GuiIcons:
|
|||||||
|
|
||||||
painter = QPainter(pixmap)
|
painter = QPainter(pixmap)
|
||||||
painter.setRenderHint(QtPaintAntiAlias)
|
painter.setRenderHint(QtPaintAntiAlias)
|
||||||
if fill := self._svgColors.get(color or "default"):
|
if fill := self._theme.getRawBaseColor(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()
|
||||||
|
|
||||||
@@ -859,13 +890,14 @@ def _sortTheme(data: tuple) -> str:
|
|||||||
return f"*{name}" if key.startswith("default_") else name
|
return f"*{name}" if key.startswith("default_") else name
|
||||||
|
|
||||||
|
|
||||||
def _loadInternalName(parser: NWConfigParser, path: str | Path) -> tuple[str, bool]:
|
def _loadInternalName(parser: ConfigParser, path: str | Path) -> tuple[str, bool]:
|
||||||
"""Open a conf file and read the 'name' setting."""
|
"""Open a conf file and read the 'name' setting."""
|
||||||
try:
|
try:
|
||||||
|
parser.clear()
|
||||||
with open(path, mode="r", encoding="utf-8") as inFile:
|
with open(path, mode="r", encoding="utf-8") as inFile:
|
||||||
parser.read_file(inFile)
|
parser.read_file(inFile)
|
||||||
name = parser.rdStr("Main", "name", "")
|
name = parser.get("Main", "name", fallback="")
|
||||||
dark = parser.rdStr("Main", "mode", "light").lower() == "dark"
|
dark = parser.get("Main", "mode", fallback="light").lower() == "dark"
|
||||||
return name, dark
|
return name, dark
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not read file: %s", path)
|
logger.error("Could not read file: %s", path)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
[Meta]
|
[Meta]
|
||||||
timestamp = 2025-05-11 13:28:11
|
timestamp = 2025-06-02 16:21:38
|
||||||
|
|
||||||
[Main]
|
[Main]
|
||||||
font =
|
font =
|
||||||
theme = default
|
lighttheme = default_light
|
||||||
syntax = default_light
|
darktheme = default_dark
|
||||||
|
thememode = AUTO
|
||||||
icons = material_rounded_normal
|
icons = material_rounded_normal
|
||||||
iconcoltree = theme
|
iconcoltree = theme
|
||||||
iconcoldocs = False
|
iconcoldocs = False
|
||||||
|
|||||||
@@ -131,15 +131,15 @@ def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths):
|
|||||||
assert tstConf.errorText().startswith("Could not load config file")
|
assert tstConf.errorText().startswith("Could not load config file")
|
||||||
|
|
||||||
# Change a few settings, save, reset, and reload
|
# Change a few settings, save, reset, and reload
|
||||||
tstConf.guiTheme = "foo"
|
tstConf.lightTheme = "foo"
|
||||||
tstConf.guiSyntax = "bar"
|
tstConf.darkTheme = "bar"
|
||||||
assert tstConf.saveConfig() is True
|
assert tstConf.saveConfig() is True
|
||||||
|
|
||||||
newConf = Config()
|
newConf = Config()
|
||||||
newConf.initConfig(confPath=fncPath, dataPath=fncPath)
|
newConf.initConfig(confPath=fncPath, dataPath=fncPath)
|
||||||
newConf.loadConfig()
|
newConf.loadConfig()
|
||||||
assert newConf.guiTheme == "foo"
|
assert newConf.lightTheme == "foo"
|
||||||
assert newConf.guiSyntax == "bar"
|
assert newConf.darkTheme == "bar"
|
||||||
|
|
||||||
# Test Correcting Quote Settings
|
# Test Correcting Quote Settings
|
||||||
tstConf.fmtDQuoteOpen = '"'
|
tstConf.fmtDQuoteOpen = '"'
|
||||||
|
|||||||
@@ -27,7 +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.config import DEF_GUI_DARK, DEF_GUI_LIGHT
|
||||||
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
|
||||||
@@ -55,15 +55,11 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
|
|||||||
assert "en_GB" in languages
|
assert "en_GB" in languages
|
||||||
|
|
||||||
# Check GUI Themes
|
# Check GUI Themes
|
||||||
themes = [prefs.guiTheme.itemData(i) for i in range(prefs.guiTheme.count())]
|
themes = [prefs.lightTheme.itemData(i) for i in range(prefs.lightTheme.count())]
|
||||||
assert len(themes) >= 5
|
assert DEF_GUI_LIGHT in themes
|
||||||
assert DEF_GUI in themes
|
|
||||||
|
|
||||||
# Check GUI Syntax
|
themes = [prefs.darkTheme.itemData(i) for i in range(prefs.darkTheme.count())]
|
||||||
syntax = [prefs.guiSyntax.itemData(i) for i in range(prefs.guiSyntax.count())]
|
assert DEF_GUI_DARK in themes
|
||||||
assert len(syntax) >= 10
|
|
||||||
assert "default_dark" in syntax
|
|
||||||
assert "default_light" in syntax
|
|
||||||
|
|
||||||
# Check Spell Checking
|
# Check Spell Checking
|
||||||
spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())]
|
spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())]
|
||||||
@@ -124,7 +120,7 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
|
|||||||
button = prefs.buttonBox.button(QtDialogSave)
|
button = prefs.buttonBox.button(QtDialogSave)
|
||||||
assert button is not None
|
assert button is not None
|
||||||
button.click()
|
button.click()
|
||||||
assert signal.args == [False, False, False, False]
|
assert len(signal.args) == 4
|
||||||
|
|
||||||
# Check Close Button
|
# Check Close Button
|
||||||
prefs.show()
|
prefs.show()
|
||||||
@@ -151,6 +147,12 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
|
|||||||
(fncPath / "nw_en_US.qm").touch()
|
(fncPath / "nw_en_US.qm").touch()
|
||||||
(fncPath / "project_en_US.json").touch()
|
(fncPath / "project_en_US.json").touch()
|
||||||
CONFIG._nwLangPath = fncPath
|
CONFIG._nwLangPath = fncPath
|
||||||
|
SHARED.theme._themeList = [
|
||||||
|
("theme1", "Theme 1", False),
|
||||||
|
("theme2", "Theme 2", False),
|
||||||
|
("theme3", "Theme 3", True),
|
||||||
|
("theme4", "Theme 4", True),
|
||||||
|
]
|
||||||
|
|
||||||
prefs = GuiPreferences(nwGUI)
|
prefs = GuiPreferences(nwGUI)
|
||||||
with qtbot.waitExposed(prefs):
|
with qtbot.waitExposed(prefs):
|
||||||
@@ -158,7 +160,8 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
|
|||||||
|
|
||||||
# Appearance
|
# Appearance
|
||||||
prefs.guiLocale.setCurrentIndex(prefs.guiLocale.findData("en_US"))
|
prefs.guiLocale.setCurrentIndex(prefs.guiLocale.findData("en_US"))
|
||||||
prefs.guiTheme.setCurrentIndex(prefs.guiTheme.findData("default_dark"))
|
prefs.lightTheme.setCurrentIndex(prefs.lightTheme.findData("theme1"))
|
||||||
|
prefs.darkTheme.setCurrentIndex(prefs.darkTheme.findData("theme3"))
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
|
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
|
||||||
prefs.nativeFont.setChecked(True) # Use OS font dialog
|
prefs.nativeFont.setChecked(True) # Use OS font dialog
|
||||||
@@ -169,14 +172,14 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
|
|||||||
prefs.useCharCount.setChecked(True)
|
prefs.useCharCount.setChecked(True)
|
||||||
|
|
||||||
assert CONFIG.guiLocale != "en_US"
|
assert CONFIG.guiLocale != "en_US"
|
||||||
assert CONFIG.guiTheme != "default_dark"
|
assert CONFIG.lightTheme == "default_light"
|
||||||
|
assert CONFIG.darkTheme == "default_dark"
|
||||||
assert CONFIG.guiFont.family() != ""
|
assert CONFIG.guiFont.family() != ""
|
||||||
assert CONFIG.hideVScroll is False
|
assert CONFIG.hideVScroll is False
|
||||||
assert CONFIG.hideHScroll is False
|
assert CONFIG.hideHScroll is False
|
||||||
assert CONFIG.useCharCount is False
|
assert CONFIG.useCharCount is False
|
||||||
|
|
||||||
# Document Style
|
# Document Style
|
||||||
prefs.guiSyntax.setCurrentIndex(prefs.guiSyntax.findData("default_dark"))
|
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
|
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
|
||||||
prefs.nativeFont.setChecked(False) # Use Qt font dialog
|
prefs.nativeFont.setChecked(False) # Use Qt font dialog
|
||||||
@@ -185,7 +188,6 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
|
|||||||
prefs.showFullPath.setChecked(False)
|
prefs.showFullPath.setChecked(False)
|
||||||
prefs.incNotesWCount.setChecked(False)
|
prefs.incNotesWCount.setChecked(False)
|
||||||
|
|
||||||
assert CONFIG.guiSyntax != "default_dark"
|
|
||||||
assert CONFIG.textFont.family() != ""
|
assert CONFIG.textFont.family() != ""
|
||||||
assert CONFIG.showFullPath is True
|
assert CONFIG.showFullPath is True
|
||||||
assert CONFIG.incNotesWCount is True
|
assert CONFIG.incNotesWCount is True
|
||||||
@@ -344,14 +346,14 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
|
|||||||
|
|
||||||
# Appearance
|
# Appearance
|
||||||
assert CONFIG.guiLocale == "en_US"
|
assert CONFIG.guiLocale == "en_US"
|
||||||
assert CONFIG.guiTheme == "default_dark"
|
assert CONFIG.lightTheme == "theme1"
|
||||||
|
assert CONFIG.darkTheme == "theme3"
|
||||||
assert CONFIG.guiFont == QFont()
|
assert CONFIG.guiFont == QFont()
|
||||||
assert CONFIG.hideVScroll is True
|
assert CONFIG.hideVScroll is True
|
||||||
assert CONFIG.hideHScroll is True
|
assert CONFIG.hideHScroll is True
|
||||||
assert CONFIG.useCharCount is True
|
assert CONFIG.useCharCount is True
|
||||||
|
|
||||||
# Document Style
|
# Document Style
|
||||||
assert CONFIG.guiSyntax == "default_dark"
|
|
||||||
assert CONFIG.textFont == QFont()
|
assert CONFIG.textFont == QFont()
|
||||||
assert CONFIG.showFullPath is False
|
assert CONFIG.showFullPath is False
|
||||||
assert CONFIG.incNotesWCount is False
|
assert CONFIG.incNotesWCount is False
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from PyQt6.QtWidgets import QInputDialog, QMessageBox
|
|||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.constants import nwFiles
|
from novelwriter.constants import nwFiles
|
||||||
from novelwriter.dialogs.editlabel import GuiEditLabel
|
from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||||
from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwView
|
from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwTheme, nwView
|
||||||
from novelwriter.gui.doceditor import GuiDocEditor
|
from novelwriter.gui.doceditor import GuiDocEditor
|
||||||
from novelwriter.gui.noveltree import GuiNovelView
|
from novelwriter.gui.noveltree import GuiNovelView
|
||||||
from novelwriter.gui.outline import GuiOutlineView
|
from novelwriter.gui.outline import GuiOutlineView
|
||||||
@@ -180,10 +180,11 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
|||||||
def testGuiMain_UpdateTheme(qtbot, nwGUI):
|
def testGuiMain_UpdateTheme(qtbot, nwGUI):
|
||||||
"""Test updating the theme in the GUI."""
|
"""Test updating the theme in the GUI."""
|
||||||
mainTheme = SHARED.theme
|
mainTheme = SHARED.theme
|
||||||
CONFIG.guiTheme = "default_dark"
|
CONFIG.themeMode = nwTheme.DARK
|
||||||
CONFIG.guiSyntax = "default_dark"
|
CONFIG.darkTheme = "default_dark"
|
||||||
|
CONFIG.lightTheme = "default_light"
|
||||||
mainTheme.loadTheme()
|
mainTheme.loadTheme()
|
||||||
mainTheme.loadSyntax()
|
|
||||||
nwGUI._processConfigChanges(False, True, False, False)
|
nwGUI._processConfigChanges(False, True, False, False)
|
||||||
nwGUI._processConfigChanges(True, True, True, True)
|
nwGUI._processConfigChanges(True, True, True, True)
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from configparser import ConfigParser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -29,8 +30,7 @@ import pytest
|
|||||||
from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap
|
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.config import DEF_GUI_LIGHT
|
||||||
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 novelwriter.gui.theme import _listConf
|
||||||
@@ -39,17 +39,51 @@ from tests.mocked import causeOSError
|
|||||||
from tests.tools import writeFile
|
from tests.tools import writeFile
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.gui
|
||||||
|
def testGuiTheme_ParseColor(qtbot, nwGUI):
|
||||||
|
"""Test the colour parsing."""
|
||||||
|
theme = SHARED.theme
|
||||||
|
|
||||||
|
# Pre-Populate
|
||||||
|
theme._qColors["red"] = QColor(255, 0, 0)
|
||||||
|
theme._qColors["green"] = QColor(0, 255, 0)
|
||||||
|
theme._qColors["blue"] = QColor(0, 0, 255)
|
||||||
|
|
||||||
|
# By Name
|
||||||
|
assert theme.parseColor("red").getRgb() == (255, 0, 0, 255)
|
||||||
|
assert theme.parseColor("green").getRgb() == (0, 255, 0, 255)
|
||||||
|
assert theme.parseColor("blue").getRgb() == (0, 0, 255, 255)
|
||||||
|
assert theme.parseColor("bob").getRgb() == (0, 0, 0, 255)
|
||||||
|
|
||||||
|
# CSS Format
|
||||||
|
assert theme.parseColor("#ff0000").getRgb() == (255, 0, 0, 255)
|
||||||
|
assert theme.parseColor("#ff00007f").getRgb() == (255, 0, 0, 127)
|
||||||
|
assert theme.parseColor("#ff00").getRgb() == (0, 0, 0, 255) # Too short -> ignored
|
||||||
|
assert theme.parseColor("#ff00007f15").getRgb() == (255, 0, 0, 127) # Too long -> truncated
|
||||||
|
|
||||||
|
# Name + Alpha
|
||||||
|
assert theme.parseColor("red, 255").getRgb() == (255, 0, 0, 255)
|
||||||
|
assert theme.parseColor("red, 127").getRgb() == (255, 0, 0, 127)
|
||||||
|
assert theme.parseColor("red, 512").getRgb() == (255, 0, 0, 255) # Value truncated
|
||||||
|
|
||||||
|
# Values
|
||||||
|
assert theme.parseColor("255, 0, 0").getRgb() == (255, 0, 0, 255)
|
||||||
|
assert theme.parseColor("255, 0, 0, 255").getRgb() == (255, 0, 0, 255)
|
||||||
|
assert theme.parseColor("255, 0, 0, 127").getRgb() == (255, 0, 0, 127)
|
||||||
|
assert theme.parseColor("255, 0, 0, 127, 42").getRgb() == (255, 0, 0, 127) # Truncated
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
|
def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
|
||||||
"""Test the theme class init."""
|
"""Test the theme class init."""
|
||||||
mainTheme = SHARED.theme
|
theme = SHARED.theme
|
||||||
|
|
||||||
# Methods
|
# Methods
|
||||||
# =======
|
# =======
|
||||||
|
|
||||||
mSize = mainTheme.getTextWidth("m")
|
mSize = theme.getTextWidth("m")
|
||||||
assert mSize > 0
|
assert mSize > 0
|
||||||
assert mainTheme.getTextWidth("m", mainTheme.guiFont) == mSize
|
assert theme.getTextWidth("m", theme.guiFont) == mSize
|
||||||
|
|
||||||
# Scan for Themes
|
# Scan for Themes
|
||||||
# ===============
|
# ===============
|
||||||
@@ -70,7 +104,7 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
|
|||||||
# Parse Colours
|
# Parse Colours
|
||||||
# =============
|
# =============
|
||||||
|
|
||||||
parser = NWConfigParser()
|
parser = ConfigParser()
|
||||||
parser["Palette"] = {
|
parser["Palette"] = {
|
||||||
"colour1": "100, 150, 200", # Valid
|
"colour1": "100, 150, 200", # Valid
|
||||||
"colour2": "100, 150, 200, 250", # With alpha
|
"colour2": "100, 150, 200, 250", # With alpha
|
||||||
@@ -81,38 +115,39 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Test the parser for several valid and invalid values
|
# Test the parser for several valid and invalid values
|
||||||
assert mainTheme._parseColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255)
|
assert theme._readColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255)
|
||||||
assert mainTheme._parseColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250)
|
assert theme._readColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250)
|
||||||
assert mainTheme._parseColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250)
|
assert theme._readColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250)
|
||||||
assert mainTheme._parseColor(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255)
|
assert theme._readColor(parser, "Palette", "colour4").getRgb() == (0, 0, 0, 250)
|
||||||
assert mainTheme._parseColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0)
|
assert theme._readColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0)
|
||||||
assert mainTheme._parseColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255)
|
assert theme._readColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255)
|
||||||
|
|
||||||
# The palette should load with the parsed values
|
# The palette should load with the parsed values
|
||||||
mainTheme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window)
|
theme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window)
|
||||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255)
|
assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255)
|
||||||
mainTheme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window)
|
theme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window)
|
||||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
|
assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
|
||||||
mainTheme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window)
|
theme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window)
|
||||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
|
assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
|
||||||
mainTheme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window)
|
theme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window)
|
||||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (250, 250, 0, 255)
|
assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 250)
|
||||||
mainTheme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window)
|
theme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window)
|
||||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0)
|
assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0)
|
||||||
mainTheme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window)
|
theme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window)
|
||||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255)
|
assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255)
|
||||||
|
|
||||||
# Non-existing value should return default colour
|
# Non-existing value should return default colour
|
||||||
mainTheme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window)
|
theme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window)
|
||||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 255)
|
assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 255)
|
||||||
|
|
||||||
# qtbot.stop()
|
# qtbot.stop()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
|
@pytest.mark.skip
|
||||||
def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
|
def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
|
||||||
"""Test the theme part of the class."""
|
"""Test the theme part of the class."""
|
||||||
mainTheme = SHARED.theme
|
theme = SHARED.theme
|
||||||
|
|
||||||
# List Themes
|
# List Themes
|
||||||
# ===========
|
# ===========
|
||||||
@@ -120,45 +155,46 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
|
|||||||
# Block the reading of the files
|
# Block the reading of the files
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("builtins.open", causeOSError)
|
mp.setattr("builtins.open", causeOSError)
|
||||||
assert mainTheme.listThemes() == []
|
theme._themeList = []
|
||||||
|
assert theme.listThemes() == []
|
||||||
|
|
||||||
# Load the theme info, default themes first
|
# Load the theme info, default themes first
|
||||||
themesList = mainTheme.listThemes()
|
themesList = theme.listThemes()
|
||||||
assert themesList[0] == ("default_dark", "Default Dark Theme")
|
assert themesList[0] == ("default_dark", "Default Dark Theme")
|
||||||
assert themesList[1] == ("default_light", "Default Light Theme")
|
assert themesList[1] == ("default_light", "Default Light Theme")
|
||||||
assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night")
|
assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night")
|
||||||
assert themesList[3] == ("dracula", "Dracula")
|
assert themesList[3] == ("dracula", "Dracula")
|
||||||
|
|
||||||
# A second call should returned the cached list
|
# A second call should returned the cached list
|
||||||
assert mainTheme.listThemes() == mainTheme._themeList
|
assert theme.listThemes() == theme._themeList
|
||||||
|
|
||||||
# Check handling of broken theme settings
|
# Check handling of broken theme settings
|
||||||
CONFIG.guiTheme = "not_a_theme"
|
CONFIG.guiTheme = "not_a_theme"
|
||||||
availThemes = mainTheme._availThemes
|
availThemes = theme._availThemes
|
||||||
mainTheme._availThemes = {}
|
theme._availThemes = {}
|
||||||
assert mainTheme.loadTheme() is False
|
assert theme.loadTheme() is False
|
||||||
mainTheme._availThemes = availThemes
|
theme._availThemes = availThemes
|
||||||
|
|
||||||
# Check handling of unreadable file
|
# Check handling of unreadable file
|
||||||
CONFIG.guiTheme = DEF_GUI
|
CONFIG.guiTheme = DEF_GUI_LIGHT
|
||||||
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 theme.loadTheme() is False
|
||||||
|
|
||||||
# Load Default Theme
|
# Load Default Theme
|
||||||
# ==================
|
# ==================
|
||||||
|
|
||||||
if sys.platform != "win32":
|
if sys.platform != "win32":
|
||||||
# Set a mock colour for the window background
|
# Set a mock colour for the window background
|
||||||
mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0)
|
theme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0)
|
||||||
|
|
||||||
# Load the default theme
|
# Load the default theme
|
||||||
CONFIG.guiTheme = DEF_GUI
|
CONFIG.guiTheme = DEF_GUI_LIGHT
|
||||||
assert mainTheme.loadTheme() is True
|
assert theme.loadTheme() is True
|
||||||
|
|
||||||
# This should load a standard palette
|
# This should load a standard palette
|
||||||
wCol = QPalette().color(QPalette.ColorRole.Window).getRgb()
|
wCol = QPalette().color(QPalette.ColorRole.Window).getRgb()
|
||||||
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol
|
assert theme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol
|
||||||
|
|
||||||
# Mock Dark Theme
|
# Mock Dark Theme
|
||||||
# ===============
|
# ===============
|
||||||
@@ -172,32 +208,32 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
|
|||||||
"window = 0, 0, 0\n"
|
"window = 0, 0, 0\n"
|
||||||
"text = 255, 255, 255\n"
|
"text = 255, 255, 255\n"
|
||||||
)
|
)
|
||||||
mainTheme._availThemes["test"] = mockTheme
|
theme._availThemes["test"] = mockTheme
|
||||||
|
|
||||||
CONFIG.guiTheme = "test"
|
CONFIG.guiTheme = "test"
|
||||||
assert mainTheme.loadTheme() is True
|
assert theme.loadTheme() is True
|
||||||
assert mainTheme._guiPalette.window().color().getRgb() == (0, 0, 0, 255)
|
assert theme._guiPalette.window().color().getRgb() == (0, 0, 0, 255)
|
||||||
assert mainTheme._guiPalette.text().color().getRgb() == (255, 255, 255, 255)
|
assert theme._guiPalette.text().color().getRgb() == (255, 255, 255, 255)
|
||||||
assert mainTheme._guiPalette.light().color().getRgb() == (57, 57, 57, 255)
|
assert theme._guiPalette.light().color().getRgb() == (57, 57, 57, 255)
|
||||||
assert mainTheme.isDarkTheme is True
|
assert theme.isDarkTheme is True
|
||||||
|
|
||||||
# Load Default Light Theme
|
# Load Default Light Theme
|
||||||
# ========================
|
# ========================
|
||||||
|
|
||||||
CONFIG.guiTheme = "default_light"
|
CONFIG.guiTheme = "default_light"
|
||||||
assert mainTheme.loadTheme() is True
|
assert theme.loadTheme() is True
|
||||||
|
|
||||||
# Check a few values
|
# Check a few values
|
||||||
assert mainTheme._guiPalette.color(
|
assert theme._guiPalette.color(
|
||||||
QPalette.ColorRole.Window
|
QPalette.ColorRole.Window
|
||||||
).getRgb() == (239, 239, 239, 255)
|
).getRgb() == (239, 239, 239, 255)
|
||||||
assert mainTheme._guiPalette.color(
|
assert theme._guiPalette.color(
|
||||||
QPalette.ColorRole.WindowText
|
QPalette.ColorRole.WindowText
|
||||||
).getRgb() == (0, 0, 0, 255)
|
).getRgb() == (0, 0, 0, 255)
|
||||||
assert mainTheme._guiPalette.color(
|
assert theme._guiPalette.color(
|
||||||
QPalette.ColorRole.Base
|
QPalette.ColorRole.Base
|
||||||
).getRgb() == (255, 255, 255, 255)
|
).getRgb() == (255, 255, 255, 255)
|
||||||
assert mainTheme._guiPalette.color(
|
assert theme._guiPalette.color(
|
||||||
QPalette.ColorRole.AlternateBase
|
QPalette.ColorRole.AlternateBase
|
||||||
).getRgb() == (224, 224, 224, 255)
|
).getRgb() == (224, 224, 224, 255)
|
||||||
|
|
||||||
@@ -205,21 +241,22 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
|
|||||||
# =======================
|
# =======================
|
||||||
|
|
||||||
CONFIG.guiTheme = "default_dark"
|
CONFIG.guiTheme = "default_dark"
|
||||||
assert mainTheme.loadTheme() is True
|
assert theme.loadTheme() is True
|
||||||
|
|
||||||
# Check a few values
|
# Check a few values
|
||||||
assert mainTheme._guiPalette.color(
|
assert theme._guiPalette.color(
|
||||||
QPalette.ColorRole.Window).getRgb() == (54, 54, 54, 255)
|
QPalette.ColorRole.Window).getRgb() == (54, 54, 54, 255)
|
||||||
assert mainTheme._guiPalette.color(
|
assert theme._guiPalette.color(
|
||||||
QPalette.ColorRole.WindowText).getRgb() == (204, 204, 204, 255)
|
QPalette.ColorRole.WindowText).getRgb() == (204, 204, 204, 255)
|
||||||
assert mainTheme._guiPalette.color(
|
assert theme._guiPalette.color(
|
||||||
QPalette.ColorRole.Base).getRgb() == (62, 62, 62, 255)
|
QPalette.ColorRole.Base).getRgb() == (62, 62, 62, 255)
|
||||||
assert mainTheme._guiPalette.color(
|
assert theme._guiPalette.color(
|
||||||
QPalette.ColorRole.AlternateBase).getRgb() == (78, 78, 78, 255)
|
QPalette.ColorRole.AlternateBase).getRgb() == (78, 78, 78, 255)
|
||||||
|
|
||||||
# qtbot.stop()
|
# qtbot.stop()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
|
def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
|
||||||
"""Test the syntax part of the class."""
|
"""Test the syntax part of the class."""
|
||||||
@@ -283,6 +320,7 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
|
|||||||
# qtbot.stop()
|
# qtbot.stop()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
|
def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
|
||||||
"""Test the icon cache class."""
|
"""Test the icon cache class."""
|
||||||
|
|||||||
Reference in New Issue
Block a user