diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index 80e297db..ec50d73b 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -40,7 +40,7 @@ from novelwriter.constants import nwItemClass from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from novelwriter.guimain import GuiMain logger = logging.getLogger(__name__) @@ -76,7 +76,7 @@ class DocMerger: return def newTargetDoc(self, srcHandle: str, docLabel: str) -> str | None: - """Create a barnd new target document based on a source handle + """Create a brand new target document based on a source handle and a new doc label. Calling this function resets the class. """ srcItem = self._project.tree[srcHandle] @@ -263,6 +263,49 @@ class DocSplitter: # END Class DocSplitter +class DocDuplicator: + """A class that will duplicate all documents and folders starting + from a given handle. + """ + + def __init__(self, project: NWProject) -> None: + self._project = project + return + + ## + # Methods + ## + + def duplicate(self, items: list[str]) -> Iterable[tuple[str, str | None]]: + """Run through a list of items, duplicate them, and copy the + text content if they are documents. + """ + if not items: + return + + nHandle = items[0] + hMap: dict[str, str | None] = {t: None for t in items} + for tHandle in items: + newItem = self._project.tree.duplicate(tHandle) + if newItem is None or newItem.itemHandle is None: + return + hMap[tHandle] = newItem.itemHandle + if newItem.itemParent in hMap: + newItem.setParent(hMap[newItem.itemParent]) + if newItem.isFileType(): + oldDoc = self._project.storage.getDocument(tHandle) + newDoc = self._project.storage.getDocument(newItem.itemHandle) + if newDoc.fileExists(): + return + newDoc.writeDocument(oldDoc.readDocument() or "") + yield newItem.itemHandle, nHandle + nHandle = None + + return + +# END Class DocDuplicator + + class ProjectBuilder: """A class to build a new project from a set of user-defined parameter provided by the New Projecty Wizard. diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 7ec29852..d4764aec 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -83,6 +83,29 @@ class NWItem: def __bool__(self) -> bool: return self._handle is not None + def __copy__(self) -> NWItem: + """Make a shallow copy of the current item.""" + item = NWItem(self._project) + item._name = self._name + item._handle = self._handle + item._parent = self._parent + item._root = self._root + item._order = self._order + item._type = self._type + item._class = self._class + item._layout = self._layout + item._status = self._status + item._import = self._import + item._active = self._active + item._expanded = self._expanded + item._heading = self._heading + item._charCount = self._charCount + item._wordCount = self._wordCount + item._paraCount = self._paraCount + item._cursorPos = self._cursorPos + item._initCount = self._initCount + return item + ## # Properties ## @@ -163,7 +186,7 @@ class NWItem: # Pack/Unpack Data ## - def pack(self) -> dict[str, dict[str, str]]: + def pack(self) -> dict: """Pack all the data in the class instance into a dictionary.""" item: dict[str, str] = {} meta: dict[str, str] = {} @@ -197,7 +220,7 @@ class NWItem: return data - def unpack(self, data: dict[str, dict[str, Any]]) -> bool: + def unpack(self, data: dict) -> bool: """Set the values from a data dictionary.""" item = data.get("itemAttr", {}) meta = data.get("metaAttr", {}) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 14501940..f8ea65b7 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -24,10 +24,11 @@ along with this program. If not, see . """ from __future__ import annotations +import copy import random import logging -from typing import TYPE_CHECKING, Any, Iterator +from typing import TYPE_CHECKING, Iterator from pathlib import Path from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType @@ -114,7 +115,17 @@ class NWTree: return True - def pack(self) -> list[dict[str, dict[str, str]]]: + def duplicate(self, sHandle: str) -> NWItem | None: + """Duplicate an item and set a new handle.""" + sItem = self.__getitem__(sHandle) + if isinstance(sItem, NWItem): + nItem = copy.copy(sItem) + if self.append(None, sItem.itemParent, nItem): + logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle) + return nItem + return None + + def pack(self) -> list[dict]: """Pack the content of the tree into a list of doctionaries of items. In the order defined by the _treeOrder list. """ @@ -125,7 +136,7 @@ class NWTree: tree.append(tItem.pack()) return tree - def unpack(self, data: list[dict[str, dict[str, Any]]]) -> None: + def unpack(self, data: list[dict]) -> None: """Iterate through all items of a list and add them to the project tree. """ @@ -348,11 +359,11 @@ class NWTree: """True if there are any items in the project.""" return bool(self._treeOrder) - def __getitem__(self, tHandle: str) -> NWItem | None: + def __getitem__(self, tHandle: str | None) -> NWItem | None: """Return a project item based on its handle. Returns None if the handle doesn't exist in the project. """ - if tHandle in self._projTree: + if tHandle and tHandle in self._projTree: return self._projTree[tHandle] logger.error("No tree item with handle '%s'", str(tHandle)) return None