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__)
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:
+16 -12
View File
@@ -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
+8 -8
View File
@@ -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
+21 -21
View File
@@ -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