From 63d2f5c57db9d22ce3b57d799a03659ca117c091 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:57:39 +0200 Subject: [PATCH] Simplify the status class --- novelwriter/core/item.py | 20 ++--- novelwriter/core/project.py | 55 +++++------- novelwriter/core/status.py | 94 ++++++++------------ novelwriter/dialogs/projectsettings.py | 118 +++++++++++-------------- novelwriter/gui/doceditor.py | 2 +- novelwriter/gui/docviewerpanel.py | 2 +- novelwriter/gui/itemdetails.py | 2 +- novelwriter/gui/outline.py | 2 +- novelwriter/gui/projtree.py | 2 +- 9 files changed, 123 insertions(+), 174 deletions(-) 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 3ce2160a..2a9e156a 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -36,7 +36,8 @@ from collections.abc import Iterable from PyQt5.QtCore import QCoreApplication from novelwriter import CONFIG, SHARED, __version__, __hexversion__ -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwStatusShape +from novelwriter.core.status import StatusEntry +from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.error import logException from novelwriter.constants import trConst, nwLabels from novelwriter.core.tree import NWTree @@ -461,15 +462,14 @@ class NWProject: def setDefaultStatusImport(self) -> None: """Set the default status and importance values.""" - square = nwStatusShape.SQUARE - self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100), square) - self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0), square) - self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0), square) - self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0), square) - self._data.itemImport.write(None, self.tr("New"), (100, 100, 100), square) - self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0), square) - self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0), square) - self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0), square) + self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100), "SQUARE") + self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0), "SQUARE") + self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0), "SQUARE") + self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0), "SQUARE") + self._data.itemImport.write(None, self.tr("New"), (100, 100, 100), "SQUARE") + self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0), "SQUARE") + self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0), "SQUARE") + self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0), "SQUARE") return def setProjectLang(self, language: str | None) -> None: @@ -492,13 +492,13 @@ class NWProject: self.setProjectChanged(True) return - def setStatusColours(self, new: list[dict], deleted: list[str]) -> bool: + def setStatus(self, update: list[tuple[str | None, StatusEntry]], remove: list[str]) -> None: """Update the list of novel file status flags.""" - return self._setStatusImport(new, deleted, self._data.itemStatus) + return self._setStatusImport(update, remove, self._data.itemStatus) - def setImportColours(self, new: list[dict], deleted: list[str]) -> bool: + def setImport(self, update: list[tuple[str | None, StatusEntry]], remove: list[str]) -> None: """Update the list of note file importance flags.""" - return self._setStatusImport(new, deleted, self._data.itemImport) + return self._setStatusImport(update, remove, self._data.itemImport) def setProjectChanged(self, status: bool) -> bool: """Toggle the project changed flag, and propagate the @@ -585,28 +585,17 @@ class NWProject: # Internal Functions ## - def _setStatusImport(self, new: list[dict], delete: list[str], target: NWStatus) -> bool: + def _setStatusImport(self, update: list[tuple[str | None, StatusEntry]], + remove: list[str], target: NWStatus) -> None: """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)) - shape = entry.get("shape", nwStatusShape.SQUARE) - if name: - order.append(target.write(key, name, cols, shape)) - - for key in delete: - target.remove(key) - - target.reorder(order) - - return True + if update or remove: + order = [target.write(k, e.name, e.color, e.shape) for k, e in update] + for key in remove: + target.remove(key) + target.reorder(order) + return def _loadProjectLocalisation(self) -> bool: """Load the language data for the current project language.""" diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 01c25067..ebf3d662 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -24,11 +24,11 @@ along with this program. If not, see . """ from __future__ import annotations +import dataclasses import logging import random from collections.abc import Iterable -from dataclasses import dataclass from math import cos, pi, sin from typing import TYPE_CHECKING, Literal @@ -46,18 +46,26 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) -@dataclass +@dataclasses.dataclass class StatusEntry: name: str - colour: QColor + color: QColor shape: nwStatusShape icon: QIcon count: int = 0 + @classmethod + def duplicate(cls, source: StatusEntry) -> StatusEntry: + """Create a shallow copy of the source object.""" + return dataclasses.replace(source) + # END Class StatusEntry +NO_ENTRY = StatusEntry("", QColor(0, 0, 0), nwStatusShape.SQUARE, QIcon(), 0) + + class NWStatus: STATUS = 1 @@ -70,9 +78,6 @@ class NWStatus: self._default = None self._iPX = CONFIG.pxInt(24) - self._defaultIcon = self.createIcon( - self._iPX, QColor(100, 100, 100), nwStatusShape.SQUARE - ) if self._type == self.STATUS: self._prefix = "s" @@ -86,44 +91,51 @@ class NWStatus: def __len__(self) -> int: return len(self._store) - def __getitem__(self, key: str) -> StatusEntry: - return self._store[key] + 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 write(self, key: str | None, name: str, col: tuple[int, int, int], - shape: nwStatusShape | str, count: int | None = None) -> str: + def write(self, key: str | None, name: str, color: tuple[int, int, int] | QColor, + shape: nwStatusShape | str, count: int = 0) -> 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) - name = simplified(name) - colour = QColor(*col) + if isinstance(color, QColor): + qColor = color + elif isinstance(color, tuple) and len(color) == 3: + qColor = QColor(*color) + else: + qColor = QColor(100, 100, 100) + if not isinstance(shape, nwStatusShape): - if shape in nwStatusShape.__members__: + try: shape = nwStatusShape[shape] - else: + except KeyError: shape = nwStatusShape.SQUARE - icon = self.createIcon(self._iPX, colour, shape) + name = simplified(name) + icon = self.createIcon(self._iPX, qColor, shape) if key and key in self._store: entry = self._store[key] entry.name = name - entry.colour = colour + entry.color = qColor entry.shape = shape entry.icon = icon - entry.count = count or 0 + entry.count = count else: - self._store[key] = StatusEntry(name, colour, shape, icon, count or 0) + self._store[key] = StatusEntry(name, qColor, shape, icon, count) if self._default is None: self._default = key @@ -156,38 +168,6 @@ 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) -> QColor: - """Return the colours associated with a given key.""" - if key and key in self._store: - return self._store[key].colour - elif self._default is not None: - 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 - 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): @@ -227,9 +207,9 @@ class NWStatus: yield (entry.name, { "key": key, "count": str(entry.count), - "red": str(entry.colour.red()), - "green": str(entry.colour.green()), - "blue": str(entry.colour.blue()), + "red": str(entry.color.red()), + "green": str(entry.color.green()), + "blue": str(entry.color.blue()), "shape": entry.shape.name, }) return diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 8d554a56..ff3663fe 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -36,7 +36,7 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import simplified -from novelwriter.core.status import NWStatus +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 @@ -182,18 +182,18 @@ class GuiProjectSettings(QDialog): rebuildTrees = False if self.statusPage.wasChanged: - newList, delList = self.statusPage.getNewList() - project.setStatusColours(newList, delList) + update, remove = self.statusPage.getNewList() + project.setStatus(update, remove) rebuildTrees = True if self.importPage.wasChanged: - newList, delList = self.importPage.getNewList() - project.setImportColours(newList, delList) + update, remove = self.importPage.getNewList() + project.setImport(update, remove) rebuildTrees = True if self.replacePage.wasChanged: - newList = self.replacePage.getNewList() - project.data.setAutoReplace(newList) + update = self.replacePage.getNewList() + project.data.setAutoReplace(update) self.newProjectSettingsReady.emit(rebuildTrees) QApplication.processEvents() @@ -308,9 +308,7 @@ class _StatusPage(NFixedPage): C_USAGE = 1 D_KEY = QtUserRole - D_COLOR = QtUserRole + 1 - D_SHAPE = QtUserRole + 2 - D_COUNT = QtUserRole + 3 + D_ENTRY = QtUserRole + 1 def __init__(self, parent: QWidget, isStatus: bool) -> None: super().__init__(parent=parent) @@ -329,10 +327,10 @@ class _StatusPage(NFixedPage): ) self._changed = False - self._colDeleted = [] + self._colDeleted: list[str] = [] self._selColour = QColor(100, 100, 100) - self.iPx = SHARED.theme.baseIconHeight + self._iPx = SHARED.theme.baseIconHeight iSz = SHARED.theme.baseIconSize bSz = SHARED.theme.buttonIconSize @@ -355,7 +353,7 @@ class _StatusPage(NFixedPage): self.listBox.setIndentation(0) for key, entry in status.iterItems(): - self._addItem(key, entry.name, entry.colour, entry.shape, entry.icon, entry.count) + self._addItem(key, StatusEntry.duplicate(entry)) # List Controls self.addButton = NIconToolButton(self, iSz, "add") @@ -376,7 +374,7 @@ 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 = 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) @@ -425,20 +423,16 @@ class _StatusPage(NFixedPage): # Methods ## - def getNewList(self) -> tuple[list, list]: + def getNewList(self) -> tuple[list[tuple[str | None, StatusEntry]], list[str]]: """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.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 + 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, self._colDeleted return [], [] def columnWidth(self) -> int: @@ -458,7 +452,7 @@ class _StatusPage(NFixedPage): ) if newCol.isValid(): self._selColour = newCol - pixmap = QPixmap(self.iPx, self.iPx) + pixmap = QPixmap(self._iPx, self._iPx) pixmap.fill(newCol) self.colButton.setIcon(QIcon(pixmap)) self.colButton.setIconSize(pixmap.rect().size()) @@ -467,34 +461,43 @@ class _StatusPage(NFixedPage): @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.C_LABEL, self.D_COUNT) > 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.C_DATA, self.D_KEY)) + self._colDeleted.append(item.data(self.C_DATA, self.D_KEY)) self._changed = True return @pyqtSlot() def _saveItem(self) -> None: """Save changes made to a status item.""" - selItem = self._getSelectedItem() - if isinstance(selItem, QTreeWidgetItem): - 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() - )) + if item := self._getSelectedItem(): + entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY) + + name = simplified(self.editName.text()) + shape = nwStatusShape.SQUARE + icon = NWStatus.createIcon(self._iPx, self._selColour, shape) + entry.name = name + entry.shape = shape + entry.color = self._selColour + entry.icon = icon + + item.setText(self.C_LABEL, name) + item.setIcon(self.C_LABEL, icon) self._changed = True return @@ -503,26 +506,21 @@ class _StatusPage(NFixedPage): """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.C_DATA, self.D_COLOR) - name = selItem.text(self.C_LABEL) - pixmap = QPixmap(self.iPx, self.iPx) - pixmap.fill(cols) - self._selColour = 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._selColour = entry.color + self.editName.setText(entry.name) + self.colButton.setIcon(entry.icon) self.editName.selectAll() self.editName.setFocus() self.editName.setEnabled(True) self.colButton.setEnabled(True) self.saveButton.setEnabled(True) else: - pixmap = QPixmap(self.iPx, self.iPx) - pixmap.fill(QColor(100, 100, 100)) self._selColour = QColor(100, 100, 100) + icon = NWStatus.createIcon(self._iPx, self._selColour, nwStatusShape.SQUARE) self.editName.setText("") - self.colButton.setIcon(QIcon(pixmap)) + self.colButton.setIcon(icon) self.editName.setEnabled(False) self.colButton.setEnabled(False) self.saveButton.setEnabled(False) @@ -532,23 +530,15 @@ class _StatusPage(NFixedPage): # Internal Functions ## - def _addItem(self, key: str | None, name: str, colour: QColor, - shape: nwStatusShape, icon: QIcon | None, count: int) -> None: + def _addItem(self, key: str | None, entry: StatusEntry) -> None: """Add a status item to the list.""" - if icon is None: - icon = NWStatus.createIcon(SHARED.theme.baseIconHeight, colour, shape) - item = QTreeWidgetItem() - item.setText(self.C_LABEL, name) - item.setIcon(self.C_LABEL, icon) - item.setText(self.C_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_COLOR, colour) - item.setData(self.C_DATA, self.D_SHAPE, shape) - item.setData(self.C_DATA, self.D_COUNT, count) - + item.setData(self.C_DATA, self.D_ENTRY, entry) self.listBox.addTopLevelItem(item) - return def _moveItem(self, step: int) -> None: diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 1f56a2b9..e4fdffd5 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -3135,7 +3135,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/outline.py b/novelwriter/gui/outline.py index 4ed2679f..05f80e3b 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -1048,7 +1048,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 7743c3f9..286b192d 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1033,7 +1033,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