Switch data tree to new model structure

This commit is contained in:
Veronica Berglyd Olsen
2024-11-17 23:24:49 +01:00
parent 7991b9b27c
commit 4f8e898440
2 changed files with 126 additions and 137 deletions
+8
View File
@@ -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
View File
@@ -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()