Re-implement row highlight, empty trash, and fix recursive permanent delete
This commit is contained in:
+26
-16
@@ -164,6 +164,22 @@ class NWProject:
|
|||||||
"""Add a new file with a given label and parent item."""
|
"""Add a new file with a given label and parent item."""
|
||||||
return self._tree.create(label, parent, nwItemType.FILE, pos=pos)
|
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:
|
def writeNewFile(self, tHandle: str, hLevel: int, isDocument: bool, text: str = "") -> bool:
|
||||||
"""Write content to a new document after it is created. This
|
"""Write content to a new document after it is created. This
|
||||||
will not run if the file exists and is not empty.
|
will not run if the file exists and is not empty.
|
||||||
@@ -208,23 +224,17 @@ class NWProject:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def removeItem(self, tHandle: str) -> bool:
|
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
|
||||||
"""Remove an item from the project. This will delete both the
|
"""Create a new note. This function is used by the document
|
||||||
project entry and a document file if it exists.
|
editor to create note files for unknown tags.
|
||||||
"""
|
"""
|
||||||
if self._tree.checkType(tHandle, nwItemType.FILE):
|
if itemClass != nwItemClass.NO_CLASS:
|
||||||
doc = self._storage.getDocument(tHandle)
|
if not (rHandle := self._tree.findRoot(itemClass)):
|
||||||
if not doc.deleteDocument():
|
rHandle = self.newRoot(itemClass)
|
||||||
SHARED.error(
|
if rHandle and (tHandle := SHARED.project.newFile(tag.title(), rHandle)):
|
||||||
self.tr("Could not delete document file."),
|
self.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
|
||||||
info=doc.getError()
|
self._tree.refreshItems([tHandle])
|
||||||
)
|
return
|
||||||
return False
|
|
||||||
|
|
||||||
self._index.deleteHandle(tHandle)
|
|
||||||
self._tree.remove(tHandle)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Project Methods
|
# Project Methods
|
||||||
|
|||||||
+47
-90
@@ -32,10 +32,10 @@ import logging
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot
|
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 (
|
from PyQt5.QtWidgets import (
|
||||||
QAbstractItemView, QAction, QFrame, QHBoxLayout, QLabel, QMenu, QShortcut,
|
QAbstractItemView, QAction, QFrame, QHBoxLayout, QLabel, QMenu, QShortcut,
|
||||||
QTreeView, QVBoxLayout, QWidget
|
QStyleOptionViewItem, QTreeView, QVBoxLayout, QWidget
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
@@ -254,12 +254,6 @@ class GuiProjectView(QWidget):
|
|||||||
self.projBar.buildQuickLinksMenu()
|
self.projBar.buildQuickLinksMenu()
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot(str, nwItemClass)
|
|
||||||
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
|
|
||||||
"""Process new not request."""
|
|
||||||
self.projTree.createNewNote(tag, itemClass)
|
|
||||||
return
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def refreshUserLabels(self, kind: str) -> None:
|
def refreshUserLabels(self, kind: str) -> None:
|
||||||
"""Refresh status or importance labels."""
|
"""Refresh status or importance labels."""
|
||||||
@@ -359,7 +353,7 @@ class GuiProjectToolBar(QWidget):
|
|||||||
self.aCollapse.triggered.connect(self.projTree.collapseAll)
|
self.aCollapse.triggered.connect(self.projTree.collapseAll)
|
||||||
|
|
||||||
self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
|
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 = NIconToolButton(self, iSz)
|
||||||
self.tbMore.setToolTip(self.tr("More Options"))
|
self.tbMore.setToolTip(self.tr("More Options"))
|
||||||
@@ -506,7 +500,7 @@ class GuiProjectTree(QTreeView):
|
|||||||
self.projView = projView
|
self.projView = projView
|
||||||
|
|
||||||
# Internal Variables
|
# Internal Variables
|
||||||
# self._actHandle = None
|
self._actHandle = None
|
||||||
|
|
||||||
# Cached Translations
|
# Cached Translations
|
||||||
self.trActive = trConst(nwLabels.ACTIVE_NAME["checked"])
|
self.trActive = trConst(nwLabels.ACTIVE_NAME["checked"])
|
||||||
@@ -566,6 +560,11 @@ class GuiProjectTree(QTreeView):
|
|||||||
# External Methods
|
# External Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def setActiveHandle(self, tHandle: str | None) -> None:
|
||||||
|
"""Set the handle to be highlighted."""
|
||||||
|
self._actHandle = tHandle
|
||||||
|
return
|
||||||
|
|
||||||
def getSelectedHandle(self) -> str | None:
|
def getSelectedHandle(self) -> str | None:
|
||||||
"""Get the currently selected handle."""
|
"""Get the currently selected handle."""
|
||||||
if (indexes := self.selectedIndexes()) and (node := self._getNode(indexes[0])):
|
if (indexes := self.selectedIndexes()) and (node := self._getNode(indexes[0])):
|
||||||
@@ -583,11 +582,7 @@ class GuiProjectTree(QTreeView):
|
|||||||
|
|
||||||
def loadModel(self) -> None:
|
def loadModel(self) -> None:
|
||||||
"""Load and prepare a new project model."""
|
"""Load and prepare a new project model."""
|
||||||
# selModel = self.selectionModel()
|
|
||||||
self.setModel(SHARED.project.tree.model)
|
self.setModel(SHARED.project.tree.model)
|
||||||
# if selModel:
|
|
||||||
# selModel.deleteLater()
|
|
||||||
# del selModel
|
|
||||||
|
|
||||||
# Lock the column sizes
|
# Lock the column sizes
|
||||||
iPx = SHARED.theme.baseIconHeight
|
iPx = SHARED.theme.baseIconHeight
|
||||||
@@ -820,7 +815,7 @@ class GuiProjectTree(QTreeView):
|
|||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Events
|
# Events and Overloads
|
||||||
##
|
##
|
||||||
|
|
||||||
def mousePressEvent(self, event: QMouseEvent) -> None:
|
def mousePressEvent(self, event: QMouseEvent) -> None:
|
||||||
@@ -839,6 +834,13 @@ class GuiProjectTree(QTreeView):
|
|||||||
)
|
)
|
||||||
return
|
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
|
# Public Slots
|
||||||
##
|
##
|
||||||
@@ -913,33 +915,55 @@ class GuiProjectTree(QTreeView):
|
|||||||
def processDeleteRequest(self, handles: list[str] = [], askFirst: bool = True) -> None:
|
def processDeleteRequest(self, handles: list[str] = [], askFirst: bool = True) -> None:
|
||||||
"""Move selected items to Trash."""
|
"""Move selected items to Trash."""
|
||||||
if handles and (model := self._getModel()):
|
if handles and (model := self._getModel()):
|
||||||
items = [model.indexFromHandle(handle) for handle in handles]
|
indices = [model.indexFromHandle(handle) for handle in handles]
|
||||||
else:
|
else:
|
||||||
items = self._selectedRows()
|
indices = self._selectedRows()
|
||||||
|
|
||||||
if items and (model := self._getModel()):
|
if indices and (model := self._getModel()):
|
||||||
if len(items) == 1 and (node := model.node(items[0])) and node.item.isRootType():
|
if len(indices) == 1 and (node := model.node(indices[0])) and node.item.isRootType():
|
||||||
if node.childCount() == 0:
|
if node.childCount() == 0:
|
||||||
SHARED.project.removeItem(node.item.itemHandle)
|
SHARED.project.removeItem(node.item.itemHandle)
|
||||||
else:
|
else:
|
||||||
SHARED.error(self.tr("Root folders can only be deleted when they are empty."))
|
SHARED.error(self.tr("Root folders can only be deleted when they are empty."))
|
||||||
return
|
return
|
||||||
|
|
||||||
if model.trashSelection(items):
|
if model.trashSelection(indices):
|
||||||
if not SHARED.question(self.tr("Permanently delete selected item(s)?")):
|
if not SHARED.question(self.tr("Permanently delete selected item(s)?")):
|
||||||
logger.info("Action cancelled by user")
|
logger.info("Action cancelled by user")
|
||||||
return
|
return
|
||||||
for node in model.nodes(items):
|
for index in indices:
|
||||||
SHARED.project.removeItem(node.item.itemHandle)
|
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:
|
elif trashNode := SHARED.project.tree.trash:
|
||||||
if askFirst and not SHARED.question(self.tr("Move selected item(s) to Trash?")):
|
if askFirst and not SHARED.question(self.tr("Move selected item(s) to Trash?")):
|
||||||
logger.info("Action cancelled by user")
|
logger.info("Action cancelled by user")
|
||||||
return
|
return
|
||||||
model.multiMove(items, model.indexFromNode(trashNode))
|
model.multiMove(indices, model.indexFromNode(trashNode))
|
||||||
|
|
||||||
return
|
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()
|
||||||
@pyqtSlot("QPoint")
|
@pyqtSlot("QPoint")
|
||||||
def openContextMenu(self, point: QPoint | None = None) -> None:
|
def openContextMenu(self, point: QPoint | None = None) -> None:
|
||||||
@@ -1031,60 +1055,6 @@ class GuiProjectTree(QTreeView):
|
|||||||
# Old Code
|
# 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:
|
def refreshUserLabels(self, kind: str) -> None:
|
||||||
"""Refresh status or importance labels."""
|
"""Refresh status or importance labels."""
|
||||||
# if kind == "s":
|
# if kind == "s":
|
||||||
@@ -1136,19 +1106,6 @@ class GuiProjectTree(QTreeView):
|
|||||||
|
|
||||||
return
|
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):
|
class _UpdatableMenu(QMenu):
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ class GuiMain(QMainWindow):
|
|||||||
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
|
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
|
||||||
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
|
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
|
||||||
self.docEditor.openDocumentRequest.connect(self._openDocument)
|
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.requestNextDocument.connect(self.openNextDocument)
|
||||||
self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
|
self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
|
||||||
self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
|
self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from PyQt5.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget
|
|||||||
from novelwriter.common import formatFileFilter
|
from novelwriter.common import formatFileFilter
|
||||||
from novelwriter.constants import nwFiles
|
from novelwriter.constants import nwFiles
|
||||||
from novelwriter.core.spellcheck import NWSpellEnchant
|
from novelwriter.core.spellcheck import NWSpellEnchant
|
||||||
|
from novelwriter.enum import nwItemClass
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
@@ -303,6 +304,12 @@ class SharedData(QObject):
|
|||||||
QDesktopServices.openUrl(QUrl(url))
|
QDesktopServices.openUrl(QUrl(url))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@pyqtSlot(str, nwItemClass)
|
||||||
|
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
|
||||||
|
"""Process new note request."""
|
||||||
|
self.project.createNewNote(tag, itemClass)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Signal Proxy
|
# Signal Proxy
|
||||||
##
|
##
|
||||||
|
|||||||
Reference in New Issue
Block a user