Files
novelWriter/novelwriter/core/itemmodel.py
T
2024-11-18 17:51:05 +01:00

267 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 20182024, 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 <https://www.gnu.org/licenses/>.
"""
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.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
##
# Properties
##
@property
def item(self) -> NWItem:
return self._item
@property
def children(self) -> list[ProjectNode]:
return self._children
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
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 addChild(self, child: ProjectNode) -> None:
child._parent = self
child._row = len(self._children)
self._children.append(child)
self.refresh()
return
def allChildren(self) -> list[ProjectNode]:
nodes: list[ProjectNode] = []
self._recursiveAppendChildren(nodes)
return nodes
##
# Internal Functions
##
def _recursiveAppendChildren(self, children: list[ProjectNode]) -> None:
for node in self._children:
children.append(node)
node._recursiveAppendChildren(children)
return
class ProjectModel(QAbstractItemModel):
__slots__ = ("_tree", "_root")
def __init__(self, tree: NWTree) -> None:
super().__init__(None)
logger.debug("Create: ProjectModel")
self._tree = tree
self._root = ProjectNode(NWItem(tree._project, "invisibleRoot"))
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)
# def addChild(self, node: ProjectNode, parent: QModelIndex) -> None:
# if parent.isValid():
# item = parent.internalPointer()
# else:
# item = self._root
# item.addChild(node)
# return
##
# Data Access
##
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)
##
# 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