""" novelWriter – Project Item Model ================================ File History: Created: 2024-11-16 [2.7b1] ProjectNode Created: 2024-11-16 [2.7b1] ProjectModel This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ from __future__ import annotations import logging from typing import TYPE_CHECKING from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt from PyQt5.QtGui import QIcon from novelwriter import SHARED from novelwriter.common import minmax from novelwriter.core.item import NWItem from novelwriter.types import QtAlignRight if TYPE_CHECKING: # pragma: no cover from novelwriter.core.tree import NWTree logger = logging.getLogger(__name__) COL_MASK = 0x0100 C_LABEL_TEXT = 0x0000 | Qt.ItemDataRole.DisplayRole C_LABEL_ICON = 0x0000 | Qt.ItemDataRole.DecorationRole C_LABEL_TIP = 0x0000 | Qt.ItemDataRole.ToolTipRole C_COUNT_TEXT = 0x0100 | Qt.ItemDataRole.DisplayRole C_COUNT_ICON = 0x0100 | Qt.ItemDataRole.DecorationRole C_COUNT_ALIGN = 0x0100 | Qt.ItemDataRole.TextAlignmentRole C_ACTIVE_ICON = 0x0200 | Qt.ItemDataRole.DecorationRole C_ACTIVE_TIP = 0x0200 | Qt.ItemDataRole.ToolTipRole C_STATUS_ICON = 0x0300 | Qt.ItemDataRole.DecorationRole C_STATUS_TIP = 0x0300 | Qt.ItemDataRole.ToolTipRole T_NodeData = str | QIcon | Qt.AlignmentFlag | None class ProjectNode: __slots__ = ("_item", "_children", "_parent", "_row", "_cache", "_count") def __init__(self, item: NWItem) -> None: self._item = item self._children: list[ProjectNode] = [] self._parent: ProjectNode | None = None self._row = 0 self._cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {} self.refresh() return def __repr__(self) -> str: return ( f"" ) ## # Properties ## @property def item(self) -> NWItem: return self._item @property def children(self) -> list[ProjectNode]: return self._children ## # Data Maintenance ## def refresh(self) -> None: cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {} # Label cache[C_LABEL_ICON] = SHARED.theme.getItemIcon( self._item.itemType, self._item.itemClass, self._item.itemLayout, self._item.mainHeading ) cache[C_LABEL_TEXT] = self._item.itemName cache[C_LABEL_TIP] = self._item.itemName # Count cache[C_COUNT_ALIGN] = QtAlignRight # Active if self._item.isFileType(): if self._item.isActive: cache[C_ACTIVE_ICON] = SHARED.theme.getIcon("checked") else: cache[C_ACTIVE_ICON] = SHARED.theme.getIcon("unchecked") else: cache[C_ACTIVE_ICON] = SHARED.theme.getIcon("noncheckable") # Status sText, sIcon = self._item.getImportStatus() cache[C_STATUS_ICON] = sIcon cache[C_STATUS_TIP] = sText self._cache = cache self.updateCount() return def updateCount(self) -> None: self._count = self._item.wordCount + sum(c._count for c in self._children) self._cache[C_COUNT_TEXT] = f"{self._count:n}" if parent := self._parent: parent.updateCount() return ## # Data Access ## def row(self) -> int: return self._row def childCount(self) -> int: return len(self._children) def data(self, column: int, role: Qt.ItemDataRole) -> T_NodeData: """""" return self._cache.get(COL_MASK*column | role) def parent(self) -> ProjectNode | None: return self._parent def child(self, row: int) -> ProjectNode | None: if 0 <= row < len(self._children): return self._children[row] return None def allChildren(self) -> list[ProjectNode]: nodes: list[ProjectNode] = [] self._recursiveAppendChildren(nodes) return nodes ## # Data Edit ## def addChild(self, child: ProjectNode, pos: int = -1) -> None: """Add a child item to this item.""" child._parent = self if 0 <= pos < len(self._children): self._children.insert(pos, child) self._refreshChildrenPos() else: child._row = len(self._children) self._children.append(child) self.refresh() return def moveChild(self, source: int, target: int) -> None: """Move a child internally.""" count = len(self._children) if (source != target) and (0 <= source < count) and (0 <= target <= count): node = self._children.pop(source) self._children.insert(target, node) self._refreshChildrenPos() return ## # Internal Functions ## def _recursiveAppendChildren(self, children: list[ProjectNode]) -> None: """Recursively add all nodes to a list.""" for node in self._children: children.append(node) node._recursiveAppendChildren(children) return def _refreshChildrenPos(self) -> None: """Update the row value on all children.""" for n, child in enumerate(self._children): child._row = n return class ProjectModel(QAbstractItemModel): __slots__ = ("_tree", "_root") def __init__(self, tree: NWTree) -> None: super().__init__() self._tree = tree self._root = ProjectNode(NWItem(tree._project, "invisibleRoot")) logger.debug("Ready: ProjectModel") return def __del__(self) -> None: logger.debug("Delete: ProjectModel") return @property def root(self) -> ProjectNode: """Return the model root item.""" return self._root ## # Model Interface ## def rowCount(self, index: QModelIndex) -> int: """Return the number of rows for an entry.""" if index.isValid(): return index.internalPointer().childCount() return self._root.childCount() def columnCount(self, index: QModelIndex) -> int: """Return the number of columns for an entry.""" return 4 def parent(self, index: QModelIndex) -> QModelIndex: """Get the parent model index of another index.""" if index.isValid(): if parent := index.internalPointer().parent(): return self.createIndex(parent.row(), 0, parent) return QModelIndex() def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex: """get the index of a child item of a parent.""" if parent.isValid(): item: ProjectNode = parent.internalPointer() else: item = self._root if not self.hasIndex(row, column, parent): return QModelIndex() if child := item.child(row): return self.createIndex(row, column, child) return QModelIndex() def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> T_NodeData: """Return display data for a project node.""" if not index.isValid(): return None node: ProjectNode = index.internalPointer() return node.data(index.column(), role) ## # Data Access ## def row(self, index: QModelIndex) -> int: """Return the row number of the index.""" if index.isValid(): node: ProjectNode = index.internalPointer() return node.row() return -1 def node(self, index: QModelIndex) -> ProjectNode | None: """Return the node for a given model index.""" if index.isValid(): return index.internalPointer() return None def indexFromHandle(self, handle: str | None) -> QModelIndex: """Get the index representing a node in the model.""" if handle and (node := self._tree.nodes.get(handle)): return self.createIndex(node.row(), 0, node) return QModelIndex() def indexFromNode(self, node: ProjectNode) -> QModelIndex: """Get the index representing a node in the model.""" return self.createIndex(node.row(), 0, node) def rootIndex(self) -> QModelIndex: """Get the index representing the root.""" return self.createIndex(0, 0, self._root) ## # Model Edit ## def insertChild(self, child: ProjectNode, parent: QModelIndex, pos: int) -> None: """Insert a node into the model at a given position.""" if parent.isValid(): node: ProjectNode = parent.internalPointer() else: node = self._root count = node.childCount() row = minmax(pos, 0, count) if pos >= 0 else count self.beginInsertRows(parent, row, row) node.addChild(child, row) self.endInsertRows() return def internalMove(self, index: QModelIndex, step: int) -> None: """Move an item internally among its siblings.""" if index.isValid(): node: ProjectNode = index.internalPointer() if parent := node.parent(): pos = index.row() new = minmax(pos + step, 0, parent.childCount() - 1) if new != pos: end = new if new < pos else new + 1 self.beginMoveRows(index.parent(), pos, pos, index.parent(), end) parent.moveChild(pos, new) self.endMoveRows() return ## # Other Methods ## def allExpanded(self) -> list[QModelIndex]: """Return a list of all expanded items.""" expanded = [] for node in self._root.allChildren(): if node._item.isExpanded: expanded.append(self.createIndex(node.row(), 0, node)) return expanded