Re-implement project item duplication

This commit is contained in:
Veronica Berglyd Olsen
2024-11-20 18:48:14 +01:00
parent 072be78991
commit bbe8683d59
7 changed files with 58 additions and 147 deletions
+18 -18
View File
@@ -258,29 +258,27 @@ class DocDuplicator:
# Methods
##
def duplicate(self, items: list[str]) -> Iterable[tuple[str, str | None]]:
def duplicate(self, items: list[str]) -> list[str]:
"""Run through a list of items, duplicate them, and copy the
text content if they are documents.
"""
result = []
after = True
if items:
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:
return
hMap[tHandle] = newItem.itemHandle
if newItem.itemParent in hMap:
newItem.setParent(hMap[newItem.itemParent])
self._project.tree.updateItemData(newItem.itemHandle)
if newItem.isFileType():
newDoc = self._project.storage.getDocument(newItem.itemHandle)
if newDoc.fileExists():
return
newDoc.writeDocument(self._project.storage.getDocumentText(tHandle))
yield newItem.itemHandle, nHandle
nHandle = None
return
if oldItem := self._project.tree[tHandle]:
pHandle = hMap.get(oldItem.itemParent or "") or oldItem.itemParent
if newItem := self._project.tree.duplicate(tHandle, pHandle, after):
hMap[tHandle] = newItem.itemHandle
if newItem.isFileType():
self._project.copyFileContent(newItem.itemHandle, tHandle)
newItem.notifyToRefresh()
result.append(newItem.itemHandle)
after = False
else:
break
return result
class DocSearch:
@@ -509,8 +507,10 @@ class ProjectBuilder:
f"%Short: {bfNote}\n\n"
)
# Also add the archive folder
# Also add the archive and trash folders
project.newRoot(nwItemClass.ARCHIVE)
project.tree.trash # Triggers the creation of Trash
project.saveProject()
project.closeProject()
+1
View File
@@ -232,6 +232,7 @@ class ProjectNode:
else:
child.item.setParent(None)
child.item.setRoot(child.item.itemHandle)
child.item.setClassDefaults(child.item.itemClass)
return
+1 -47
View File
@@ -26,12 +26,10 @@ from __future__ import annotations
import json
import logging
from collections.abc import Iterable
from enum import Enum
from functools import partial
from pathlib import Path
from time import time
from typing import TYPE_CHECKING
from PyQt5.QtCore import QCoreApplication
@@ -51,9 +49,6 @@ from novelwriter.core.tree import NWTree
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__)
@@ -209,7 +204,7 @@ class NWProject:
text = self._storage.getDocumentText(sHandle)
self._storage.getDocument(tHandle).writeDocument(text)
sItem.setLayout(tItem.itemLayout)
self._index.scanText(tHandle, text)
self._index.reIndexHandle(tHandle)
return True
@@ -494,47 +489,6 @@ class NWProject:
# Class Methods
##
def iterProjectItems(self) -> Iterable[NWItem]:
"""This function ensures that the item tree loaded is sent to
the GUI tree view in such a way that the tree can be built. That
is, the parent item must be sent before its child. In principle,
a proper XML file will already ensure that, but in the event the
order has been altered, or a file is orphaned, this function is
capable of handling it.
"""
sentItems = set()
iterItems = self._tree.handles()
n = 0
nMax = min(len(iterItems), 10000)
while n < nMax:
tHandle = iterItems[n]
tItem = self._tree[tHandle]
n += 1
if tItem is None:
# Technically a bug
continue
elif tItem.itemParent is None:
# Item is a root, or already been identified as orphaned
sentItems.add(tHandle)
yield tItem
elif tItem.itemParent in sentItems:
# Item's parent has been sent, so all is fine
sentItems.add(tHandle)
yield tItem
elif tItem.itemParent in iterItems:
# Item's parent exists, but hasn't been sent yet, so add
# it again to the end, but make sure this doesn't get
# out hand, so we cap at 10000 items
logger.warning("Item '%s' found before its parent", tHandle)
iterItems.append(tHandle)
nMax = min(len(iterItems), 10000)
else:
# Item is orphaned
logger.error("Item '%s' has no parent in current tree", tHandle)
tItem.setParent(None)
yield tItem
return
def updateWordCounts(self) -> None:
"""Update the total word count values."""
novel, notes = self._tree.sumWords()
+9 -28
View File
@@ -115,10 +115,6 @@ class NWTree:
del oldModel
return
def handles(self) -> list[str]:
"""Returns a copy of the list of all the active handles."""
return list(self._items.keys())
def add(self, item: NWItem, pos: int = -1) -> bool:
"""Add a project item into the project tree."""
if pHandle := item.itemParent:
@@ -128,6 +124,7 @@ class NWTree:
self._model.insertChild(node, index, pos)
self._nodes[item.itemHandle] = node
self._items[item.itemHandle] = item
self._project.setProjectChanged(True)
else:
logger.error("Could not locate parent of '%s'", item.itemHandle)
return False
@@ -136,6 +133,7 @@ class NWTree:
self._model.insertChild(node, QModelIndex(), pos)
self._nodes[item.itemHandle] = node
self._items[item.itemHandle] = item
self._project.setProjectChanged(True)
else:
logger.error("Invalid project item '%s'", item.itemHandle)
return False
@@ -181,18 +179,17 @@ class NWTree:
nwItem.setType(itemType)
nwItem.setClass(itemClass)
if self.add(nwItem, pos):
self._project.setProjectChanged(True)
return tHandle
return None
def duplicate(self, sHandle: str) -> NWItem | None:
def duplicate(self, sHandle: str, pHandle: str | None, putAfter: bool) -> NWItem | None:
"""Duplicate an item and set a new handle."""
# sItem = self.__getitem__(sHandle)
# if isinstance(sItem, NWItem):
# nItem = NWItem.duplicate(sItem, self._makeHandle())
# if self.append(nItem):
# logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle)
# return nItem
if sNode := self._nodes.get(sHandle):
nItem = NWItem.duplicate(sNode.item, self._makeHandle())
nItem.setParent(pHandle)
if self.add(nItem, (sNode.row() + 1) if putAfter else -1):
logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle)
return nItem
return None
def pack(self) -> list[dict]:
@@ -416,22 +413,6 @@ class NWTree:
yield node.item.itemHandle, node.item
return
def isTrash(self, tHandle: str) -> bool:
"""Check if an item is in or is the trash folder."""
# tItem = self.__getitem__(tHandle)
# if tItem is None:
# return True
# if tItem.itemClass == nwItemClass.TRASH:
# return True
# if self._trash is not None:
# if tHandle == self._trash:
# return True
# elif tItem.itemParent == self._trash:
# return True
# elif tItem.itemRoot == self._trash:
# return True
return False
def findRoot(self, itemClass: nwItemClass | None) -> str | None:
"""Find the first root item for a given class."""
for node in self._model.root.children:
+22 -39
View File
@@ -41,7 +41,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import qtLambda
from novelwriter.constants import nwLabels, nwStyles, nwUnicode, trConst
from novelwriter.core.coretools import DocMerger, DocSplitter
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
from novelwriter.core.item import NWItem
from novelwriter.core.itemmodel import ProjectModel, ProjectNode
from novelwriter.dialogs.docmerge import GuiDocMerge
@@ -594,11 +594,11 @@ class GuiProjectTree(QTreeView):
def loadModel(self) -> None:
"""Load and prepare a new project model."""
selModel = self.selectionModel()
# selModel = self.selectionModel()
self.setModel(SHARED.project.tree.model)
if selModel:
selModel.deleteLater()
del selModel
# if selModel:
# selModel.deleteLater()
# del selModel
# Lock the column sizes
iPx = SHARED.theme.baseIconHeight
@@ -814,6 +814,22 @@ class GuiProjectTree(QTreeView):
return True
def duplicateFromHandle(self, tHandle: str) -> None:
"""Duplicate the item hierarchy from a given item."""
itemTree = [tHandle]
itemTree.extend(SHARED.project.tree.subTree(tHandle))
if itemTree:
if len(itemTree) == 1:
question = self.tr("Do you want to duplicate this document?")
else:
question = self.tr("Do you want to duplicate this item and all child items?")
if SHARED.question(question):
docDup = DocDuplicator(SHARED.project)
dHandles = docDup.duplicate(itemTree)
if len(dHandles) != len(itemTree):
SHARED.warn(self.tr("Could not duplicate all items."))
return
##
# Events
##
@@ -1159,39 +1175,6 @@ class GuiProjectTree(QTreeView):
# self._scrollTimer.stop()
return
##
# Internal Functions
##
def _duplicateFromHandle(self, tHandle: str) -> bool:
"""Duplicate the item hierarchy from a given item."""
# itemTree = self.getTreeFromHandle(tHandle)
# nItems = len(itemTree)
# if nItems == 0:
# return False
# elif nItems == 1:
# question = self.tr("Do you want to duplicate this document?")
# else:
# question = self.tr("Do you want to duplicate this item and all child items?")
# if not SHARED.question(question):
# return False
# docDup = DocDuplicator(SHARED.project)
# dupCount = 0
# for dHandle, nHandle in docDup.duplicate(itemTree):
# SHARED.project.index.reIndexHandle(dHandle)
# self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True)
# self._alertTreeChange(dHandle, flush=False)
# dupCount += 1
# if dupCount != nItems:
# SHARED.warn(self.tr("Could not duplicate all items."))
# self.saveTreeOrder()
return True
class _UpdatableMenu(QMenu):
@@ -1324,7 +1307,7 @@ class _TreeContextMenu(QMenu):
self._expandCollapse()
if isFile:
action = self.addAction(self.tr("Duplicate"))
action.triggered.connect(qtLambda(self._tree._duplicateFromHandle, self._handle))
action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle))
self._deleteOrTrash()
return
+3 -11
View File
@@ -419,8 +419,8 @@ class _FilterTab(NFixedPage):
logger.debug("Building project tree")
self._treeMap = {}
self.optTree.clear()
for nwItem in SHARED.project.iterProjectItems():
for node in SHARED.project.tree.model.root.allChildren():
nwItem = node.item
tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent
rHandle = nwItem.itemRoot
@@ -429,8 +429,6 @@ class _FilterTab(NFixedPage):
continue
isFile = nwItem.isFileType()
isActive = nwItem.isActive
if nwItem.isInactiveClass() or not self._build.isRootAllowed(rHandle):
continue
@@ -439,18 +437,12 @@ class _FilterTab(NFixedPage):
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
)
if isFile:
iconName = "checked" if isActive else "unchecked"
else:
iconName = "noncheckable"
trItem = QTreeWidgetItem()
trItem.setIcon(self.C_NAME, itemIcon)
trItem.setText(self.C_NAME, nwItem.itemName)
trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
trItem.setData(self.C_DATA, self.D_FILE, isFile)
trItem.setIcon(self.C_ACTIVE, SHARED.theme.getIcon(iconName))
trItem.setIcon(self.C_ACTIVE, nwItem.getActiveStatus()[1])
trItem.setTextAlignment(self.C_NAME, QtAlignLeft)
if pHandle is None and nwItem.isRootType():
+4 -4
View File
@@ -181,10 +181,10 @@ def buildTestProject(obj: object, projPath: Path) -> None:
# Creating a minimal project with a few root folders and a
# single chapter folder with a single file.
nrHandle = project.newRoot(nwItemClass.NOVEL, "Novel")
project.newRoot(nwItemClass.PLOT, "Plot")
project.newRoot(nwItemClass.CHARACTER, "Characters")
project.newRoot(nwItemClass.WORLD, "World")
nrHandle = project.newRoot(nwItemClass.NOVEL)
project.newRoot(nwItemClass.PLOT)
project.newRoot(nwItemClass.CHARACTER)
project.newRoot(nwItemClass.WORLD)
tdHandle = project.newFile("Title Page", nrHandle)
cfHandle = project.newFolder("New Chapter", nrHandle) or ""