Reimplement item deletion

This commit is contained in:
Veronica Berglyd Olsen
2024-11-19 23:26:01 +01:00
parent 3532306833
commit 54685c8184
5 changed files with 69 additions and 353 deletions
+1 -3
View File
@@ -521,10 +521,8 @@ class ProjectBuilder:
f"%Short: {bfNote}\n\n" f"%Short: {bfNote}\n\n"
) )
# Also add the archive and trash folders # Also add the archive folder
project.newRoot(nwItemClass.ARCHIVE) project.newRoot(nwItemClass.ARCHIVE)
project.trashFolder()
project.saveProject() project.saveProject()
project.closeProject() project.closeProject()
+16
View File
@@ -34,6 +34,7 @@ from PyQt5.QtGui import QIcon
from novelwriter import SHARED from novelwriter import SHARED
from novelwriter.common import minmax from novelwriter.common import minmax
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.enum import nwItemClass
from novelwriter.types import QtAlignRight from novelwriter.types import QtAlignRight
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
@@ -214,6 +215,7 @@ class ProjectNode:
if self._parent: if self._parent:
child.item.setParent(self.item.itemHandle) child.item.setParent(self.item.itemHandle)
child.item.setRoot(self.item.itemRoot) child.item.setRoot(self.item.itemRoot)
child.item.setClassDefaults(self.item.itemClass)
else: else:
child.item.setParent(None) child.item.setParent(None)
child.item.setRoot(child.item.itemHandle) child.item.setRoot(child.item.itemHandle)
@@ -290,6 +292,10 @@ class ProjectModel(QAbstractItemModel):
return index.internalPointer() return index.internalPointer()
return None return None
def nodes(self, indices: list[QModelIndex]) -> list[ProjectNode]:
"""Return the nodes for a list of model indices."""
return [i.internalPointer() for i in indices if i.isValid() and i.column() == 0]
def indexFromHandle(self, handle: str | None) -> QModelIndex: def indexFromHandle(self, handle: str | None) -> QModelIndex:
"""Get the index representing a node in the model.""" """Get the index representing a node in the model."""
if handle and (node := self._tree.nodes.get(handle)): if handle and (node := self._tree.nodes.get(handle)):
@@ -342,6 +348,16 @@ class ProjectModel(QAbstractItemModel):
self.endMoveRows() self.endMoveRows()
return return
def trashSelection(self, indices: list[QModelIndex]) -> bool:
"""Check if a selection of indices are all in trash or not."""
for index in indices:
if index.isValid():
node: ProjectNode = index.internalPointer()
print(node)
if node.item.itemClass != nwItemClass.TRASH:
return False
return True
def multiMove(self, indices: list[QModelIndex], target: QModelIndex) -> None: def multiMove(self, indices: list[QModelIndex], target: QModelIndex) -> None:
"""Move multiple items to a new location.""" """Move multiple items to a new location."""
if target.isValid(): if target.isValid():
+5 -13
View File
@@ -154,7 +154,7 @@ class NWProject:
# Item Methods # Item Methods
## ##
def newRoot(self, itemClass: nwItemClass, pos: int = -1) -> str | None: def newRoot(self, itemClass: nwItemClass, pos: int = -1) -> str:
"""Add a new root folder to the project. If label is not set, """Add a new root folder to the project. If label is not set,
use the class label. use the class label.
""" """
@@ -218,27 +218,19 @@ class NWProject:
project entry and a document file if it exists. project entry and a document file if it exists.
""" """
if self._tree.checkType(tHandle, nwItemType.FILE): if self._tree.checkType(tHandle, nwItemType.FILE):
delDoc = self._storage.getDocument(tHandle) doc = self._storage.getDocument(tHandle)
if not delDoc.deleteDocument(): if not doc.deleteDocument():
SHARED.error( SHARED.error(
self.tr("Could not delete document file."), self.tr("Could not delete document file."),
info=delDoc.getError() info=doc.getError()
) )
return False return False
self._index.deleteHandle(tHandle) self._index.deleteHandle(tHandle)
del self._tree[tHandle] self._tree.remove(tHandle)
return True return True
def trashFolder(self) -> str:
"""Add the special trash root folder to the project."""
# trashHandle = self._tree.trashRoot
# if trashHandle is None:
# label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
# return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
return ""
## ##
# Project Methods # Project Methods
## ##
+24 -17
View File
@@ -28,7 +28,7 @@ import random
from collections.abc import Iterable, Iterator from collections.abc import Iterable, Iterator
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Literal, overload
from PyQt5.QtCore import QModelIndex from PyQt5.QtCore import QModelIndex
@@ -141,6 +141,29 @@ class NWTree:
return False return False
return True return True
def remove(self, tHandle: str) -> bool:
"""Remove an item from the project tree."""
if (node := self._nodes.get(tHandle)) and tHandle in self._items:
index = self._model.indexFromNode(node)
if index.isValid() and self._model.removeChild(index.parent(), index.row()):
del self._nodes[tHandle]
del self._items[tHandle]
return True
@overload # pragma: no cover
def create(
self, label: str, parent: None, itemType: Literal[nwItemType.ROOT],
itemClass: nwItemClass, pos: int = -1
) -> str:
pass
@overload # pragma: no cover
def create(
self, label: str, parent: str | None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS, pos: int = -1
) -> str | None:
pass
def create( def create(
self, label: str, parent: str | None, itemType: nwItemType, self, label: str, parent: str | None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS, pos: int = -1, itemClass: nwItemClass = nwItemClass.NO_CLASS, pos: int = -1,
@@ -431,22 +454,6 @@ class NWTree:
logger.error("No tree item with handle '%s'", str(tHandle)) logger.error("No tree item with handle '%s'", str(tHandle))
return None return None
def __delitem__(self, tHandle: str) -> None:
"""Remove an item from the internal lists and dictionaries."""
# if tHandle in self._order and tHandle in self._tree:
# self._order.remove(tHandle)
# del self._tree[tHandle]
# else:
# logger.warning("Failed to delete item '%s': item not found", tHandle)
# return
# if tHandle in self._roots:
# del self._roots[tHandle]
# if tHandle == self._trash:
# self._trash = None
return
def __contains__(self, tHandle: str) -> bool: def __contains__(self, tHandle: str) -> bool:
"""Checks if a handle exists in the tree.""" """Checks if a handle exists in the tree."""
return tHandle in self._items return tHandle in self._items
+23 -320
View File
@@ -34,7 +34,7 @@ from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon, QMouseEvent, QPalette from PyQt5.QtGui import QIcon, QMouseEvent, QPalette
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QAction, QFrame, QHBoxLayout, QHeaderView, QLabel, QAbstractItemView, QAction, QFrame, QHBoxLayout, QHeaderView, QLabel,
QMenu, QShortcut, QTreeView, QTreeWidgetItem, QVBoxLayout, QWidget QMenu, QShortcut, QTreeView, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
@@ -61,7 +61,6 @@ class GuiProjectView(QWidget):
""" """
# Signals triggered when the meta data values of items change # Signals triggered when the meta data values of items change
# treeItemChanged = pyqtSignal(str)
rootFolderChanged = pyqtSignal(str) rootFolderChanged = pyqtSignal(str)
wordCountsChanged = pyqtSignal() wordCountsChanged = pyqtSignal()
@@ -189,7 +188,7 @@ class GuiProjectView(QWidget):
self.projTree.addAction(delete) self.projTree.addAction(delete)
self.projTree.addAction(trash) self.projTree.addAction(trash)
rename.triggered.connect(self.renameTreeItem) rename.triggered.connect(self.renameTreeItem)
delete.triggered.connect(self.projTree.moveItemsToTrash) delete.triggered.connect(self.projTree.processDeleteRequest)
return return
## ##
@@ -815,17 +814,29 @@ class GuiProjectTree(QTreeView):
return return
@pyqtSlot() @pyqtSlot()
def moveItemsToTrash(self, askFirst: bool = True) -> None: def processDeleteRequest(self, askFirst: bool = True) -> None:
"""Move selected items to Trash.""" """Move selected items to Trash."""
if ( if (items := self._selectedRows()) and (model := self._getModel()):
(items := self._selectedRows()) if len(items) == 1 and (node := model.node(items[0])) and node.item.isRootType():
and (model := self._getModel()) if node.childCount() == 0:
and (trashNode := SHARED.project.tree.trash) SHARED.project.removeItem(node.item.itemHandle)
): else:
if askFirst and not SHARED.question(self.tr("Move selected items to Trash?")): SHARED.error(self.tr("Root folders can only be deleted when they are empty."))
logger.info("Action cancelled by user")
return return
model.multiMove(items, model.indexFromNode(trashNode))
if model.trashSelection(items):
if askFirst:
if not SHARED.question(self.tr("Permanently delete selected item(s)?")):
logger.info("Action cancelled by user")
return
for node in model.nodes(items):
SHARED.project.removeItem(node.item.itemHandle)
elif trashNode := SHARED.project.tree.trash:
if askFirst and not SHARED.question(self.tr("Move selected item(s) to Trash?")):
logger.info("Action cancelled by user")
return
model.multiMove(items, model.indexFromNode(trashNode))
return return
## ##
@@ -890,44 +901,6 @@ class GuiProjectTree(QTreeView):
# self.revealNewTreeItem(tHandle, wordCount=True) # self.revealNewTreeItem(tHandle, wordCount=True)
return return
def requestDeleteItem(self, tHandle: str | None = None) -> bool:
"""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 SHARED.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 = SHARED.project.tree.trashRoot
# if tHandle == trashHandle:
# logger.error("Cannot delete the Trash folder")
# return False
# nwItem = SHARED.project.tree[tHandle]
# if nwItem is None:
# return False
# if SHARED.project.tree.isTrash(tHandle) or nwItem.isRootType():
# status = self.permDeleteItem(tHandle)
# else:
# status = self.moveItemToTrash(tHandle)
# return status
return False
@pyqtSlot() @pyqtSlot()
def emptyTrash(self) -> bool: def emptyTrash(self) -> bool:
"""Permanently delete all documents in the Trash folder. This """Permanently delete all documents in the Trash folder. This
@@ -969,68 +942,6 @@ class GuiProjectTree(QTreeView):
return True return True
def permDeleteItem(self, tHandle: str, askFirst: bool = True, flush: bool = True) -> bool:
"""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 = SHARED.project.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
# if trItemS.childCount() > 0:
# SHARED.error(self.tr("Root folders can only be deleted when they are empty."))
# return False
# logger.debug("Permanently deleting root folder '%s'", tHandle)
# tIndex = self.indexOfTopLevelItem(trItemS)
# self.takeTopLevelItem(tIndex)
# SHARED.project.removeItem(tHandle)
# self._treeMap.pop(tHandle, None)
# self._alertTreeChange(tHandle, flush=True)
# # These are not emitted by the alert function because the
# # item has already been deleted
# self.projView.rootFolderChanged.emit(tHandle)
# self.projView.treeItemChanged.emit(tHandle)
# else:
# if askFirst:
# msgYes = SHARED.question(
# 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)
# # If a non-root item ends up on root due to a bug, allow it
# # to still be deleted from invisible root (see #2108)
# trItemP = trItemS.parent() or self.invisibleRootItem()
# tIndex = trItemP.indexOfChild(trItemS)
# trItemP.takeChild(tIndex)
# for dHandle in reversed(self.getTreeFromHandle(tHandle)):
# SHARED.closeEditor(dHandle)
# SHARED.project.removeItem(dHandle)
# self._treeMap.pop(dHandle, None)
# self._alertTreeChange(tHandle, flush=flush)
# self.projView.wordCountsChanged.emit()
# # This is not emitted by the alert function because the item
# # has already been deleted
# self.projView.treeItemChanged.emit(tHandle)
return True
def refreshUserLabels(self, kind: str) -> None: def refreshUserLabels(self, kind: str) -> None:
"""Refresh status or importance labels.""" """Refresh status or importance labels."""
# if kind == "s": # if kind == "s":
@@ -1119,22 +1030,6 @@ class GuiProjectTree(QTreeView):
return return
# def getSelectedHandle(self) -> str | None:
# """Get the currently selected handle. If multiple items are
# selected, return the first.
# """
# # if items := self.selectedItems():
# # return items[0].data(self.C_DATA, self.D_HANDLE)
# return None
# def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None:
# """Set a specific handle as the selected item."""
# # if tHandle in self._treeMap:
# # self.setCurrentItem(self._treeMap[tHandle])
# # if (indexes := self.selectedIndexes()) and doScroll:
# # self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
# return
def setActiveHandle(self, tHandle: str | None) -> None: def setActiveHandle(self, tHandle: str | None) -> None:
"""Highlight the rows associated with a given handle.""" """Highlight the rows associated with a given handle."""
# brushOn = self.palette().alternateBase() # brushOn = self.palette().alternateBase()
@@ -1204,36 +1099,6 @@ class GuiProjectTree(QTreeView):
# Private Slots # Private Slots
## ##
# @pyqtSlot(QModelIndex)
# def _treeSingleClick(self, index: QModelIndex) -> None:
# """The user changed which item is selected."""
# if isinstance(model := self.model(), ProjectModel) and (node := model.node(index)):
# self.projView.selectedItemChanged.emit(node.item.itemHandle)
# # # When selecting multiple items, don't allow including root
# # # items in the selection and instead deselect them
# # items = self.selectedItems()
# # if items and len(items) > 1:
# # for item in items:
# # if item.parent() is None:
# # item.setSelected(False)
# return
# @pyqtSlot(QModelIndex)
# def _treeDoubleClick(self, index: QModelIndex) -> None:
# """Capture a double-click event and either request the document
# for editing if it is a file, or expand/close the node if not.
# """
# if isinstance(model := self.model(), ProjectModel) and (node := model.node(index)):
# if node.item.isFileType():
# self.projView.openDocumentRequest.emit(
# node.item.itemHandle, nwDocMode.EDIT, "", True
# )
# else:
# self.setExpanded(index, not self.isExpanded(index))
# return
@pyqtSlot() @pyqtSlot()
def _doAutoScroll(self) -> None: def _doAutoScroll(self) -> None:
"""Scroll one item up or down based on direction value.""" """Scroll one item up or down based on direction value."""
@@ -1249,24 +1114,6 @@ class GuiProjectTree(QTreeView):
# Events # Events
## ##
# def mousePressEvent(self, event: QMouseEvent) -> None:
# """Overload mousePressEvent to clear selection if clicking the
# mouse in a blank area of the tree view, and to load a document
# for viewing if the user middle-clicked.
# """
# super().mousePressEvent(event)
# if event.button() == QtMouseLeft:
# selItem = self.indexAt(event.pos())
# if not selItem.isValid():
# self.clearSelection()
# elif event.button() == QtMouseMiddle:
# selItem = self.itemAt(event.pos())
# if selItem:
# tHandle = selItem.data(self.C_DATA, self.D_HANDLE)
# if (tItem := SHARED.project.tree[tHandle]) and tItem.isFileType():
# self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
# return
# def startDrag(self, dropAction: Qt.DropAction) -> None: # def startDrag(self, dropAction: Qt.DropAction) -> None:
# """Capture the drag and drop handling to pop alerts.""" # """Capture the drag and drop handling to pop alerts."""
# super().startDrag(dropAction) # super().startDrag(dropAction)
@@ -1343,38 +1190,6 @@ class GuiProjectTree(QTreeView):
# Internal Functions # Internal Functions
## ##
def _postItemMove(self, tHandle: str) -> None:
"""Run various maintenance tasks for a moved item."""
# trItemS = self._getTreeItem(tHandle)
# nwItemS = SHARED.project.tree[tHandle]
# trItemP = trItemS.parent() if trItemS else None
# if trItemP is None or nwItemS is None:
# logger.error("Failed to find new parent item of '%s'", tHandle)
# return
# # Update item parent handle in the project
# pHandle = trItemP.data(self.C_DATA, self.D_HANDLE)
# nwItemS.setParent(pHandle)
# trItemP.setExpanded(True)
# logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
# mHandles = self.getTreeFromHandle(tHandle)
# logger.debug("A total of %d item(s) were moved", len(mHandles))
# for mHandle in mHandles:
# logger.debug("Updating item '%s'", mHandle)
# SHARED.project.tree.updateItemData(mHandle)
# if nwItemS.isInactiveClass():
# SHARED.project.index.deleteHandle(mHandle)
# else:
# SHARED.project.index.reIndexHandle(mHandle)
# if mItem := SHARED.project.tree[mHandle]:
# self.setTreeItemValues(mItem)
# # Update word count
# self.propagateCount(tHandle, nwItemS.wordCount, countChildren=True)
return
def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool: def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool:
"""Merge an item's child documents into a single document.""" """Merge an item's child documents into a single document."""
# logger.info("Request to merge items under handle '%s'", tHandle) # logger.info("Request to merge items under handle '%s'", tHandle)
@@ -1523,118 +1338,6 @@ class GuiProjectTree(QTreeView):
return True return True
def _scanChildren(self, itemList: list, tItem: QTreeWidgetItem, tIndex: int) -> list[str]:
"""This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem.
"""
# tHandle = tItem.data(self.C_DATA, self.D_HANDLE)
# cCount = tItem.childCount()
# # Update tree-related meta data
# nwItem = SHARED.project.tree[tHandle]
# if nwItem is not None:
# nwItem.setExpanded(tItem.isExpanded() and cCount > 0)
# nwItem.setOrder(tIndex)
# itemList.append(tHandle)
# for i in range(cCount):
# self._scanChildren(itemList, tItem.child(i), i)
return itemList
def _addTreeItem(self, nwItem: NWItem | None,
nHandle: str | None = None) -> QTreeWidgetItem | None:
"""Create a QTreeWidgetItem from an NWItem and add it to the
project tree. Returns the widget if the item is valid, otherwise
a None is returned.
"""
# if not nwItem:
# logger.error("Invalid item cannot be added to project tree")
# return None
# tHandle = nwItem.itemHandle
# pHandle = nwItem.itemParent
# newItem = QTreeWidgetItem()
# newItem.setText(self.C_NAME, "")
# newItem.setText(self.C_COUNT, "0")
# newItem.setText(self.C_ACTIVE, "")
# newItem.setText(self.C_STATUS, "")
# newItem.setTextAlignment(self.C_NAME, QtAlignLeft)
# newItem.setTextAlignment(self.C_COUNT, QtAlignRight)
# newItem.setTextAlignment(self.C_ACTIVE, QtAlignLeft)
# newItem.setTextAlignment(self.C_STATUS, QtAlignLeft)
# newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
# newItem.setData(self.C_DATA, self.D_WORDS, 0)
# if pHandle is None and nwItem.isRootType():
# pItem = self.invisibleRootItem()
# elif pHandle and pHandle in self._treeMap:
# pItem = self._treeMap[pHandle]
# else:
# SHARED.error(self.tr(
# "There is nowhere to add item with name '{0}'."
# ).format(nwItem.itemName))
# return None
# byIndex = -1
# if nHandle is not None and nHandle in self._treeMap:
# byIndex = pItem.indexOfChild(self._treeMap[nHandle])
# if byIndex >= 0:
# pItem.insertChild(byIndex + 1, newItem)
# else:
# pItem.addChild(newItem)
# self._treeMap[tHandle] = newItem
# self.propagateCount(tHandle, nwItem.wordCount, countChildren=True)
# self.setTreeItemValues(nwItem)
# newItem.setExpanded(nwItem.isExpanded)
# return newItem
return None
def _addTrashRoot(self) -> QTreeWidgetItem | None:
"""Adds the trash root folder if it doesn't already exist in the
project tree.
"""
# trashHandle = SHARED.project.trashFolder()
# if trashHandle is None:
# return None
# trItem = self._getTreeItem(trashHandle)
# if trItem is None:
# trItem = self._addTreeItem(SHARED.project.tree[trashHandle])
# if trItem is not None:
# trItem.setExpanded(True)
# self._alertTreeChange(trashHandle, flush=True)
# return trItem
return None
def _alertTreeChange(self, tHandle: str | None, flush: bool = False) -> None:
"""Update information on tree change state, and emit necessary
signals. A flush is only needed if an item is moved, created or
deleted.
"""
# SHARED.project.setProjectChanged(True)
# if flush:
# self.saveTreeOrder()
# if tHandle is None or tHandle not in SHARED.project.tree:
# return
# tItem = SHARED.project.tree[tHandle]
# if tItem and tItem.isRootType():
# self.projView.rootFolderChanged.emit(tHandle)
# self.projView.treeItemChanged.emit(tHandle)
return
class _UpdatableMenu(QMenu): class _UpdatableMenu(QMenu):