Re-implement project item duplication
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -232,6 +232,7 @@ class ProjectNode:
|
||||
else:
|
||||
child.item.setParent(None)
|
||||
child.item.setRoot(child.item.itemHandle)
|
||||
child.item.setClassDefaults(child.item.itemClass)
|
||||
return
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user