Switch data tree to new model structure
This commit is contained in:
@@ -67,10 +67,18 @@ class ProjectNode:
|
|||||||
self.refresh()
|
self.refresh()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Properties
|
||||||
|
##
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def item(self) -> NWItem:
|
def item(self) -> NWItem:
|
||||||
return self._item
|
return self._item
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self) -> list[ProjectNode]:
|
||||||
|
return self._children
|
||||||
|
|
||||||
def refresh(self) -> None:
|
def refresh(self) -> None:
|
||||||
cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {}
|
cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {}
|
||||||
|
|
||||||
|
|||||||
+118
-137
@@ -30,7 +30,6 @@ from collections.abc import Iterable, Iterator
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Literal, overload
|
from typing import TYPE_CHECKING, Literal, overload
|
||||||
|
|
||||||
from novelwriter.common import isHandle
|
|
||||||
from novelwriter.constants import nwFiles
|
from novelwriter.constants import nwFiles
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
from novelwriter.core.itemmodel import ProjectModel, ProjectNode
|
from novelwriter.core.itemmodel import ProjectModel, ProjectNode
|
||||||
@@ -72,9 +71,9 @@ class NWTree:
|
|||||||
|
|
||||||
self._project = project
|
self._project = project
|
||||||
|
|
||||||
self._tree: dict[str, NWItem] = {} # Holds all the items of the project
|
# self._tree: dict[str, NWItem] = {} # Holds all the items of the project
|
||||||
self._order: list[str] = [] # The order of the tree items in the tree view
|
# self._order: list[str] = [] # The order of the tree items in the tree view
|
||||||
self._roots: dict[str, NWItem] = {} # The root items of the tree
|
# self._roots: dict[str, NWItem] = {} # The root items of the tree
|
||||||
|
|
||||||
self._model = ProjectModel(self)
|
self._model = ProjectModel(self)
|
||||||
self._items: dict[str, NWItem] = {}
|
self._items: dict[str, NWItem] = {}
|
||||||
@@ -108,9 +107,9 @@ class NWTree:
|
|||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
"""Clear the item tree entirely."""
|
"""Clear the item tree entirely."""
|
||||||
self._tree = {}
|
# self._tree = {}
|
||||||
self._order = []
|
# self._order = []
|
||||||
self._roots = {}
|
# self._roots = {}
|
||||||
|
|
||||||
self._model = ProjectModel(self)
|
self._model = ProjectModel(self)
|
||||||
self._items = {}
|
self._items = {}
|
||||||
@@ -122,7 +121,7 @@ class NWTree:
|
|||||||
|
|
||||||
def handles(self) -> list[str]:
|
def handles(self) -> list[str]:
|
||||||
"""Returns a copy of the list of all the active handles."""
|
"""Returns a copy of the list of all the active handles."""
|
||||||
return self._order.copy()
|
return list(self._items.keys())
|
||||||
|
|
||||||
@overload # pragma: no cover
|
@overload # pragma: no cover
|
||||||
def create(self, label: str, parent: None, itemType: Literal[nwItemType.ROOT],
|
def create(self, label: str, parent: None, itemType: Literal[nwItemType.ROOT],
|
||||||
@@ -134,53 +133,53 @@ class NWTree:
|
|||||||
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str | None:
|
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str | None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def create(self, label, parent, itemType, itemClass=nwItemClass.NO_CLASS):
|
def create(self, label, parent, itemType, itemClass=nwItemClass.NO_CLASS) -> str | None:
|
||||||
"""Create a new item in the project tree, and return its handle.
|
"""Create a new item in the project tree, and return its handle.
|
||||||
If the item cannot be added to the project because of an invalid
|
If the item cannot be added to the project because of an invalid
|
||||||
parent, None is returned. For root elements, this cannot occur.
|
parent, None is returned. For root elements, this cannot occur.
|
||||||
"""
|
"""
|
||||||
parent = None if itemType == nwItemType.ROOT else parent
|
# parent = None if itemType == nwItemType.ROOT else parent
|
||||||
if parent is None or parent in self._order:
|
# if parent is None or parent in self._order:
|
||||||
tHandle = self._makeHandle()
|
# tHandle = self._makeHandle()
|
||||||
newItem = NWItem(self._project, tHandle)
|
# newItem = NWItem(self._project, tHandle)
|
||||||
newItem.setName(label)
|
# newItem.setName(label)
|
||||||
newItem.setParent(parent)
|
# newItem.setParent(parent)
|
||||||
newItem.setType(itemType)
|
# newItem.setType(itemType)
|
||||||
newItem.setClass(itemClass)
|
# newItem.setClass(itemClass)
|
||||||
self.append(newItem)
|
# self.append(newItem)
|
||||||
self.updateItemData(tHandle)
|
# self.updateItemData(tHandle)
|
||||||
return tHandle
|
# return tHandle
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def append(self, nwItem: NWItem) -> bool:
|
def append(self, nwItem: NWItem) -> bool:
|
||||||
"""Add a new item to the end of the tree."""
|
"""Add a new item to the end of the tree."""
|
||||||
tHandle = nwItem.itemHandle
|
# tHandle = nwItem.itemHandle
|
||||||
pHandle = nwItem.itemParent
|
# pHandle = nwItem.itemParent
|
||||||
|
|
||||||
if not isHandle(tHandle):
|
# if not isHandle(tHandle):
|
||||||
logger.warning("Invalid item handle '%s' detected, skipping", tHandle)
|
# logger.warning("Invalid item handle '%s' detected, skipping", tHandle)
|
||||||
return False
|
# return False
|
||||||
|
|
||||||
if tHandle in self._tree:
|
# if tHandle in self._tree:
|
||||||
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
|
# logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
|
||||||
return False
|
# return False
|
||||||
|
|
||||||
logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
# logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
||||||
|
|
||||||
if nwItem.isRootType():
|
# if nwItem.isRootType():
|
||||||
logger.debug("Item '%s' is a root item", str(tHandle))
|
# logger.debug("Item '%s' is a root item", str(tHandle))
|
||||||
self._roots[tHandle] = nwItem
|
# self._roots[tHandle] = nwItem
|
||||||
if nwItem.itemClass == nwItemClass.TRASH:
|
# if nwItem.itemClass == nwItemClass.TRASH:
|
||||||
if self._trash is None:
|
# if self._trash is None:
|
||||||
logger.debug("Item '%s' is the trash folder", str(tHandle))
|
# logger.debug("Item '%s' is the trash folder", str(tHandle))
|
||||||
self._trash = tHandle
|
# self._trash = tHandle
|
||||||
else:
|
# else:
|
||||||
logger.error("Only one trash folder allowed")
|
# logger.error("Only one trash folder allowed")
|
||||||
return False
|
# return False
|
||||||
|
|
||||||
self._tree[tHandle] = nwItem
|
# self._tree[tHandle] = nwItem
|
||||||
self._order.append(tHandle)
|
# self._order.append(tHandle)
|
||||||
self._setTreeChanged(True)
|
# self._setTreeChanged(True)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -204,13 +203,6 @@ class NWTree:
|
|||||||
"Model tree is inconsitent with nodes map, %d != %d",
|
"Model tree is inconsitent with nodes map, %d != %d",
|
||||||
len(nodes), len(self._nodes)
|
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]
|
return [node.item.pack() for node in nodes]
|
||||||
|
|
||||||
def unpack(self, data: list[dict]) -> None:
|
def unpack(self, data: list[dict]) -> None:
|
||||||
@@ -219,11 +211,12 @@ class NWTree:
|
|||||||
"""
|
"""
|
||||||
self.clear()
|
self.clear()
|
||||||
for item in data:
|
for item in data:
|
||||||
nwItem = NWItem(self._project, "") # Handle is set by unpack()
|
nwItem = NWItem(self._project, "")
|
||||||
if nwItem.unpack(item):
|
if nwItem.unpack(item):
|
||||||
self._items[nwItem.itemHandle] = nwItem
|
self._items[nwItem.itemHandle] = nwItem
|
||||||
self.append(nwItem)
|
if nwItem.itemClass == nwItemClass.TRASH:
|
||||||
|
logger.debug("Item '%s' is the trash folder", str(nwItem.itemHandle))
|
||||||
|
self._trash = nwItem.itemHandle
|
||||||
return
|
return
|
||||||
|
|
||||||
def buildModel(self) -> None:
|
def buildModel(self) -> None:
|
||||||
@@ -276,7 +269,7 @@ class NWTree:
|
|||||||
"""
|
"""
|
||||||
storage = self._project.storage
|
storage = self._project.storage
|
||||||
files = set(storage.scanContent())
|
files = set(storage.scanContent())
|
||||||
for tHandle in self._order:
|
for tHandle in self._nodes:
|
||||||
if self.updateItemData(tHandle):
|
if self.updateItemData(tHandle):
|
||||||
logger.debug("Checking item '%s' ... OK", tHandle)
|
logger.debug("Checking item '%s' ... OK", tHandle)
|
||||||
files.discard(tHandle) # Remove it from the record
|
files.discard(tHandle) # Remove it from the record
|
||||||
@@ -297,7 +290,7 @@ class NWTree:
|
|||||||
oName, oParent, oClass, oLayout = aDoc.getMeta()
|
oName, oParent, oClass, oLayout = aDoc.getMeta()
|
||||||
|
|
||||||
oName = oName or cHandle
|
oName = oName or cHandle
|
||||||
oParent = oParent if oParent in self._order else None
|
oParent = oParent if oParent in self._nodes else None
|
||||||
oClass = oClass or nwItemClass.NOVEL
|
oClass = oClass or nwItemClass.NOVEL
|
||||||
oLayout = oLayout or nwItemLayout.NOTE
|
oLayout = oLayout or nwItemLayout.NOTE
|
||||||
|
|
||||||
@@ -333,38 +326,33 @@ class NWTree:
|
|||||||
if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
|
if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
tocList = []
|
entries = []
|
||||||
tocLen = 0
|
maxLen = 0
|
||||||
for tHandle in self._order:
|
for node in self._model.root.allChildren():
|
||||||
tItem = self.__getitem__(tHandle)
|
item = node.item
|
||||||
if tItem is None:
|
file = f"{item.itemHandle}.nwd"
|
||||||
continue
|
if (contentPath / file).is_file():
|
||||||
|
|
||||||
tFile = tHandle+".nwd"
|
|
||||||
if (contentPath / tFile).is_file():
|
|
||||||
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
|
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
|
||||||
str(Path("content") / tFile),
|
str(Path("content") / file),
|
||||||
tItem.itemClass.name,
|
item.itemClass.name,
|
||||||
tItem.itemLayout.name,
|
item.itemLayout.name,
|
||||||
tItem.itemName,
|
item.itemName,
|
||||||
)
|
)
|
||||||
tocList.append(tocLine)
|
entries.append(tocLine)
|
||||||
tocLen = max(tocLen, len(tocLine))
|
maxLen = max(maxLen, len(tocLine))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Dump the text
|
with open(runtimePath / nwFiles.TOC_TXT, mode="w", encoding="utf-8") as toc:
|
||||||
tocText = runtimePath / nwFiles.TOC_TXT
|
toc.write("\n")
|
||||||
with open(tocText, mode="w", encoding="utf-8") as outFile:
|
toc.write("Table of Contents\n")
|
||||||
outFile.write("\n")
|
toc.write("=================\n")
|
||||||
outFile.write("Table of Contents\n")
|
toc.write("\n")
|
||||||
outFile.write("=================\n")
|
toc.write("{0:<25s} {1:<9s} {2:<8s} {3:s}\n".format(
|
||||||
outFile.write("\n")
|
|
||||||
outFile.write("{0:<25s} {1:<9s} {2:<8s} {3:s}\n".format(
|
|
||||||
"File Name", "Class", "Layout", "Document Label"
|
"File Name", "Class", "Layout", "Document Label"
|
||||||
))
|
))
|
||||||
outFile.write("-"*max(tocLen, 62) + "\n")
|
toc.write("-"*max(maxLen, 62) + "\n")
|
||||||
outFile.write("\n".join(tocList))
|
toc.write("\n".join(entries))
|
||||||
outFile.write("\n")
|
toc.write("\n")
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not write ToC file")
|
logger.error("Could not write ToC file")
|
||||||
@@ -377,16 +365,11 @@ class NWTree:
|
|||||||
"""Loop over all entries and add up the word counts."""
|
"""Loop over all entries and add up the word counts."""
|
||||||
noteWords = 0
|
noteWords = 0
|
||||||
novelWords = 0
|
novelWords = 0
|
||||||
for tHandle in self._order:
|
for item in self._items.values():
|
||||||
tItem = self.__getitem__(tHandle)
|
if item.itemLayout == nwItemLayout.NOTE:
|
||||||
if tItem is None:
|
noteWords += item.wordCount
|
||||||
continue
|
elif item.itemLayout == nwItemLayout.DOCUMENT:
|
||||||
if tItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
novelWords += item.wordCount
|
||||||
pass
|
|
||||||
elif tItem.itemLayout == nwItemLayout.NOTE:
|
|
||||||
noteWords += tItem.wordCount
|
|
||||||
else:
|
|
||||||
novelWords += tItem.wordCount
|
|
||||||
return novelWords, noteWords
|
return novelWords, noteWords
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -453,17 +436,16 @@ class NWTree:
|
|||||||
def rootClasses(self) -> set[nwItemClass]:
|
def rootClasses(self) -> set[nwItemClass]:
|
||||||
"""Return a set of all root classes in use by the project."""
|
"""Return a set of all root classes in use by the project."""
|
||||||
rootClasses = set()
|
rootClasses = set()
|
||||||
for nwItem in self._roots.values():
|
for node in self._model.root.children:
|
||||||
rootClasses.add(nwItem.itemClass)
|
rootClasses.add(node.item.itemClass)
|
||||||
return rootClasses
|
return rootClasses
|
||||||
|
|
||||||
def iterRoots(self, itemClass: nwItemClass | None) -> Iterable[tuple[str, NWItem]]:
|
def iterRoots(self, itemClass: nwItemClass | None) -> Iterable[tuple[str, NWItem]]:
|
||||||
"""Iterate over all root items of a given class in order."""
|
"""Iterate over all root items of a given class in order."""
|
||||||
for tHandle in self._order:
|
for node in self._model.root.children:
|
||||||
nwItem = self.__getitem__(tHandle)
|
if node.item.isRootType():
|
||||||
if isinstance(nwItem, NWItem) and nwItem.isRootType():
|
if itemClass is None or node.item.itemClass == itemClass:
|
||||||
if itemClass is None or nwItem.itemClass == itemClass:
|
yield node.item.itemHandle, node.item
|
||||||
yield tHandle, nwItem
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def isTrash(self, tHandle: str) -> bool:
|
def isTrash(self, tHandle: str) -> bool:
|
||||||
@@ -484,12 +466,9 @@ class NWTree:
|
|||||||
|
|
||||||
def findRoot(self, itemClass: nwItemClass | None) -> str | None:
|
def findRoot(self, itemClass: nwItemClass | None) -> str | None:
|
||||||
"""Find the first root item for a given class."""
|
"""Find the first root item for a given class."""
|
||||||
for aRoot in self._roots:
|
for node in self._model.root.children:
|
||||||
tItem = self.__getitem__(aRoot)
|
if node.item.itemClass == itemClass:
|
||||||
if tItem is None:
|
return node.item.itemHandle
|
||||||
continue
|
|
||||||
if itemClass == tItem.itemClass:
|
|
||||||
return tItem.itemHandle
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -498,20 +477,20 @@ class NWTree:
|
|||||||
|
|
||||||
def setOrder(self, newOrder: list[str]) -> None:
|
def setOrder(self, newOrder: list[str]) -> None:
|
||||||
"""Reorders the tree based on a list of items."""
|
"""Reorders the tree based on a list of items."""
|
||||||
tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._tree]
|
# tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._tree]
|
||||||
if not (len(tmpOrder) == len(newOrder) == len(self._order)):
|
# if not (len(tmpOrder) == len(newOrder) == len(self._order)):
|
||||||
# Something is wrong, so let's debug it
|
# # Something is wrong, so let's debug it
|
||||||
for tHandle in newOrder:
|
# for tHandle in newOrder:
|
||||||
if tHandle not in self._tree:
|
# if tHandle not in self._tree:
|
||||||
logger.error("Handle '%s' in new tree order is not in old order", tHandle)
|
# logger.error("Handle '%s' in new tree order is not in old order", tHandle)
|
||||||
for tHandle in self._order:
|
# for tHandle in self._order:
|
||||||
if tHandle not in tmpOrder:
|
# if tHandle not in tmpOrder:
|
||||||
logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
|
# logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
|
||||||
|
|
||||||
# Save the temp list
|
# # Save the temp list
|
||||||
self._order = tmpOrder
|
# self._order = tmpOrder
|
||||||
self._setTreeChanged(True)
|
# self._setTreeChanged(True)
|
||||||
logger.debug("Project tree order updated")
|
# logger.debug("Project tree order updated")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -521,49 +500,51 @@ class NWTree:
|
|||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
"""The number of items in the project."""
|
"""The number of items in the project."""
|
||||||
return len(self._order)
|
return len(self._items)
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
def __bool__(self) -> bool:
|
||||||
"""True if there are any items in the project."""
|
"""True if there are any items in the project."""
|
||||||
return bool(self._order)
|
return bool(self._items)
|
||||||
|
|
||||||
def __getitem__(self, tHandle: str | None) -> NWItem | None:
|
def __getitem__(self, tHandle: str | None) -> NWItem | None:
|
||||||
"""Return a project item based on its handle. Returns None if
|
"""Return a project item based on its handle. Returns None if
|
||||||
the handle doesn't exist in the project.
|
the handle doesn't exist in the project.
|
||||||
"""
|
"""
|
||||||
if tHandle and tHandle in self._tree:
|
if tHandle and tHandle in self._items:
|
||||||
return self._tree[tHandle]
|
return self._items[tHandle]
|
||||||
logger.error("No tree item with handle '%s'", str(tHandle))
|
logger.error("No tree item with handle '%s'", str(tHandle))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __delitem__(self, tHandle: str) -> None:
|
def __delitem__(self, tHandle: str) -> None:
|
||||||
"""Remove an item from the internal lists and dictionaries."""
|
"""Remove an item from the internal lists and dictionaries."""
|
||||||
if tHandle in self._order and tHandle in self._tree:
|
# if tHandle in self._order and tHandle in self._tree:
|
||||||
self._order.remove(tHandle)
|
# self._order.remove(tHandle)
|
||||||
del self._tree[tHandle]
|
# del self._tree[tHandle]
|
||||||
else:
|
# else:
|
||||||
logger.warning("Failed to delete item '%s': item not found", tHandle)
|
# logger.warning("Failed to delete item '%s': item not found", tHandle)
|
||||||
return
|
# return
|
||||||
|
|
||||||
if tHandle in self._roots:
|
# if tHandle in self._roots:
|
||||||
del self._roots[tHandle]
|
# del self._roots[tHandle]
|
||||||
if tHandle == self._trash:
|
# if tHandle == self._trash:
|
||||||
self._trash = None
|
# self._trash = None
|
||||||
|
|
||||||
self._setTreeChanged(True)
|
# self._setTreeChanged(True)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def __contains__(self, tHandle: str) -> bool:
|
def __contains__(self, tHandle: str) -> bool:
|
||||||
"""Checks if a handle exists in the tree."""
|
"""Checks if a handle exists in the tree."""
|
||||||
return tHandle in self._order
|
return tHandle in self._items
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[NWItem]:
|
def __iter__(self) -> Iterator[NWItem]:
|
||||||
"""Iterate through project items."""
|
"""Iterate through project items."""
|
||||||
for tHandle in self._order:
|
for node in self._model.root.allChildren():
|
||||||
tItem = self._tree.get(tHandle)
|
yield node.item
|
||||||
if isinstance(tItem, NWItem):
|
# for tHandle in self._order:
|
||||||
yield tItem
|
# tItem = self._tree.get(tHandle)
|
||||||
|
# if isinstance(tItem, NWItem):
|
||||||
|
# yield tItem
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -585,7 +566,7 @@ class NWTree:
|
|||||||
"""
|
"""
|
||||||
logger.debug("Generating new handle")
|
logger.debug("Generating new handle")
|
||||||
handle = f"{random.getrandbits(52):013x}"
|
handle = f"{random.getrandbits(52):013x}"
|
||||||
if handle in self._tree:
|
if handle in self._items:
|
||||||
logger.warning("Duplicate handle encountered! Retrying ...")
|
logger.warning("Duplicate handle encountered! Retrying ...")
|
||||||
handle = self._makeHandle()
|
handle = self._makeHandle()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user