Re-implement row highlight, empty trash, and fix recursive permanent delete

This commit is contained in:
Veronica Berglyd Olsen
2024-11-20 23:07:40 +01:00
parent a45ab2f017
commit 2f71ec68d8
4 changed files with 81 additions and 107 deletions
+26 -16
View File
@@ -164,6 +164,22 @@ class NWProject:
"""Add a new file with a given label and parent item."""
return self._tree.create(label, parent, nwItemType.FILE, pos=pos)
def removeItem(self, tHandle: str) -> bool:
"""Remove an item from the project. This will delete both the
project entry and a document file if it exists.
"""
if self._tree.checkType(tHandle, nwItemType.FILE):
doc = self._storage.getDocument(tHandle)
if not doc.deleteDocument():
SHARED.error(
self.tr("Could not delete document file."),
info=doc.getError()
)
return False
self._index.deleteHandle(tHandle)
self._tree.remove(tHandle)
return True
def writeNewFile(self, tHandle: str, hLevel: int, isDocument: bool, text: str = "") -> bool:
"""Write content to a new document after it is created. This
will not run if the file exists and is not empty.
@@ -208,23 +224,17 @@ class NWProject:
return True
def removeItem(self, tHandle: str) -> bool:
"""Remove an item from the project. This will delete both the
project entry and a document file if it exists.
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
"""Create a new note. This function is used by the document
editor to create note files for unknown tags.
"""
if self._tree.checkType(tHandle, nwItemType.FILE):
doc = self._storage.getDocument(tHandle)
if not doc.deleteDocument():
SHARED.error(
self.tr("Could not delete document file."),
info=doc.getError()
)
return False
self._index.deleteHandle(tHandle)
self._tree.remove(tHandle)
return True
if itemClass != nwItemClass.NO_CLASS:
if not (rHandle := self._tree.findRoot(itemClass)):
rHandle = self.newRoot(itemClass)
if rHandle and (tHandle := SHARED.project.newFile(tag.title(), rHandle)):
self.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
self._tree.refreshItems([tHandle])
return
##
# Project Methods
+47 -90
View File
@@ -32,10 +32,10 @@ import logging
from enum import Enum
from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon, QMouseEvent, QPalette
from PyQt5.QtGui import QIcon, QMouseEvent, QPainter, QPalette
from PyQt5.QtWidgets import (
QAbstractItemView, QAction, QFrame, QHBoxLayout, QLabel, QMenu, QShortcut,
QTreeView, QVBoxLayout, QWidget
QStyleOptionViewItem, QTreeView, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
@@ -254,12 +254,6 @@ class GuiProjectView(QWidget):
self.projBar.buildQuickLinksMenu()
return
@pyqtSlot(str, nwItemClass)
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
"""Process new not request."""
self.projTree.createNewNote(tag, itemClass)
return
@pyqtSlot(str)
def refreshUserLabels(self, kind: str) -> None:
"""Refresh status or importance labels."""
@@ -359,7 +353,7 @@ class GuiProjectToolBar(QWidget):
self.aCollapse.triggered.connect(self.projTree.collapseAll)
self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
self.aEmptyTrash.triggered.connect(qtLambda(self.projTree.emptyTrash))
self.aEmptyTrash.triggered.connect(self.projTree.emptyTrash)
self.tbMore = NIconToolButton(self, iSz)
self.tbMore.setToolTip(self.tr("More Options"))
@@ -506,7 +500,7 @@ class GuiProjectTree(QTreeView):
self.projView = projView
# Internal Variables
# self._actHandle = None
self._actHandle = None
# Cached Translations
self.trActive = trConst(nwLabels.ACTIVE_NAME["checked"])
@@ -566,6 +560,11 @@ class GuiProjectTree(QTreeView):
# External Methods
##
def setActiveHandle(self, tHandle: str | None) -> None:
"""Set the handle to be highlighted."""
self._actHandle = tHandle
return
def getSelectedHandle(self) -> str | None:
"""Get the currently selected handle."""
if (indexes := self.selectedIndexes()) and (node := self._getNode(indexes[0])):
@@ -583,11 +582,7 @@ class GuiProjectTree(QTreeView):
def loadModel(self) -> None:
"""Load and prepare a new project model."""
# selModel = self.selectionModel()
self.setModel(SHARED.project.tree.model)
# if selModel:
# selModel.deleteLater()
# del selModel
# Lock the column sizes
iPx = SHARED.theme.baseIconHeight
@@ -820,7 +815,7 @@ class GuiProjectTree(QTreeView):
return
##
# Events
# Events and Overloads
##
def mousePressEvent(self, event: QMouseEvent) -> None:
@@ -839,6 +834,13 @@ class GuiProjectTree(QTreeView):
)
return
def drawRow(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None:
"""Draw a box on the active row."""
if (node := self._getNode(index)) and node.item.itemHandle == self._actHandle:
painter.fillRect(opt.rect, self.palette().alternateBase())
super().drawRow(painter, opt, index)
return
##
# Public Slots
##
@@ -913,33 +915,55 @@ class GuiProjectTree(QTreeView):
def processDeleteRequest(self, handles: list[str] = [], askFirst: bool = True) -> None:
"""Move selected items to Trash."""
if handles and (model := self._getModel()):
items = [model.indexFromHandle(handle) for handle in handles]
indices = [model.indexFromHandle(handle) for handle in handles]
else:
items = self._selectedRows()
indices = self._selectedRows()
if items and (model := self._getModel()):
if len(items) == 1 and (node := model.node(items[0])) and node.item.isRootType():
if indices and (model := self._getModel()):
if len(indices) == 1 and (node := model.node(indices[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
if model.trashSelection(items):
if model.trashSelection(indices):
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)
for index in indices:
if node := model.node(index):
for child in reversed(node.allChildren()):
SHARED.project.removeItem(child.item.itemHandle)
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))
model.multiMove(indices, model.indexFromNode(trashNode))
return
@pyqtSlot()
def emptyTrash(self) -> None:
"""Permanently delete all documents in the Trash folder. This
function only asks for confirmation once, and calls the regular
deleteItem function for each document in the Trash folder.
"""
if trash := SHARED.project.tree.trash:
if not (nodes := trash.allChildren()):
SHARED.info(self.tr("The Trash folder is already empty."))
return
if not SHARED.question(
self.tr("Permanently delete {0} file(s) from Trash?").format(len(nodes))
):
logger.info("Action cancelled by user")
return
for node in reversed(nodes):
SHARED.project.removeItem(node.item.itemHandle)
return
@pyqtSlot()
@pyqtSlot("QPoint")
def openContextMenu(self, point: QPoint | None = None) -> None:
@@ -1031,60 +1055,6 @@ class GuiProjectTree(QTreeView):
# Old Code
# =========================================================================================== #
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
"""Create a new note. This function is used by the document
editor to create note files for unknown tags.
"""
# if itemClass != nwItemClass.NO_CLASS:
# if not (rHandle := SHARED.project.tree.findRoot(itemClass)):
# self.newTreeItem(nwItemType.ROOT, itemClass)
# rHandle = SHARED.project.tree.findRoot(itemClass)
# if rHandle and (tHandle := SHARED.project.newFile(tag, rHandle)):
# SHARED.project.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
# self.revealNewTreeItem(tHandle, wordCount=True)
return
@pyqtSlot()
def emptyTrash(self) -> bool:
"""Permanently delete all documents in the Trash folder. This
function only asks for confirmation once, and calls the regular
deleteItem function for each document in the Trash folder.
"""
# if not SHARED.hasProject:
# logger.error("No project open")
# return False
# trashHandle = SHARED.project.tree.trashRoot
# logger.debug("Emptying Trash folder")
# if trashHandle is None:
# SHARED.info(self.tr("There is currently no Trash folder in this project."))
# return False
# trashItems = self.getTreeFromHandle(trashHandle)
# if trashHandle in trashItems:
# trashItems.remove(trashHandle)
# nTrash = len(trashItems)
# if nTrash == 0:
# SHARED.info(self.tr("The Trash folder is already empty."))
# return False
# if not SHARED.question(self.trPermDelete.format(nTrash)):
# logger.info("Action cancelled by user")
# return False
# logger.debug("Deleting %d file(s) from Trash", nTrash)
# for tHandle in reversed(self.getTreeFromHandle(trashHandle)):
# if tHandle == trashHandle:
# continue
# self.permDeleteItem(tHandle, askFirst=False, flush=False)
# if nTrash > 0:
# self._alertTreeChange(trashHandle, flush=True)
return True
def refreshUserLabels(self, kind: str) -> None:
"""Refresh status or importance labels."""
# if kind == "s":
@@ -1136,19 +1106,6 @@ class GuiProjectTree(QTreeView):
return
def setActiveHandle(self, tHandle: str | None) -> None:
"""Highlight the rows associated with a given handle."""
# brushOn = self.palette().alternateBase()
# brushOff = self.palette().base()
# if (pHandle := self._actHandle) and (item := self._treeMap.get(pHandle)):
# for i in range(self.columnCount()):
# item.setBackground(i, brushOff)
# if tHandle and (item := self._treeMap.get(tHandle)):
# for i in range(self.columnCount()):
# item.setBackground(i, brushOn)
# self._actHandle = tHandle or None
return
class _UpdatableMenu(QMenu):
+1 -1
View File
@@ -261,7 +261,7 @@ class GuiMain(QMainWindow):
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
self.docEditor.openDocumentRequest.connect(self._openDocument)
self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote)
self.docEditor.requestNewNoteCreation.connect(SHARED.createNewNote)
self.docEditor.requestNextDocument.connect(self.openNextDocument)
self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
+7
View File
@@ -37,6 +37,7 @@ from PyQt5.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget
from novelwriter.common import formatFileFilter
from novelwriter.constants import nwFiles
from novelwriter.core.spellcheck import NWSpellEnchant
from novelwriter.enum import nwItemClass
if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.project import NWProject
@@ -303,6 +304,12 @@ class SharedData(QObject):
QDesktopServices.openUrl(QUrl(url))
return
@pyqtSlot(str, nwItemClass)
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
"""Process new note request."""
self.project.createNewNote(tag, itemClass)
return
##
# Signal Proxy
##