Split up the project tree deleteItem function
This commit is contained in:
@@ -202,6 +202,23 @@ class NWProject():
|
|||||||
|
|
||||||
return True
|
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):
|
def trashFolder(self):
|
||||||
"""Add the special trash root folder to the project.
|
"""Add the special trash root folder to the project.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ class GuiMainMenu(QMenuBar):
|
|||||||
# Project > Delete
|
# Project > Delete
|
||||||
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
|
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
|
||||||
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
|
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)
|
self.projMenu.addAction(self.aDeleteItem)
|
||||||
|
|
||||||
# Project > Empty Trash
|
# Project > Empty Trash
|
||||||
|
|||||||
+129
-120
@@ -39,7 +39,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QVBoxLayout, QWidget
|
QVBoxLayout, QWidget
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter.core import NWDoc, DocMerger
|
from novelwriter.core import DocMerger
|
||||||
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||||
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
|
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
|
||||||
from novelwriter.constants import nwHeaders, trConst, nwLabels
|
from novelwriter.constants import nwHeaders, trConst, nwLabels
|
||||||
@@ -106,7 +106,7 @@ class GuiProjectView(QWidget):
|
|||||||
self.renameTreeItem = self.projTree.renameTreeItem
|
self.renameTreeItem = self.projTree.renameTreeItem
|
||||||
self.getTreeFromHandle = self.projTree.getTreeFromHandle
|
self.getTreeFromHandle = self.projTree.getTreeFromHandle
|
||||||
self.emptyTrash = self.projTree.emptyTrash
|
self.emptyTrash = self.projTree.emptyTrash
|
||||||
self.deleteItem = self.projTree.deleteItem
|
self.requestDeleteItem = self.projTree.requestDeleteItem
|
||||||
self.setTreeItemValues = self.projTree.setTreeItemValues
|
self.setTreeItemValues = self.projTree.setTreeItemValues
|
||||||
self.propagateCount = self.projTree.propagateCount
|
self.propagateCount = self.projTree.propagateCount
|
||||||
self.getSelectedHandle = self.projTree.getSelectedHandle
|
self.getSelectedHandle = self.projTree.getSelectedHandle
|
||||||
@@ -627,6 +627,42 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
theList = self._scanChildren(theList, theItem, 0)
|
theList = self._scanChildren(theList, theItem, 0)
|
||||||
return theList
|
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):
|
def emptyTrash(self):
|
||||||
"""Permanently delete all documents in the Trash folder. This
|
"""Permanently delete all documents in the Trash folder. This
|
||||||
function only asks for confirmation once, and calls the regular
|
function only asks for confirmation once, and calls the regular
|
||||||
@@ -667,35 +703,17 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
for tHandle in reversed(self.getTreeFromHandle(trashHandle)):
|
for tHandle in reversed(self.getTreeFromHandle(trashHandle)):
|
||||||
if tHandle == trashHandle:
|
if tHandle == trashHandle:
|
||||||
continue
|
continue
|
||||||
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
|
self.permanentlyDeleteItem(tHandle, askFirst=False, flush=False)
|
||||||
|
|
||||||
if nTrash > 0:
|
if nTrash > 0:
|
||||||
self._alertTreeChange(trashHandle, flush=True)
|
self._alertTreeChange(trashHandle, flush=True)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def deleteItem(self, tHandle=None, alreadyAsked=False, bulkAction=False):
|
def moveItemToTrash(self, tHandle, askFirst=True, flush=True):
|
||||||
"""Delete an item from the project tree. As a first step, files are
|
"""Move an item to Trash. Root folders cannot be moved to Trash,
|
||||||
moved to the Trash folder. Permanent deletion is a second step. This
|
so such a request is cancelled.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
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)
|
trItemS = self._getTreeItem(tHandle)
|
||||||
nwItemS = self.theProject.tree[tHandle]
|
nwItemS = self.theProject.tree[tHandle]
|
||||||
|
|
||||||
@@ -703,91 +721,99 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
logger.error("Could not find tree item for deletion")
|
logger.error("Could not find tree item for deletion")
|
||||||
return False
|
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)
|
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():
|
if nwItemS.isRootType():
|
||||||
# Only an empty ROOT folder can be deleted
|
# Only an empty ROOT folder can be deleted
|
||||||
logger.debug("User requested a root folder '%s' deleted", tHandle)
|
if trItemS.childCount() > 0:
|
||||||
tIndex = self.indexOfTopLevelItem(trItemS)
|
|
||||||
if trItemS.childCount() == 0:
|
|
||||||
self.takeTopLevelItem(tIndex)
|
|
||||||
self._deleteTreeItem(tHandle)
|
|
||||||
self._alertTreeChange(tHandle, flush=True)
|
|
||||||
else:
|
|
||||||
self.mainGui.makeAlert(self.tr(
|
self.mainGui.makeAlert(self.tr(
|
||||||
"Cannot delete root folder. It is not empty. "
|
"Root folders can only be deleted when they are empty."
|
||||||
"Recursive deletion is not supported. "
|
|
||||||
"Please delete the content first."
|
|
||||||
), nwAlert.ERROR)
|
), nwAlert.ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
elif nwItemS.isFolderType() and trItemS.childCount() == 0:
|
logger.debug("Permanently deleting root folder '%s'", tHandle)
|
||||||
# An empty FOLDER is just deleted without any further checks
|
|
||||||
logger.debug("User requested an empty folder '%s' deleted", 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()
|
trItemP = trItemS.parent()
|
||||||
tIndex = trItemP.indexOfChild(trItemS)
|
tIndex = trItemP.indexOfChild(trItemS)
|
||||||
trItemP.takeChild(tIndex)
|
trItemP.takeChild(tIndex)
|
||||||
self._deleteTreeItem(tHandle)
|
|
||||||
self._alertTreeChange(tHandle, flush=autoFlush)
|
|
||||||
|
|
||||||
else:
|
for dHandle in reversed(itemList):
|
||||||
# A populated FOLDER or a FILE requires confirmtation
|
if self.mainGui.docEditor.docHandle() == dHandle:
|
||||||
logger.debug("User requested a file or folder '%s' deleted", tHandle)
|
self.mainGui.closeDocument()
|
||||||
trItemP = trItemS.parent()
|
self.theProject.removeItem(tHandle)
|
||||||
trItemT = self._addTrashRoot()
|
self._treeMap.pop(tHandle, None)
|
||||||
if trItemP is None or trItemT is None:
|
|
||||||
logger.error("Could not delete item")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.theProject.tree.isTrash(tHandle):
|
self._alertTreeChange(tHandle, flush=flush)
|
||||||
# If the file is in the trash folder already, as the
|
self.projView.wordCountsChanged.emit()
|
||||||
# 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)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1158,11 +1184,11 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
|
if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
|
||||||
ctxMenu.addAction(
|
ctxMenu.addAction(
|
||||||
self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle)
|
self.tr("Delete Permanently"), lambda: self.permanentlyDeleteItem(tHandle)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ctxMenu.addAction(
|
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))
|
ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
|
||||||
@@ -1285,23 +1311,6 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
"""
|
"""
|
||||||
return self._treeMap.get(tHandle, None)
|
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):
|
def _toggleItemExported(self, tHandle):
|
||||||
"""Toggle the exported status of an item.
|
"""Toggle the exported status of an item.
|
||||||
"""
|
"""
|
||||||
@@ -1438,10 +1447,10 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
for sHandle in reversed(mrgData.get("finalItems", [])):
|
for sHandle in reversed(mrgData.get("finalItems", [])):
|
||||||
trItem = self._getTreeItem(sHandle)
|
trItem = self._getTreeItem(sHandle)
|
||||||
if isinstance(trItem, QTreeWidgetItem) and trItem.childCount() == 0:
|
if isinstance(trItem, QTreeWidgetItem) and trItem.childCount() == 0:
|
||||||
self.deleteItem(sHandle, alreadyAsked=True, bulkAction=True)
|
self.moveItemToTrash(sHandle, askFirst=False, flush=False)
|
||||||
self._alertTreeChange(sHandle, flush=False)
|
|
||||||
|
|
||||||
self._alertTreeChange(mHandle, flush=True)
|
self._alertTreeChange(mHandle, flush=True)
|
||||||
|
self.projView.wordCountsChanged.emit()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1548,10 +1557,10 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
if tHandle is None:
|
if tHandle is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
tItem = self.theProject.tree[tHandle]
|
if tHandle not in self.theProject.tree:
|
||||||
if tItem is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
tItem = self.theProject.tree[tHandle]
|
||||||
if tItem.isRootType():
|
if tItem.isRootType():
|
||||||
self.projView.rootFolderChanged.emit(tHandle)
|
self.projView.rootFolderChanged.emit(tHandle)
|
||||||
|
|
||||||
|
|||||||
@@ -504,9 +504,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
|
|||||||
assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None)
|
assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None)
|
||||||
newHandle = nwGUI.projView.getSelectedHandle()
|
newHandle = nwGUI.projView.getSelectedHandle()
|
||||||
assert nwGUI.theProject.tree["0000000000020"] is not None
|
assert nwGUI.theProject.tree["0000000000020"] is not None
|
||||||
assert nwGUI.projView.deleteItem()
|
assert nwGUI.projView.requestDeleteItem()
|
||||||
assert nwGUI.projView.setSelectedHandle(newHandle)
|
assert nwGUI.projView.setSelectedHandle(newHandle)
|
||||||
assert nwGUI.projView.deleteItem()
|
assert nwGUI.projView.requestDeleteItem()
|
||||||
assert nwGUI.theProject.tree["0000000000024"] is not None # Trash
|
assert nwGUI.theProject.tree["0000000000024"] is not None # Trash
|
||||||
assert nwGUI.saveProject()
|
assert nwGUI.saveProject()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user