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)")
# 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":
@@ -406,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 (
+83 -76
View File
@@ -27,7 +27,8 @@ from __future__ import annotations
import logging
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 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 novelwriter import CONFIG
from novelwriter.common import minmax, simplified
from novelwriter.common import simplified
from novelwriter.enum import nwStatusShape
from novelwriter.types import QtPaintAnitAlias, QtTransparent
@@ -45,6 +46,18 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__)
@dataclass
class StatusEntry:
name: str
colour: QColor
shape: nwStatusShape
icon: QIcon
count: int = 0
# END Class StatusEntry
class NWStatus:
STATUS = 1
@@ -53,11 +66,13 @@ class NWStatus:
def __init__(self, kind: Literal[1, 2]) -> None:
self._type = kind
self._store = {}
self._store: dict[str, StatusEntry] = {}
self._default = None
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:
self._prefix = "s"
@@ -68,8 +83,18 @@ class NWStatus:
return
def write(self, key: str | None, name: str, col: tuple, shape: nwStatusShape | str,
count: int | None = None) -> str:
def __len__(self) -> int:
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
key is generated.
"""
@@ -80,26 +105,25 @@ class NWStatus:
if len(col) != 3:
col = (100, 100, 100)
cR = minmax(col[0], 0, 255)
cG = minmax(col[1], 0, 255)
cB = minmax(col[2], 0, 255)
name = simplified(name)
colour = QColor(*col)
if not isinstance(shape, nwStatusShape):
if shape in nwStatusShape.__members__:
shape = nwStatusShape[shape]
else:
shape = nwStatusShape.SQUARE
if count is None:
count = self._store.get(key, {}).get("count", 0)
icon = self.createIcon(self._iPX, colour, shape)
self._store[key] = {
"name": name,
"icon": self._createIcon(cR, cG, cB, shape),
"cols": (cR, cG, cB),
"count": count,
"shape": shape,
}
if key and key in self._store:
entry = self._store[key]
entry.name = name
entry.colour = colour
entry.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:
self._default = key
@@ -110,7 +134,7 @@ class NWStatus:
"""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:
if self._store[key].count > 0:
return False
del self._store[key]
@@ -135,33 +159,33 @@ class NWStatus:
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"]
return self._store[key].name
elif self._default is not None:
return self._store[self._default]["name"]
return self._store[self._default].name
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."""
if key and key in self._store:
return self._store[key]["cols"]
return self._store[key].colour
elif self._default is not None:
return self._store[self._default]["cols"]
return 100, 100, 100
return self._store[self._default].colour
return QColor(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"]
return self._store[key].count
elif self._default is not None:
return self._store[self._default]["count"]
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"]
return self._store[key].icon
elif self._default is not None:
return self._store[self._default]["icon"]
return self._store[self._default].icon
return self._defaultIcon
def reorder(self, order: list[str]) -> bool:
@@ -188,28 +212,49 @@ class NWStatus:
def resetCounts(self) -> None:
"""Clear the counts of references to the status entries."""
for key in self._store:
self._store[key]["count"] = 0
self._store[key].count = 0
return
def increment(self, key: str | None) -> None:
"""Increment the counter for a given entry."""
if key and key in self._store:
self._store[key]["count"] += 1
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]),
"shape": data["shape"].name,
"count": str(entry.count),
"red": str(entry.colour.red()),
"green": str(entry.colour.green()),
"blue": str(entry.colour.blue()),
"shape": entry.shape.name,
})
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
##
@@ -239,44 +284,6 @@ class NWStatus:
return False
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
+45 -34
View File
@@ -36,6 +36,8 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED
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.modified import NComboBox, NIconToolButton
from novelwriter.extensions.pagedsidebar import NPagedSideBar
@@ -301,12 +303,14 @@ 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_COLOR = QtUserRole + 1
D_SHAPE = QtUserRole + 2
D_COUNT = QtUserRole + 3
def __init__(self, parent: QWidget, isStatus: bool) -> None:
super().__init__(parent=parent)
@@ -332,6 +336,11 @@ class _StatusPage(NFixedPage):
iSz = SHARED.theme.baseIconSize
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
self.pageTitle = NColourLabel(
pageLabel, SHARED.theme.helpText, parent=self,
@@ -342,11 +351,11 @@ class _StatusPage(NFixedPage):
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)
for key, entry in status.items():
self._addItem(key, entry["name"], entry["cols"], entry["count"])
for key, entry in status.iterItems():
self._addItem(key, entry.name, entry.colour, entry.shape, entry.icon, entry.count)
# List Controls
self.addButton = NIconToolButton(self, iSz, "add")
@@ -424,9 +433,10 @@ class _StatusPage(NFixedPage):
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),
"key": item.data(self.C_DATA, self.D_KEY),
"name": item.text(self.C_DATA),
"cols": item.data(self.C_DATA, self.D_COLOR),
"shape": item.data(self.C_DATA, self.D_SHAPE),
})
return newList, self._colDeleted
return [], []
@@ -457,7 +467,7 @@ class _StatusPage(NFixedPage):
@pyqtSlot()
def _newItem(self) -> None:
"""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
return
@@ -467,11 +477,11 @@ class _StatusPage(NFixedPage):
selItem = self._getSelectedItem()
if isinstance(selItem, QTreeWidgetItem):
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."))
else:
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
return
@@ -480,9 +490,9 @@ class _StatusPage(NFixedPage):
"""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, (
selItem.setText(self.C_LABEL, simplified(self.editName.text()))
selItem.setIcon(self.C_LABEL, self.colButton.icon())
selItem.setData(self.C_DATA, self.D_COLOR, (
self._selColour.red(), self._selColour.green(), self._selColour.blue()
))
self._changed = True
@@ -495,11 +505,11 @@ class _StatusPage(NFixedPage):
"""
selItem = self._getSelectedItem()
if isinstance(selItem, QTreeWidgetItem):
cols = selItem.data(self.COL_LABEL, self.COL_ROLE)
name = selItem.text(self.COL_LABEL)
cols = selItem.data(self.C_DATA, self.D_COLOR)
name = selItem.text(self.C_LABEL)
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols))
self._selColour = QColor(*cols)
pixmap.fill(cols)
self._selColour = cols
self.editName.setText(name)
self.colButton.setIcon(QIcon(pixmap))
self.editName.selectAll()
@@ -522,19 +532,20 @@ class _StatusPage(NFixedPage):
# Internal Functions
##
def _addItem(self, key: str | None, name: str,
colour: tuple[int, int, int], count: int) -> None:
def _addItem(self, key: str | None, name: str, colour: QColor,
shape: nwStatusShape, icon: QIcon | None, count: int) -> None:
"""Add a status item to the list."""
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*colour))
if icon is None:
icon = NWStatus.createIcon(SHARED.theme.baseIconHeight, colour, shape)
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, name)
item.setIcon(self.C_LABEL, icon)
item.setText(self.C_USAGE, self._usageString(count))
item.setData(self.C_DATA, self.D_KEY, key)
item.setData(self.C_DATA, self.D_COLOR, colour)
item.setData(self.C_DATA, self.D_SHAPE, shape)
item.setData(self.C_DATA, self.D_COUNT, count)
self.listBox.addTopLevelItem(item)
@@ -571,11 +582,11 @@ 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)
# END Class _StatusPage
+6 -6
View File
@@ -1855,11 +1855,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 +1872,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: