Split up the project tree deleteItem function

This commit is contained in:
Veronica Berglyd Olsen
2022-10-09 19:21:05 +02:00
parent 0f3d598f8d
commit 2bda53e971
4 changed files with 149 additions and 123 deletions
+17
View File
@@ -202,6 +202,23 @@ class NWProject():
return True
def removeItem(self, tHandle):
"""Remove an item from the project. This will delete both the
project entry and a document file if it exists.
"""
if self._projTree.checkType(tHandle, nwItemType.FILE):
delDoc = NWDoc(self, tHandle)
if not delDoc.deleteDocument():
self.mainGui.makeAlert([
self.tr("Could not delete document file."), delDoc.getError()
], nwAlert.ERROR)
return False
self._projIndex.deleteHandle(tHandle)
del self._projTree[tHandle]
return True
def trashFolder(self):
"""Add the special trash root folder to the project.
"""
+1 -1
View File
@@ -171,7 +171,7 @@ class GuiMainMenu(QMenuBar):
# Project > Delete
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.deleteItem(None))
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None))
self.projMenu.addAction(self.aDeleteItem)
# Project > Empty Trash
+129 -120
View File
@@ -39,7 +39,7 @@ from PyQt5.QtWidgets import (
QVBoxLayout, QWidget
)
from novelwriter.core import NWDoc, DocMerger
from novelwriter.core import DocMerger
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
from novelwriter.constants import nwHeaders, trConst, nwLabels
@@ -106,7 +106,7 @@ class GuiProjectView(QWidget):
self.renameTreeItem = self.projTree.renameTreeItem
self.getTreeFromHandle = self.projTree.getTreeFromHandle
self.emptyTrash = self.projTree.emptyTrash
self.deleteItem = self.projTree.deleteItem
self.requestDeleteItem = self.projTree.requestDeleteItem
self.setTreeItemValues = self.projTree.setTreeItemValues
self.propagateCount = self.projTree.propagateCount
self.getSelectedHandle = self.projTree.getSelectedHandle
@@ -627,6 +627,42 @@ class GuiProjectTree(QTreeWidget):
theList = self._scanChildren(theList, theItem, 0)
return theList
def requestDeleteItem(self, tHandle=None):
"""Request an item deleted from the project tree. This function
can be called on any item, and will check whether to attempt a
permanent deletion or moving the item to Trash.
"""
if not self.mainGui.hasProject:
logger.error("No project open")
return False
if not self.hasFocus():
logger.info("Delete action blocked due to no widget focus")
return False
if tHandle is None:
tHandle = self.getSelectedHandle()
if tHandle is None:
logger.error("There is no item to delete")
return False
trashHandle = self.theProject.tree.trashRoot()
if tHandle == trashHandle:
logger.error("Cannot delete the Trash folder")
return False
nwItem = self.theProject.tree[tHandle]
if nwItem is None:
return False
if self.theProject.tree.isTrash(tHandle) or nwItem.isRootType():
self.permanentlyDeleteItem(tHandle)
else:
self.moveItemToTrash(tHandle)
return True
def emptyTrash(self):
"""Permanently delete all documents in the Trash folder. This
function only asks for confirmation once, and calls the regular
@@ -667,35 +703,17 @@ class GuiProjectTree(QTreeWidget):
for tHandle in reversed(self.getTreeFromHandle(trashHandle)):
if tHandle == trashHandle:
continue
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
self.permanentlyDeleteItem(tHandle, askFirst=False, flush=False)
if nTrash > 0:
self._alertTreeChange(trashHandle, flush=True)
return True
def deleteItem(self, tHandle=None, alreadyAsked=False, bulkAction=False):
"""Delete an item from the project tree. As a first step, files are
moved to the Trash folder. Permanent deletion is a second step. This
second step also deletes the item from the project object as well as
delete the files on disk. Root folders are deleted if they're empty
only, and the deletion is always permanent.
def moveItemToTrash(self, tHandle, askFirst=True, flush=True):
"""Move an item to Trash. Root folders cannot be moved to Trash,
so such a request is cancelled.
"""
if not self.mainGui.hasProject:
logger.error("No project open")
return False
if not self.hasFocus() and not bulkAction:
logger.info("Delete action blocked due to no widget focus")
return False
if tHandle is None:
tHandle = self.getSelectedHandle()
if tHandle is None:
logger.error("There is no item to delete")
return False
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.tree[tHandle]
@@ -703,91 +721,99 @@ class GuiProjectTree(QTreeWidget):
logger.error("Could not find tree item for deletion")
return False
if self.theProject.tree.isTrash(tHandle):
logger.error("Item is already in the Trash folder")
return False
if nwItemS.isRootType():
logger.error("Root folders cannot be moved to Trash")
return False
logger.debug("User requested file or folder '%s' move to Trash", tHandle)
trItemP = trItemS.parent()
trItemT = self._addTrashRoot()
if trItemP is None or trItemT is None:
logger.error("Could not delete item")
return False
if askFirst:
msgYes = self.mainGui.askQuestion(
self.tr("Delete"),
self.tr("Move '{0}' to Trash?").format(nwItemS.itemName),
)
if not msgYes:
logger.info("Action cancelled by user")
return False
wCount = self._getItemWordCount(tHandle)
autoFlush = not bulkAction
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
self._postItemMove(tHandle, wCount)
self._recordLastMove(trItemS, trItemP, tIndex)
self._alertTreeChange(tHandle, flush=flush)
logger.debug("Moved item '%s' to Trash", tHandle)
return True
def permanentlyDeleteItem(self, tHandle, askFirst=True, flush=False):
"""Permanently delete a tree item from the project and the map.
Root items are handled a little different than other items.
"""
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.tree[tHandle]
if trItemS is None or nwItemS is None:
logger.error("Could not find tree item for deletion")
return False
if nwItemS.isRootType():
# Only an empty ROOT folder can be deleted
logger.debug("User requested a root folder '%s' deleted", tHandle)
tIndex = self.indexOfTopLevelItem(trItemS)
if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle)
self._alertTreeChange(tHandle, flush=True)
else:
if trItemS.childCount() > 0:
self.mainGui.makeAlert(self.tr(
"Cannot delete root folder. It is not empty. "
"Recursive deletion is not supported. "
"Please delete the content first."
"Root folders can only be deleted when they are empty."
), nwAlert.ERROR)
return False
elif nwItemS.isFolderType() and trItemS.childCount() == 0:
# An empty FOLDER is just deleted without any further checks
logger.debug("User requested an empty folder '%s' deleted", tHandle)
logger.debug("Permanently deleting root folder '%s'", tHandle)
tIndex = self.indexOfTopLevelItem(trItemS)
self.takeTopLevelItem(tIndex)
self.theProject.removeItem(tHandle)
self._treeMap.pop(tHandle, None)
self._alertTreeChange(tHandle, flush=True)
else:
if askFirst:
msgYes = self.mainGui.askQuestion(
self.tr("Delete"),
self.tr("Permanently delete '{0}'?").format(nwItemS.itemName)
)
if not msgYes:
logger.info("Action cancelled by user")
return False
logger.debug("Permanently deleting item '%s'", tHandle)
self.propagateCount(tHandle, 0)
itemList = self.getTreeFromHandle(tHandle)
trItemP = trItemS.parent()
tIndex = trItemP.indexOfChild(trItemS)
trItemP.takeChild(tIndex)
self._deleteTreeItem(tHandle)
self._alertTreeChange(tHandle, flush=autoFlush)
else:
# A populated FOLDER or a FILE requires confirmtation
logger.debug("User requested a file or folder '%s' deleted", tHandle)
trItemP = trItemS.parent()
trItemT = self._addTrashRoot()
if trItemP is None or trItemT is None:
logger.error("Could not delete item")
return False
for dHandle in reversed(itemList):
if self.mainGui.docEditor.docHandle() == dHandle:
self.mainGui.closeDocument()
self.theProject.removeItem(tHandle)
self._treeMap.pop(tHandle, None)
if self.theProject.tree.isTrash(tHandle):
# If the file is in the trash folder already, as the
# user if they want to permanently delete the file.
doPermanent = False
if not alreadyAsked:
msgYes = self.mainGui.askQuestion(
self.tr("Delete"),
self.tr("Permanently delete '{0}'?").format(nwItemS.itemName)
)
if msgYes:
doPermanent = True
else:
doPermanent = True
if doPermanent:
logger.debug("Permanently deleting item with handle '%s'", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
for dHandle in reversed(self.getTreeFromHandle(tHandle)):
if self.mainGui.docEditor.docHandle() == dHandle:
self.mainGui.closeDocument()
self._deleteTreeItem(dHandle)
self._alertTreeChange(tHandle, flush=autoFlush)
self.projView.wordCountsChanged.emit()
else:
# The item is not already in the trash folder, so we
# move it there.
if not alreadyAsked:
msgYes = self.mainGui.askQuestion(
self.tr("Delete"),
self.tr("Move '{0}' to Trash?").format(nwItemS.itemName),
)
else:
msgYes = True
if msgYes:
logger.debug("Moving item '%s' to trash", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
self._postItemMove(tHandle, wCount)
self._recordLastMove(trItemS, trItemP, tIndex)
self._alertTreeChange(tHandle, flush=autoFlush)
self._alertTreeChange(tHandle, flush=flush)
self.projView.wordCountsChanged.emit()
return True
@@ -1158,11 +1184,11 @@ class GuiProjectTree(QTreeWidget):
if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
ctxMenu.addAction(
self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle)
self.tr("Delete Permanently"), lambda: self.permanentlyDeleteItem(tHandle)
)
else:
ctxMenu.addAction(
self.tr("Move to Trash"), lambda: self.deleteItem(tHandle)
self.tr("Move to Trash"), lambda: self.moveItemToTrash(tHandle)
)
ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
@@ -1285,23 +1311,6 @@ class GuiProjectTree(QTreeWidget):
"""
return self._treeMap.get(tHandle, None)
def _deleteTreeItem(self, tHandle):
"""Permanently delete a tree item from the project and the map.
"""
if self.theProject.tree.checkType(tHandle, nwItemType.FILE):
delDoc = NWDoc(self.theProject, tHandle)
if not delDoc.deleteDocument():
self.mainGui.makeAlert([
self.tr("Could not delete document file."), delDoc.getError()
], nwAlert.ERROR)
return False
self.theProject.index.deleteHandle(tHandle)
del self.theProject.tree[tHandle]
self._treeMap.pop(tHandle, None)
return True
def _toggleItemExported(self, tHandle):
"""Toggle the exported status of an item.
"""
@@ -1438,10 +1447,10 @@ class GuiProjectTree(QTreeWidget):
for sHandle in reversed(mrgData.get("finalItems", [])):
trItem = self._getTreeItem(sHandle)
if isinstance(trItem, QTreeWidgetItem) and trItem.childCount() == 0:
self.deleteItem(sHandle, alreadyAsked=True, bulkAction=True)
self._alertTreeChange(sHandle, flush=False)
self.moveItemToTrash(sHandle, askFirst=False, flush=False)
self._alertTreeChange(mHandle, flush=True)
self.projView.wordCountsChanged.emit()
return
@@ -1548,10 +1557,10 @@ class GuiProjectTree(QTreeWidget):
if tHandle is None:
return
tItem = self.theProject.tree[tHandle]
if tItem is None:
if tHandle not in self.theProject.tree:
return
tItem = self.theProject.tree[tHandle]
if tItem.isRootType():
self.projView.rootFolderChanged.emit(tHandle)
+2 -2
View File
@@ -504,9 +504,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None)
newHandle = nwGUI.projView.getSelectedHandle()
assert nwGUI.theProject.tree["0000000000020"] is not None
assert nwGUI.projView.deleteItem()
assert nwGUI.projView.requestDeleteItem()
assert nwGUI.projView.setSelectedHandle(newHandle)
assert nwGUI.projView.deleteItem()
assert nwGUI.projView.requestDeleteItem()
assert nwGUI.theProject.tree["0000000000024"] is not None # Trash
assert nwGUI.saveProject()