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"
)
# Also add the archive and trash folders
# Also add the archive folder
project.newRoot(nwItemClass.ARCHIVE)
project.trashFolder()
project.saveProject()
project.closeProject()
+16
View File
@@ -34,6 +34,7 @@ from PyQt5.QtGui import QIcon
from novelwriter import SHARED
from novelwriter.common import minmax
from novelwriter.core.item import NWItem
from novelwriter.enum import nwItemClass
from novelwriter.types import QtAlignRight
if TYPE_CHECKING: # pragma: no cover
@@ -214,6 +215,7 @@ class ProjectNode:
if self._parent:
child.item.setParent(self.item.itemHandle)
child.item.setRoot(self.item.itemRoot)
child.item.setClassDefaults(self.item.itemClass)
else:
child.item.setParent(None)
child.item.setRoot(child.item.itemHandle)
@@ -290,6 +292,10 @@ class ProjectModel(QAbstractItemModel):
return index.internalPointer()
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:
"""Get the index representing a node in the model."""
if handle and (node := self._tree.nodes.get(handle)):
@@ -342,6 +348,16 @@ class ProjectModel(QAbstractItemModel):
self.endMoveRows()
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:
"""Move multiple items to a new location."""
if target.isValid():
+5 -13
View File
@@ -154,7 +154,7 @@ class NWProject:
# 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,
use the class label.
"""
@@ -218,27 +218,19 @@ class NWProject:
project entry and a document file if it exists.
"""
if self._tree.checkType(tHandle, nwItemType.FILE):
delDoc = self._storage.getDocument(tHandle)
if not delDoc.deleteDocument():
doc = self._storage.getDocument(tHandle)
if not doc.deleteDocument():
SHARED.error(
self.tr("Could not delete document file."),
info=delDoc.getError()
info=doc.getError()
)
return False
self._index.deleteHandle(tHandle)
del self._tree[tHandle]
self._tree.remove(tHandle)
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
##
+24 -17
View File
@@ -28,7 +28,7 @@ import random
from collections.abc import Iterable, Iterator
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal, overload
from PyQt5.QtCore import QModelIndex
@@ -141,6 +141,29 @@ class NWTree:
return False
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(
self, label: str, parent: str | None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS, pos: int = -1,
@@ -431,22 +454,6 @@ class NWTree:
logger.error("No tree item with handle '%s'", str(tHandle))
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:
"""Checks if a handle exists in the tree."""
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.QtWidgets import (
QAbstractItemView, QAction, QFrame, QHBoxLayout, QHeaderView, QLabel,
QMenu, QShortcut, QTreeView, QTreeWidgetItem, QVBoxLayout, QWidget
QMenu, QShortcut, QTreeView, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
@@ -61,7 +61,6 @@ class GuiProjectView(QWidget):
"""
# Signals triggered when the meta data values of items change
# treeItemChanged = pyqtSignal(str)
rootFolderChanged = pyqtSignal(str)
wordCountsChanged = pyqtSignal()
@@ -189,7 +188,7 @@ class GuiProjectView(QWidget):
self.projTree.addAction(delete)
self.projTree.addAction(trash)
rename.triggered.connect(self.renameTreeItem)
delete.triggered.connect(self.projTree.moveItemsToTrash)
delete.triggered.connect(self.projTree.processDeleteRequest)
return
##
@@ -815,17 +814,29 @@ class GuiProjectTree(QTreeView):
return
@pyqtSlot()
def moveItemsToTrash(self, askFirst: bool = True) -> None:
def processDeleteRequest(self, askFirst: bool = True) -> None:
"""Move selected items to Trash."""
if (
(items := self._selectedRows())
and (model := self._getModel())
and (trashNode := SHARED.project.tree.trash)
):
if askFirst and not SHARED.question(self.tr("Move selected items to Trash?")):
logger.info("Action cancelled by user")
if (items := self._selectedRows()) and (model := self._getModel()):
if len(items) == 1 and (node := model.node(items[0])) and node.item.isRootType():
if node.childCount() == 0:
SHARED.project.removeItem(node.item.itemHandle)
else:
SHARED.error(self.tr("Root folders can only be deleted when they are empty."))
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
##
@@ -890,44 +901,6 @@ class GuiProjectTree(QTreeView):
# self.revealNewTreeItem(tHandle, wordCount=True)
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()
def emptyTrash(self) -> bool:
"""Permanently delete all documents in the Trash folder. This
@@ -969,68 +942,6 @@ class GuiProjectTree(QTreeView):
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:
"""Refresh status or importance labels."""
# if kind == "s":
@@ -1119,22 +1030,6 @@ class GuiProjectTree(QTreeView):
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:
"""Highlight the rows associated with a given handle."""
# brushOn = self.palette().alternateBase()
@@ -1204,36 +1099,6 @@ class GuiProjectTree(QTreeView):
# 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()
def _doAutoScroll(self) -> None:
"""Scroll one item up or down based on direction value."""
@@ -1249,24 +1114,6 @@ class GuiProjectTree(QTreeView):
# 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:
# """Capture the drag and drop handling to pop alerts."""
# super().startDrag(dropAction)
@@ -1343,38 +1190,6 @@ class GuiProjectTree(QTreeView):
# 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:
"""Merge an item's child documents into a single document."""
# logger.info("Request to merge items under handle '%s'", tHandle)
@@ -1523,118 +1338,6 @@ class GuiProjectTree(QTreeView):
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):