From fe46a82e63dd065b5ee0ce58bbcea6da9c7ef283 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:55:12 +0200 Subject: [PATCH] Change how colours are stored for status labels to use theme colour or hex format --- novelwriter/core/projectxml.py | 15 ++++++++---- novelwriter/core/status.py | 28 ++++++++++++--------- novelwriter/gui/theme.py | 16 ++++++------ tests/test_gui/test_gui_theme.py | 42 ++++++++++++++++---------------- 4 files changed, 55 insertions(+), 46 deletions(-) diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 1e0e146b..a20649d2 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -46,7 +46,7 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) FILE_VERSION = "1.5" # The current project file format version -FILE_REVISION = "5" # The current project file format revision +FILE_REVISION = "6" # The current project file format revision HEX_VERSION = 0x0105 NUM_VERSION = { @@ -111,6 +111,8 @@ class ProjectXMLReader: nodes. 2.5. Rev 5: Added novelChars and notesChars attributes to content node. 2.7 RC 1. + Rev 6: Replaced red, green and blue attributes with a single + color attribute. 2.8 Beta 1. """ def __init__(self, path: str | Path) -> None: @@ -439,12 +441,15 @@ class ProjectXMLReader: for xEntry in xItem: if xEntry.tag == "entry": key = xEntry.attrib.get("key", None) - red = checkInt(xEntry.attrib.get("red", 0), 0) - green = checkInt(xEntry.attrib.get("green", 0), 0) - blue = checkInt(xEntry.attrib.get("blue", 0), 0) + red = checkInt(xEntry.attrib.get("red", 0), 0) # Deprecated in 1.5 Rev 6 + green = checkInt(xEntry.attrib.get("green", 0), 0) # Deprecated in 1.5 Rev 6 + blue = checkInt(xEntry.attrib.get("blue", 0), 0) # Deprecated in 1.5 Rev 6 + color = xEntry.attrib.get("color") # Added in 1.5 Rev 6 count = checkInt(xEntry.attrib.get("count", 0), 0) shape = xEntry.attrib.get("shape", "") - sObject.add(key, xEntry.text or "", (red, green, blue), shape, count) + if color is None: + color = f"{red}, {green}, {blue}" + sObject.add(key, xEntry.text or "", color, shape, count) return def _parseDictKeyText(self, xItem: ET.Element) -> dict: diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index f7d9b584..1fdf5396 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -35,6 +35,7 @@ from PyQt6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPixmap, QPolygon from novelwriter import SHARED from novelwriter.common import simplified +from novelwriter.constants import nwLabels from novelwriter.enum import nwStatusShape from novelwriter.types import QtPaintAntiAlias, QtTransparent @@ -43,12 +44,15 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +CUSTOM_COL = "custom" + @dataclasses.dataclass class StatusEntry: name: str color: QColor + theme: str shape: nwStatusShape icon: QIcon count: int = 0 @@ -62,7 +66,7 @@ class StatusEntry: return status -NO_ENTRY = StatusEntry("", QColor(0, 0, 0), nwStatusShape.SQUARE, QIcon(), 0) +NO_ENTRY = StatusEntry("", QColor(0, 0, 0), CUSTOM_COL, nwStatusShape.SQUARE, QIcon(), 0) T_UpdateEntry = list[tuple[str | None, StatusEntry]] T_StatusKind = Literal["s", "i"] @@ -97,15 +101,12 @@ class NWStatus: # Methods ## - def add(self, key: str | None, name: str, color: tuple[int, int, int], - shape: str, count: int) -> str: + def add(self, key: str | None, name: str, color: str, shape: str, count: int) -> str: """Add or update a status entry. If the key is invalid, a new key is generated. """ - if isinstance(color, tuple) and len(color) == 3: - qColor = QColor(*color) - else: - qColor = QColor(100, 100, 100) + qColor = SHARED.theme.parseColor(color) + theme = color if color in nwLabels.THEME_COLORS else CUSTOM_COL try: iShape = nwStatusShape[shape] @@ -115,7 +116,7 @@ class NWStatus: key = self._checkKey(key) name = simplified(name) icon = self.createIcon(self._height, qColor, iShape) - self._store[key] = StatusEntry(name, qColor, iShape, icon, count) + self._store[key] = StatusEntry(name, qColor, theme, iShape, icon, count) if self._default is None: self._default = key @@ -157,12 +158,14 @@ class NWStatus: def pack(self) -> Iterable[tuple[str, dict]]: """Pack the status entries into a dictionary.""" for key, entry in self._store.items(): + if entry.theme == CUSTOM_COL: + color = entry.color.name(QColor.NameFormat.HexRgb) + else: + color = entry.theme yield (entry.name, { "key": key, "count": str(entry.count), - "red": str(entry.color.red()), - "green": str(entry.color.green()), - "blue": str(entry.color.blue()), + "color": color, "shape": entry.shape.name, }) return @@ -179,8 +182,9 @@ class NWStatus: try: shape = nwStatusShape[str(data[0])] color = QColor(str(data[1])) + theme = CUSTOM_COL if data[1].startswith("#") else data[1] icon = NWStatus.createIcon(self._height, color, shape) - return StatusEntry(simplified(data[2]), color, shape, icon) + return StatusEntry(simplified(data[2]), color, theme, shape, icon) except Exception: logger.error("Could not parse entry %s", str(data)) return None diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index ee0e559f..fbde3028 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -40,7 +40,7 @@ from PyQt6.QtWidgets import QApplication from novelwriter import CONFIG 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, DEF_TREECOL from novelwriter.constants import nwLabels from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme from novelwriter.error import logException @@ -438,7 +438,7 @@ class GuiTheme: self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey) # Set project override colours - if (override := CONFIG.iconColTree) != "theme": + if (override := CONFIG.iconColTree) != DEF_TREECOL: color = self._qColors.get(override, QtBlack) self._setBaseColor("root", color) self._setBaseColor("folder", color) @@ -465,11 +465,7 @@ class GuiTheme: """Load a standard style sheet.""" return self._styleSheets.get(name, "") - ## - # Internal Functions - ## - - def _parseColor(self, value: str, default: QColor = QtBlack) -> QColor: + def parseColor(self, value: str, default: QColor = QtBlack) -> QColor: """Parse a string as a colour value.""" if value in self._qColors: # Named colour @@ -500,6 +496,10 @@ class GuiTheme: return QColor(*result) return default + ## + # Internal Functions + ## + def _setBaseColor(self, key: str, color: QColor) -> None: """Set the colour for a named colour.""" self._qColors[key] = QColor(color) @@ -560,7 +560,7 @@ class GuiTheme: def _readColor(self, parser: ConfigParser, section: str, name: str) -> QColor: """Parse a colour value from a config string.""" - return self._parseColor(parser.get(section, name, fallback="default")) + return self.parseColor(parser.get(section, name, fallback="default")) def _setPalette( self, parser: ConfigParser, section: str, name: str, value: QPalette.ColorRole diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 27e5915d..04e4c62b 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -55,37 +55,37 @@ def testGuiTheme_ParseColor(): theme._qColors["grey"] = QColor(127, 127, 127) # 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) + 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() == (0, 0, 0, 255) # Too long -> ignored + 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() == (0, 0, 0, 255) # Too long -> ignored # 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 + 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 # Name + Lighter - assert theme._parseColor("grey:L100").getRgb() == (127, 127, 127, 255) - assert theme._parseColor("grey:L150").getRgb() == (190, 190, 190, 255) - assert theme._parseColor("grey:L50").getRgb() == (63, 63, 63, 255) + assert theme.parseColor("grey:L100").getRgb() == (127, 127, 127, 255) + assert theme.parseColor("grey:L150").getRgb() == (190, 190, 190, 255) + assert theme.parseColor("grey:L50").getRgb() == (63, 63, 63, 255) # Name + Darker - assert theme._parseColor("grey:D100").getRgb() == (127, 127, 127, 255) - assert theme._parseColor("grey:D150").getRgb() == (85, 85, 85, 255) - assert theme._parseColor("grey:D50").getRgb() == (254, 254, 254, 255) + assert theme.parseColor("grey:D100").getRgb() == (127, 127, 127, 255) + assert theme.parseColor("grey:D150").getRgb() == (85, 85, 85, 255) + assert theme.parseColor("grey:D50").getRgb() == (254, 254, 254, 255) # 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 + 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