Switch status entries from dict to dataclass

This commit is contained in:
Veronica Berglyd Olsen
2024-04-10 01:08:57 +02:00
parent 29c10542b1
commit 127588699e
4 changed files with 138 additions and 120 deletions
+4 -4
View File
@@ -358,8 +358,8 @@ class ProjectXMLReader:
logger.debug("Parsing <content> section (legacy format)") logger.debug("Parsing <content> section (legacy format)")
# Create maps to look up name -> key for status and importance # Create maps to look up name -> key for status and importance
statusMap = {entry.get("name"): key for key, entry in data.itemStatus.items()} sMap: dict[str | None, str] = {e.name: k for k, e in data.itemStatus.iterItems()}
importMap = {entry.get("name"): key for key, entry in data.itemImport.items()} iMap: dict[str | None, str] = {e.name: k for k, e in data.itemImport.iterItems()}
for xItem in xSection: for xItem in xSection:
if xItem.tag != "item": if xItem.tag != "item":
@@ -406,9 +406,9 @@ class ProjectXMLReader:
# Status was split into separate status/import with a key in 1.4 # Status was split into separate status/import with a key in 1.4
if item.get("class", "") in ("NOVEL", "ARCHIVE"): if item.get("class", "") in ("NOVEL", "ARCHIVE"):
name["status"] = statusMap.get(tmpStatus, None) name["status"] = sMap.get(tmpStatus, None)
else: else:
name["import"] = importMap.get(tmpStatus, None) name["import"] = iMap.get(tmpStatus, None)
# A number of layouts were removed in 1.3 # A number of layouts were removed in 1.3
if item.get("layout", "") in ( if item.get("layout", "") in (
+83 -76
View File
@@ -27,7 +27,8 @@ from __future__ import annotations
import logging import logging
import random import random
from collections.abc import ItemsView, Iterable, Iterator, KeysView, ValuesView from collections.abc import Iterable
from dataclasses import dataclass
from math import cos, pi, sin from math import cos, pi, sin
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
@@ -35,7 +36,7 @@ from PyQt5.QtCore import QPointF, Qt
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor, QPolygonF from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor, QPolygonF
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.common import minmax, simplified from novelwriter.common import simplified
from novelwriter.enum import nwStatusShape from novelwriter.enum import nwStatusShape
from novelwriter.types import QtPaintAnitAlias, QtTransparent from novelwriter.types import QtPaintAnitAlias, QtTransparent
@@ -45,6 +46,18 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dataclass
class StatusEntry:
name: str
colour: QColor
shape: nwStatusShape
icon: QIcon
count: int = 0
# END Class StatusEntry
class NWStatus: class NWStatus:
STATUS = 1 STATUS = 1
@@ -53,11 +66,13 @@ class NWStatus:
def __init__(self, kind: Literal[1, 2]) -> None: def __init__(self, kind: Literal[1, 2]) -> None:
self._type = kind self._type = kind
self._store = {} self._store: dict[str, StatusEntry] = {}
self._default = None self._default = None
self._iPX = CONFIG.pxInt(24) self._iPX = CONFIG.pxInt(24)
self._defaultIcon = self._createIcon(100, 100, 100, nwStatusShape.SQUARE) self._defaultIcon = self.createIcon(
self._iPX, QColor(100, 100, 100), nwStatusShape.SQUARE
)
if self._type == self.STATUS: if self._type == self.STATUS:
self._prefix = "s" self._prefix = "s"
@@ -68,8 +83,18 @@ class NWStatus:
return return
def write(self, key: str | None, name: str, col: tuple, shape: nwStatusShape | str, def __len__(self) -> int:
count: int | None = None) -> str: return len(self._store)
def __getitem__(self, key: str) -> StatusEntry:
return self._store[key]
##
# Methods
##
def write(self, key: str | None, name: str, col: tuple[int, int, int],
shape: nwStatusShape | str, count: int | None = None) -> str:
"""Add or update a status entry. If the key is invalid, a new """Add or update a status entry. If the key is invalid, a new
key is generated. key is generated.
""" """
@@ -80,26 +105,25 @@ class NWStatus:
if len(col) != 3: if len(col) != 3:
col = (100, 100, 100) col = (100, 100, 100)
cR = minmax(col[0], 0, 255)
cG = minmax(col[1], 0, 255)
cB = minmax(col[2], 0, 255)
name = simplified(name) name = simplified(name)
colour = QColor(*col)
if not isinstance(shape, nwStatusShape): if not isinstance(shape, nwStatusShape):
if shape in nwStatusShape.__members__: if shape in nwStatusShape.__members__:
shape = nwStatusShape[shape] shape = nwStatusShape[shape]
else: else:
shape = nwStatusShape.SQUARE shape = nwStatusShape.SQUARE
if count is None: icon = self.createIcon(self._iPX, colour, shape)
count = self._store.get(key, {}).get("count", 0)
self._store[key] = { if key and key in self._store:
"name": name, entry = self._store[key]
"icon": self._createIcon(cR, cG, cB, shape), entry.name = name
"cols": (cR, cG, cB), entry.colour = colour
"count": count, entry.shape = shape
"shape": shape, entry.icon = icon
} entry.count = count or 0
else:
self._store[key] = StatusEntry(name, colour, shape, icon, count or 0)
if self._default is None: if self._default is None:
self._default = key self._default = key
@@ -110,7 +134,7 @@ class NWStatus:
"""Remove an entry in the list, except if the count > 0.""" """Remove an entry in the list, except if the count > 0."""
if key not in self._store: if key not in self._store:
return False return False
if self._store[key]["count"] > 0: if self._store[key].count > 0:
return False return False
del self._store[key] del self._store[key]
@@ -135,33 +159,33 @@ class NWStatus:
def name(self, key: str | None) -> str: def name(self, key: str | None) -> str:
"""Return the name associated with a given key.""" """Return the name associated with a given key."""
if key and key in self._store: if key and key in self._store:
return self._store[key]["name"] return self._store[key].name
elif self._default is not None: elif self._default is not None:
return self._store[self._default]["name"] return self._store[self._default].name
return "" return ""
def cols(self, key: str | None) -> tuple[int, int, int]: def cols(self, key: str | None) -> QColor:
"""Return the colours associated with a given key.""" """Return the colours associated with a given key."""
if key and key in self._store: if key and key in self._store:
return self._store[key]["cols"] return self._store[key].colour
elif self._default is not None: elif self._default is not None:
return self._store[self._default]["cols"] return self._store[self._default].colour
return 100, 100, 100 return QColor(100, 100, 100)
def count(self, key: str | None) -> int: def count(self, key: str | None) -> int:
"""Return the count associated with a given key.""" """Return the count associated with a given key."""
if key and key in self._store: if key and key in self._store:
return self._store[key]["count"] return self._store[key].count
elif self._default is not None: elif self._default is not None:
return self._store[self._default]["count"] return self._store[self._default].count
return 0 return 0
def icon(self, key: str | None) -> QIcon: def icon(self, key: str | None) -> QIcon:
"""Return the icon associated with a given key.""" """Return the icon associated with a given key."""
if key and key in self._store: if key and key in self._store:
return self._store[key]["icon"] return self._store[key].icon
elif self._default is not None: elif self._default is not None:
return self._store[self._default]["icon"] return self._store[self._default].icon
return self._defaultIcon return self._defaultIcon
def reorder(self, order: list[str]) -> bool: def reorder(self, order: list[str]) -> bool:
@@ -188,28 +212,49 @@ class NWStatus:
def resetCounts(self) -> None: def resetCounts(self) -> None:
"""Clear the counts of references to the status entries.""" """Clear the counts of references to the status entries."""
for key in self._store: for key in self._store:
self._store[key]["count"] = 0 self._store[key].count = 0
return return
def increment(self, key: str | None) -> None: def increment(self, key: str | None) -> None:
"""Increment the counter for a given entry.""" """Increment the counter for a given entry."""
if key and key in self._store: if key and key in self._store:
self._store[key]["count"] += 1 self._store[key].count += 1
return return
def pack(self) -> Iterable[tuple[str, dict]]: def pack(self) -> Iterable[tuple[str, dict]]:
"""Pack the status entries into a dictionary.""" """Pack the status entries into a dictionary."""
for key, data in self._store.items(): for key, entry in self._store.items():
yield (data["name"], { yield (entry.name, {
"key": key, "key": key,
"count": str(data["count"]), "count": str(entry.count),
"red": str(data["cols"][0]), "red": str(entry.colour.red()),
"green": str(data["cols"][1]), "green": str(entry.colour.green()),
"blue": str(data["cols"][2]), "blue": str(entry.colour.blue()),
"shape": data["shape"].name, "shape": entry.shape.name,
}) })
return return
def iterItems(self) -> Iterable[tuple[str, StatusEntry]]:
"""Yield entries from the status icons."""
yield from self._store.items()
@staticmethod
def createIcon(height: int, colour: 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), colour)
painter.end()
return QIcon(pixmap.scaled(
height, height,
Qt.AspectRatioMode.IgnoreAspectRatio,
Qt.TransformationMode.SmoothTransformation
))
## ##
# Internal Functions # Internal Functions
## ##
@@ -239,44 +284,6 @@ class NWStatus:
return False return False
return True return True
def _createIcon(self, red: int, green: int, blue: int, 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), QColor(red, green, blue))
painter.end()
return QIcon(pixmap.scaled(
self._iPX, self._iPX,
Qt.AspectRatioMode.IgnoreAspectRatio,
Qt.TransformationMode.SmoothTransformation
))
##
# Iterator Bits
##
def __len__(self) -> int:
return len(self._store)
def __getitem__(self, key: str) -> dict:
return self._store[key]
def __iter__(self) -> Iterator[dict]:
return iter(self._store)
def keys(self) -> KeysView[str]:
return self._store.keys()
def items(self) -> ItemsView[str, dict]:
return self._store.items()
def values(self) -> ValuesView[dict]:
return self._store.values()
# END Class NWStatus # END Class NWStatus
+45 -34
View File
@@ -36,6 +36,8 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import simplified from novelwriter.common import simplified
from novelwriter.core.status import NWStatus
from novelwriter.enum import nwStatusShape
from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollableForm from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollableForm
from novelwriter.extensions.modified import NComboBox, NIconToolButton from novelwriter.extensions.modified import NComboBox, NIconToolButton
from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.pagedsidebar import NPagedSideBar
@@ -301,12 +303,14 @@ class _SettingsPage(NScrollableForm):
class _StatusPage(NFixedPage): class _StatusPage(NFixedPage):
COL_LABEL = 0 C_DATA = 0
COL_USAGE = 1 C_LABEL = 0
C_USAGE = 1
KEY_ROLE = QtUserRole D_KEY = QtUserRole
COL_ROLE = QtUserRole + 1 D_COLOR = QtUserRole + 1
NUM_ROLE = QtUserRole + 2 D_SHAPE = QtUserRole + 2
D_COUNT = QtUserRole + 3
def __init__(self, parent: QWidget, isStatus: bool) -> None: def __init__(self, parent: QWidget, isStatus: bool) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -332,6 +336,11 @@ class _StatusPage(NFixedPage):
iSz = SHARED.theme.baseIconSize iSz = SHARED.theme.baseIconSize
bSz = SHARED.theme.buttonIconSize bSz = SHARED.theme.buttonIconSize
# Labels
self.trCountNone = self.tr("Not in use")
self.trCountOne = self.tr("Used once")
self.trCountMore = self.tr("Used by {0} items")
# Title # Title
self.pageTitle = NColourLabel( self.pageTitle = NColourLabel(
pageLabel, SHARED.theme.helpText, parent=self, pageLabel, SHARED.theme.helpText, parent=self,
@@ -342,11 +351,11 @@ class _StatusPage(NFixedPage):
self.listBox = QTreeWidget(self) self.listBox = QTreeWidget(self)
self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")]) self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")])
self.listBox.itemSelectionChanged.connect(self._selectedItem) 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.setIndentation(0)
for key, entry in status.items(): for key, entry in status.iterItems():
self._addItem(key, entry["name"], entry["cols"], entry["count"]) self._addItem(key, entry.name, entry.colour, entry.shape, entry.icon, entry.count)
# List Controls # List Controls
self.addButton = NIconToolButton(self, iSz, "add") self.addButton = NIconToolButton(self, iSz, "add")
@@ -424,9 +433,10 @@ class _StatusPage(NFixedPage):
item = self.listBox.topLevelItem(n) item = self.listBox.topLevelItem(n)
if item is not None: if item is not None:
newList.append({ newList.append({
"key": item.data(self.COL_LABEL, self.KEY_ROLE), "key": item.data(self.C_DATA, self.D_KEY),
"name": item.text(self.COL_LABEL), "name": item.text(self.C_DATA),
"cols": item.data(self.COL_LABEL, self.COL_ROLE), "cols": item.data(self.C_DATA, self.D_COLOR),
"shape": item.data(self.C_DATA, self.D_SHAPE),
}) })
return newList, self._colDeleted return newList, self._colDeleted
return [], [] return [], []
@@ -457,7 +467,7 @@ class _StatusPage(NFixedPage):
@pyqtSlot() @pyqtSlot()
def _newItem(self) -> None: def _newItem(self) -> None:
"""Create a new status item.""" """Create a new status item."""
self._addItem(None, self.tr("New Item"), (100, 100, 100), 0) # self._addItem(None, self.tr("New Item"), (100, 100, 100), 0)
self._changed = True self._changed = True
return return
@@ -467,11 +477,11 @@ class _StatusPage(NFixedPage):
selItem = self._getSelectedItem() selItem = self._getSelectedItem()
if isinstance(selItem, QTreeWidgetItem): if isinstance(selItem, QTreeWidgetItem):
iRow = self.listBox.indexOfTopLevelItem(selItem) iRow = self.listBox.indexOfTopLevelItem(selItem)
if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0: if selItem.data(self.C_LABEL, self.D_COUNT) > 0:
SHARED.error(self.tr("Cannot delete a status item that is in use.")) SHARED.error(self.tr("Cannot delete a status item that is in use."))
else: else:
self.listBox.takeTopLevelItem(iRow) self.listBox.takeTopLevelItem(iRow)
self._colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) self._colDeleted.append(selItem.data(self.C_DATA, self.D_KEY))
self._changed = True self._changed = True
return return
@@ -480,9 +490,9 @@ class _StatusPage(NFixedPage):
"""Save changes made to a status item.""" """Save changes made to a status item."""
selItem = self._getSelectedItem() selItem = self._getSelectedItem()
if isinstance(selItem, QTreeWidgetItem): if isinstance(selItem, QTreeWidgetItem):
selItem.setText(self.COL_LABEL, simplified(self.editName.text())) selItem.setText(self.C_LABEL, simplified(self.editName.text()))
selItem.setIcon(self.COL_LABEL, self.colButton.icon()) selItem.setIcon(self.C_LABEL, self.colButton.icon())
selItem.setData(self.COL_LABEL, self.COL_ROLE, ( selItem.setData(self.C_DATA, self.D_COLOR, (
self._selColour.red(), self._selColour.green(), self._selColour.blue() self._selColour.red(), self._selColour.green(), self._selColour.blue()
)) ))
self._changed = True self._changed = True
@@ -495,11 +505,11 @@ class _StatusPage(NFixedPage):
""" """
selItem = self._getSelectedItem() selItem = self._getSelectedItem()
if isinstance(selItem, QTreeWidgetItem): if isinstance(selItem, QTreeWidgetItem):
cols = selItem.data(self.COL_LABEL, self.COL_ROLE) cols = selItem.data(self.C_DATA, self.D_COLOR)
name = selItem.text(self.COL_LABEL) name = selItem.text(self.C_LABEL)
pixmap = QPixmap(self.iPx, self.iPx) pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols)) pixmap.fill(cols)
self._selColour = QColor(*cols) self._selColour = cols
self.editName.setText(name) self.editName.setText(name)
self.colButton.setIcon(QIcon(pixmap)) self.colButton.setIcon(QIcon(pixmap))
self.editName.selectAll() self.editName.selectAll()
@@ -522,19 +532,20 @@ class _StatusPage(NFixedPage):
# Internal Functions # Internal Functions
## ##
def _addItem(self, key: str | None, name: str, def _addItem(self, key: str | None, name: str, colour: QColor,
colour: tuple[int, int, int], count: int) -> None: shape: nwStatusShape, icon: QIcon | None, count: int) -> None:
"""Add a status item to the list.""" """Add a status item to the list."""
pixmap = QPixmap(self.iPx, self.iPx) if icon is None:
pixmap.fill(QColor(*colour)) icon = NWStatus.createIcon(SHARED.theme.baseIconHeight, colour, shape)
item = QTreeWidgetItem() item = QTreeWidgetItem()
item.setText(self.COL_LABEL, name) item.setText(self.C_LABEL, name)
item.setIcon(self.COL_LABEL, QIcon(pixmap)) item.setIcon(self.C_LABEL, icon)
item.setData(self.COL_LABEL, self.KEY_ROLE, key) item.setText(self.C_USAGE, self._usageString(count))
item.setData(self.COL_LABEL, self.COL_ROLE, colour) item.setData(self.C_DATA, self.D_KEY, key)
item.setData(self.COL_LABEL, self.NUM_ROLE, count) item.setData(self.C_DATA, self.D_COLOR, colour)
item.setText(self.COL_USAGE, self._usageString(count)) item.setData(self.C_DATA, self.D_SHAPE, shape)
item.setData(self.C_DATA, self.D_COUNT, count)
self.listBox.addTopLevelItem(item) self.listBox.addTopLevelItem(item)
@@ -571,11 +582,11 @@ class _StatusPage(NFixedPage):
def _usageString(self, count: int) -> str: def _usageString(self, count: int) -> str:
"""Generate usage string.""" """Generate usage string."""
if count == 0: if count == 0:
return self.tr("Not in use") return self.trCountNone
elif count == 1: elif count == 1:
return self.tr("Used once") return self.trCountOne
else: else:
return self.tr("Used by {0} items").format(count) return self.trCountMore.format(count)
# END Class _StatusPage # END Class _StatusPage
+6 -6
View File
@@ -1855,11 +1855,11 @@ class _TreeContextMenu(QMenu):
if self._item.isNovelLike(): if self._item.isNovelLike():
menu = self.addMenu(self.tr("Set Status to ...")) menu = self.addMenu(self.tr("Set Status to ..."))
current = self._item.itemStatus current = self._item.itemStatus
for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.items()): for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.iterItems()):
name = entry["name"] name = entry.name
if not multi and current == key: if not multi and current == key:
name += f" ({nwUnicode.U_CHECK})" name += f" ({nwUnicode.U_CHECK})"
action = menu.addAction(entry["icon"], name) action = menu.addAction(entry.icon, name)
if multi: if multi:
action.triggered.connect(lambda n, key=key: self._iterSetItemStatus(key)) action.triggered.connect(lambda n, key=key: self._iterSetItemStatus(key))
else: else:
@@ -1872,11 +1872,11 @@ class _TreeContextMenu(QMenu):
else: else:
menu = self.addMenu(self.tr("Set Importance to ...")) menu = self.addMenu(self.tr("Set Importance to ..."))
current = self._item.itemImport current = self._item.itemImport
for n, (key, entry) in enumerate(SHARED.project.data.itemImport.items()): for n, (key, entry) in enumerate(SHARED.project.data.itemImport.iterItems()):
name = entry["name"] name = entry.name
if not multi and current == key: if not multi and current == key:
name += f" ({nwUnicode.U_CHECK})" name += f" ({nwUnicode.U_CHECK})"
action = menu.addAction(entry["icon"], name) action = menu.addAction(entry.icon, name)
if multi: if multi:
action.triggered.connect(lambda n, key=key: self._iterSetItemImport(key)) action.triggered.connect(lambda n, key=key: self._iterSetItemImport(key))
else: else: