diff --git a/novelwriter/constants.py b/novelwriter/constants.py index a1e2b84a..48487d77 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -25,7 +25,7 @@ from __future__ import annotations from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP -from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline +from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline, nwStatusShape def trConst(text: str) -> str: @@ -268,6 +268,34 @@ class nwLabels: nwBuildFmt.J_HTML: ".json", nwBuildFmt.J_NWD: ".json", } + SHAPES_PLAIN = { + nwStatusShape.SQUARE: QT_TRANSLATE_NOOP("Constant", "Square"), + nwStatusShape.TRIANGLE: QT_TRANSLATE_NOOP("Constant", "Triangle"), + nwStatusShape.NABLA: QT_TRANSLATE_NOOP("Constant", "Nabla"), + nwStatusShape.DIAMOND: QT_TRANSLATE_NOOP("Constant", "Diamond"), + nwStatusShape.PENTAGON: QT_TRANSLATE_NOOP("Constant", "Pentagon"), + nwStatusShape.HEXAGON: QT_TRANSLATE_NOOP("Constant", "Hexagon"), + nwStatusShape.STAR: QT_TRANSLATE_NOOP("Constant", "Star"), + nwStatusShape.PACMAN: QT_TRANSLATE_NOOP("Constant", "Pacman"), + } + SHAPES_CIRCLE = { + nwStatusShape.CIRCLE_Q: QT_TRANSLATE_NOOP("Constant", "1/4 Circle"), + nwStatusShape.CIRCLE_H: QT_TRANSLATE_NOOP("Constant", "Half Circle"), + nwStatusShape.CIRCLE_T: QT_TRANSLATE_NOOP("Constant", "3/4 Circle"), + nwStatusShape.CIRCLE: QT_TRANSLATE_NOOP("Constant", "Full Circle"), + } + SHAPES_BARS = { + nwStatusShape.BARS_1: QT_TRANSLATE_NOOP("Constant", "1 Bar"), + nwStatusShape.BARS_2: QT_TRANSLATE_NOOP("Constant", "2 Bars"), + nwStatusShape.BARS_3: QT_TRANSLATE_NOOP("Constant", "3 Bars"), + nwStatusShape.BARS_4: QT_TRANSLATE_NOOP("Constant", "4 Bars"), + } + SHAPES_BLOCKS = { + nwStatusShape.BLOCK_1: QT_TRANSLATE_NOOP("Constant", "1 Block"), + nwStatusShape.BLOCK_2: QT_TRANSLATE_NOOP("Constant", "2 Blocks"), + nwStatusShape.BLOCK_3: QT_TRANSLATE_NOOP("Constant", "3 Blocks"), + nwStatusShape.BLOCK_4: QT_TRANSLATE_NOOP("Constant", "4 Blocks"), + } FILE_FILTERS = { "*.txt": QT_TRANSLATE_NOOP("Constant", "Text files"), "*.md": QT_TRANSLATE_NOOP("Constant", "Markdown files"), diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index b8fc5ba1..925db901 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -104,7 +104,7 @@ class DocMerger: docText = self._project.storage.getDocumentText(srcHandle).rstrip("\n") if addComment: docInfo = srcItem.describeMe() - docSt, _ = srcItem.getImportStatus(incIcon=False) + docSt, _ = srcItem.getImportStatus() cmtLine = f"% {cmtPrefix} {docInfo}: {srcItem.itemName} [{docSt}]\n\n" docText = cmtLine + docText diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index a52b8a36..35dba0fe 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -25,7 +25,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any, Literal, overload +from typing import TYPE_CHECKING, Any from PyQt5.QtGui import QIcon @@ -308,25 +308,15 @@ class NWItem: return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, "")) - @overload # pragma: no cover - def getImportStatus(self, incIcon: Literal[True] = True) -> tuple[str, QIcon]: - pass - - @overload # pragma: no cover - def getImportStatus(self, incIcon: Literal[False]) -> tuple[str, None]: - pass - - def getImportStatus(self, incIcon=True): + def getImportStatus(self) -> tuple[str, QIcon]: """Return the relevant importance or status label and icon for the current item based on its class. """ if self.isNovelLike(): - stName = self._project.data.itemStatus.name(self._status) - stIcon = self._project.data.itemStatus.icon(self._status) if incIcon else None + entry = self._project.data.itemStatus[self._status] else: - stName = self._project.data.itemImport.name(self._import) - stIcon = self._project.data.itemImport.icon(self._import) if incIcon else None - return stName, stIcon + entry = self._project.data.itemImport[self._import] + return entry.name, entry.icon ## # Checker Methods diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 4a399c24..08a3dd11 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -26,33 +26,32 @@ from __future__ import annotations import json import logging +from collections.abc import Iterable from enum import Enum +from functools import partial +from pathlib import Path from time import time from typing import TYPE_CHECKING -from pathlib import Path -from functools import partial -from collections.abc import Iterable from PyQt5.QtCore import QCoreApplication from novelwriter import CONFIG, SHARED, __version__, __hexversion__ -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout -from novelwriter.error import logException -from novelwriter.constants import trConst, nwLabels -from novelwriter.core.tree import NWTree -from novelwriter.core.index import NWIndex -from novelwriter.core.options import OptionState -from novelwriter.core.storage import NWStorage, NWStorageOpen -from novelwriter.core.sessions import NWSessionLog -from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState -from novelwriter.core.projectdata import NWProjectData from novelwriter.common import ( checkStringNone, formatInt, formatTimeStamp, getFileSize, hexToInt, makeFileNameSafe, minmax ) +from novelwriter.constants import trConst, nwLabels +from novelwriter.core.index import NWIndex +from novelwriter.core.options import OptionState +from novelwriter.core.projectdata import NWProjectData +from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState +from novelwriter.core.sessions import NWSessionLog +from novelwriter.core.storage import NWStorage, NWStorageOpen +from novelwriter.core.tree import NWTree +from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout +from novelwriter.error import logException if TYPE_CHECKING: # pragma: no cover from novelwriter.core.item import NWItem - from novelwriter.core.status import NWStatus logger = logging.getLogger(__name__) @@ -461,14 +460,14 @@ class NWProject: def setDefaultStatusImport(self) -> None: """Set the default status and importance values.""" - self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100)) - self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0)) - self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0)) - self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0)) - self._data.itemImport.write(None, self.tr("New"), (100, 100, 100)) - self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0)) - self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0)) - self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0)) + self._data.itemStatus.add(None, self.tr("New"), (100, 100, 100), "SQUARE", 0) + self._data.itemStatus.add(None, self.tr("Note"), (200, 50, 0), "SQUARE", 0) + self._data.itemStatus.add(None, self.tr("Draft"), (200, 150, 0), "SQUARE", 0) + self._data.itemStatus.add(None, self.tr("Finished"), (50, 200, 0), "SQUARE", 0) + self._data.itemImport.add(None, self.tr("New"), (100, 100, 100), "SQUARE", 0) + self._data.itemImport.add(None, self.tr("Minor"), (200, 50, 0), "SQUARE", 0) + self._data.itemImport.add(None, self.tr("Major"), (200, 150, 0), "SQUARE", 0) + self._data.itemImport.add(None, self.tr("Main"), (50, 200, 0), "SQUARE", 0) return def setProjectLang(self, language: str | None) -> None: @@ -491,14 +490,6 @@ class NWProject: self.setProjectChanged(True) return - def setStatusColours(self, new: list[dict], deleted: list[str]) -> bool: - """Update the list of novel file status flags.""" - return self._setStatusImport(new, deleted, self._data.itemStatus) - - def setImportColours(self, new: list[dict], deleted: list[str]) -> bool: - """Update the list of note file importance flags.""" - return self._setStatusImport(new, deleted, self._data.itemImport) - def setProjectChanged(self, status: bool) -> bool: """Toggle the project changed flag, and propagate the information to the GUI statusbar. @@ -584,28 +575,6 @@ class NWProject: # Internal Functions ## - def _setStatusImport(self, new: list[dict], delete: list[str], target: NWStatus) -> bool: - """Update the list of novel file status or importance flags, and - delete those that have been requested deleted. - """ - if not (new or delete): - return False - - order = [] - for entry in new: - key = entry.get("key", None) - name = entry.get("name", "") - cols = entry.get("cols", (100, 100, 100)) - if name: - order.append(target.write(key, name, cols)) - - for key in delete: - target.remove(key) - - target.reorder(order) - - return True - def _loadProjectLocalisation(self) -> bool: """Load the language data for the current project language.""" if self._data.language is None or CONFIG._nwLangPath is None: diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 52db58f6..ac28ed4a 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -46,7 +46,7 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) FILE_VERSION = "1.5" # The current project file format version -FILE_REVISION = "3" # The current project file format revision +FILE_REVISION = "4" # The current project file format revision HEX_VERSION = 0x0105 NUM_VERSION = { @@ -109,6 +109,8 @@ class ProjectXMLReader: Rev 2: Drops the title node from project and adds the TEMPLATE class for items. 2.3 Beta 1. Rev 3: Added TEMPLATE class. 2.3. + Rev 4: Added shape attribute to status and importance entry + nodes. 2.5. """ def __init__(self, path: str | Path) -> None: @@ -356,8 +358,8 @@ class ProjectXMLReader: logger.debug("Parsing section (legacy format)") # Create maps to look up name -> key for status and importance - statusMap = {entry.get("name"): key for key, entry in data.itemStatus.items()} - importMap = {entry.get("name"): key for key, entry in data.itemImport.items()} + sMap: dict[str | None, str] = {e.name: k for k, e in data.itemStatus.iterItems()} + iMap: dict[str | None, str] = {e.name: k for k, e in data.itemImport.iterItems()} for xItem in xSection: if xItem.tag != "item": @@ -404,9 +406,9 @@ class ProjectXMLReader: # Status was split into separate status/import with a key in 1.4 if item.get("class", "") in ("NOVEL", "ARCHIVE"): - name["status"] = statusMap.get(tmpStatus, None) + name["status"] = sMap.get(tmpStatus, None) else: - name["import"] = importMap.get(tmpStatus, None) + name["import"] = iMap.get(tmpStatus, None) # A number of layouts were removed in 1.3 if item.get("layout", "") in ( @@ -436,7 +438,8 @@ class ProjectXMLReader: green = checkInt(xEntry.attrib.get("green", 0), 0) blue = checkInt(xEntry.attrib.get("blue", 0), 0) count = checkInt(xEntry.attrib.get("count", 0), 0) - sObject.write(key, xEntry.text or "", (red, green, blue), count) + shape = xEntry.attrib.get("shape", "") + sObject.add(key, xEntry.text or "", (red, green, blue), shape, count) return def _parseDictKeyText(self, xItem: ET.Element) -> dict: diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 554bdeed..6b85ccec 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -24,17 +24,19 @@ along with this program. If not, see . """ from __future__ import annotations -import random +import dataclasses import logging +import random -from typing import TYPE_CHECKING, Literal -from collections.abc import ItemsView, Iterable, Iterator, KeysView, ValuesView +from collections.abc import Iterable +from typing import TYPE_CHECKING -from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor -from PyQt5.QtCore import QRectF +from PyQt5.QtCore import QPointF, Qt +from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor, QPolygonF -from novelwriter import CONFIG -from novelwriter.common import minmax, simplified +from novelwriter import SHARED +from novelwriter.common import simplified +from novelwriter.enum import nwStatusShape from novelwriter.types import QtPaintAnitAlias, QtTransparent if TYPE_CHECKING: # pragma: no cover @@ -43,83 +45,94 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) +@dataclasses.dataclass +class StatusEntry: + + name: str + color: QColor + shape: nwStatusShape + icon: QIcon + count: int = 0 + + @classmethod + def duplicate(cls, source: StatusEntry) -> StatusEntry: + """Create a deep copy of the source object.""" + cls = dataclasses.replace(source) + cls.color = QColor(source.color) + cls.icon = QIcon(source.icon) + return cls + +# END Class StatusEntry + + +NO_ENTRY = StatusEntry("", QColor(0, 0, 0), nwStatusShape.SQUARE, QIcon(), 0) + + class NWStatus: - STATUS = 1 - IMPORT = 2 + STATUS = "s" + IMPORT = "i" - def __init__(self, kind: Literal[1, 2]) -> None: + __slots__ = ("_store", "_default", "_prefix", "_height") - self._type = kind - self._store = {} + def __init__(self, prefix: str) -> None: + self._store: dict[str, StatusEntry] = {} 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!") - + self._prefix = prefix[:1] + self._height = SHARED.theme.baseIconHeight return - def write(self, key: str | None, name: str, col: tuple, count: int | None = None) -> str: + def __len__(self) -> int: + return len(self._store) + + def __getitem__(self, key: str | None) -> StatusEntry: + """Return the entry associated with a given key.""" + if key and key in self._store: + return self._store[key] + elif self._default is not None: + return self._store[self._default] + return NO_ENTRY + + ## + # Methods + ## + + def add(self, key: str | None, name: str, color: tuple[int, int, int], + shape: str, count: int) -> 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) + if isinstance(color, tuple) and len(color) == 3: + qColor = QColor(*color) + else: + qColor = QColor(100, 100, 100) - cR = minmax(col[0], 0, 255) - cG = minmax(col[1], 0, 255) - cB = minmax(col[2], 0, 255) + try: + iShape = nwStatusShape[shape] + except KeyError: + iShape = nwStatusShape.SQUARE + + key = self._checkKey(key) 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, - } + icon = self.createIcon(self._height, qColor, iShape) + self._store[key] = StatusEntry(name, qColor, iShape, icon, 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 + def update(self, update: list[tuple[str | None, StatusEntry]]) -> None: + """Update the list of statuses, and from removed list.""" + self._store.clear() + for key, entry in update: + self._store[self._checkKey(key)] = entry - del self._store[key] + # Check if we need a new default + if self._default not in self._store: + self._default = next(iter(self._store)) if self._store else None - keys = list(self._store.keys()) - if key == self._default: - if len(keys) > 0: - self._default = keys[0] - else: - self._default = None - - return True + return def check(self, value: str) -> str: """Check the key against the stored status names.""" @@ -129,93 +142,51 @@ class NWStatus: 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 + for entry in self._store.values(): + entry.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 + self._store[key].count += 1 return def pack(self) -> Iterable[tuple[str, dict]]: """Pack the status entries into a dictionary.""" - for key, data in self._store.items(): - yield (data["name"], { + for key, entry in self._store.items(): + yield (entry.name, { "key": key, - "count": str(data["count"]), - "red": str(data["cols"][0]), - "green": str(data["cols"][1]), - "blue": str(data["cols"][2]), + "count": str(entry.count), + "red": str(entry.color.red()), + "green": str(entry.color.green()), + "blue": str(entry.color.blue()), + "shape": entry.shape.name, }) 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 + def iterItems(self) -> Iterable[tuple[str, StatusEntry]]: + """Yield entries from the status icons.""" + yield from self._store.items() + + @staticmethod + def createIcon(height: int, color: QColor, shape: nwStatusShape) -> QIcon: + """Generate an icon for a status label.""" + pixmap = QPixmap(48, 48) + pixmap.fill(QtTransparent) + + painter = QPainter(pixmap) + painter.setRenderHint(QtPaintAnitAlias) + painter.fillPath(_SHAPES.getShape(shape), color) + painter.end() + + return QIcon(pixmap.scaled( + height, height, + Qt.AspectRatioMode.IgnoreAspectRatio, + Qt.TransformationMode.SmoothTransformation + )) ## # Internal Functions @@ -246,38 +217,120 @@ class NWStatus: 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(QtTransparent) - - painter = QPainter(pixmap) - painter.setRenderHint(QtPaintAnitAlias) - 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() + def _checkKey(self, key: str | None) -> str: + """Check key is valid, and if not, generate one.""" + return key if self._isKey(key) else self._newKey() # END Class NWStatus + + +class _ShapeCache: + + def __init__(self) -> None: + self._cache: dict[nwStatusShape, QPainterPath] = {} + return + + def getShape(self, shape: nwStatusShape) -> QPainterPath: + """Return a painter shape for an icon.""" + if shape in self._cache: + return self._cache[shape] + + path = QPainterPath() + if shape == nwStatusShape.SQUARE: + path.addRoundedRect(2.0, 2.0, 44.0, 44.0, 4.0, 4.0) + elif shape == nwStatusShape.TRIANGLE: + path.addPolygon(QPolygonF([ + QPointF(24.00, 3.00), + QPointF(43.92, 37.50), + QPointF(4.08, 37.50), + ])) + elif shape == nwStatusShape.NABLA: + path.addPolygon(QPolygonF([ + QPointF(24.00, 48.00), + QPointF(4.08, 14.50), + QPointF(43.92, 14.50), + ])) + elif shape == nwStatusShape.DIAMOND: + path.addPolygon(QPolygonF([ + QPointF(24.00, 2.00), + QPointF(44.00, 24.00), + QPointF(24.00, 46.00), + QPointF(4.00, 24.00), + ])) + elif shape == nwStatusShape.PENTAGON: + path.addPolygon(QPolygonF([ + QPointF(24.00, 1.50), + QPointF(45.87, 17.39), + QPointF(37.52, 43.11), + QPointF(10.48, 43.11), + QPointF(2.13, 17.39), + ])) + elif shape == nwStatusShape.HEXAGON: + path.addPolygon(QPolygonF([ + QPointF(24.00, 1.50), + QPointF(43.92, 13.00), + QPointF(43.92, 36.00), + QPointF(24.00, 47.50), + QPointF(4.08, 36.00), + QPointF(4.08, 13.00), + ])) + elif shape == nwStatusShape.STAR: + path.addPolygon(QPolygonF([ + QPointF(24.00, 0.50), QPointF(31.05, 14.79), + QPointF(46.83, 17.08), QPointF(35.41, 28.21), + QPointF(38.11, 43.92), QPointF(24.00, 36.50), + QPointF(9.89, 43.92), QPointF(12.59, 28.21), + QPointF(1.17, 17.08), QPointF(15.37, 16.16), + ])) + elif shape == nwStatusShape.PACMAN: + path.moveTo(24.0, 24.0) + path.arcTo(2.0, 2.0, 44.0, 44.0, 40.0, 280.0) + elif shape == nwStatusShape.CIRCLE_Q: + path.moveTo(24.0, 24.0) + path.arcTo(2.0, 2.0, 44.0, 44.0, 0.0, 90.0) + elif shape == nwStatusShape.CIRCLE_H: + path.moveTo(24.0, 24.0) + path.arcTo(2.0, 2.0, 44.0, 44.0, -90.0, 180.0) + elif shape == nwStatusShape.CIRCLE_T: + path.moveTo(24.0, 24.0) + path.arcTo(2.0, 2.0, 44.0, 44.0, -180.0, 270.0) + elif shape == nwStatusShape.CIRCLE: + path.addEllipse(2.0, 2.0, 44.0, 44.0) + elif shape == nwStatusShape.BARS_1: + path.addRoundedRect(2.0, 2.0, 8.0, 44.0, 4.0, 4.0) + elif shape == nwStatusShape.BARS_2: + path.addRoundedRect(2.0, 2.0, 8.0, 44.0, 4.0, 4.0) + path.addRoundedRect(14.0, 2.0, 8.0, 44.0, 4.0, 4.0) + elif shape == nwStatusShape.BARS_3: + path.addRoundedRect(2.0, 2.0, 8.0, 44.0, 4.0, 4.0) + path.addRoundedRect(14.0, 2.0, 8.0, 44.0, 4.0, 4.0) + path.addRoundedRect(26.0, 2.0, 8.0, 44.0, 4.0, 4.0) + elif shape == nwStatusShape.BARS_4: + path.addRoundedRect(2.0, 2.0, 8.0, 44.0, 4.0, 4.0) + path.addRoundedRect(14.0, 2.0, 8.0, 44.0, 4.0, 4.0) + path.addRoundedRect(26.0, 2.0, 8.0, 44.0, 4.0, 4.0) + path.addRoundedRect(38.0, 2.0, 8.0, 44.0, 4.0, 4.0) + elif shape == nwStatusShape.BLOCK_1: + path.addRoundedRect(2.0, 2.0, 20.0, 20.0, 4.0, 4.0) + elif shape == nwStatusShape.BLOCK_2: + path.addRoundedRect(2.0, 2.0, 20.0, 20.0, 4.0, 4.0) + path.addRoundedRect(24.0, 2.0, 20.0, 20.0, 4.0, 4.0) + elif shape == nwStatusShape.BLOCK_3: + path.addRoundedRect(2.0, 2.0, 20.0, 20.0, 4.0, 4.0) + path.addRoundedRect(2.0, 24.0, 20.0, 20.0, 4.0, 4.0) + path.addRoundedRect(24.0, 2.0, 20.0, 20.0, 4.0, 4.0) + elif shape == nwStatusShape.BLOCK_4: + path.addRoundedRect(2.0, 2.0, 20.0, 20.0, 4.0, 4.0) + path.addRoundedRect(2.0, 24.0, 20.0, 20.0, 4.0, 4.0) + path.addRoundedRect(24.0, 2.0, 20.0, 20.0, 4.0, 4.0) + path.addRoundedRect(24.0, 24.0, 20.0, 20.0, 4.0, 4.0) + + self._cache[shape] = path + + return path + +# END Class _ShapeCache + + +# Create Singleton +_SHAPES = _ShapeCache() diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 62e791df..10077c84 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -27,20 +27,26 @@ from __future__ import annotations import logging from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QColor, QIcon, QPixmap +from PyQt5.QtGui import QCloseEvent, QColor from PyQt5.QtWidgets import ( - QApplication, QColorDialog, QDialog, QDialogButtonBox, QHBoxLayout, - QLineEdit, QPushButton, QStackedWidget, QTreeWidget, QTreeWidgetItem, - QVBoxLayout, QWidget + QAbstractItemView, QApplication, QColorDialog, QDialog, QDialogButtonBox, QHBoxLayout, + QLineEdit, QMenu, QStackedWidget, QToolButton, QTreeWidget, + QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED from novelwriter.common import simplified +from novelwriter.constants import nwLabels +from novelwriter.core.status import NWStatus, StatusEntry +from novelwriter.enum import nwStatusShape from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollableForm from novelwriter.extensions.modified import NComboBox, NIconToolButton from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtDialogCancel, QtDialogSave, QtUserRole +from novelwriter.types import ( + QtDialogCancel, QtDialogSave, QtSizeMinimum, QtSizeMinimumExpanding, + QtUserRole +) logger = logging.getLogger(__name__) @@ -179,19 +185,19 @@ class GuiProjectSettings(QDialog): rebuildTrees = False - if self.statusPage.wasChanged: - newList, delList = self.statusPage.getNewList() - project.setStatusColours(newList, delList) + if self.statusPage.changed: + logger.debug("Updating status labels") + project.data.itemStatus.update(self.statusPage.getNewList()) rebuildTrees = True - if self.importPage.wasChanged: - newList, delList = self.importPage.getNewList() - project.setImportColours(newList, delList) + if self.importPage.changed: + logger.debug("Updating importance labels") + project.data.itemImport.update(self.importPage.getNewList()) rebuildTrees = True - if self.replacePage.wasChanged: - newList = self.replacePage.getNewList() - project.data.setAutoReplace(newList) + if self.replacePage.changed: + logger.debug("Updating auto-replace settings") + project.data.setAutoReplace(self.replacePage.getNewList()) self.newProjectSettingsReady.emit(rebuildTrees) QApplication.processEvents() @@ -301,12 +307,12 @@ class _SettingsPage(NScrollableForm): class _StatusPage(NFixedPage): - COL_LABEL = 0 - COL_USAGE = 1 + C_DATA = 0 + C_LABEL = 0 + C_USAGE = 1 - KEY_ROLE = QtUserRole - COL_ROLE = QtUserRole + 1 - NUM_ROLE = QtUserRole + 2 + D_KEY = QtUserRole + D_ENTRY = QtUserRole + 1 def __init__(self, parent: QWidget, isStatus: bool) -> None: super().__init__(parent=parent) @@ -325,12 +331,21 @@ class _StatusPage(NFixedPage): ) self._changed = False - self._colDeleted = [] - self._selColour = QColor(100, 100, 100) + self._color = QColor(100, 100, 100) + self._shape = nwStatusShape.SQUARE + self._icons = {} - self.iPx = SHARED.theme.baseIconHeight + self._iPx = SHARED.theme.baseIconHeight iSz = SHARED.theme.baseIconSize - bSz = SHARED.theme.buttonIconSize + bPd = CONFIG.pxInt(4) + + iColor = self.palette().text().color() + + # Labels + self.trCountNone = self.tr("Not in use") + self.trCountOne = self.tr("Used once") + self.trCountMore = self.tr("Used by {0} items") + self.trSelColor = self.tr("Select Colour") # Title self.pageTitle = NColourLabel( @@ -341,12 +356,14 @@ class _StatusPage(NFixedPage): # List Box self.listBox = QTreeWidget(self) self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")]) - self.listBox.itemSelectionChanged.connect(self._selectedItem) - self.listBox.setColumnWidth(self.COL_LABEL, wCol0) + self.listBox.setColumnWidth(self.C_LABEL, wCol0) self.listBox.setIndentation(0) + self.listBox.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.listBox.itemSelectionChanged.connect(self._selectionChanged) - for key, entry in status.items(): - self._addItem(key, entry["name"], entry["cols"], entry["count"]) + for key, entry in status.iterItems(): + self._addItem(key, StatusEntry.duplicate(entry)) # List Controls self.addButton = NIconToolButton(self, iSz, "add") @@ -367,16 +384,43 @@ class _StatusPage(NFixedPage): self.editName.setPlaceholderText(self.tr("Select item to edit")) self.editName.setEnabled(False) - self.colPixmap = QPixmap(self.iPx, self.iPx) - self.colPixmap.fill(QColor(100, 100, 100)) - self.colButton = QPushButton(QIcon(self.colPixmap), self.tr("Colour"), self) - self.colButton.setIconSize(bSz) - self.colButton.setEnabled(False) - self.colButton.clicked.connect(self._selectColour) + buttonStyle = ( + f"QToolButton {{padding: 0 {bPd}px;}} " + "QToolButton::menu-indicator {image: none;}" + ) - self.saveButton = QPushButton(self.tr("Save"), self) - self.saveButton.setEnabled(False) - self.saveButton.clicked.connect(self._saveItem) + self.colorButton = NIconToolButton(self, iSz) + self.colorButton.setToolTip(self.tr("Colour")) + self.colorButton.setSizePolicy(QtSizeMinimum, QtSizeMinimumExpanding) + self.colorButton.setStyleSheet(buttonStyle) + self.colorButton.setEnabled(False) + self.colorButton.clicked.connect(self._selectColour) + + def buildMenu(menu: QMenu, items: dict[nwStatusShape, str]) -> None: + for shape, label in items.items(): + icon = NWStatus.createIcon(self._iPx, iColor, shape) + action = menu.addAction(icon, label) + action.triggered.connect(lambda _, shape=shape: self._selectShape(shape)) + self._icons[shape] = icon + + self.shapeMenu = QMenu(self) + buildMenu(self.shapeMenu, nwLabels.SHAPES_PLAIN) + buildMenu(self.shapeMenu.addMenu(self.tr("Circles ...")), nwLabels.SHAPES_CIRCLE) + buildMenu(self.shapeMenu.addMenu(self.tr("Bars ...")), nwLabels.SHAPES_BARS) + buildMenu(self.shapeMenu.addMenu(self.tr("Blocks ...")), nwLabels.SHAPES_BLOCKS) + + self.shapeButton = NIconToolButton(self, iSz) + self.shapeButton.setMenu(self.shapeMenu) + self.shapeButton.setToolTip(self.tr("Shape")) + self.shapeButton.setSizePolicy(QtSizeMinimum, QtSizeMinimumExpanding) + self.shapeButton.setStyleSheet(buttonStyle) + self.shapeButton.setEnabled(False) + + self.applyButton = QToolButton(self) + self.applyButton.setText(self.tr("Apply")) + self.applyButton.setSizePolicy(QtSizeMinimum, QtSizeMinimumExpanding) + self.applyButton.setEnabled(False) + self.applyButton.clicked.connect(self._applyChanges) # Assemble self.listControls = QVBoxLayout() @@ -387,28 +431,30 @@ class _StatusPage(NFixedPage): self.listControls.addStretch(1) self.editBox = QHBoxLayout() - self.editBox.addWidget(self.editName) - self.editBox.addWidget(self.colButton) - self.editBox.addWidget(self.saveButton) + self.editBox.addWidget(self.editName, 1) + self.editBox.addWidget(self.colorButton, 0) + self.editBox.addWidget(self.shapeButton, 0) + self.editBox.addWidget(self.applyButton, 0) self.mainBox = QVBoxLayout() - self.mainBox.addWidget(self.listBox) - self.mainBox.addLayout(self.editBox) + self.mainBox.addWidget(self.listBox, 1) + self.mainBox.addLayout(self.editBox, 0) self.innerBox = QHBoxLayout() - self.innerBox.addLayout(self.mainBox) - self.innerBox.addLayout(self.listControls) + self.innerBox.addLayout(self.mainBox, 1) + self.innerBox.addLayout(self.listControls, 0) self.outerBox = QVBoxLayout() - self.outerBox.addWidget(self.pageTitle) - self.outerBox.addLayout(self.innerBox) + self.outerBox.addWidget(self.pageTitle, 0) + self.outerBox.addLayout(self.innerBox, 1) self.setCentralLayout(self.outerBox) + self._setButtonIcons() return @property - def wasChanged(self) -> bool: + def changed(self) -> bool: """The user changed these settings.""" return self._changed @@ -416,20 +462,17 @@ class _StatusPage(NFixedPage): # Methods ## - def getNewList(self) -> tuple[list, list]: + def getNewList(self) -> list[tuple[str | None, StatusEntry]]: """Return list of entries.""" if self._changed: - newList = [] + update = [] for n in range(self.listBox.topLevelItemCount()): - item = self.listBox.topLevelItem(n) - if item is not None: - newList.append({ - "key": item.data(self.COL_LABEL, self.KEY_ROLE), - "name": item.text(self.COL_LABEL), - "cols": item.data(self.COL_LABEL, self.COL_ROLE), - }) - return newList, self._colDeleted - return [], [] + if item := self.listBox.topLevelItem(n): + key = item.data(self.C_DATA, self.D_KEY) + entry = item.data(self.C_DATA, self.D_ENTRY) + update.append((key, entry)) + return update + return [] def columnWidth(self) -> int: """Return the size of the header column.""" @@ -442,124 +485,119 @@ class _StatusPage(NFixedPage): @pyqtSlot() def _selectColour(self) -> None: """Open a dialog to select the status icon colour.""" - if self._selColour is not None: - newCol = QColorDialog.getColor( - self._selColour, self, self.tr("Select Colour") - ) - if newCol.isValid(): - self._selColour = newCol - pixmap = QPixmap(self.iPx, self.iPx) - pixmap.fill(newCol) - self.colButton.setIcon(QIcon(pixmap)) - self.colButton.setIconSize(pixmap.rect().size()) + if (color := QColorDialog.getColor(self._color, self, self.trSelColor)).isValid(): + self._color = color + self._setButtonIcons() return @pyqtSlot() def _newItem(self) -> None: """Create a new status item.""" - self._addItem(None, self.tr("New Item"), (100, 100, 100), 0) + color = QColor(100, 100, 100) + shape = nwStatusShape.SQUARE + icon = NWStatus.createIcon(self._iPx, color, shape) + self._addItem(None, StatusEntry(self.tr("New Item"), color, shape, icon, 0)) self._changed = True return @pyqtSlot() def _delItem(self) -> None: """Delete a status item.""" - selItem = self._getSelectedItem() - if isinstance(selItem, QTreeWidgetItem): - iRow = self.listBox.indexOfTopLevelItem(selItem) - if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0: + if item := self._getSelectedItem(): + iRow = self.listBox.indexOfTopLevelItem(item) + entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY) + if entry.count > 0: SHARED.error(self.tr("Cannot delete a status item that is in use.")) else: self.listBox.takeTopLevelItem(iRow) - self._colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) self._changed = True return @pyqtSlot() - def _saveItem(self) -> None: + def _applyChanges(self) -> None: """Save changes made to a status item.""" - selItem = self._getSelectedItem() - if isinstance(selItem, QTreeWidgetItem): - selItem.setText(self.COL_LABEL, simplified(self.editName.text())) - selItem.setIcon(self.COL_LABEL, self.colButton.icon()) - selItem.setData(self.COL_LABEL, self.COL_ROLE, ( - self._selColour.red(), self._selColour.green(), self._selColour.blue() - )) + if item := self._getSelectedItem(): + entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY) + + name = simplified(self.editName.text()) + icon = NWStatus.createIcon(self._iPx, self._color, self._shape) + + entry.name = name + entry.color = self._color + entry.shape = self._shape + entry.icon = icon + + item.setText(self.C_LABEL, name) + item.setIcon(self.C_LABEL, icon) + self._changed = True + return @pyqtSlot() - def _selectedItem(self) -> None: + def _selectionChanged(self) -> None: """Extract the info of a selected item and populate the settings boxes and button. If no item is selected, clear the form. """ - selItem = self._getSelectedItem() - if isinstance(selItem, QTreeWidgetItem): - cols = selItem.data(self.COL_LABEL, self.COL_ROLE) - name = selItem.text(self.COL_LABEL) - pixmap = QPixmap(self.iPx, self.iPx) - pixmap.fill(QColor(*cols)) - self._selColour = QColor(*cols) - self.editName.setText(name) - self.colButton.setIcon(QIcon(pixmap)) + if item := self._getSelectedItem(): + entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY) + self._color = entry.color + self._shape = entry.shape + self._setButtonIcons() + + self.editName.setText(entry.name) self.editName.selectAll() self.editName.setFocus() + self.editName.setEnabled(True) - self.colButton.setEnabled(True) - self.saveButton.setEnabled(True) + self.colorButton.setEnabled(True) + self.shapeButton.setEnabled(True) + self.applyButton.setEnabled(True) + else: - pixmap = QPixmap(self.iPx, self.iPx) - pixmap.fill(QColor(100, 100, 100)) - self._selColour = QColor(100, 100, 100) + self._color = QColor(100, 100, 100) + self._shape = nwStatusShape.SQUARE + self._setButtonIcons() self.editName.setText("") - self.colButton.setIcon(QIcon(pixmap)) + self.editName.setEnabled(False) - self.colButton.setEnabled(False) - self.saveButton.setEnabled(False) + self.colorButton.setEnabled(False) + self.shapeButton.setEnabled(False) + self.applyButton.setEnabled(False) return ## # Internal Functions ## - def _addItem(self, key: str | None, name: str, - colour: tuple[int, int, int], count: int) -> None: + def _selectShape(self, shape: nwStatusShape) -> None: + """Set the current shape.""" + self._shape = shape + self._setButtonIcons() + return + + def _addItem(self, key: str | None, entry: StatusEntry) -> None: """Add a status item to the list.""" - pixmap = QPixmap(self.iPx, self.iPx) - pixmap.fill(QColor(*colour)) - item = QTreeWidgetItem() - item.setText(self.COL_LABEL, name) - item.setIcon(self.COL_LABEL, QIcon(pixmap)) - item.setData(self.COL_LABEL, self.KEY_ROLE, key) - item.setData(self.COL_LABEL, self.COL_ROLE, colour) - item.setData(self.COL_LABEL, self.NUM_ROLE, count) - item.setText(self.COL_USAGE, self._usageString(count)) - + item.setText(self.C_LABEL, entry.name) + item.setIcon(self.C_LABEL, entry.icon) + item.setText(self.C_USAGE, self._usageString(entry.count)) + item.setData(self.C_DATA, self.D_KEY, key) + item.setData(self.C_DATA, self.D_ENTRY, entry) self.listBox.addTopLevelItem(item) - return def _moveItem(self, step: int) -> None: """Move and item up or down step.""" - selItem = self._getSelectedItem() - if selItem is None: - return - - tIndex = self.listBox.indexOfTopLevelItem(selItem) - nChild = self.listBox.topLevelItemCount() - nIndex = tIndex + step - if nIndex < 0 or nIndex >= nChild: - return - - cItem = self.listBox.takeTopLevelItem(tIndex) - self.listBox.insertTopLevelItem(nIndex, cItem) - self.listBox.clearSelection() - - if cItem is not None: - cItem.setSelected(True) - self._changed = True - + if item := self._getSelectedItem(): + tIdx = self.listBox.indexOfTopLevelItem(item) + nItm = self.listBox.topLevelItemCount() + nIdx = tIdx + step + if (0 <= nIdx < nItm) and (cItem := self.listBox.takeTopLevelItem(tIdx)): + self.listBox.insertTopLevelItem(nIdx, cItem) + self.listBox.clearSelection() + cItem.setSelected(True) + self._changed = True return def _getSelectedItem(self) -> QTreeWidgetItem | None: @@ -571,19 +609,26 @@ class _StatusPage(NFixedPage): def _usageString(self, count: int) -> str: """Generate usage string.""" if count == 0: - return self.tr("Not in use") + return self.trCountNone elif count == 1: - return self.tr("Used once") + return self.trCountOne else: - return self.tr("Used by {0} items").format(count) + return self.trCountMore.format(count) + + def _setButtonIcons(self) -> None: + """Set the colour of the colour button.""" + icon = NWStatus.createIcon(self._iPx, self._color, nwStatusShape.SQUARE) + self.colorButton.setIcon(icon) + self.shapeButton.setIcon(self._icons[self._shape]) + return # END Class _StatusPage class _ReplacePage(NFixedPage): - COL_KEY = 0 - COL_REPL = 1 + C_KEY = 0 + C_REPL = 1 def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -605,15 +650,17 @@ class _ReplacePage(NFixedPage): # List Box self.listBox = QTreeWidget(self) self.listBox.setHeaderLabels([self.tr("Keyword"), self.tr("Replace With")]) - self.listBox.setColumnWidth(self.COL_KEY, wCol0) + self.listBox.setColumnWidth(self.C_KEY, wCol0) self.listBox.setIndentation(0) - self.listBox.itemSelectionChanged.connect(self._selectedItem) + self.listBox.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.listBox.itemSelectionChanged.connect(self._selectionChanged) for aKey, aVal in SHARED.project.data.autoReplace.items(): newItem = QTreeWidgetItem(["<%s>" % aKey, aVal]) self.listBox.addTopLevelItem(newItem) - self.listBox.sortByColumn(self.COL_KEY, Qt.SortOrder.AscendingOrder) + self.listBox.sortByColumn(self.C_KEY, Qt.SortOrder.AscendingOrder) self.listBox.setSortingEnabled(True) # List Controls @@ -633,8 +680,10 @@ class _ReplacePage(NFixedPage): self.editValue.setEnabled(False) self.editValue.setMaxLength(80) - self.saveButton = QPushButton(self.tr("Save"), self) - self.saveButton.clicked.connect(self._saveEntry) + self.applyButton = QToolButton(self) + self.applyButton.setText(self.tr("Apply")) + self.applyButton.setSizePolicy(QtSizeMinimum, QtSizeMinimumExpanding) + self.applyButton.clicked.connect(self._applyChanges) # Assemble self.listControls = QVBoxLayout() @@ -645,11 +694,11 @@ class _ReplacePage(NFixedPage): self.editBox = QHBoxLayout() self.editBox.addWidget(self.editKey, 4) self.editBox.addWidget(self.editValue, 5) - self.editBox.addWidget(self.saveButton, 0) + self.editBox.addWidget(self.applyButton, 0) self.mainBox = QVBoxLayout() - self.mainBox.addWidget(self.listBox) - self.mainBox.addLayout(self.editBox) + self.mainBox.addWidget(self.listBox, 1) + self.mainBox.addLayout(self.editBox, 0) self.innerBox = QHBoxLayout() self.innerBox.addLayout(self.mainBox) @@ -664,7 +713,7 @@ class _ReplacePage(NFixedPage): return @property - def wasChanged(self) -> bool: + def changed(self) -> bool: """The user changed these settings.""" return self._changed @@ -672,15 +721,13 @@ class _ReplacePage(NFixedPage): # Methods ## - def getNewList(self) -> dict: + def getNewList(self) -> dict[str, str]: """Extract the list from the widget.""" new = {} for n in range(self.listBox.topLevelItemCount()): - if tItem := self.listBox.topLevelItem(n): - aKey = self._stripNotAllowed(tItem.text(0)) - aVal = tItem.text(1) - if len(aKey) > 0: - new[aKey] = aVal + if item := self.listBox.topLevelItem(n): + if key := self._stripNotAllowed(item.text(self.C_KEY)): + new[key] = item.text(self.C_REPL) return new def columnWidth(self) -> int: @@ -692,51 +739,48 @@ class _ReplacePage(NFixedPage): ## @pyqtSlot() - def _selectedItem(self) -> None: + def _selectionChanged(self) -> None: """Extract the details from the selected item and populate the edit form. """ - if selItem := self._getSelectedItem(): - editKey = self._stripNotAllowed(selItem.text(0)) - editVal = selItem.text(1) - self.editKey.setText(editKey) - self.editValue.setText(editVal) + if item := self._getSelectedItem(): + self.editKey.setText(self._stripNotAllowed(item.text(self.C_KEY))) + self.editValue.setText(item.text(self.C_REPL)) self.editKey.setEnabled(True) self.editValue.setEnabled(True) self.editKey.selectAll() self.editKey.setFocus() + else: + self.editKey.setText("") + self.editValue.setText("") + self.editKey.setEnabled(False) + self.editValue.setEnabled(False) return @pyqtSlot() - def _saveEntry(self) -> None: + def _applyChanges(self) -> None: """Save the form data into the list widget.""" - if selItem := self._getSelectedItem(): - newKey = self.editKey.text() - newVal = self.editValue.text() - saveKey = self._stripNotAllowed(newKey) - if len(saveKey) > 0 and len(newVal) > 0: - selItem.setText(self.COL_KEY, "<%s>" % saveKey) - selItem.setText(self.COL_REPL, newVal) - self.editKey.clear() - self.editValue.clear() - self.editKey.setEnabled(False) - self.editValue.setEnabled(False) - self.listBox.clearSelection() + if item := self._getSelectedItem(): + key = self._stripNotAllowed(self.editKey.text()) + value = self.editValue.text() + if key and value: + item.setText(self.C_KEY, f"<{key}>") + item.setText(self.C_REPL, value) self._changed = True return @pyqtSlot() def _addEntry(self) -> None: """Add a new list entry.""" - saveKey = "" % (self.listBox.topLevelItemCount() + 1) - self.listBox.addTopLevelItem(QTreeWidgetItem([saveKey, ""])) + key = f"" + self.listBox.addTopLevelItem(QTreeWidgetItem([key, ""])) return @pyqtSlot() def _delEntry(self) -> None: """Delete the selected entry.""" - if selItem := self._getSelectedItem(): - self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem)) + if item := self._getSelectedItem(): + self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(item)) self._changed = True return diff --git a/novelwriter/enum.py b/novelwriter/enum.py index d905e587..96d97f15 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -205,3 +205,29 @@ class nwBuildFmt(Enum): J_NWD = 7 # END Enum nwBuildFormat + + +class nwStatusShape(Enum): + + SQUARE = 0 + TRIANGLE = 1 + NABLA = 2 + DIAMOND = 3 + PENTAGON = 4 + HEXAGON = 5 + STAR = 6 + PACMAN = 7 + CIRCLE_Q = 8 + CIRCLE_H = 9 + CIRCLE_T = 10 + CIRCLE = 11 + BARS_1 = 12 + BARS_2 = 13 + BARS_3 = 14 + BARS_4 = 15 + BLOCK_1 = 16 + BLOCK_2 = 17 + BLOCK_3 = 18 + BLOCK_4 = 19 + +# END Enum nwStatusShape diff --git a/novelwriter/extensions/circularprogress.py b/novelwriter/extensions/circularprogress.py index bbc2f64a..23ca7d3d 100644 --- a/novelwriter/extensions/circularprogress.py +++ b/novelwriter/extensions/circularprogress.py @@ -27,10 +27,11 @@ from math import ceil from PyQt5.QtCore import QRect from PyQt5.QtGui import QBrush, QColor, QPaintEvent, QPainter, QPen -from PyQt5.QtWidgets import QProgressBar, QSizePolicy, QWidget +from PyQt5.QtWidgets import QProgressBar, QWidget from novelwriter.types import ( - QtPaintAnitAlias, QtAlignCenter, QtRoundCap, QtSolidLine, QtTransparent + QtPaintAnitAlias, QtAlignCenter, QtRoundCap, QtSizeFixed, QtSolidLine, + QtTransparent ) @@ -59,7 +60,7 @@ class NProgressCircle(QProgressBar): bar=self.palette().highlight().color(), text=self.palette().text().color() ) - self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + self.setSizePolicy(QtSizeFixed, QtSizeFixed) self.setFixedWidth(size) self.setFixedHeight(size) return diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index 4eb50cff..b83a2662 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -25,6 +25,8 @@ along with this program. If not, see . """ from __future__ import annotations +from enum import Enum + from PyQt5.QtCore import QSize, Qt from PyQt5.QtGui import QWheelEvent from PyQt5.QtWidgets import QComboBox, QDoubleSpinBox, QSpinBox, QToolButton, QWidget @@ -46,7 +48,7 @@ class NComboBox(QComboBox): event.ignore() return - def setCurrentData(self, data: str, default: str) -> None: + def setCurrentData(self, data: str | Enum, default: str | Enum) -> None: """Set the current index from data, with a fallback.""" idx = self.findData(data) self.setCurrentIndex(self.findData(default) if idx < 0 else idx) diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index a3c53455..7709509f 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -28,11 +28,14 @@ from __future__ import annotations from PyQt5.QtGui import QColor, QPaintEvent, QPainter, QPolygon from PyQt5.QtCore import QPoint, QRectF, QSize, Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QAbstractButton, QAction, QButtonGroup, QLabel, QSizePolicy, QStyle, + QAbstractButton, QAction, QButtonGroup, QLabel, QStyle, QStyleOptionToolButton, QToolBar, QToolButton, QWidget ) -from novelwriter.types import QtPaintAnitAlias, QtAlignLeft, QtMouseOver, QtNoBrush, QtNoPen +from novelwriter.types import ( + QtPaintAnitAlias, QtAlignLeft, QtMouseOver, QtNoBrush, QtNoPen, + QtSizeExpanding, QtSizeFixed +) class NPagedSideBar(QToolBar): @@ -59,7 +62,7 @@ class NPagedSideBar(QToolBar): self.setOrientation(Qt.Orientation.Vertical) stretch = QWidget(self) - stretch.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + stretch.setSizePolicy(QtSizeExpanding, QtSizeExpanding) self._stretchAction = self.addWidget(stretch) return @@ -119,7 +122,7 @@ class _NPagedToolButton(QToolButton): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.setSizePolicy(QtSizeExpanding, QtSizeFixed) self.setCheckable(True) fH = self.fontMetrics().height() @@ -197,7 +200,7 @@ class _NPagedToolLabel(QLabel): def __init__(self, parent: QWidget, textColor: QColor | None = None) -> None: super().__init__(parent=parent) - self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.setSizePolicy(QtSizeExpanding, QtSizeFixed) fH = self.fontMetrics().height() self._bH = round(fH * 1.7) diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py index e35bd79a..1c04a158 100644 --- a/novelwriter/extensions/switch.py +++ b/novelwriter/extensions/switch.py @@ -25,10 +25,10 @@ from __future__ import annotations from PyQt5.QtGui import QMouseEvent, QPainter, QPaintEvent, QResizeEvent from PyQt5.QtCore import QEvent, QPropertyAnimation, Qt, pyqtProperty -from PyQt5.QtWidgets import QAbstractButton, QSizePolicy, QWidget +from PyQt5.QtWidgets import QAbstractButton, QWidget from novelwriter import CONFIG, SHARED -from novelwriter.types import QtPaintAnitAlias, QtMouseLeft, QtNoPen +from novelwriter.types import QtPaintAnitAlias, QtMouseLeft, QtNoPen, QtSizeFixed class NSwitch(QAbstractButton): @@ -46,7 +46,7 @@ class NSwitch(QAbstractButton): self._rR = self._xR - self._rB self.setCheckable(True) - self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + self.setSizePolicy(QtSizeFixed, QtSizeFixed) self.setFixedWidth(self._xW) self.setFixedHeight(self._xH) self._offset = self._xR diff --git a/novelwriter/extensions/switchbox.py b/novelwriter/extensions/switchbox.py index d41587ba..1cff6a9c 100644 --- a/novelwriter/extensions/switchbox.py +++ b/novelwriter/extensions/switchbox.py @@ -25,10 +25,13 @@ from __future__ import annotations from PyQt5.QtGui import QIcon from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import QGridLayout, QLabel, QScrollArea, QSizePolicy, QWidget +from PyQt5.QtWidgets import QGridLayout, QLabel, QScrollArea, QWidget from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAlignLeft, QtAlignRight, QtAlignRightMiddle +from novelwriter.types import ( + QtAlignLeft, QtAlignRight, QtAlignRightMiddle, QtSizeMinimum, + QtSizeMinimumExpanding +) class NSwitchBox(QScrollArea): @@ -59,7 +62,7 @@ class NSwitchBox(QScrollArea): self._content.setColumnStretch(1, 1) self._widget = QWidget(self) - self._widget.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Minimum) + self._widget.setSizePolicy(QtSizeMinimumExpanding, QtSizeMinimum) self._widget.setLayout(self._content) self.setWidgetResizable(True) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 08a963e7..4d603289 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -3136,7 +3136,7 @@ class GuiDocEditFooter(QWidget): sText = "" else: iPx = round(0.9*SHARED.theme.baseIconHeight) - status, icon = self._tItem.getImportStatus(incIcon=True) + status, icon = self._tItem.getImportStatus() sIcon = icon.pixmap(iPx, iPx) sText = f"{status} / {self._tItem.describeMe()}" diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py index f96e44e6..ed4d9ba6 100644 --- a/novelwriter/gui/docviewerpanel.py +++ b/novelwriter/gui/docviewerpanel.py @@ -450,7 +450,7 @@ class _ViewPanelKeyWords(QTreeWidget): nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading ) - impLabel, impIcon = nwItem.getImportStatus(incIcon=True) + impLabel, impIcon = nwItem.getImportStatus() iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5 hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel) diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 5e9ed84d..1a90d2a3 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -253,7 +253,7 @@ class GuiItemDetails(QWidget): # Status # ====== - status, icon = nwItem.getImportStatus(incIcon=True) + status, icon = nwItem.getImportStatus() self.statusIcon.setPixmap(icon.pixmap(iPx, iPx)) self.statusData.setText(status) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 7eea96d8..6643df47 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -35,8 +35,8 @@ from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSlot, pyqtSignal from PyQt5.QtGui import QFocusEvent, QFont, QMouseEvent, QPalette, QResizeEvent from PyQt5.QtWidgets import ( QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, - QInputDialog, QMenu, QSizePolicy, QToolTip, QTreeWidget, QTreeWidgetItem, - QVBoxLayout, QWidget + QInputDialog, QMenu, QToolTip, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget ) from novelwriter import CONFIG, SHARED @@ -47,7 +47,10 @@ from novelwriter.enum import nwDocMode, nwItemClass, nwOutline from novelwriter.extensions.modified import NIconToolButton from novelwriter.extensions.novelselector import NovelSelector from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON -from novelwriter.types import QtAlignRight, QtDecoration, QtMouseLeft, QtMouseMiddle, QtUserRole +from novelwriter.types import ( + QtAlignRight, QtDecoration, QtMouseLeft, QtMouseMiddle, QtSizeExpanding, + QtUserRole +) if TYPE_CHECKING: # pragma: no cover from novelwriter.guimain import GuiMain @@ -215,7 +218,7 @@ class GuiNovelToolBar(QWidget): self.novelValue.setFont(selFont) self.novelValue.setListFormat(self.tr("Outline of {0}")) self.novelValue.setMinimumWidth(CONFIG.pxInt(150)) - self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.novelValue.setSizePolicy(QtSizeExpanding, QtSizeExpanding) self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot) self.tbNovel = NIconToolButton(self, iSz) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 4ed2679f..380455ce 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -36,20 +36,19 @@ from enum import Enum from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, QT_TRANSLATE_NOOP from PyQt5.QtWidgets import ( QAbstractItemView, QAction, QFileDialog, QFrame, QGridLayout, QGroupBox, - QHBoxLayout, QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar, - QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget + QHBoxLayout, QLabel, QMenu, QScrollArea, QSplitter, QToolBar, QToolButton, + QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED -from novelwriter.enum import ( - nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline -) +from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline from novelwriter.error import logException from novelwriter.common import checkInt, formatFileFilter, makeFileNameSafe from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels from novelwriter.extensions.novelselector import NovelSelector from novelwriter.types import ( - QtAlignLeftTop, QtAlignRight, QtAlignRightTop, QtDecoration, QtUserRole + QtAlignLeftTop, QtAlignRight, QtAlignRightTop, QtDecoration, + QtSizeExpanding, QtUserRole ) @@ -217,7 +216,7 @@ class GuiOutlineToolBar(QToolBar): self.setContentsMargins(0, 0, 0, 0) stretch = QWidget(self) - stretch.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + stretch.setSizePolicy(QtSizeExpanding, QtSizeExpanding) # Novel Selector self.novelLabel = QLabel(self.tr("Outline of"), self) @@ -1048,7 +1047,7 @@ class GuiOutlineDetails(QScrollArea): self.titleLabel.setText(self.tr(self.LVL_MAP.get(novIdx.level, "H1"))) self.titleValue.setText(novIdx.title) - itemStatus, _ = nwItem.getImportStatus(incIcon=False) + itemStatus, _ = nwItem.getImportStatus() self.fileValue.setText(nwItem.itemName) self.itemValue.setText(itemStatus) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 5ba158af..8074325b 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -38,8 +38,8 @@ from PyQt5.QtGui import ( ) from PyQt5.QtWidgets import ( QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView, - QLabel, QMenu, QShortcut, QSizePolicy, QTreeWidget, QTreeWidgetItem, - QVBoxLayout, QWidget + QLabel, QMenu, QShortcut, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget ) from novelwriter import CONFIG, SHARED @@ -54,7 +54,10 @@ from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout from novelwriter.extensions.modified import NIconToolButton from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON -from novelwriter.types import QtAlignLeft, QtAlignRight, QtMouseLeft, QtMouseMiddle, QtUserRole +from novelwriter.types import ( + QtAlignLeft, QtAlignRight, QtMouseLeft, QtMouseMiddle, QtSizeExpanding, + QtUserRole, +) if TYPE_CHECKING: # pragma: no cover from novelwriter.guimain import GuiMain @@ -274,7 +277,7 @@ class GuiProjectToolBar(QWidget): self.viewLabel = QLabel(self.tr("Project Content"), self) self.viewLabel.setFont(SHARED.theme.guiFontB) self.viewLabel.setContentsMargins(0, 0, 0, 0) - self.viewLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.viewLabel.setSizePolicy(QtSizeExpanding, QtSizeExpanding) # Quick Links self.mQuick = QMenu(self) @@ -1033,7 +1036,7 @@ class GuiProjectTree(QTreeWidget): if trItem is None or nwItem is None: return - itemStatus, statusIcon = nwItem.getImportStatus(incIcon=True) + itemStatus, statusIcon = nwItem.getImportStatus() hLevel = nwItem.mainHeading itemIcon = SHARED.theme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel @@ -1855,11 +1858,11 @@ class _TreeContextMenu(QMenu): if self._item.isNovelLike(): menu = self.addMenu(self.tr("Set Status to ...")) current = self._item.itemStatus - for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.items()): - name = entry["name"] + for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.iterItems()): + name = entry.name if not multi and current == key: name += f" ({nwUnicode.U_CHECK})" - action = menu.addAction(entry["icon"], name) + action = menu.addAction(entry.icon, name) if multi: action.triggered.connect(lambda n, key=key: self._iterSetItemStatus(key)) else: @@ -1872,11 +1875,11 @@ class _TreeContextMenu(QMenu): else: menu = self.addMenu(self.tr("Set Importance to ...")) current = self._item.itemImport - for n, (key, entry) in enumerate(SHARED.project.data.itemImport.items()): - name = entry["name"] + for n, (key, entry) in enumerate(SHARED.project.data.itemImport.iterItems()): + name = entry.name if not multi and current == key: name += f" ({nwUnicode.U_CHECK})" - action = menu.addAction(entry["icon"], name) + action = menu.addAction(entry.icon, name) if multi: action.triggered.connect(lambda n, key=key: self._iterSetItemImport(key)) else: diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 1cec67d5..782a7109 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -36,8 +36,8 @@ from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter from PyQt5.QtWidgets import ( QAbstractItemView, QApplication, QDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, - QSizePolicy, QSplitter, QStackedWidget, QTabWidget, QTextBrowser, - QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget + QSplitter, QStackedWidget, QTabWidget, QTextBrowser, QTreeWidget, + QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED @@ -54,7 +54,7 @@ from novelwriter.tools.manusbuild import GuiManuscriptBuild from novelwriter.tools.manussettings import GuiBuildSettings from novelwriter.types import ( QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignRight, QtAlignTop, - QtUserRole + QtSizeExpanding, QtSizeIgnored, QtUserRole ) if TYPE_CHECKING: # pragma: no cover @@ -1023,16 +1023,14 @@ class _StatsWidget(QWidget): @pyqtSlot(bool) def _toggleView(self, state: bool) -> None: """Toggle minimal or maximal view.""" - ignored = QSizePolicy.Policy.Ignored - expanded = QSizePolicy.Policy.Expanding if state: self.mainStack.setCurrentWidget(self.maxWidget) - self.maxWidget.setSizePolicy(expanded, expanded) - self.minWidget.setSizePolicy(ignored, ignored) + self.maxWidget.setSizePolicy(QtSizeExpanding, QtSizeExpanding) + self.minWidget.setSizePolicy(QtSizeIgnored, QtSizeIgnored) else: self.mainStack.setCurrentWidget(self.minWidget) - self.maxWidget.setSizePolicy(ignored, ignored) - self.minWidget.setSizePolicy(expanded, expanded) + self.maxWidget.setSizePolicy(QtSizeIgnored, QtSizeIgnored) + self.minWidget.setSizePolicy(QtSizeExpanding, QtSizeExpanding) self.maxWidget.adjustSize() self.minWidget.adjustSize() self.mainStack.adjustSize() diff --git a/novelwriter/types.py b/novelwriter/types.py index 56e59604..49e670ce 100644 --- a/novelwriter/types.py +++ b/novelwriter/types.py @@ -25,7 +25,7 @@ from __future__ import annotations from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor, QPainter, QTextCursor -from PyQt5.QtWidgets import QDialogButtonBox, QStyle +from PyQt5.QtWidgets import QDialogButtonBox, QSizePolicy, QStyle # Qt Alignment Flags @@ -88,3 +88,11 @@ QtKeepAnchor = QTextCursor.MoveMode.KeepAnchor QtMoveAnchor = QTextCursor.MoveMode.MoveAnchor QtMoveLeft = QTextCursor.MoveOperation.Left QtMoveRight = QTextCursor.MoveOperation.Right + +# Size Policy + +QtSizeExpanding = QSizePolicy.Policy.Expanding +QtSizeFixed = QSizePolicy.Policy.Fixed +QtSizeIgnored = QSizePolicy.Policy.Ignored +QtSizeMinimum = QSizePolicy.Policy.Minimum +QtSizeMinimumExpanding = QSizePolicy.Policy.MinimumExpanding diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 24170a34..716b21e7 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -20,19 +20,20 @@ D - New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Minor - Major - Main + None + Background + Minor + Major + Main @@ -57,7 +58,7 @@ Chapter One - + Making a Scene @@ -78,7 +79,7 @@ - We Found John! + We Found John! @@ -90,7 +91,7 @@ - Chapter One + Chapter One @@ -102,11 +103,11 @@ - John Smith + John Smith - Jane Smith + Jane Smith @@ -114,15 +115,15 @@ - Earth + Earth - Space + Space - Mars + Mars diff --git a/tests/files/nwProject-1.5.nwx b/tests/files/nwProject-1.5.nwx index 7823c057..35eab583 100644 --- a/tests/files/nwProject-1.5.nwx +++ b/tests/files/nwProject-1.5.nwx @@ -1,5 +1,5 @@ - + Sample Project Jane Smith @@ -20,19 +20,19 @@ D - New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Minor - Major - Main + None + Minor + Major + Main diff --git a/tests/mocked.py b/tests/mocked.py index 65b65d93..5f9fc6a3 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -75,7 +75,7 @@ class MockStatusBar: class MockTheme: def __init__(self): - self.baseIconHeight = 10 + self.baseIconHeight = 20 return def getPixmap(self, *a): diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index 2422fc02..4aba7481 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 028da14c..4bfbb568 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/coreTools_DocDuplicator_nwProject.nwx b/tests/reference/coreTools_DocDuplicator_nwProject.nwx index bab9a82d..5f6685a6 100644 --- a/tests/reference/coreTools_DocDuplicator_nwProject.nwx +++ b/tests/reference/coreTools_DocDuplicator_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx b/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx index 60c0fce3..01d3a0ef 100644 --- a/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx +++ b/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Project A Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx b/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx index 86980406..ed43d5cd 100644 --- a/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx +++ b/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Project B Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index c3ee941f..2c6603eb 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index aa9c3a74..f5119a55 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/projectXML_ReadLegacy10.nwx b/tests/reference/projectXML_ReadLegacy10.nwx index a8f9e945..70885cfd 100644 --- a/tests/reference/projectXML_ReadLegacy10.nwx +++ b/tests/reference/projectXML_ReadLegacy10.nwx @@ -1,5 +1,5 @@ - + Sample Project Jay Doh @@ -20,19 +20,19 @@ D - New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Minor - Major - Main + None + Minor + Major + Main diff --git a/tests/reference/projectXML_ReadLegacy11.nwx b/tests/reference/projectXML_ReadLegacy11.nwx index c216f88a..1d93bd02 100644 --- a/tests/reference/projectXML_ReadLegacy11.nwx +++ b/tests/reference/projectXML_ReadLegacy11.nwx @@ -1,5 +1,5 @@ - + Sample Project Jay Doh @@ -20,19 +20,19 @@ D - New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Minor - Major - Main + None + Minor + Major + Main diff --git a/tests/reference/projectXML_ReadLegacy12.nwx b/tests/reference/projectXML_ReadLegacy12.nwx index b3cad791..d344f378 100644 --- a/tests/reference/projectXML_ReadLegacy12.nwx +++ b/tests/reference/projectXML_ReadLegacy12.nwx @@ -1,5 +1,5 @@ - + Sample Project Jay Doh @@ -20,19 +20,19 @@ D - New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Minor - Major - Main + None + Minor + Major + Main diff --git a/tests/reference/projectXML_ReadLegacy13.nwx b/tests/reference/projectXML_ReadLegacy13.nwx index 4bd25a9a..05a4dfa5 100644 --- a/tests/reference/projectXML_ReadLegacy13.nwx +++ b/tests/reference/projectXML_ReadLegacy13.nwx @@ -1,5 +1,5 @@ - + Sample Project Jay Doh @@ -20,19 +20,19 @@ D - New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Minor - Major - Main + None + Minor + Major + Main diff --git a/tests/reference/projectXML_ReadLegacy14.nwx b/tests/reference/projectXML_ReadLegacy14.nwx index 1ca92061..2a7e24d0 100644 --- a/tests/reference/projectXML_ReadLegacy14.nwx +++ b/tests/reference/projectXML_ReadLegacy14.nwx @@ -1,5 +1,5 @@ - + Sample Project Jay Doh @@ -20,19 +20,19 @@ D - New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Minor - Major - Main + None + Minor + Major + Main diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 151d89da..65cf7b5f 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -553,8 +553,8 @@ def testCoreItem_ClassDefaults(mockGUI): def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd): """Test packing and unpacking entries for the NWItem class.""" project = NWProject() - project.data.itemStatus.write(None, "New", (100, 100, 100)) - project.data.itemImport.write(None, "New", (100, 100, 100)) + project.data.itemStatus.add(None, "New", (100, 100, 100), "SQUARE", 0) + project.data.itemImport.add(None, "New", (100, 100, 100), "SQUARE", 0) # Invalid item = NWItem(project, "0000000000000") diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 8a4c0b8f..3304fc14 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -400,115 +400,6 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd): # END Test testCoreProject_AccessItems -@pytest.mark.core -def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd): - """Test the status and importance flag handling.""" - project = NWProject() - mockRnd.reset() - buildTestProject(project, fncPath) - - statusKeys = [C.sNew, C.sNote, C.sDraft, C.sFinished] - importKeys = [C.iNew, C.iMinor, C.iMajor, C.iMain] - - # Change Status - # ============= - - project.tree[C.hNovelRoot].setStatus(statusKeys[3]) # type: ignore - project.tree[C.hPlotRoot].setStatus(statusKeys[2]) # type: ignore - project.tree[C.hCharRoot].setStatus(statusKeys[1]) # type: ignore - project.tree[C.hWorldRoot].setStatus(statusKeys[3]) # type: ignore - - assert project.tree[C.hNovelRoot].itemStatus == statusKeys[3] # type: ignore - assert project.tree[C.hPlotRoot].itemStatus == statusKeys[2] # type: ignore - assert project.tree[C.hCharRoot].itemStatus == statusKeys[1] # type: ignore - assert project.tree[C.hWorldRoot].itemStatus == statusKeys[3] # type: ignore - - newList = [ - {"key": statusKeys[0], "name": "New", "cols": (1, 1, 1)}, - {"key": statusKeys[1], "name": "Draft", "cols": (2, 2, 2)}, # These are swapped - {"key": statusKeys[2], "name": "Note", "cols": (3, 3, 3)}, # These are swapped - {"key": statusKeys[3], "name": "Edited", "cols": (4, 4, 4)}, # Renamed - {"key": None, "name": "Finished", "cols": (5, 5, 5)}, # New, reused name - ] - assert project.setStatusColours(None, None) is False # type: ignore - assert project.setStatusColours([], []) is False - assert project.setStatusColours(newList, []) is True - - assert project.data.itemStatus.name(statusKeys[0]) == "New" - assert project.data.itemStatus.name(statusKeys[1]) == "Draft" - assert project.data.itemStatus.name(statusKeys[2]) == "Note" - assert project.data.itemStatus.name(statusKeys[3]) == "Edited" - assert project.data.itemStatus.cols(statusKeys[0]) == (1, 1, 1) - assert project.data.itemStatus.cols(statusKeys[1]) == (2, 2, 2) - assert project.data.itemStatus.cols(statusKeys[2]) == (3, 3, 3) - assert project.data.itemStatus.cols(statusKeys[3]) == (4, 4, 4) - - # Check the new entry - lastKey = project.data.itemStatus.check("s000010") - assert lastKey == "s000010" - assert project.data.itemStatus.name(lastKey) == "Finished" - assert project.data.itemStatus.cols(lastKey) == (5, 5, 5) - - # Delete last entry - assert project.setStatusColours([], [lastKey]) is True - assert project.data.itemStatus.name(lastKey) == "New" - - # Change Importance - # ================= - - fHandle = project.newFile("Jane Doe", C.hCharRoot) - project.tree[fHandle].setImport(importKeys[3]) # type: ignore - - assert project.tree[fHandle].itemImport == importKeys[3] # type: ignore - newList = [ - {"key": importKeys[0], "name": "New", "cols": (1, 1, 1)}, - {"key": importKeys[1], "name": "Minor", "cols": (2, 2, 2)}, - {"key": importKeys[2], "name": "Major", "cols": (3, 3, 3)}, - {"key": importKeys[3], "name": "Min", "cols": (4, 4, 4)}, - {"key": None, "name": "Max", "cols": (5, 5, 5)}, - ] - assert project.setImportColours(None, None) is False # type: ignore - assert project.setImportColours([], []) is False - assert project.setImportColours(newList, []) is True - - assert project.data.itemImport.name(importKeys[0]) == "New" - assert project.data.itemImport.name(importKeys[1]) == "Minor" - assert project.data.itemImport.name(importKeys[2]) == "Major" - assert project.data.itemImport.name(importKeys[3]) == "Min" - assert project.data.itemImport.cols(importKeys[0]) == (1, 1, 1) - assert project.data.itemImport.cols(importKeys[1]) == (2, 2, 2) - assert project.data.itemImport.cols(importKeys[2]) == (3, 3, 3) - assert project.data.itemImport.cols(importKeys[3]) == (4, 4, 4) - - # Check the new entry - lastKey = project.data.itemImport.check("i000012") - assert lastKey == "i000012" - assert project.data.itemImport.name(lastKey) == "Max" - assert project.data.itemImport.cols(lastKey) == (5, 5, 5) - - # Delete last entry - assert project.setImportColours([], [lastKey]) is True - assert project.data.itemImport.name(lastKey) == "New" - - # Delete Status/Import - # ==================== - - project.data.itemStatus.resetCounts() - for key in list(project.data.itemStatus.keys()): - assert project.data.itemStatus.remove(key) is True - - project.data.itemImport.resetCounts() - for key in list(project.data.itemImport.keys()): - assert project.data.itemImport.remove(key) is True - - assert len(project.data.itemStatus) == 0 - assert len(project.data.itemImport) == 0 - assert project.saveProject() is True - project.closeProject() - -# END Test testCoreProject_StatusImport - - @pytest.mark.core def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd): """Test other project class methods and functions.""" diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py index 534aaed2..394e29cf 100644 --- a/tests/test_core/test_core_projectxml.py +++ b/tests/test_core/test_core_projectxml.py @@ -27,9 +27,12 @@ from shutil import copyfile from datetime import datetime from novelwriter.constants import nwFiles +from novelwriter.enum import nwStatusShape from tools import cmpFiles, writeFile from mocked import causeOSError +from PyQt5.QtGui import QColor + from novelwriter.core.item import NWItem from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState from novelwriter.core.projectdata import NWProjectData @@ -131,7 +134,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath): assert xmlReader.state == XMLReadState.PARSED_OK assert xmlReader.xmlRoot == "novelWriterXML" assert xmlReader.xmlVersion == 0x0105 - assert xmlReader.xmlRevision == 3 + assert xmlReader.xmlRevision == 4 assert xmlReader.appVersion == "2.0-rc1" assert xmlReader.hexVersion == 0x020000c1 @@ -154,44 +157,57 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath): assert data.getLastHandle("novelTree") == "7031beac91f75" assert data.getLastHandle("outline") == "7031beac91f75" - assert data.itemStatus.name("sf12341") == "New" - assert data.itemStatus.name("sf24ce6") == "Notes" - assert data.itemStatus.name("sc24b8f") == "Started" - assert data.itemStatus.name("s90e6c9") == "1st Draft" - assert data.itemStatus.name("sd51c5b") == "2nd Draft" - assert data.itemStatus.name("s8ae72a") == "3rd Draft" - assert data.itemStatus.name("s78ea90") == "Finished" + assert data.itemStatus["sf12341"].name == "New" + assert data.itemStatus["sf24ce6"].name == "Notes" + assert data.itemStatus["sc24b8f"].name == "Started" + assert data.itemStatus["s90e6c9"].name == "1st Draft" + assert data.itemStatus["sd51c5b"].name == "2nd Draft" + assert data.itemStatus["s8ae72a"].name == "3rd Draft" + assert data.itemStatus["s78ea90"].name == "Finished" - assert data.itemImport.name("ia857f0") == "None" - assert data.itemImport.name("icfb3a5") == "Minor" - assert data.itemImport.name("i2d7a54") == "Major" - assert data.itemImport.name("i56be10") == "Main" + assert data.itemImport["ia857f0"].name == "None" + assert data.itemImport["icfb3a5"].name == "Minor" + assert data.itemImport["i2d7a54"].name == "Major" + assert data.itemImport["i56be10"].name == "Main" - assert data.itemStatus.cols("sf12341") == (100, 100, 100) - assert data.itemStatus.cols("sf24ce6") == (200, 50, 0) - assert data.itemStatus.cols("sc24b8f") == (182, 60, 0) - assert data.itemStatus.cols("s90e6c9") == (193, 129, 0) - assert data.itemStatus.cols("sd51c5b") == (193, 129, 0) - assert data.itemStatus.cols("s8ae72a") == (193, 129, 0) - assert data.itemStatus.cols("s78ea90") == (58, 180, 58) + assert data.itemStatus["sf12341"].color == QColor(100, 100, 100) + assert data.itemStatus["sf24ce6"].color == QColor(200, 50, 0) + assert data.itemStatus["sc24b8f"].color == QColor(182, 60, 0) + assert data.itemStatus["s90e6c9"].color == QColor(193, 129, 0) + assert data.itemStatus["sd51c5b"].color == QColor(193, 129, 0) + assert data.itemStatus["s8ae72a"].color == QColor(193, 129, 0) + assert data.itemStatus["s78ea90"].color == QColor(58, 180, 58) - assert data.itemImport.cols("ia857f0") == (100, 100, 100) - assert data.itemImport.cols("icfb3a5") == (0, 122, 188) - assert data.itemImport.cols("i2d7a54") == (21, 0, 180) - assert data.itemImport.cols("i56be10") == (117, 0, 175) + assert data.itemImport["ia857f0"].color == QColor(100, 100, 100) + assert data.itemImport["icfb3a5"].color == QColor(0, 122, 188) + assert data.itemImport["i2d7a54"].color == QColor(21, 0, 180) + assert data.itemImport["i56be10"].color == QColor(117, 0, 175) - assert data.itemStatus.count("sf12341") == 4 - assert data.itemStatus.count("sf24ce6") == 2 - assert data.itemStatus.count("sc24b8f") == 3 - assert data.itemStatus.count("s90e6c9") == 7 - assert data.itemStatus.count("sd51c5b") == 0 - assert data.itemStatus.count("s8ae72a") == 0 - assert data.itemStatus.count("s78ea90") == 1 + assert data.itemStatus["sf12341"].shape == nwStatusShape.SQUARE + assert data.itemStatus["sf24ce6"].shape == nwStatusShape.SQUARE + assert data.itemStatus["sc24b8f"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s90e6c9"].shape == nwStatusShape.SQUARE + assert data.itemStatus["sd51c5b"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s8ae72a"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s78ea90"].shape == nwStatusShape.SQUARE - assert data.itemImport.count("ia857f0") == 5 - assert data.itemImport.count("icfb3a5") == 2 - assert data.itemImport.count("i2d7a54") == 2 - assert data.itemImport.count("i56be10") == 1 + assert data.itemImport["ia857f0"].shape == nwStatusShape.SQUARE + assert data.itemImport["icfb3a5"].shape == nwStatusShape.SQUARE + assert data.itemImport["i2d7a54"].shape == nwStatusShape.SQUARE + assert data.itemImport["i56be10"].shape == nwStatusShape.SQUARE + + assert data.itemStatus["sf12341"].count == 4 + assert data.itemStatus["sf24ce6"].count == 2 + assert data.itemStatus["sc24b8f"].count == 3 + assert data.itemStatus["s90e6c9"].count == 7 + assert data.itemStatus["sd51c5b"].count == 0 + assert data.itemStatus["s8ae72a"].count == 0 + assert data.itemStatus["s78ea90"].count == 1 + + assert data.itemImport["ia857f0"].count == 5 + assert data.itemImport["icfb3a5"].count == 2 + assert data.itemImport["i2d7a54"].count == 2 + assert data.itemImport["i56be10"].count == 1 # Compare content dumpFile = tstPaths.outDir / "projectXML_ReadCurrent.json" @@ -272,44 +288,57 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd): assert data.getLastHandle("novelTree") is None # Doesn't exist in 1.0 assert data.getLastHandle("outline") is None # Doesn't exist in 1.0 - assert data.itemStatus.name("s000000") == "New" - assert data.itemStatus.name("s000001") == "Notes" - assert data.itemStatus.name("s000002") == "Started" - assert data.itemStatus.name("s000003") == "1st Draft" - assert data.itemStatus.name("s000004") == "2nd Draft" - assert data.itemStatus.name("s000005") == "3rd Draft" - assert data.itemStatus.name("s000006") == "Finished" + assert data.itemStatus["s000000"].name == "New" + assert data.itemStatus["s000001"].name == "Notes" + assert data.itemStatus["s000002"].name == "Started" + assert data.itemStatus["s000003"].name == "1st Draft" + assert data.itemStatus["s000004"].name == "2nd Draft" + assert data.itemStatus["s000005"].name == "3rd Draft" + assert data.itemStatus["s000006"].name == "Finished" - assert data.itemImport.name("i000007") == "None" - assert data.itemImport.name("i000008") == "Minor" - assert data.itemImport.name("i000009") == "Major" - assert data.itemImport.name("i00000a") == "Main" + assert data.itemImport["i000007"].name == "None" + assert data.itemImport["i000008"].name == "Minor" + assert data.itemImport["i000009"].name == "Major" + assert data.itemImport["i00000a"].name == "Main" - assert data.itemStatus.cols("s000000") == (100, 100, 100) - assert data.itemStatus.cols("s000001") == (200, 50, 0) - assert data.itemStatus.cols("s000002") == (182, 60, 0) - assert data.itemStatus.cols("s000003") == (193, 129, 0) - assert data.itemStatus.cols("s000004") == (193, 129, 0) - assert data.itemStatus.cols("s000005") == (193, 129, 0) - assert data.itemStatus.cols("s000006") == (58, 180, 58) + assert data.itemStatus["s000000"].color == QColor(100, 100, 100) + assert data.itemStatus["s000001"].color == QColor(200, 50, 0) + assert data.itemStatus["s000002"].color == QColor(182, 60, 0) + assert data.itemStatus["s000003"].color == QColor(193, 129, 0) + assert data.itemStatus["s000004"].color == QColor(193, 129, 0) + assert data.itemStatus["s000005"].color == QColor(193, 129, 0) + assert data.itemStatus["s000006"].color == QColor(58, 180, 58) - assert data.itemImport.cols("i000007") == (100, 100, 100) - assert data.itemImport.cols("i000008") == (0, 122, 188) - assert data.itemImport.cols("i000009") == (21, 0, 180) - assert data.itemImport.cols("i00000a") == (117, 0, 175) + assert data.itemImport["i000007"].color == QColor(100, 100, 100) + assert data.itemImport["i000008"].color == QColor(0, 122, 188) + assert data.itemImport["i000009"].color == QColor(21, 0, 180) + assert data.itemImport["i00000a"].color == QColor(117, 0, 175) - assert data.itemStatus.count("s000000") == 0 - assert data.itemStatus.count("s000001") == 0 - assert data.itemStatus.count("s000002") == 0 - assert data.itemStatus.count("s000003") == 0 - assert data.itemStatus.count("s000004") == 0 - assert data.itemStatus.count("s000005") == 0 - assert data.itemStatus.count("s000006") == 0 + assert data.itemStatus["s000000"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000001"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000002"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000003"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000004"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000005"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000006"].shape == nwStatusShape.SQUARE - assert data.itemImport.count("i000007") == 0 - assert data.itemImport.count("i000008") == 0 - assert data.itemImport.count("i000009") == 0 - assert data.itemImport.count("i00000a") == 0 + assert data.itemImport["i000007"].shape == nwStatusShape.SQUARE + assert data.itemImport["i000008"].shape == nwStatusShape.SQUARE + assert data.itemImport["i000009"].shape == nwStatusShape.SQUARE + assert data.itemImport["i00000a"].shape == nwStatusShape.SQUARE + + assert data.itemStatus["s000000"].count == 0 + assert data.itemStatus["s000001"].count == 0 + assert data.itemStatus["s000002"].count == 0 + assert data.itemStatus["s000003"].count == 0 + assert data.itemStatus["s000004"].count == 0 + assert data.itemStatus["s000005"].count == 0 + assert data.itemStatus["s000006"].count == 0 + + assert data.itemImport["i000007"].count == 0 + assert data.itemImport["i000008"].count == 0 + assert data.itemImport["i000009"].count == 0 + assert data.itemImport["i00000a"].count == 0 # Compare content dumpFile = tstPaths.outDir / "projectXML_ReadLegacy10.json" @@ -325,7 +354,7 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd): for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore item.unpack(entry) - status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] + status[item.itemHandle] = item.getImportStatus()[0] packedContent.append(item.pack()) assert status == { @@ -406,44 +435,57 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd): assert data.getLastHandle("novelTree") is None # Doesn't exist in 1.1 assert data.getLastHandle("outline") is None # Doesn't exist in 1.1 - assert data.itemStatus.name("s000000") == "New" - assert data.itemStatus.name("s000001") == "Notes" - assert data.itemStatus.name("s000002") == "Started" - assert data.itemStatus.name("s000003") == "1st Draft" - assert data.itemStatus.name("s000004") == "2nd Draft" - assert data.itemStatus.name("s000005") == "3rd Draft" - assert data.itemStatus.name("s000006") == "Finished" + assert data.itemStatus["s000000"].name == "New" + assert data.itemStatus["s000001"].name == "Notes" + assert data.itemStatus["s000002"].name == "Started" + assert data.itemStatus["s000003"].name == "1st Draft" + assert data.itemStatus["s000004"].name == "2nd Draft" + assert data.itemStatus["s000005"].name == "3rd Draft" + assert data.itemStatus["s000006"].name == "Finished" - assert data.itemImport.name("i000007") == "None" - assert data.itemImport.name("i000008") == "Minor" - assert data.itemImport.name("i000009") == "Major" - assert data.itemImport.name("i00000a") == "Main" + assert data.itemImport["i000007"].name == "None" + assert data.itemImport["i000008"].name == "Minor" + assert data.itemImport["i000009"].name == "Major" + assert data.itemImport["i00000a"].name == "Main" - assert data.itemStatus.cols("s000000") == (100, 100, 100) - assert data.itemStatus.cols("s000001") == (200, 50, 0) - assert data.itemStatus.cols("s000002") == (182, 60, 0) - assert data.itemStatus.cols("s000003") == (193, 129, 0) - assert data.itemStatus.cols("s000004") == (193, 129, 0) - assert data.itemStatus.cols("s000005") == (193, 129, 0) - assert data.itemStatus.cols("s000006") == (58, 180, 58) + assert data.itemStatus["s000000"].color == QColor(100, 100, 100) + assert data.itemStatus["s000001"].color == QColor(200, 50, 0) + assert data.itemStatus["s000002"].color == QColor(182, 60, 0) + assert data.itemStatus["s000003"].color == QColor(193, 129, 0) + assert data.itemStatus["s000004"].color == QColor(193, 129, 0) + assert data.itemStatus["s000005"].color == QColor(193, 129, 0) + assert data.itemStatus["s000006"].color == QColor(58, 180, 58) - assert data.itemImport.cols("i000007") == (100, 100, 100) - assert data.itemImport.cols("i000008") == (0, 122, 188) - assert data.itemImport.cols("i000009") == (21, 0, 180) - assert data.itemImport.cols("i00000a") == (117, 0, 175) + assert data.itemImport["i000007"].color == QColor(100, 100, 100) + assert data.itemImport["i000008"].color == QColor(0, 122, 188) + assert data.itemImport["i000009"].color == QColor(21, 0, 180) + assert data.itemImport["i00000a"].color == QColor(117, 0, 175) - assert data.itemStatus.count("s000000") == 0 - assert data.itemStatus.count("s000001") == 0 - assert data.itemStatus.count("s000002") == 0 - assert data.itemStatus.count("s000003") == 0 - assert data.itemStatus.count("s000004") == 0 - assert data.itemStatus.count("s000005") == 0 - assert data.itemStatus.count("s000006") == 0 + assert data.itemStatus["s000000"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000001"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000002"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000003"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000004"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000005"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000006"].shape == nwStatusShape.SQUARE - assert data.itemImport.count("i000007") == 0 - assert data.itemImport.count("i000008") == 0 - assert data.itemImport.count("i000009") == 0 - assert data.itemImport.count("i00000a") == 0 + assert data.itemImport["i000007"].shape == nwStatusShape.SQUARE + assert data.itemImport["i000008"].shape == nwStatusShape.SQUARE + assert data.itemImport["i000009"].shape == nwStatusShape.SQUARE + assert data.itemImport["i00000a"].shape == nwStatusShape.SQUARE + + assert data.itemStatus["s000000"].count == 0 + assert data.itemStatus["s000001"].count == 0 + assert data.itemStatus["s000002"].count == 0 + assert data.itemStatus["s000003"].count == 0 + assert data.itemStatus["s000004"].count == 0 + assert data.itemStatus["s000005"].count == 0 + assert data.itemStatus["s000006"].count == 0 + + assert data.itemImport["i000007"].count == 0 + assert data.itemImport["i000008"].count == 0 + assert data.itemImport["i000009"].count == 0 + assert data.itemImport["i00000a"].count == 0 # Compare content dumpFile = tstPaths.outDir / "projectXML_ReadLegacy11.json" @@ -459,7 +501,7 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd): for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore item.unpack(entry) - status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] + status[item.itemHandle] = item.getImportStatus()[0] packedContent.append(item.pack()) assert status == { @@ -540,44 +582,57 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd): assert data.getLastHandle("novelTree") is None # Doesn't exist in 1.2 assert data.getLastHandle("outline") is None # Doesn't exist in 1.2 - assert data.itemStatus.name("s000000") == "New" - assert data.itemStatus.name("s000001") == "Notes" - assert data.itemStatus.name("s000002") == "Started" - assert data.itemStatus.name("s000003") == "1st Draft" - assert data.itemStatus.name("s000004") == "2nd Draft" - assert data.itemStatus.name("s000005") == "3rd Draft" - assert data.itemStatus.name("s000006") == "Finished" + assert data.itemStatus["s000000"].name == "New" + assert data.itemStatus["s000001"].name == "Notes" + assert data.itemStatus["s000002"].name == "Started" + assert data.itemStatus["s000003"].name == "1st Draft" + assert data.itemStatus["s000004"].name == "2nd Draft" + assert data.itemStatus["s000005"].name == "3rd Draft" + assert data.itemStatus["s000006"].name == "Finished" - assert data.itemImport.name("i000007") == "None" - assert data.itemImport.name("i000008") == "Minor" - assert data.itemImport.name("i000009") == "Major" - assert data.itemImport.name("i00000a") == "Main" + assert data.itemImport["i000007"].name == "None" + assert data.itemImport["i000008"].name == "Minor" + assert data.itemImport["i000009"].name == "Major" + assert data.itemImport["i00000a"].name == "Main" - assert data.itemStatus.cols("s000000") == (100, 100, 100) - assert data.itemStatus.cols("s000001") == (200, 50, 0) - assert data.itemStatus.cols("s000002") == (182, 60, 0) - assert data.itemStatus.cols("s000003") == (193, 129, 0) - assert data.itemStatus.cols("s000004") == (193, 129, 0) - assert data.itemStatus.cols("s000005") == (193, 129, 0) - assert data.itemStatus.cols("s000006") == (58, 180, 58) + assert data.itemStatus["s000000"].color == QColor(100, 100, 100) + assert data.itemStatus["s000001"].color == QColor(200, 50, 0) + assert data.itemStatus["s000002"].color == QColor(182, 60, 0) + assert data.itemStatus["s000003"].color == QColor(193, 129, 0) + assert data.itemStatus["s000004"].color == QColor(193, 129, 0) + assert data.itemStatus["s000005"].color == QColor(193, 129, 0) + assert data.itemStatus["s000006"].color == QColor(58, 180, 58) - assert data.itemImport.cols("i000007") == (100, 100, 100) - assert data.itemImport.cols("i000008") == (0, 122, 188) - assert data.itemImport.cols("i000009") == (21, 0, 180) - assert data.itemImport.cols("i00000a") == (117, 0, 175) + assert data.itemImport["i000007"].color == QColor(100, 100, 100) + assert data.itemImport["i000008"].color == QColor(0, 122, 188) + assert data.itemImport["i000009"].color == QColor(21, 0, 180) + assert data.itemImport["i00000a"].color == QColor(117, 0, 175) - assert data.itemStatus.count("s000000") == 0 - assert data.itemStatus.count("s000001") == 0 - assert data.itemStatus.count("s000002") == 0 - assert data.itemStatus.count("s000003") == 0 - assert data.itemStatus.count("s000004") == 0 - assert data.itemStatus.count("s000005") == 0 - assert data.itemStatus.count("s000006") == 0 + assert data.itemStatus["s000000"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000001"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000002"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000003"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000004"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000005"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000006"].shape == nwStatusShape.SQUARE - assert data.itemImport.count("i000007") == 0 - assert data.itemImport.count("i000008") == 0 - assert data.itemImport.count("i000009") == 0 - assert data.itemImport.count("i00000a") == 0 + assert data.itemImport["i000007"].shape == nwStatusShape.SQUARE + assert data.itemImport["i000008"].shape == nwStatusShape.SQUARE + assert data.itemImport["i000009"].shape == nwStatusShape.SQUARE + assert data.itemImport["i00000a"].shape == nwStatusShape.SQUARE + + assert data.itemStatus["s000000"].count == 0 + assert data.itemStatus["s000001"].count == 0 + assert data.itemStatus["s000002"].count == 0 + assert data.itemStatus["s000003"].count == 0 + assert data.itemStatus["s000004"].count == 0 + assert data.itemStatus["s000005"].count == 0 + assert data.itemStatus["s000006"].count == 0 + + assert data.itemImport["i000007"].count == 0 + assert data.itemImport["i000008"].count == 0 + assert data.itemImport["i000009"].count == 0 + assert data.itemImport["i00000a"].count == 0 # Compare content dumpFile = tstPaths.outDir / "projectXML_ReadLegacy12.json" @@ -593,7 +648,7 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd): for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore item.unpack(entry) - status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] + status[item.itemHandle] = item.getImportStatus()[0] packedContent.append(item.pack()) assert status == { @@ -677,44 +732,57 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd): assert data.getLastHandle("novelTree") is None # Doesn't exist in 1.3 assert data.getLastHandle("outline") is None # Doesn't exist in 1.3 - assert data.itemStatus.name("s000000") == "New" - assert data.itemStatus.name("s000001") == "Notes" - assert data.itemStatus.name("s000002") == "Started" - assert data.itemStatus.name("s000003") == "1st Draft" - assert data.itemStatus.name("s000004") == "2nd Draft" - assert data.itemStatus.name("s000005") == "3rd Draft" - assert data.itemStatus.name("s000006") == "Finished" + assert data.itemStatus["s000000"].name == "New" + assert data.itemStatus["s000001"].name == "Notes" + assert data.itemStatus["s000002"].name == "Started" + assert data.itemStatus["s000003"].name == "1st Draft" + assert data.itemStatus["s000004"].name == "2nd Draft" + assert data.itemStatus["s000005"].name == "3rd Draft" + assert data.itemStatus["s000006"].name == "Finished" - assert data.itemImport.name("i000007") == "None" - assert data.itemImport.name("i000008") == "Minor" - assert data.itemImport.name("i000009") == "Major" - assert data.itemImport.name("i00000a") == "Main" + assert data.itemImport["i000007"].name == "None" + assert data.itemImport["i000008"].name == "Minor" + assert data.itemImport["i000009"].name == "Major" + assert data.itemImport["i00000a"].name == "Main" - assert data.itemStatus.cols("s000000") == (100, 100, 100) - assert data.itemStatus.cols("s000001") == (200, 50, 0) - assert data.itemStatus.cols("s000002") == (182, 60, 0) - assert data.itemStatus.cols("s000003") == (193, 129, 0) - assert data.itemStatus.cols("s000004") == (193, 129, 0) - assert data.itemStatus.cols("s000005") == (193, 129, 0) - assert data.itemStatus.cols("s000006") == (58, 180, 58) + assert data.itemStatus["s000000"].color == QColor(100, 100, 100) + assert data.itemStatus["s000001"].color == QColor(200, 50, 0) + assert data.itemStatus["s000002"].color == QColor(182, 60, 0) + assert data.itemStatus["s000003"].color == QColor(193, 129, 0) + assert data.itemStatus["s000004"].color == QColor(193, 129, 0) + assert data.itemStatus["s000005"].color == QColor(193, 129, 0) + assert data.itemStatus["s000006"].color == QColor(58, 180, 58) - assert data.itemImport.cols("i000007") == (100, 100, 100) - assert data.itemImport.cols("i000008") == (0, 122, 188) - assert data.itemImport.cols("i000009") == (21, 0, 180) - assert data.itemImport.cols("i00000a") == (117, 0, 175) + assert data.itemImport["i000007"].color == QColor(100, 100, 100) + assert data.itemImport["i000008"].color == QColor(0, 122, 188) + assert data.itemImport["i000009"].color == QColor(21, 0, 180) + assert data.itemImport["i00000a"].color == QColor(117, 0, 175) - assert data.itemStatus.count("s000000") == 0 - assert data.itemStatus.count("s000001") == 0 - assert data.itemStatus.count("s000002") == 0 - assert data.itemStatus.count("s000003") == 0 - assert data.itemStatus.count("s000004") == 0 - assert data.itemStatus.count("s000005") == 0 - assert data.itemStatus.count("s000006") == 0 + assert data.itemStatus["s000000"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000001"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000002"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000003"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000004"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000005"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s000006"].shape == nwStatusShape.SQUARE - assert data.itemImport.count("i000007") == 0 - assert data.itemImport.count("i000008") == 0 - assert data.itemImport.count("i000009") == 0 - assert data.itemImport.count("i00000a") == 0 + assert data.itemImport["i000007"].shape == nwStatusShape.SQUARE + assert data.itemImport["i000008"].shape == nwStatusShape.SQUARE + assert data.itemImport["i000009"].shape == nwStatusShape.SQUARE + assert data.itemImport["i00000a"].shape == nwStatusShape.SQUARE + + assert data.itemStatus["s000000"].count == 0 + assert data.itemStatus["s000001"].count == 0 + assert data.itemStatus["s000002"].count == 0 + assert data.itemStatus["s000003"].count == 0 + assert data.itemStatus["s000004"].count == 0 + assert data.itemStatus["s000005"].count == 0 + assert data.itemStatus["s000006"].count == 0 + + assert data.itemImport["i000007"].count == 0 + assert data.itemImport["i000008"].count == 0 + assert data.itemImport["i000009"].count == 0 + assert data.itemImport["i00000a"].count == 0 # Compare content dumpFile = tstPaths.outDir / "projectXML_ReadLegacy13.json" @@ -730,7 +798,7 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd): for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore item.unpack(entry) - status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] + status[item.itemHandle] = item.getImportStatus()[0] packedContent.append(item.pack()) assert status == { @@ -814,44 +882,56 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd): assert data.getLastHandle("novelTree") is None # Doesn't exist in 1.3 assert data.getLastHandle("outline") is None # Doesn't exist in 1.3 - assert data.itemStatus.name("sf12341") == "New" - assert data.itemStatus.name("sf24ce6") == "Notes" - assert data.itemStatus.name("sc24b8f") == "Started" - assert data.itemStatus.name("s90e6c9") == "1st Draft" - assert data.itemStatus.name("sd51c5b") == "2nd Draft" - assert data.itemStatus.name("s8ae72a") == "3rd Draft" - assert data.itemStatus.name("s78ea90") == "Finished" + assert data.itemStatus["sf12341"].name == "New" + assert data.itemStatus["sf24ce6"].name == "Notes" + assert data.itemStatus["sc24b8f"].name == "Started" + assert data.itemStatus["s90e6c9"].name == "1st Draft" + assert data.itemStatus["sd51c5b"].name == "2nd Draft" + assert data.itemStatus["s8ae72a"].name == "3rd Draft" + assert data.itemStatus["s78ea90"].name == "Finished" - assert data.itemImport.name("ia857f0") == "None" - assert data.itemImport.name("icfb3a5") == "Minor" - assert data.itemImport.name("i2d7a54") == "Major" - assert data.itemImport.name("i56be10") == "Main" + assert data.itemImport["ia857f0"].name == "None" + assert data.itemImport["icfb3a5"].name == "Minor" + assert data.itemImport["i2d7a54"].name == "Major" + assert data.itemImport["i56be10"].name == "Main" - assert data.itemStatus.cols("sf12341") == (100, 100, 100) - assert data.itemStatus.cols("sf24ce6") == (200, 50, 0) - assert data.itemStatus.cols("sc24b8f") == (182, 60, 0) - assert data.itemStatus.cols("s90e6c9") == (193, 129, 0) - assert data.itemStatus.cols("sd51c5b") == (193, 129, 0) - assert data.itemStatus.cols("s8ae72a") == (193, 129, 0) - assert data.itemStatus.cols("s78ea90") == (58, 180, 58) + assert data.itemStatus["sf12341"].color == QColor(100, 100, 100) + assert data.itemStatus["sf24ce6"].color == QColor(200, 50, 0) + assert data.itemStatus["sc24b8f"].color == QColor(182, 60, 0) + assert data.itemStatus["s90e6c9"].color == QColor(193, 129, 0) + assert data.itemStatus["sd51c5b"].color == QColor(193, 129, 0) + assert data.itemStatus["s8ae72a"].color == QColor(193, 129, 0) + assert data.itemStatus["s78ea90"].color == QColor(58, 180, 58) - assert data.itemImport.cols("ia857f0") == (100, 100, 100) - assert data.itemImport.cols("icfb3a5") == (0, 122, 188) - assert data.itemImport.cols("i2d7a54") == (21, 0, 180) - assert data.itemImport.cols("i56be10") == (117, 0, 175) + assert data.itemImport["ia857f0"].color == QColor(100, 100, 100) + assert data.itemImport["icfb3a5"].color == QColor(0, 122, 188) + assert data.itemImport["i2d7a54"].color == QColor(21, 0, 180) + assert data.itemImport["i56be10"].color == QColor(117, 0, 175) - assert data.itemStatus.count("sf12341") == 4 - assert data.itemStatus.count("sf24ce6") == 2 - assert data.itemStatus.count("sc24b8f") == 3 - assert data.itemStatus.count("s90e6c9") == 7 - assert data.itemStatus.count("sd51c5b") == 0 - assert data.itemStatus.count("s8ae72a") == 0 - assert data.itemStatus.count("s78ea90") == 1 + assert data.itemStatus["sf12341"].shape == nwStatusShape.SQUARE + assert data.itemStatus["sf24ce6"].shape == nwStatusShape.SQUARE + assert data.itemStatus["sc24b8f"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s90e6c9"].shape == nwStatusShape.SQUARE + assert data.itemStatus["sd51c5b"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s8ae72a"].shape == nwStatusShape.SQUARE + assert data.itemStatus["s78ea90"].shape == nwStatusShape.SQUARE - assert data.itemImport.count("ia857f0") == 5 - assert data.itemImport.count("icfb3a5") == 2 - assert data.itemImport.count("i2d7a54") == 2 - assert data.itemImport.count("i56be10") == 1 + assert data.itemImport["ia857f0"].shape == nwStatusShape.SQUARE + assert data.itemImport["icfb3a5"].shape == nwStatusShape.SQUARE + assert data.itemImport["i2d7a54"].shape == nwStatusShape.SQUARE + assert data.itemImport["i56be10"].shape == nwStatusShape.SQUARE + + assert data.itemStatus["sf12341"].count == 4 + assert data.itemStatus["sf24ce6"].count == 2 + assert data.itemStatus["sc24b8f"].count == 3 + assert data.itemStatus["s90e6c9"].count == 7 + assert data.itemStatus["sd51c5b"].count == 0 + assert data.itemStatus["s8ae72a"].count == 0 + assert data.itemStatus["s78ea90"].count == 1 + assert data.itemImport["ia857f0"].count == 5 + assert data.itemImport["icfb3a5"].count == 2 + assert data.itemImport["i2d7a54"].count == 2 + assert data.itemImport["i56be10"].count == 1 # Compare content dumpFile = tstPaths.outDir / "projectXML_ReadLegacy14.json" @@ -867,7 +947,7 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd): for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore item.unpack(entry) - status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] + status[item.itemHandle] = item.getImportStatus()[0] packedContent.append(item.pack()) assert status == { diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index 9e30d9c4..7c6b2fc4 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -24,24 +24,50 @@ import pytest from tools import C -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QColor, QIcon -from novelwriter.core.status import NWStatus +from novelwriter.core.status import NWStatus, StatusEntry, _ShapeCache +from novelwriter.enum import nwStatusShape statusKeys = [C.sNew, C.sNote, C.sDraft, C.sFinished] importKeys = [C.iNew, C.iMinor, C.iMajor, C.iMain] @pytest.mark.core -def testCoreStatus_Internal(mockRnd): - """Test all the internal functions of the NWStatus class. - """ +def testCoreStatus_StatusEntry(): + """Test the StatusEntry class.""" + color = QColor(255, 0, 0) + icon = NWStatus.createIcon(24, color, nwStatusShape.CIRCLE) + entry = StatusEntry("Test", color, nwStatusShape.CIRCLE, icon, 42) + + # Check values + assert entry.name == "Test" + assert entry.color is color + assert entry.shape == nwStatusShape.CIRCLE + assert entry.icon is icon + assert entry.count == 42 + + # Make a copy + other = StatusEntry.duplicate(entry) + assert other is not entry + + # Check copy is not the same + assert other.name == "Test" + assert other.color is not color # Not the same object + assert other.color == color # But same colours + assert other.shape == nwStatusShape.CIRCLE + assert other.icon is not icon # Not the same icon, but a copy + assert other.count == 42 + +# END Test testCoreStatus_StatusEntry + + +@pytest.mark.core +def testCoreStatus_Internal(mockGUI, mockRnd): + """Test all the internal functions of the NWStatus class.""" nStatus = NWStatus(NWStatus.STATUS) nImport = NWStatus(NWStatus.IMPORT) - with pytest.raises(Exception): - NWStatus(999) - # Generate Key # ============ @@ -49,14 +75,14 @@ def testCoreStatus_Internal(mockRnd): assert nStatus._newKey() == statusKeys[1] # Key collision, should move to key 3 - nStatus.write(statusKeys[2], "Crash", (0, 0, 0)) + nStatus.add(statusKeys[2], "Crash", (0, 0, 0), "SQUARE", 0) assert nStatus._newKey() == statusKeys[3] assert nImport._newKey() == importKeys[0] assert nImport._newKey() == importKeys[1] # Key collision, should move to key 3 - nImport.write(importKeys[2], "Crash", (0, 0, 0)) + nImport.add(importKeys[2], "Crash", (0, 0, 0), "SQUARE", 0) assert nImport._newKey() == importKeys[3] # Check Key @@ -82,81 +108,94 @@ def testCoreStatus_Internal(mockRnd): assert nImport._isKey("i12345F") is False # Not a lower case hex value assert nImport._isKey("i12345f") is True # Valid hex value + assert nStatus._checkKey(None) == "s000008" # Creates next key + assert nStatus._checkKey("s654321") == "s654321" # Status key accepted + assert nStatus._checkKey("i123456") == "s000009" # Import key not accepted + + assert nImport._checkKey(None) == "i00000a" # Creates next key + assert nImport._checkKey("s654321") == "i00000b" # Status key not accepted + assert nImport._checkKey("i123456") == "i123456" # Import key accepted + # END Test testCoreStatus_Internal @pytest.mark.core -def testCoreStatus_Iterator(mockRnd): - """Test the iterator functions of the NWStatus class. - """ +def testCoreStatus_Iterator(mockGUI, mockRnd): + """Test the iterator functions of the NWStatus class.""" nStatus = NWStatus(NWStatus.STATUS) - - nStatus.write(None, "New", (100, 100, 100)) - nStatus.write(None, "Note", (200, 50, 0)) - nStatus.write(None, "Draft", (200, 150, 0)) - nStatus.write(None, "Finished", (50, 200, 0)) + nStatus.add(None, "New", (100, 100, 100), "SQUARE", 0) + nStatus.add(None, "Note", (200, 50, 0), "CIRCLE", 1) + nStatus.add(None, "Draft", (200, 150, 0), "SQUARE", 2) + nStatus.add(None, "Finished", (50, 200, 0), "CIRCLE", 3) # Direct access entry = nStatus[statusKeys[0]] - assert entry["cols"] == (100, 100, 100) - assert entry["name"] == "New" - assert entry["count"] == 0 - assert isinstance(entry["icon"], QIcon) + assert entry.color == QColor(100, 100, 100) + assert entry.name == "New" + assert entry.count == 0 + assert isinstance(entry.icon, QIcon) - # Iterate - entries = list(nStatus) - assert len(entries) == 4 + # Length + assert len(nStatus._store) == 4 assert len(nStatus) == 4 - # Keys - assert list(nStatus.keys()) == statusKeys + # Content : Keys + assert [k for k, _ in nStatus.iterItems()] == [ + "s000000", "s000001", "s000002", "s000003" + ] - # Items - for index, (key, entry) in enumerate(nStatus.items()): - assert key == statusKeys[index] - assert "cols" in entry - assert "name" in entry - assert "count" in entry - assert "icon" in entry + # Content : Names + assert [e.name for _, e in nStatus.iterItems()] == [ + "New", "Note", "Draft", "Finished" + ] - # Valuse - for entry in nStatus.values(): - assert "cols" in entry - assert "name" in entry - assert "count" in entry - assert "icon" in entry + # Content : Colours + assert [e.color for _, e in nStatus.iterItems()] == [ + QColor(100, 100, 100), QColor(200, 50, 0), QColor(200, 150, 0), QColor(50, 200, 0) + ] + + # Content : Shape + assert [e.shape for _, e in nStatus.iterItems()] == [ + nwStatusShape.SQUARE, nwStatusShape.CIRCLE, nwStatusShape.SQUARE, nwStatusShape.CIRCLE + ] + + # Content : Count + assert [e.count for _, e in nStatus.iterItems()] == [0, 1, 2, 3] # END Test testCoreStatus_Iterator @pytest.mark.core -def testCoreStatus_Entries(mockRnd): - """Test all the simple setters for the NWStatus class. - """ +def testCoreStatus_Entries(mockGUI, mockRnd): + """Test all the simple setters for the NWStatus class.""" nStatus = NWStatus(NWStatus.STATUS) - # Write - # ===== + # Add + # === - # Have a key - nStatus.write(statusKeys[0], "Entry 1", (200, 100, 50)) - assert nStatus[statusKeys[0]]["name"] == "Entry 1" - assert nStatus[statusKeys[0]]["cols"] == (200, 100, 50) + # Has a key + nStatus.add(statusKeys[0], "Entry 1", (200, 100, 50), "SQUARE", 0) + assert nStatus[statusKeys[0]].name == "Entry 1" + assert nStatus[statusKeys[0]].color == QColor(200, 100, 50) + assert nStatus[statusKeys[0]].shape == nwStatusShape.SQUARE - # Don't have a key - nStatus.write(None, "Entry 2", (210, 110, 60)) - assert nStatus[statusKeys[1]]["name"] == "Entry 2" - assert nStatus[statusKeys[1]]["cols"] == (210, 110, 60) + # Doesn't have a key + nStatus.add(None, "Entry 2", (210, 110, 60), "SQUARE", 0) + assert nStatus[statusKeys[1]].name == "Entry 2" + assert nStatus[statusKeys[1]].color == QColor(210, 110, 60) + assert nStatus[statusKeys[1]].shape == nwStatusShape.SQUARE - # Wrong colour spec - nStatus.write(None, "Entry 3", "what?") - assert nStatus[statusKeys[2]]["name"] == "Entry 3" - assert nStatus[statusKeys[2]]["cols"] == (100, 100, 100) + # Wrong colour spec, unknown shape + nStatus.add(None, "Entry 3", "what?", "", 0) # type: ignore + assert nStatus[statusKeys[2]].name == "Entry 3" + assert nStatus[statusKeys[2]].color == QColor(100, 100, 100) + assert nStatus[statusKeys[2]].shape == nwStatusShape.SQUARE # Wrong colour count - nStatus.write(None, "Entry 4", (10, 20)) - assert nStatus[statusKeys[3]]["name"] == "Entry 4" - assert nStatus[statusKeys[3]]["cols"] == (100, 100, 100) + nStatus.add(None, "Entry 4", (10, 20), "CIRCLE", 0) # type: ignore + assert nStatus[statusKeys[3]].name == "Entry 4" + assert nStatus[statusKeys[3]].color == QColor(100, 100, 100) + assert nStatus[statusKeys[3]].shape == nwStatusShape.CIRCLE # Check # ===== @@ -171,29 +210,38 @@ def testCoreStatus_Entries(mockRnd): # Name Access # =========== - assert nStatus.name(statusKeys[0]) == "Entry 1" - assert nStatus.name(statusKeys[1]) == "Entry 2" - assert nStatus.name(statusKeys[2]) == "Entry 3" - assert nStatus.name(statusKeys[3]) == "Entry 4" - assert nStatus.name("blablabla") == "Entry 1" + assert nStatus[statusKeys[0]].name == "Entry 1" + assert nStatus[statusKeys[1]].name == "Entry 2" + assert nStatus[statusKeys[2]].name == "Entry 3" + assert nStatus[statusKeys[3]].name == "Entry 4" + assert nStatus["blablabla"].name == "Entry 1" # Colour Access # ============= - assert nStatus.cols(statusKeys[0]) == (200, 100, 50) - assert nStatus.cols(statusKeys[1]) == (210, 110, 60) - assert nStatus.cols(statusKeys[2]) == (100, 100, 100) - assert nStatus.cols(statusKeys[3]) == (100, 100, 100) - assert nStatus.cols("blablabla") == (200, 100, 50) + assert nStatus[statusKeys[0]].color == QColor(200, 100, 50) + assert nStatus[statusKeys[1]].color == QColor(210, 110, 60) + assert nStatus[statusKeys[2]].color == QColor(100, 100, 100) + assert nStatus[statusKeys[3]].color == QColor(100, 100, 100) + assert nStatus["blablabla"].color == QColor(200, 100, 50) # Icon Access # =========== - assert isinstance(nStatus.icon(statusKeys[0]), QIcon) - assert isinstance(nStatus.icon(statusKeys[1]), QIcon) - assert isinstance(nStatus.icon(statusKeys[2]), QIcon) - assert isinstance(nStatus.icon(statusKeys[3]), QIcon) - assert isinstance(nStatus.icon("blablabla"), QIcon) + assert isinstance(nStatus[statusKeys[0]].icon, QIcon) + assert isinstance(nStatus[statusKeys[1]].icon, QIcon) + assert isinstance(nStatus[statusKeys[2]].icon, QIcon) + assert isinstance(nStatus[statusKeys[3]].icon, QIcon) + assert isinstance(nStatus["blablabla"].icon, QIcon) + + # Shape Access + # ============ + + assert nStatus[statusKeys[0]].shape == nwStatusShape.SQUARE + assert nStatus[statusKeys[1]].shape == nwStatusShape.SQUARE + assert nStatus[statusKeys[2]].shape == nwStatusShape.SQUARE + assert nStatus[statusKeys[3]].shape == nwStatusShape.CIRCLE + assert nStatus["blablabla"].shape == nwStatusShape.SQUARE # Increment and Count Access # ========================== @@ -203,50 +251,32 @@ def testCoreStatus_Entries(mockRnd): for _ in range(n): nStatus.increment(statusKeys[i]) - assert nStatus.count(statusKeys[0]) == countTo[0] - assert nStatus.count(statusKeys[1]) == countTo[1] - assert nStatus.count(statusKeys[2]) == countTo[2] - assert nStatus.count(statusKeys[3]) == countTo[3] - assert nStatus.count("blablabla") == countTo[0] + assert nStatus[statusKeys[0]].count == countTo[0] + assert nStatus[statusKeys[1]].count == countTo[1] + assert nStatus[statusKeys[2]].count == countTo[2] + assert nStatus[statusKeys[3]].count == countTo[3] + assert nStatus["blablabla"].count == countTo[0] nStatus.resetCounts() - assert nStatus.count(statusKeys[0]) == 0 - assert nStatus.count(statusKeys[1]) == 0 - assert nStatus.count(statusKeys[2]) == 0 - assert nStatus.count(statusKeys[3]) == 0 + assert nStatus[statusKeys[0]].count == 0 + assert nStatus[statusKeys[1]].count == 0 + assert nStatus[statusKeys[2]].count == 0 + assert nStatus[statusKeys[3]].count == 0 - # Reorder - # ======= + # Update + # ====== - cOrder = list(nStatus.keys()) - assert cOrder == statusKeys + assert list(nStatus._store.keys()) == statusKeys - # Wrong length - assert nStatus.reorder([]) is False + # Reverse + order: list[tuple[str | None, StatusEntry]] = list(nStatus.iterItems()) + nStatus.update(list(reversed(order))) + assert list(nStatus._store.keys()) == list(reversed(statusKeys)) - # No change - assert nStatus.reorder(cOrder) is False - - # Actual reaorder - nOrder = [ - statusKeys[0], - statusKeys[2], - statusKeys[1], - statusKeys[3], - ] - assert nStatus.reorder(nOrder) is True - assert list(nStatus.keys()) == nOrder - - # Add an unknown key - wOrder = nOrder.copy() - wOrder[3] = nStatus._newKey() - assert nStatus.reorder(wOrder) is False - assert list(nStatus.keys()) == nOrder - - # Put it back - assert nStatus.reorder(cOrder) is True - assert list(nStatus.keys()) == cOrder + # Restore + nStatus.update(order) + assert list(nStatus._store.keys()) == statusKeys # Default # ======= @@ -255,56 +285,41 @@ def testCoreStatus_Entries(mockRnd): nStatus._default = None assert nStatus.check("Entry 5") == "" - assert nStatus.name("blablabla") == "" - assert nStatus.cols("blablabla") == (100, 100, 100) - assert nStatus.count("blablabla") == 0 - assert isinstance(nStatus.icon("blablabla"), QIcon) + assert nStatus["blablabla"].name == "" + assert nStatus["blablabla"].color == QColor(0, 0, 0) + assert nStatus["blablabla"].shape == nwStatusShape.SQUARE + assert nStatus["blablabla"].icon.isNull() + assert nStatus["blablabla"].count == 0 nStatus._default = default # Remove # ====== + # This uses update with deleted items - # Non-existing entry - assert nStatus.remove("blablabla") is False + order: list[tuple[str | None, StatusEntry]] = list(nStatus.iterItems()) - # Non-zero entry - nStatus.increment(statusKeys[3]) - assert nStatus.remove(statusKeys[3]) is False + # Remove Entry 0 + nStatus.update([order[1], order[3], order[2]]) + assert list(nStatus._store.keys()) == [statusKeys[1], statusKeys[3], statusKeys[2]] + assert nStatus._default == statusKeys[1] - # Delete last entry - nStatus.resetCounts() - lastName = nStatus.name(statusKeys[3]) - assert lastName == "Entry 4" - assert nStatus.remove(statusKeys[3]) is True - assert nStatus.check(statusKeys[3]) == nStatus._default - assert nStatus.check(lastName) == nStatus._default - - # Delete default entry, Entry 2 is new default - firstName = nStatus.name(nStatus._default) - assert firstName == "Entry 1" - assert nStatus.remove(nStatus._default) is True - assert nStatus.name(firstName) == "Entry 2" - - # Remove remaining entries - assert nStatus.remove(statusKeys[1]) is True - assert nStatus.remove(statusKeys[2]) is True - - assert len(nStatus) == 0 - assert nStatus._default is None + # Remove Entry 1 + nStatus.update([order[3], order[2]]) + assert list(nStatus._store.keys()) == [statusKeys[3], statusKeys[2]] + assert nStatus._default == statusKeys[3] # END Test testCoreStatus_Entries @pytest.mark.core -def testCoreStatus_PackUnpack(mockRnd): - """Test all the pack/unpack of the NWStatus class. - """ +def testCoreStatus_Pack(mockGUI, mockRnd): + """Test data packing of the NWStatus class.""" nStatus = NWStatus(NWStatus.STATUS) - nStatus.write(None, "New", (100, 100, 100)) - nStatus.write(None, "Note", (200, 50, 0)) - nStatus.write(None, "Draft", (200, 150, 0)) - nStatus.write(None, "Finished", (50, 200, 0)) + nStatus.add(None, "New", (100, 100, 100), "SQUARE", 0) + nStatus.add(None, "Note", (200, 50, 0), "CIRCLE", 0) + nStatus.add(None, "Draft", (200, 150, 0), "SQUARE", 0) + nStatus.add(None, "Finished", (50, 200, 0), "SQUARE", 0) countTo = [3, 5, 7, 9] for i, n in enumerate(countTo): @@ -318,52 +333,85 @@ def testCoreStatus_PackUnpack(mockRnd): "count": "3", "red": "100", "green": "100", - "blue": "100" + "blue": "100", + "shape": "SQUARE", }), ("Note", { "key": statusKeys[1], "count": "5", "red": "200", "green": "50", - "blue": "0" + "blue": "0", + "shape": "CIRCLE", }), ("Draft", { "key": statusKeys[2], "count": "7", "red": "200", "green": "150", - "blue": "0" + "blue": "0", + "shape": "SQUARE", }), ("Finished", { "key": statusKeys[3], "count": "9", "red": "50", "green": "200", - "blue": "0" + "blue": "0", + "shape": "SQUARE", }), ] - # Unpack - nStatus = NWStatus(NWStatus.STATUS) - nStatus.unpack({ - statusKeys[0]: {"label": "New0", "colour": (100, 100, 100), "count": countTo[0]}, - statusKeys[1]: {"label": "New1", "colour": (150, 150, 150), "count": countTo[1]}, - statusKeys[2]: {"label": "New2", "colour": (200, 200, 200), "count": countTo[2]}, - statusKeys[3]: {"label": "New3", "colour": (250, 250, 250), "count": countTo[3]}, - }) - assert len(nStatus._store) == 4 - assert list(nStatus._store.keys()) == statusKeys - assert nStatus._store[statusKeys[0]]["name"] == "New0" - assert nStatus._store[statusKeys[1]]["name"] == "New1" - assert nStatus._store[statusKeys[2]]["name"] == "New2" - assert nStatus._store[statusKeys[3]]["name"] == "New3" - assert nStatus._store[statusKeys[0]]["cols"] == (100, 100, 100) - assert nStatus._store[statusKeys[1]]["cols"] == (150, 150, 150) - assert nStatus._store[statusKeys[2]]["cols"] == (200, 200, 200) - assert nStatus._store[statusKeys[3]]["cols"] == (250, 250, 250) - assert nStatus._store[statusKeys[0]]["count"] == countTo[0] - assert nStatus._store[statusKeys[1]]["count"] == countTo[1] - assert nStatus._store[statusKeys[2]]["count"] == countTo[2] - assert nStatus._store[statusKeys[3]]["count"] == countTo[3] +# END Test testCoreStatus_Pack -# END Test testCoreStatus_PackUnpack + +@pytest.mark.core +def testCoreStatus_ShapeCache(): + """Test the _ShapeCache class.""" + shapes = _ShapeCache() + + # Generate all shapes + square = shapes.getShape(nwStatusShape.SQUARE) + circleQ = shapes.getShape(nwStatusShape.CIRCLE_Q) + circleH = shapes.getShape(nwStatusShape.CIRCLE_H) + circleT = shapes.getShape(nwStatusShape.CIRCLE_T) + circle = shapes.getShape(nwStatusShape.CIRCLE) + triangle = shapes.getShape(nwStatusShape.TRIANGLE) + nabla = shapes.getShape(nwStatusShape.NABLA) + diamond = shapes.getShape(nwStatusShape.DIAMOND) + pentagon = shapes.getShape(nwStatusShape.PENTAGON) + hexagon = shapes.getShape(nwStatusShape.HEXAGON) + star = shapes.getShape(nwStatusShape.STAR) + pacman = shapes.getShape(nwStatusShape.PACMAN) + bars1 = shapes.getShape(nwStatusShape.BARS_1) + bars2 = shapes.getShape(nwStatusShape.BARS_2) + bars3 = shapes.getShape(nwStatusShape.BARS_3) + bars4 = shapes.getShape(nwStatusShape.BARS_4) + block1 = shapes.getShape(nwStatusShape.BLOCK_1) + block2 = shapes.getShape(nwStatusShape.BLOCK_2) + block3 = shapes.getShape(nwStatusShape.BLOCK_3) + block4 = shapes.getShape(nwStatusShape.BLOCK_4) + + # Request again should return from cache + assert shapes.getShape(nwStatusShape.SQUARE) is square + assert shapes.getShape(nwStatusShape.CIRCLE_Q) is circleQ + assert shapes.getShape(nwStatusShape.CIRCLE_H) is circleH + assert shapes.getShape(nwStatusShape.CIRCLE_T) is circleT + assert shapes.getShape(nwStatusShape.CIRCLE) is circle + assert shapes.getShape(nwStatusShape.TRIANGLE) is triangle + assert shapes.getShape(nwStatusShape.NABLA) is nabla + assert shapes.getShape(nwStatusShape.DIAMOND) is diamond + assert shapes.getShape(nwStatusShape.PENTAGON) is pentagon + assert shapes.getShape(nwStatusShape.HEXAGON) is hexagon + assert shapes.getShape(nwStatusShape.STAR) is star + assert shapes.getShape(nwStatusShape.PACMAN) is pacman + assert shapes.getShape(nwStatusShape.BARS_1) is bars1 + assert shapes.getShape(nwStatusShape.BARS_2) is bars2 + assert shapes.getShape(nwStatusShape.BARS_3) is bars3 + assert shapes.getShape(nwStatusShape.BARS_4) is bars4 + assert shapes.getShape(nwStatusShape.BLOCK_1) is block1 + assert shapes.getShape(nwStatusShape.BLOCK_2) is block2 + assert shapes.getShape(nwStatusShape.BLOCK_3) is block3 + assert shapes.getShape(nwStatusShape.BLOCK_4) is block4 + +# END Test testCoreStatus_ShapeCache diff --git a/tests/test_dialogs/test_dlg_projectsettings.py b/tests/test_dialogs/test_dlg_projectsettings.py index 9831ea1f..498dfda5 100644 --- a/tests/test_dialogs/test_dlg_projectsettings.py +++ b/tests/test_dialogs/test_dlg_projectsettings.py @@ -24,14 +24,13 @@ import pytest from tools import C, buildTestProject -from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor from PyQt5.QtWidgets import QDialog, QAction, QColorDialog from novelwriter import CONFIG, SHARED from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.projectsettings import GuiProjectSettings -from novelwriter.enum import nwItemType +from novelwriter.enum import nwItemType, nwStatusShape from novelwriter.types import QtMouseLeft KEY_DELAY = 1 @@ -169,8 +168,8 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn nwGUI.rebuildTrees() project.countStatus() - assert [e["count"] for _, e in project.data.itemStatus.items()] == [2, 0, 2, 1] - assert [e["count"] for _, e in project.data.itemImport.items()] == [3, 0, 2, 1] + assert [e.count for _, e in project.data.itemStatus.iterItems()] == [2, 0, 2, 1] + assert [e.count for _, e in project.data.itemImport.iterItems()] == [3, 0, 2, 1] # Create Dialog projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_STATUS) @@ -182,20 +181,20 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn status = projSettings.statusPage - assert status.wasChanged is False - assert status.getNewList() == ([], []) + assert status.changed is False + assert status.getNewList() == [] assert status.listBox.topLevelItemCount() == 4 # Can't delete the first item (it's in use) status.listBox.clearSelection() status.listBox.setCurrentItem(status.listBox.topLevelItem(0)) - qtbot.mouseClick(status.delButton, QtMouseLeft) + status.delButton.click() assert status.listBox.topLevelItemCount() == 4 # Can delete the second item status.listBox.clearSelection() status.listBox.setCurrentItem(status.listBox.topLevelItem(1)) - qtbot.mouseClick(status.delButton, QtMouseLeft) + status.delButton.click() assert status.listBox.topLevelItemCount() == 3 # Add a new item @@ -204,39 +203,38 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn status.addButton.click() status.listBox.setCurrentItem(status.listBox.topLevelItem(3)) status.editName.setText("Final") - status.colButton.click() - status.saveButton.click() + status.colorButton.click() + status._selectShape(nwStatusShape.CIRCLE) + status.applyButton.click() assert status.listBox.topLevelItemCount() == 4 - assert status.wasChanged is True - assert status.getNewList() == ( - [ - { - "key": C.sNew, - "name": "New", - "cols": (100, 100, 100) - }, { - "key": C.sDraft, - "name": "Draft", - "cols": (200, 150, 0) - }, { - "key": C.sFinished, - "name": "Finished", - "cols": (50, 200, 0) - }, { - "key": None, - "name": "Final", - "cols": (20, 30, 40) - } - ], [ - C.sNote # Deleted item - ] - ) + assert status.changed is True + update = status.getNewList() + + assert update[0][0] == C.sNew + assert update[0][1].name == "New" + assert update[0][1].color == QColor(100, 100, 100) + assert update[0][1].shape == nwStatusShape.SQUARE + + assert update[1][0] == C.sDraft + assert update[1][1].name == "Draft" + assert update[1][1].color == QColor(200, 150, 0) + assert update[1][1].shape == nwStatusShape.SQUARE + + assert update[2][0] == C.sFinished + assert update[2][1].name == "Finished" + assert update[2][1].color == QColor(50, 200, 0) + assert update[2][1].shape == nwStatusShape.SQUARE + + assert update[3][0] is None + assert update[3][1].name == "Final" + assert update[3][1].color == QColor(20, 30, 40) + assert update[3][1].shape == nwStatusShape.CIRCLE # Move items, none selected -> no change status.listBox.clearSelection() status._moveItem(1) - assert [x["key"] for x in status.getNewList()[0]] == [ + assert [x[0] for x in status.getNewList()] == [ C.sNew, C.sDraft, C.sFinished, None ] @@ -244,7 +242,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn status.listBox.clearSelection() status.listBox.setCurrentItem(status.listBox.topLevelItem(0)) status._moveItem(-1) - assert [x["key"] for x in status.getNewList()[0]] == [ + assert [x[0] for x in status.getNewList()] == [ C.sNew, C.sDraft, C.sFinished, None ] @@ -252,13 +250,13 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn status.listBox.clearSelection() status.listBox.setCurrentItem(status.listBox.topLevelItem(3)) status._moveItem(-1) - assert [x["key"] for x in status.getNewList()[0]] == [ + assert [x[0] for x in status.getNewList()] == [ C.sNew, C.sDraft, None, C.sFinished ] # Move items, same selected, move down -> allowed status._moveItem(1) - assert [x["key"] for x in status.getNewList()[0]] == [ + assert [x[0] for x in status.getNewList()] == [ C.sNew, C.sDraft, C.sFinished, None ] @@ -271,62 +269,58 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn # Delete unused entry importance.listBox.clearSelection() importance.listBox.setCurrentItem(importance.listBox.topLevelItem(1)) - qtbot.mouseClick(importance.delButton, QtMouseLeft) + importance.delButton.click() assert importance.listBox.topLevelItemCount() == 3 # Add a new entry with monkeypatch.context() as mp: mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40)) - qtbot.mouseClick(importance.addButton, QtMouseLeft) + importance.addButton.click() importance.listBox.clearSelection() importance.listBox.setCurrentItem(importance.listBox.topLevelItem(3)) - for _ in range(8): - qtbot.keyClick(importance.editName, Qt.Key.Key_Backspace, delay=KEY_DELAY) - for c in "Final": - qtbot.keyClick(importance.editName, c, delay=KEY_DELAY) - qtbot.mouseClick(importance.colButton, QtMouseLeft) - qtbot.mouseClick(importance.saveButton, QtMouseLeft) + importance.editName.setText("Final") + importance.colorButton.click() + importance._selectShape(nwStatusShape.TRIANGLE) + importance.applyButton.click() assert importance.listBox.topLevelItemCount() == 4 - assert importance.wasChanged is True - assert importance.getNewList() == ( - [ - { - "key": C.iNew, - "name": "New", - "cols": (100, 100, 100) - }, { - "key": C.iMajor, - "name": "Major", - "cols": (200, 150, 0) - }, { - "key": C.iMain, - "name": "Main", - "cols": (50, 200, 0) - }, { - "key": None, - "name": "Final", - "cols": (20, 30, 40) - } - ], [ - C.iMinor # Deleted item - ] - ) + assert importance.changed is True + update = importance.getNewList() + + assert update[0][0] == C.iNew + assert update[0][1].name == "New" + assert update[0][1].color == QColor(100, 100, 100) + assert update[0][1].shape == nwStatusShape.SQUARE + + assert update[1][0] == C.iMajor + assert update[1][1].name == "Major" + assert update[1][1].color == QColor(200, 150, 0) + assert update[1][1].shape == nwStatusShape.SQUARE + + assert update[2][0] == C.iMain + assert update[2][1].name == "Main" + assert update[2][1].color == QColor(50, 200, 0) + assert update[2][1].shape == nwStatusShape.SQUARE + + assert update[3][0] is None + assert update[3][1].name == "Final" + assert update[3][1].color == QColor(20, 30, 40) + assert update[3][1].shape == nwStatusShape.TRIANGLE # Check Project projSettings._doSave() - statusItems = dict(project.data.itemStatus.items()) - assert statusItems[C.sNew]["name"] == "New" - assert statusItems[C.sDraft]["name"] == "Draft" - assert statusItems[C.sFinished]["name"] == "Finished" - assert statusItems["s000013"]["name"] == "Final" + statusItems = dict(project.data.itemStatus.iterItems()) + assert statusItems[C.sNew].name == "New" + assert statusItems[C.sDraft].name == "Draft" + assert statusItems[C.sFinished].name == "Finished" + assert statusItems["s000013"].name == "Final" - importItems = dict(project.data.itemImport.items()) - assert importItems[C.iNew]["name"] == "New" - assert importItems[C.iMajor]["name"] == "Major" - assert importItems[C.iMain]["name"] == "Main" - assert importItems["i000014"]["name"] == "Final" + importItems = dict(project.data.itemImport.iterItems()) + assert importItems[C.iNew].name == "New" + assert importItems[C.iMajor].name == "Major" + assert importItems[C.iMain].name == "Main" + assert importItems["i000014"].name == "Final" # qtbot.stop() @@ -363,7 +357,7 @@ def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Nothing to save or delete replace.listBox.clearSelection() - replace._saveEntry() + replace._applyChanges() replace._delEntry() assert replace.listBox.topLevelItemCount() == 2 @@ -381,7 +375,7 @@ def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, projPath, mockRnd): replace.editValue.setText("") for c in "With This Stuff ": qtbot.keyClick(replace.editValue, c, delay=KEY_DELAY) - qtbot.mouseClick(replace.saveButton, QtMouseLeft) + qtbot.mouseClick(replace.applyButton, QtMouseLeft) assert replace.listBox.topLevelItem(2).text(0) == "" # type: ignore assert replace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore