diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index c89af5ee..efa6de18 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -256,6 +256,8 @@ class NWItem: self._paraCount = 0 self._cursorPos = 0 + self._initCount = self._wordCount + return True @classmethod diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index 4b8a574e..eb93d9cb 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -60,13 +60,17 @@ class ProjectNode: def __init__(self, item: NWItem) -> None: self._item = item - self._children = [] + self._children: list[ProjectNode] = [] self._parent: ProjectNode | None = None self._row = 0 self._cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {} self.refresh() return + @property + def item(self) -> NWItem: + return self._item + def refresh(self) -> None: cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {} @@ -132,17 +136,38 @@ class ProjectNode: 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__ = ("_root", "_map") + def __init__(self, tree: NWTree) -> None: super().__init__(None) self._root = ProjectNode(NWItem(tree._project, "")) return - def setRoot(self, root: ProjectNode) -> None: - self._root = root - return + @property + def root(self) -> ProjectNode: + return self._root + + ## + # Model Access + ## def rowCount(self, index: QModelIndex) -> int: if index.isValid(): @@ -159,7 +184,7 @@ class ProjectModel(QAbstractItemModel): return QModelIndex() def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex: - if parent and parent.isValid(): + if parent.isValid(): item = parent.internalPointer() else: item = self._root @@ -178,10 +203,22 @@ class ProjectModel(QAbstractItemModel): node = index.internalPointer() return node.data(index.column(), role) - def addChild(self, node: ProjectNode, parent: QModelIndex) -> None: - if parent and parent.isValid(): - item = parent.internalPointer() - else: - item = self._root - item.addChild(node) - return + # def addChild(self, node: ProjectNode, parent: QModelIndex) -> None: + # if parent.isValid(): + # item = parent.internalPointer() + # else: + # item = self._root + # item.addChild(node) + # return + + ## + # 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 diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 08ca284f..ff8a5292 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -63,7 +63,10 @@ class NWTree: also used for file names. """ - __slots__ = ("_project", "_tree", "_order", "_roots", "_model", "_nodes", "_trash", "_changed") + __slots__ = ( + "_project", "_tree", "_order", "_roots", + "_model", "_items", "_nodes", "_trash", "_changed", + ) def __init__(self, project: NWProject) -> None: @@ -74,6 +77,7 @@ class NWTree: self._roots: dict[str, NWItem] = {} # The root items of the tree self._model = ProjectModel(self) + self._items: dict[str, NWItem] = {} self._nodes: dict[str, ProjectNode] = {} self._trash = None # The handle of the trash root folder @@ -103,6 +107,11 @@ class NWTree: self._tree = {} self._order = [] self._roots = {} + + self._model = ProjectModel(self) + self._items = {} + self._nodes = {} + self._trash = None self._changed = False return @@ -185,12 +194,20 @@ class NWTree: """Pack the content of the tree into a list of dictionaries of items. In the order defined by the _treeOrder list. """ - tree = [] - for tHandle in self._order: - tItem = self.__getitem__(tHandle) - if tItem: - tree.append(tItem.pack()) - return tree + nodes = self._model.root.allChildren() + if len(nodes) != len(self._nodes): + logger.warning( + "Model tree is inconsitent with nodes map, %d != %d", + len(nodes), len(self._nodes) + ) + + # tree = [] + # for tHandle in self._order: + # tItem = self.__getitem__(tHandle) + # if tItem: + # tree.append(tItem.pack()) + + return [node.item.pack() for node in nodes] def unpack(self, data: list[dict]) -> None: """Iterate through all items of a list and add them to the @@ -200,31 +217,51 @@ class NWTree: for item in data: nwItem = NWItem(self._project, "") # Handle is set by unpack() if nwItem.unpack(item): + self._items[nwItem.itemHandle] = nwItem self.append(nwItem) - nwItem.saveInitialCount() + return def buildModel(self) -> None: """""" - root = ProjectNode(NWItem(self._project, "")) - for item in self._tree.values(): - node = ProjectNode(item) - self._nodes[item.itemHandle] = node - if pHandle := item.itemParent: - if parent := self._nodes.get(pHandle): - parent.addChild(node) - else: - logger.error("Could not add item '%s'", item.itemHandle) - else: - root.addChild(node) - self._model.beginInsertRows(self._model.index(0, 0), 0, 0) - self._model.setRoot(root) + later: dict[str, NWItem] = self._items.copy() + for _ in range(999): + later = self._buildTree(later) + if len(later) == 0: + break + else: + logger.error("Not all items could be added to project tree") + for item in later.values(): + item.setParent(None) + self._model.endInsertRows() self._model.layoutChanged.emit() return + def _buildTree(self, items: dict[str, NWItem]) -> dict[str, NWItem]: + """""" + remains: dict[str, NWItem] = {} + for handle, item in items.items(): + if pHandle := item.itemParent: + if parent := self._nodes.get(pHandle): + node = ProjectNode(item) + parent.addChild(node) + self._nodes[handle] = node + elif pHandle in items: + remains[handle] = item + logger.warning("Item '%s' found before its parent", handle) + else: + item.setParent(None) + logger.error("Item '%s' has no parent in current tree", handle) + elif item.isRootType(): + node = ProjectNode(item) + self._model.root.addChild(node) + self._nodes[handle] = node + + return remains + def checkConsistency(self, prefix: str) -> tuple[int, int]: """Check the project tree consistency. Also check the content folder and add back files that were discovered but were not diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 295993b3..6dc3c0c2 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -535,6 +535,11 @@ class GuiProjectTree2(QTreeView): treeHeader.resizeSection(self.C_ACTIVE, iPx + cMg) treeHeader.resizeSection(self.C_STATUS, iPx + cMg) + self.blockSignals(True) + for index in SHARED.project.tree.model.allExpanded(): + self.setExpanded(index, True) + self.blockSignals(False) + return diff --git a/tests/mocked.py b/tests/mocked.py index afa62d25..986fd244 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -20,7 +20,7 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt5.QtGui import QPixmap +from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtWidgets import QWidget @@ -73,6 +73,12 @@ class MockTheme: def getPixmap(self, *a): return QPixmap() + def getIcon(self, *a) -> QIcon: + return QIcon() + + def getItemIcon(self, *a, **k) -> QIcon: + return QIcon() + class MockApp: