Switch status entries from dict to dataclass
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user