Change how colours are stored for status labels to use theme colour or hex format
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user