""" novelWriter – Project Item Status Class ======================================= File History: Created: 2019-05-19 [0.1.3] Rewritten: 2022-04-05 [2.0b1] This file is a part of novelWriter Copyright 2018–2023, Veronica Berglyd Olsen This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ from __future__ import annotations import random import logging from typing import TYPE_CHECKING, ItemsView, Iterator, KeysView, Literal, ValuesView from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor from PyQt5.QtCore import QRectF, Qt from novelwriter import CONFIG from novelwriter.common import minmax, simplified if TYPE_CHECKING: # pragma: no cover from typing import TypeGuard # Requires Python 3.10 logger = logging.getLogger(__name__) class NWStatus: STATUS = 1 IMPORT = 2 def __init__(self, kind: Literal[1, 2]) -> None: self._type = kind self._store = {} self._default = None self._iPX = CONFIG.pxInt(24) pA = CONFIG.pxInt(2) pB = CONFIG.pxInt(20) pR = float(CONFIG.pxInt(4)) self._iconPath = QPainterPath() self._iconPath.addRoundedRect(QRectF(pA, pA, pB, pB), pR, pR) self._defaultIcon = self._createIcon(100, 100, 100) if self._type == self.STATUS: self._prefix = "s" elif self._type == self.IMPORT: self._prefix = "i" else: raise Exception("This is a bug!") return def write(self, key: str | None, name: str, col: tuple, count: int | None = None) -> str: """Add or update a status entry. If the key is invalid, a new key is generated. """ if not self._isKey(key): key = self._newKey() if not isinstance(col, tuple): col = (100, 100, 100) if len(col) != 3: col = (100, 100, 100) cR = minmax(col[0], 0, 255) cG = minmax(col[1], 0, 255) cB = minmax(col[2], 0, 255) name = simplified(name) if count is None: count = self._store.get(key, {}).get("count", 0) self._store[key] = { "name": name, "icon": self._createIcon(cR, cG, cB), "cols": (cR, cG, cB), "count": count, } if self._default is None: self._default = key return key def remove(self, key: str) -> bool: """Remove an entry in the list, except if the count > 0.""" if key not in self._store: return False if self._store[key]["count"] > 0: return False del self._store[key] keys = list(self._store.keys()) if key == self._default: if len(keys) > 0: self._default = keys[0] else: self._default = None return True def check(self, value: str) -> str: """Check the key against the stored status names.""" if self._isKey(value) and value in self._store: return value elif self._default is not None: return self._default return "" def name(self, key: str | None) -> str: """Return the name associated with a given key.""" if key and key in self._store: return self._store[key]["name"] elif self._default is not None: return self._store[self._default]["name"] return "" def cols(self, key: str | None) -> tuple[int, int, int]: """Return the colours associated with a given key.""" if key and key in self._store: return self._store[key]["cols"] elif self._default is not None: return self._store[self._default]["cols"] return 100, 100, 100 def count(self, key: str | None) -> int: """Return the count associated with a given key.""" if key and key in self._store: return self._store[key]["count"] elif self._default is not None: return self._store[self._default]["count"] return 0 def icon(self, key: str | None) -> QIcon: """Return the icon associated with a given key.""" if key and key in self._store: return self._store[key]["icon"] elif self._default is not None: return self._store[self._default]["icon"] return self._defaultIcon def reorder(self, order: list[str]) -> bool: """Reorder the items according to list.""" if len(order) != len(self._store): logger.error("Length mismatch between new and old order") return False if order == list(self._store.keys()): return False store = {} for key in order: if key in self._store: store[key] = self._store[key] else: logger.error("Unknown key '%s' in order", key) return False self._store = store return True def resetCounts(self) -> None: """Clear the counts of references to the status entries.""" for key in self._store: self._store[key]["count"] = 0 return def increment(self, key: str | None) -> None: """Increment the counter for a given entry.""" if key and key in self._store: self._store[key]["count"] += 1 return def pack(self) -> Iterator[tuple[str, dict]]: """Pack the status entries into a dictionary.""" for key, data in self._store.items(): yield (data["name"], { "key": key, "count": str(data["count"]), "red": str(data["cols"][0]), "green": str(data["cols"][1]), "blue": str(data["cols"][2]), }) return def unpack(self, data: dict) -> None: """Unpack a data dictionary and set the class values.""" self._store = {} self._default = None for key, entry in data.items(): label = entry.get("label", "") colour = entry.get("colour", (100, 100, 100)) count = entry.get("count", 0) self.write(key, label, colour, count) return ## # Internal Functions ## def _newKey(self) -> str: """Generate a new key for a status flag. This method is recursive, but should only fail if there is an issue with the random number generator or the user has added a lot of status flags. The Python recursion limit is given the job to handle the extreme case and will cause an app crash. """ key = f"{self._prefix}{random.getrandbits(24):06x}" if key in self._store: key = self._newKey() return key def _isKey(self, value: str | None) -> TypeGuard[str]: """Check if a value is a key or not.""" if not isinstance(value, str): return False if len(value) != 7: return False if value[0] != self._prefix: return False for c in value[1:]: if c not in "0123456789abcdef": return False return True def _createIcon(self, red: int, green: int, blue: int) -> QIcon: """Generate an icon for a status label.""" pixmap = QPixmap(self._iPX, self._iPX) pixmap.fill(Qt.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.Antialiasing) painter.fillPath(self._iconPath, QColor(red, green, blue)) painter.end() return QIcon(pixmap) ## # Iterator Bits ## def __len__(self) -> int: return len(self._store) def __getitem__(self, key: str) -> dict: return self._store[key] def __iter__(self) -> Iterator[dict]: return iter(self._store) def keys(self) -> KeysView[str]: return self._store.keys() def items(self) -> ItemsView[str, dict]: return self._store.items() def values(self) -> ValuesView[dict]: return self._store.values() # END Class NWStatus