Change how colours are stored for status labels to use theme colour or hex format

This commit is contained in:
Veronica Berglyd Olsen
2025-06-12 16:55:12 +02:00
parent 67d49a8f2b
commit fe46a82e63
4 changed files with 55 additions and 46 deletions
+10 -5
View File
@@ -46,7 +46,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
FILE_VERSION = "1.5" # The current project file format version 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 HEX_VERSION = 0x0105
NUM_VERSION = { NUM_VERSION = {
@@ -111,6 +111,8 @@ class ProjectXMLReader:
nodes. 2.5. nodes. 2.5.
Rev 5: Added novelChars and notesChars attributes to content Rev 5: Added novelChars and notesChars attributes to content
node. 2.7 RC 1. 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: def __init__(self, path: str | Path) -> None:
@@ -439,12 +441,15 @@ class ProjectXMLReader:
for xEntry in xItem: for xEntry in xItem:
if xEntry.tag == "entry": if xEntry.tag == "entry":
key = xEntry.attrib.get("key", None) key = xEntry.attrib.get("key", None)
red = checkInt(xEntry.attrib.get("red", 0), 0) red = checkInt(xEntry.attrib.get("red", 0), 0) # Deprecated in 1.5 Rev 6
green = checkInt(xEntry.attrib.get("green", 0), 0) green = checkInt(xEntry.attrib.get("green", 0), 0) # Deprecated in 1.5 Rev 6
blue = checkInt(xEntry.attrib.get("blue", 0), 0) 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) count = checkInt(xEntry.attrib.get("count", 0), 0)
shape = xEntry.attrib.get("shape", "") 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 return
def _parseDictKeyText(self, xItem: ET.Element) -> dict: def _parseDictKeyText(self, xItem: ET.Element) -> dict:
+16 -12
View File
@@ -35,6 +35,7 @@ from PyQt6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPixmap, QPolygon
from novelwriter import SHARED from novelwriter import SHARED
from novelwriter.common import simplified from novelwriter.common import simplified
from novelwriter.constants import nwLabels
from novelwriter.enum import nwStatusShape from novelwriter.enum import nwStatusShape
from novelwriter.types import QtPaintAntiAlias, QtTransparent from novelwriter.types import QtPaintAntiAlias, QtTransparent
@@ -43,12 +44,15 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CUSTOM_COL = "custom"
@dataclasses.dataclass @dataclasses.dataclass
class StatusEntry: class StatusEntry:
name: str name: str
color: QColor color: QColor
theme: str
shape: nwStatusShape shape: nwStatusShape
icon: QIcon icon: QIcon
count: int = 0 count: int = 0
@@ -62,7 +66,7 @@ class StatusEntry:
return status 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_UpdateEntry = list[tuple[str | None, StatusEntry]]
T_StatusKind = Literal["s", "i"] T_StatusKind = Literal["s", "i"]
@@ -97,15 +101,12 @@ class NWStatus:
# Methods # Methods
## ##
def add(self, key: str | None, name: str, color: tuple[int, int, int], def add(self, key: str | None, name: str, color: str, shape: str, count: int) -> str:
shape: str, count: int) -> str:
"""Add or update a status entry. If the key is invalid, a new """Add or update a status entry. If the key is invalid, a new
key is generated. key is generated.
""" """
if isinstance(color, tuple) and len(color) == 3: qColor = SHARED.theme.parseColor(color)
qColor = QColor(*color) theme = color if color in nwLabels.THEME_COLORS else CUSTOM_COL
else:
qColor = QColor(100, 100, 100)
try: try:
iShape = nwStatusShape[shape] iShape = nwStatusShape[shape]
@@ -115,7 +116,7 @@ class NWStatus:
key = self._checkKey(key) key = self._checkKey(key)
name = simplified(name) name = simplified(name)
icon = self.createIcon(self._height, qColor, iShape) 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: if self._default is None:
self._default = key self._default = key
@@ -157,12 +158,14 @@ class NWStatus:
def pack(self) -> Iterable[tuple[str, dict]]: def pack(self) -> Iterable[tuple[str, dict]]:
"""Pack the status entries into a dictionary.""" """Pack the status entries into a dictionary."""
for key, entry in self._store.items(): 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, { yield (entry.name, {
"key": key, "key": key,
"count": str(entry.count), "count": str(entry.count),
"red": str(entry.color.red()), "color": color,
"green": str(entry.color.green()),
"blue": str(entry.color.blue()),
"shape": entry.shape.name, "shape": entry.shape.name,
}) })
return return
@@ -179,8 +182,9 @@ class NWStatus:
try: try:
shape = nwStatusShape[str(data[0])] shape = nwStatusShape[str(data[0])]
color = QColor(str(data[1])) color = QColor(str(data[1]))
theme = CUSTOM_COL if data[1].startswith("#") else data[1]
icon = NWStatus.createIcon(self._height, color, shape) 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: except Exception:
logger.error("Could not parse entry %s", str(data)) logger.error("Could not parse entry %s", str(data))
return None return None
+8 -8
View File
@@ -40,7 +40,7 @@ from PyQt6.QtWidgets import QApplication
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.common import checkInt, 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, DEF_TREECOL
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
from novelwriter.error import logException from novelwriter.error import logException
@@ -438,7 +438,7 @@ class GuiTheme:
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey) self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey)
# Set project override colours # Set project override colours
if (override := CONFIG.iconColTree) != "theme": if (override := CONFIG.iconColTree) != DEF_TREECOL:
color = self._qColors.get(override, QtBlack) color = self._qColors.get(override, QtBlack)
self._setBaseColor("root", color) self._setBaseColor("root", color)
self._setBaseColor("folder", color) self._setBaseColor("folder", color)
@@ -465,11 +465,7 @@ class GuiTheme:
"""Load a standard style sheet.""" """Load a standard style sheet."""
return self._styleSheets.get(name, "") return self._styleSheets.get(name, "")
## def parseColor(self, value: str, default: QColor = QtBlack) -> QColor:
# Internal Functions
##
def _parseColor(self, value: str, default: QColor = QtBlack) -> QColor:
"""Parse a string as a colour value.""" """Parse a string as a colour value."""
if value in self._qColors: if value in self._qColors:
# Named colour # Named colour
@@ -500,6 +496,10 @@ class GuiTheme:
return QColor(*result) return QColor(*result)
return default return default
##
# Internal Functions
##
def _setBaseColor(self, key: str, color: QColor) -> None: def _setBaseColor(self, key: str, color: QColor) -> None:
"""Set the colour for a named colour.""" """Set the colour for a named colour."""
self._qColors[key] = QColor(color) self._qColors[key] = QColor(color)
@@ -560,7 +560,7 @@ class GuiTheme:
def _readColor(self, parser: ConfigParser, 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 self._parseColor(parser.get(section, name, fallback="default")) return self.parseColor(parser.get(section, name, fallback="default"))
def _setPalette( def _setPalette(
self, parser: ConfigParser, section: str, name: str, value: QPalette.ColorRole self, parser: ConfigParser, section: str, name: str, value: QPalette.ColorRole
+21 -21
View File
@@ -55,37 +55,37 @@ def testGuiTheme_ParseColor():
theme._qColors["grey"] = QColor(127, 127, 127) theme._qColors["grey"] = QColor(127, 127, 127)
# By Name # By Name
assert theme._parseColor("red").getRgb() == (255, 0, 0, 255) assert theme.parseColor("red").getRgb() == (255, 0, 0, 255)
assert theme._parseColor("green").getRgb() == (0, 255, 0, 255) assert theme.parseColor("green").getRgb() == (0, 255, 0, 255)
assert theme._parseColor("blue").getRgb() == (0, 0, 255, 255) assert theme.parseColor("blue").getRgb() == (0, 0, 255, 255)
assert theme._parseColor("bob").getRgb() == (0, 0, 0, 255) assert theme.parseColor("bob").getRgb() == (0, 0, 0, 255)
# CSS Format # CSS Format
assert theme._parseColor("#ff0000").getRgb() == (255, 0, 0, 255) assert theme.parseColor("#ff0000").getRgb() == (255, 0, 0, 255)
assert theme._parseColor("#ff00007f").getRgb() == (255, 0, 0, 127) assert theme.parseColor("#ff00007f").getRgb() == (255, 0, 0, 127)
assert theme._parseColor("#ff00").getRgb() == (0, 0, 0, 255) # Too short -> ignored 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("#ff00007f15").getRgb() == (0, 0, 0, 255) # Too long -> ignored
# Name + Alpha # Name + Alpha
assert theme._parseColor("red:255").getRgb() == (255, 0, 0, 255) assert theme.parseColor("red:255").getRgb() == (255, 0, 0, 255)
assert theme._parseColor("red:127").getRgb() == (255, 0, 0, 127) 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:512").getRgb() == (255, 0, 0, 255) # Value truncated
# Name + Lighter # Name + Lighter
assert theme._parseColor("grey:L100").getRgb() == (127, 127, 127, 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:L150").getRgb() == (190, 190, 190, 255)
assert theme._parseColor("grey:L50").getRgb() == (63, 63, 63, 255) assert theme.parseColor("grey:L50").getRgb() == (63, 63, 63, 255)
# Name + Darker # Name + Darker
assert theme._parseColor("grey:D100").getRgb() == (127, 127, 127, 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:D150").getRgb() == (85, 85, 85, 255)
assert theme._parseColor("grey:D50").getRgb() == (254, 254, 254, 255) assert theme.parseColor("grey:D50").getRgb() == (254, 254, 254, 255)
# Values # Values
assert theme._parseColor("255, 0, 0").getRgb() == (255, 0, 0, 255) 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, 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").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, 127, 42").getRgb() == (255, 0, 0, 127) # Truncated
@pytest.mark.gui @pytest.mark.gui