Add project tree navigation shortcuts (#1488)
This commit is contained in:
@@ -92,6 +92,10 @@ The main shorcuts are as follows:
|
|||||||
":kbd:`Shift`:kbd:`F1`", "Open the local user manual (PDF) if it is available."
|
":kbd:`Shift`:kbd:`F1`", "Open the local user manual (PDF) if it is available."
|
||||||
":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document."
|
":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document."
|
||||||
":kbd:`Shift`:kbd:`F6`", "Open the :guilabel:`Project Details` dialog."
|
":kbd:`Shift`:kbd:`F6`", "Open the :guilabel:`Project Details` dialog."
|
||||||
|
":kbd:`Shift`:kbd:`Up`", "Go to the previous item at same level in the project tree."
|
||||||
|
":kbd:`Shift`:kbd:`Down`", "Go to the next item at same level in the project tree."
|
||||||
|
":kbd:`Shift`:kbd:`Left`", "Go to the parent item in the project tree."
|
||||||
|
":kbd:`Shift`:kbd:`Right`", "Go to the first child item in the project tree."
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
On macOS, replace :kbd:`Ctrl` with :kbd:`Cmd`.
|
On macOS, replace :kbd:`Ctrl` with :kbd:`Cmd`.
|
||||||
|
|||||||
+14
-18
@@ -76,7 +76,7 @@ class NWIndex:
|
|||||||
a rebuild of the index data.
|
a rebuild of the index data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, project: NWProject):
|
def __init__(self, project: NWProject) -> None:
|
||||||
|
|
||||||
self._project = project
|
self._project = project
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ class NWIndex:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"<NWIndex project='{self._project.data.name}'>"
|
return f"<NWIndex project='{self._project.data.name}'>"
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -99,14 +99,14 @@ class NWIndex:
|
|||||||
##
|
##
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def indexBroken(self):
|
def indexBroken(self) -> bool:
|
||||||
return self._indexBroken
|
return self._indexBroken
|
||||||
|
|
||||||
##
|
##
|
||||||
# Public Methods
|
# Public Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
def clearIndex(self):
|
def clearIndex(self) -> None:
|
||||||
"""Clear the index dictionaries and time stamps."""
|
"""Clear the index dictionaries and time stamps."""
|
||||||
self._tagsIndex.clear()
|
self._tagsIndex.clear()
|
||||||
self._itemIndex.clear()
|
self._itemIndex.clear()
|
||||||
@@ -114,7 +114,7 @@ class NWIndex:
|
|||||||
self._rootChange = {}
|
self._rootChange = {}
|
||||||
return
|
return
|
||||||
|
|
||||||
def rebuildIndex(self):
|
def rebuildIndex(self) -> None:
|
||||||
"""Rebuild the entire index from scratch."""
|
"""Rebuild the entire index from scratch."""
|
||||||
self.clearIndex()
|
self.clearIndex()
|
||||||
for nwItem in self._project.tree:
|
for nwItem in self._project.tree:
|
||||||
@@ -125,22 +125,20 @@ class NWIndex:
|
|||||||
self._indexBroken = False
|
self._indexBroken = False
|
||||||
return
|
return
|
||||||
|
|
||||||
def deleteHandle(self, tHandle: str):
|
def deleteHandle(self, tHandle: str) -> None:
|
||||||
"""Delete all entries of a given document handle."""
|
"""Delete all entries of a given document handle."""
|
||||||
logger.debug("Removing item '%s' from the index", tHandle)
|
logger.debug("Removing item '%s' from the index", tHandle)
|
||||||
for tTag in self._itemIndex.allItemTags(tHandle):
|
for tTag in self._itemIndex.allItemTags(tHandle):
|
||||||
del self._tagsIndex[tTag]
|
del self._tagsIndex[tTag]
|
||||||
|
|
||||||
del self._itemIndex[tHandle]
|
del self._itemIndex[tHandle]
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def reIndexHandle(self, tHandle: str) -> bool:
|
def reIndexHandle(self, tHandle: str | None) -> bool:
|
||||||
"""Put a file back into the index. This is used when files are
|
"""Put a file back into the index. This is used when files are
|
||||||
moved from the archive or trash folders back into the active
|
moved from the archive or trash folders back into the active
|
||||||
project.
|
project.
|
||||||
"""
|
"""
|
||||||
if not self._project.tree.checkType(tHandle, nwItemType.FILE):
|
if tHandle is None or not self._project.tree.checkType(tHandle, nwItemType.FILE):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.debug("Re-indexing item '%s'", tHandle)
|
logger.debug("Re-indexing item '%s'", tHandle)
|
||||||
@@ -163,9 +161,8 @@ class NWIndex:
|
|||||||
# Load and Save Index to/from File
|
# Load and Save Index to/from File
|
||||||
##
|
##
|
||||||
|
|
||||||
def loadIndex(self):
|
def loadIndex(self) -> bool:
|
||||||
"""Load index from last session from the project meta folder.
|
"""Load index from last session from the project meta folder."""
|
||||||
"""
|
|
||||||
indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
|
indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
|
||||||
if not isinstance(indexFile, Path):
|
if not isinstance(indexFile, Path):
|
||||||
return False
|
return False
|
||||||
@@ -292,7 +289,7 @@ class NWIndex:
|
|||||||
# Internal Indexer Helpers
|
# Internal Indexer Helpers
|
||||||
##
|
##
|
||||||
|
|
||||||
def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict):
|
def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict) -> None:
|
||||||
"""Scan an active document for meta data."""
|
"""Scan an active document for meta data."""
|
||||||
nTitle = 0 # Line Number of the previous title
|
nTitle = 0 # Line Number of the previous title
|
||||||
cTitle = TT_NONE # Tag of the current title
|
cTitle = TT_NONE # Tag of the current title
|
||||||
@@ -355,7 +352,7 @@ class NWIndex:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _scanInactive(self, nwItem: NWItem, text: str):
|
def _scanInactive(self, nwItem: NWItem, text: str) -> None:
|
||||||
"""Scan an inactive document for meta data."""
|
"""Scan an inactive document for meta data."""
|
||||||
for aLine in text.splitlines():
|
for aLine in text.splitlines():
|
||||||
if aLine.startswith("#"):
|
if aLine.startswith("#"):
|
||||||
@@ -387,9 +384,8 @@ class NWIndex:
|
|||||||
self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC)
|
self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _indexKeyword(
|
def _indexKeyword(self, tHandle: str, line: str, sTitle: str,
|
||||||
self, tHandle: str, line: str, sTitle: str, itemClass: nwItemClass, tags: dict
|
itemClass: nwItemClass, tags: dict) -> None:
|
||||||
):
|
|
||||||
"""Validate and save the information about a reference to a tag
|
"""Validate and save the information about a reference to a tag
|
||||||
in another file, or the setting of a tag in the file. A record
|
in another file, or the setting of a tag in the file. A record
|
||||||
of active tags is updated so that no longer used tags can be
|
of active tags is updated so that no longer used tags can be
|
||||||
|
|||||||
+161
-159
@@ -32,8 +32,8 @@ from enum import Enum
|
|||||||
from time import time
|
from time import time
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt5.QtGui import QPalette
|
from PyQt5.QtGui import QDropEvent, QMouseEvent, QPalette
|
||||||
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import QPoint, Qt, QSize, pyqtSignal, pyqtSlot
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
|
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
|
||||||
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
|
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
|
||||||
@@ -41,7 +41,7 @@ from PyQt5.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG
|
||||||
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwWidget
|
from novelwriter.common import minmax
|
||||||
from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels
|
from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
|
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
|
||||||
@@ -49,6 +49,9 @@ from novelwriter.dialogs.docmerge import GuiDocMerge
|
|||||||
from novelwriter.dialogs.docsplit import GuiDocSplit
|
from novelwriter.dialogs.docsplit import GuiDocSplit
|
||||||
from novelwriter.dialogs.editlabel import GuiEditLabel
|
from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||||
from novelwriter.dialogs.projsettings import GuiProjectSettings
|
from novelwriter.dialogs.projsettings import GuiProjectSettings
|
||||||
|
from novelwriter.enum import (
|
||||||
|
nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwWidget
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from novelwriter.guimain import GuiMain
|
from novelwriter.guimain import GuiMain
|
||||||
@@ -74,7 +77,7 @@ class GuiProjectView(QWidget):
|
|||||||
# Requests for the main GUI
|
# Requests for the main GUI
|
||||||
projectSettingsRequest = pyqtSignal(int)
|
projectSettingsRequest = pyqtSignal(int)
|
||||||
|
|
||||||
def __init__(self, mainGui: GuiMain):
|
def __init__(self, mainGui: GuiMain) -> None:
|
||||||
super().__init__(parent=mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
self.mainGui = mainGui
|
self.mainGui = mainGui
|
||||||
@@ -104,6 +107,26 @@ class GuiProjectView(QWidget):
|
|||||||
self.keyMoveDn.setContext(Qt.WidgetShortcut)
|
self.keyMoveDn.setContext(Qt.WidgetShortcut)
|
||||||
self.keyMoveDn.activated.connect(lambda: self.projTree.moveTreeItem(1))
|
self.keyMoveDn.activated.connect(lambda: self.projTree.moveTreeItem(1))
|
||||||
|
|
||||||
|
self.keyGoPrev = QShortcut(self.projTree)
|
||||||
|
self.keyGoPrev.setKey("Shift+Up")
|
||||||
|
self.keyGoPrev.setContext(Qt.WidgetShortcut)
|
||||||
|
self.keyGoPrev.activated.connect(lambda: self.projTree.moveToNextItem(-1))
|
||||||
|
|
||||||
|
self.keyGoNext = QShortcut(self.projTree)
|
||||||
|
self.keyGoNext.setKey("Shift+Down")
|
||||||
|
self.keyGoNext.setContext(Qt.WidgetShortcut)
|
||||||
|
self.keyGoNext.activated.connect(lambda: self.projTree.moveToNextItem(1))
|
||||||
|
|
||||||
|
self.keyGoUp = QShortcut(self.projTree)
|
||||||
|
self.keyGoUp.setKey("Shift+Left")
|
||||||
|
self.keyGoUp.setContext(Qt.WidgetShortcut)
|
||||||
|
self.keyGoUp.activated.connect(lambda: self.projTree.moveToLevel(-1))
|
||||||
|
|
||||||
|
self.keyGoDown = QShortcut(self.projTree)
|
||||||
|
self.keyGoDown.setKey("Shift+Right")
|
||||||
|
self.keyGoDown.setContext(Qt.WidgetShortcut)
|
||||||
|
self.keyGoDown.activated.connect(lambda: self.projTree.moveToLevel(1))
|
||||||
|
|
||||||
self.keyUndoMv = QShortcut(self.projTree)
|
self.keyUndoMv = QShortcut(self.projTree)
|
||||||
self.keyUndoMv.setKey("Ctrl+Shift+Z")
|
self.keyUndoMv.setKey("Ctrl+Shift+Z")
|
||||||
self.keyUndoMv.setContext(Qt.WidgetShortcut)
|
self.keyUndoMv.setContext(Qt.WidgetShortcut)
|
||||||
@@ -132,83 +155,71 @@ class GuiProjectView(QWidget):
|
|||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
def updateTheme(self):
|
def updateTheme(self) -> None:
|
||||||
"""Update theme elements.
|
"""Update theme elements."""
|
||||||
"""
|
|
||||||
self.projBar.updateTheme()
|
self.projBar.updateTheme()
|
||||||
self.populateTree()
|
self.populateTree()
|
||||||
return
|
return
|
||||||
|
|
||||||
def initSettings(self):
|
def initSettings(self) -> None:
|
||||||
"""Initialise GUI elements that depend on specific settings.
|
"""Initialise GUI elements that depend on specific settings."""
|
||||||
"""
|
|
||||||
self.projTree.initSettings()
|
self.projTree.initSettings()
|
||||||
return
|
return
|
||||||
|
|
||||||
def clearProject(self):
|
def clearProject(self) -> None:
|
||||||
"""Clear project-related GUI content.
|
"""Clear project-related GUI content."""
|
||||||
"""
|
|
||||||
self.projBar.clearContent()
|
self.projBar.clearContent()
|
||||||
self.projBar.setEnabled(False)
|
self.projBar.setEnabled(False)
|
||||||
self.projTree.clearTree()
|
self.projTree.clearTree()
|
||||||
return
|
return
|
||||||
|
|
||||||
def openProjectTasks(self):
|
def openProjectTasks(self) -> None:
|
||||||
"""Run open project tasks.
|
"""Run open project tasks."""
|
||||||
"""
|
|
||||||
self.projBar.buildQuickLinkMenu()
|
self.projBar.buildQuickLinkMenu()
|
||||||
self.projBar.setEnabled(True)
|
self.projBar.setEnabled(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
def saveProjectTasks(self):
|
def saveProjectTasks(self) -> None:
|
||||||
"""Run save project tasks.
|
"""Run save project tasks."""
|
||||||
"""
|
|
||||||
self.projTree.saveTreeOrder()
|
self.projTree.saveTreeOrder()
|
||||||
return
|
return
|
||||||
|
|
||||||
def populateTree(self):
|
def populateTree(self) -> None:
|
||||||
"""Build the tree structure from project data.
|
"""Build the tree structure from project data."""
|
||||||
"""
|
|
||||||
self.projTree.buildTree()
|
self.projTree.buildTree()
|
||||||
return
|
return
|
||||||
|
|
||||||
def setFocus(self):
|
def setFocus(self) -> None:
|
||||||
"""Forward the set focus call to the tree widget.
|
"""Forward the set focus call to the tree widget."""
|
||||||
"""
|
|
||||||
self.projTree.setFocus()
|
self.projTree.setFocus()
|
||||||
return
|
return
|
||||||
|
|
||||||
def treeHasFocus(self):
|
def treeHasFocus(self) -> bool:
|
||||||
"""Check if the project tree has focus.
|
"""Check if the project tree has focus."""
|
||||||
"""
|
|
||||||
return self.projTree.hasFocus()
|
return self.projTree.hasFocus()
|
||||||
|
|
||||||
def renameTreeItem(self, tHandle=None):
|
def renameTreeItem(self, tHandle: str | None = None) -> bool:
|
||||||
"""External request to rename an item or the currently selected
|
"""External request to rename an item or the currently selected
|
||||||
item. This is triggered by the global menu or keyboard shortcut.
|
item. This is triggered by the global menu or keyboard shortcut.
|
||||||
"""
|
"""
|
||||||
if tHandle is None:
|
if tHandle is None:
|
||||||
tHandle = self.projTree.getSelectedHandle()
|
tHandle = self.projTree.getSelectedHandle()
|
||||||
if tHandle:
|
return self.projTree.renameTreeItem(tHandle) if tHandle else False
|
||||||
return self.projTree.renameTreeItem(tHandle)
|
|
||||||
return
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Public Slots
|
# Public Slots
|
||||||
##
|
##
|
||||||
|
|
||||||
@pyqtSlot(str, int, int, int)
|
@pyqtSlot(str, int, int, int)
|
||||||
def updateCounts(self, tHandle, cCount, wCount, pCount):
|
def updateCounts(self, tHandle: str, cCount: int, wCount: int, pCount: int) -> None:
|
||||||
"""Slot for updating the word count of a specific item.
|
"""Slot for updating the word count of a specific item."""
|
||||||
"""
|
|
||||||
self.projTree.propagateCount(tHandle, wCount, countChildren=True)
|
self.projTree.propagateCount(tHandle, wCount, countChildren=True)
|
||||||
self.wordCountsChanged.emit()
|
self.wordCountsChanged.emit()
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def updateRootItem(self, tHandle):
|
def updateRootItem(self, tHandle: str) -> None:
|
||||||
"""If any root item changes, rebuild the quick link root menu.
|
"""If any root item changes, rebuild the quick link menu."""
|
||||||
"""
|
|
||||||
self.projBar.buildQuickLinkMenu()
|
self.projBar.buildQuickLinkMenu()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -217,7 +228,7 @@ class GuiProjectView(QWidget):
|
|||||||
|
|
||||||
class GuiProjectToolBar(QWidget):
|
class GuiProjectToolBar(QWidget):
|
||||||
|
|
||||||
def __init__(self, projView):
|
def __init__(self, projView: GuiProjectView) -> None:
|
||||||
super().__init__(parent=projView)
|
super().__init__(parent=projView)
|
||||||
|
|
||||||
logger.debug("Create: GuiProjectToolBar")
|
logger.debug("Create: GuiProjectToolBar")
|
||||||
@@ -237,7 +248,7 @@ class GuiProjectToolBar(QWidget):
|
|||||||
# Widget Label
|
# Widget Label
|
||||||
self.viewLabel = QLabel("<b>%s</b>" % self.tr("Project Content"))
|
self.viewLabel = QLabel("<b>%s</b>" % self.tr("Project Content"))
|
||||||
self.viewLabel.setContentsMargins(0, 0, 0, 0)
|
self.viewLabel.setContentsMargins(0, 0, 0, 0)
|
||||||
self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self.viewLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||||
|
|
||||||
# Quick Links
|
# Quick Links
|
||||||
self.mQuick = QMenu()
|
self.mQuick = QMenu()
|
||||||
@@ -341,9 +352,8 @@ class GuiProjectToolBar(QWidget):
|
|||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
def updateTheme(self):
|
def updateTheme(self) -> None:
|
||||||
"""Update theme elements.
|
"""Update theme elements."""
|
||||||
"""
|
|
||||||
qPalette = self.palette()
|
qPalette = self.palette()
|
||||||
qPalette.setBrush(QPalette.Window, qPalette.base())
|
qPalette.setBrush(QPalette.Window, qPalette.base())
|
||||||
self.setPalette(qPalette)
|
self.setPalette(qPalette)
|
||||||
@@ -376,17 +386,14 @@ class GuiProjectToolBar(QWidget):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def clearContent(self):
|
def clearContent(self) -> None:
|
||||||
"""Clear dynamic content on the tool bar.
|
"""Clear dynamic content on the tool bar."""
|
||||||
"""
|
|
||||||
self.mQuick.clear()
|
self.mQuick.clear()
|
||||||
return
|
return
|
||||||
|
|
||||||
def buildQuickLinkMenu(self):
|
def buildQuickLinkMenu(self) -> None:
|
||||||
"""Build the quick link menu.
|
"""Build the quick link menu."""
|
||||||
"""
|
|
||||||
logger.debug("Rebuilding quick links menu")
|
logger.debug("Rebuilding quick links menu")
|
||||||
|
|
||||||
self.mQuick.clear()
|
self.mQuick.clear()
|
||||||
for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(None)):
|
for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(None)):
|
||||||
aRoot = self.mQuick.addAction(nwItem.itemName)
|
aRoot = self.mQuick.addAction(nwItem.itemName)
|
||||||
@@ -395,16 +402,14 @@ class GuiProjectToolBar(QWidget):
|
|||||||
aRoot.triggered.connect(
|
aRoot.triggered.connect(
|
||||||
lambda n, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True)
|
lambda n, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _buildRootMenu(self):
|
def _buildRootMenu(self) -> None:
|
||||||
"""Build the rood folder menu.
|
"""Build the rood folder menu."""
|
||||||
"""
|
|
||||||
def addClass(itemClass):
|
def addClass(itemClass):
|
||||||
aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass]))
|
aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass]))
|
||||||
aNew.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[itemClass]))
|
aNew.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[itemClass]))
|
||||||
@@ -431,7 +436,7 @@ class GuiProjectToolBar(QWidget):
|
|||||||
##
|
##
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def _treeSelectionChanged(self, tHandle):
|
def _treeSelectionChanged(self, tHandle: str) -> None:
|
||||||
"""Toggle the visibility of the new item entries for novel
|
"""Toggle the visibility of the new item entries for novel
|
||||||
documents. They should only be visible if novel documents can
|
documents. They should only be visible if novel documents can
|
||||||
actually be added.
|
actually be added.
|
||||||
@@ -470,7 +475,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
# Internal Variables
|
# Internal Variables
|
||||||
self._treeMap = {}
|
self._treeMap = {}
|
||||||
self._lastMove = {}
|
self._lastMove = {}
|
||||||
self._timeChanged = 0
|
self._timeChanged = 0.0
|
||||||
|
|
||||||
# Build GUI
|
# Build GUI
|
||||||
# =========
|
# =========
|
||||||
@@ -553,7 +558,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self.clear()
|
self.clear()
|
||||||
self._treeMap = {}
|
self._treeMap = {}
|
||||||
self._lastMove = {}
|
self._lastMove = {}
|
||||||
self._timeChanged = 0
|
self._timeChanged = 0.0
|
||||||
return
|
return
|
||||||
|
|
||||||
def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
|
def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
|
||||||
@@ -658,11 +663,11 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def revealNewTreeItem(self, tHandle: str, nHandle: str | None = None,
|
def revealNewTreeItem(self, tHandle: str | None, nHandle: str | None = None,
|
||||||
wordCount: bool = False) -> bool:
|
wordCount: bool = False) -> bool:
|
||||||
"""Reveal a newly added project item in the project tree."""
|
"""Reveal a newly added project item in the project tree."""
|
||||||
nwItem = self.theProject.tree[tHandle]
|
nwItem = self.theProject.tree[tHandle] if tHandle else None
|
||||||
if not nwItem:
|
if tHandle is None or nwItem is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
trItem = self._addTreeItem(nwItem, nHandle)
|
trItem = self._addTreeItem(nwItem, nHandle)
|
||||||
@@ -683,21 +688,21 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def moveTreeItem(self, nStep: int) -> bool:
|
def moveTreeItem(self, step: int) -> bool:
|
||||||
"""Move an item up or down in the tree."""
|
"""Move an item up or down in the tree."""
|
||||||
tHandle = self.getSelectedHandle()
|
tHandle = self.getSelectedHandle()
|
||||||
trItem = self._getTreeItem(tHandle)
|
tItem = self._getTreeItem(tHandle)
|
||||||
if trItem is None:
|
if tItem is None:
|
||||||
logger.debug("No item selected")
|
logger.debug("No item selected")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
pItem = trItem.parent()
|
pItem = tItem.parent()
|
||||||
isExp = trItem.isExpanded()
|
isExp = tItem.isExpanded()
|
||||||
if pItem is None:
|
if pItem is None:
|
||||||
tIndex = self.indexOfTopLevelItem(trItem)
|
tIndex = self.indexOfTopLevelItem(tItem)
|
||||||
nChild = self.topLevelItemCount()
|
nChild = self.topLevelItemCount()
|
||||||
|
|
||||||
nIndex = tIndex + nStep
|
nIndex = tIndex + step
|
||||||
if nIndex < 0 or nIndex >= nChild:
|
if nIndex < 0 or nIndex >= nChild:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -705,10 +710,10 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self.insertTopLevelItem(nIndex, cItem)
|
self.insertTopLevelItem(nIndex, cItem)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
tIndex = pItem.indexOfChild(trItem)
|
tIndex = pItem.indexOfChild(tItem)
|
||||||
nChild = pItem.childCount()
|
nChild = pItem.childCount()
|
||||||
|
|
||||||
nIndex = tIndex + nStep
|
nIndex = tIndex + step
|
||||||
if nIndex < 0 or nIndex >= nChild:
|
if nIndex < 0 or nIndex >= nChild:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -717,11 +722,32 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self._recordLastMove(cItem, pItem, tIndex)
|
self._recordLastMove(cItem, pItem, tIndex)
|
||||||
|
|
||||||
self._alertTreeChange(tHandle, flush=True)
|
self._alertTreeChange(tHandle, flush=True)
|
||||||
self.setCurrentItem(trItem)
|
self.setCurrentItem(tItem)
|
||||||
trItem.setExpanded(isExp)
|
tItem.setExpanded(isExp)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def moveToNextItem(self, step: int) -> None:
|
||||||
|
"""Move to the next item of the same tree level."""
|
||||||
|
tHandle = self.getSelectedHandle()
|
||||||
|
tItem = self._getTreeItem(tHandle) if tHandle else None
|
||||||
|
if tItem:
|
||||||
|
pItem = tItem.parent() or self.invisibleRootItem()
|
||||||
|
next = minmax(pItem.indexOfChild(tItem) + step, 0, pItem.childCount() - 1)
|
||||||
|
self.setCurrentItem(pItem.child(next))
|
||||||
|
return
|
||||||
|
|
||||||
|
def moveToLevel(self, step: int) -> None:
|
||||||
|
"""Move to the next item in the parent/child chain."""
|
||||||
|
tHandle = self.getSelectedHandle()
|
||||||
|
tItem = self._getTreeItem(tHandle) if tHandle else None
|
||||||
|
if tItem:
|
||||||
|
if step < 0 and tItem.parent():
|
||||||
|
self.setCurrentItem(tItem.parent())
|
||||||
|
elif step > 0 and tItem.childCount() > 0:
|
||||||
|
self.setCurrentItem(tItem.child(0))
|
||||||
|
return
|
||||||
|
|
||||||
def renameTreeItem(self, tHandle: str) -> bool:
|
def renameTreeItem(self, tHandle: str) -> bool:
|
||||||
"""Open a dialog to edit the label of an item."""
|
"""Open a dialog to edit the label of an item."""
|
||||||
tItem = self.theProject.tree[tHandle]
|
tItem = self.theProject.tree[tHandle]
|
||||||
@@ -791,13 +817,13 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if self.theProject.tree.isTrash(tHandle) or nwItem.isRootType():
|
if self.theProject.tree.isTrash(tHandle) or nwItem.isRootType():
|
||||||
status = self.permanentlyDeleteItem(tHandle)
|
status = self.permDeleteItem(tHandle)
|
||||||
else:
|
else:
|
||||||
status = self.moveItemToTrash(tHandle)
|
status = self.moveItemToTrash(tHandle)
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def emptyTrash(self):
|
def emptyTrash(self) -> bool:
|
||||||
"""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
|
||||||
deleteItem function for each document in the Trash folder.
|
deleteItem function for each document in the Trash folder.
|
||||||
@@ -838,14 +864,14 @@ 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.permanentlyDeleteItem(tHandle, askFirst=False, flush=False)
|
self.permDeleteItem(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 moveItemToTrash(self, tHandle, askFirst=True, flush=True):
|
def moveItemToTrash(self, tHandle: str, askFirst: bool = True, flush: bool = True) -> bool:
|
||||||
"""Move an item to Trash. Root folders cannot be moved to Trash,
|
"""Move an item to Trash. Root folders cannot be moved to Trash,
|
||||||
so such a request is cancelled.
|
so such a request is cancelled.
|
||||||
"""
|
"""
|
||||||
@@ -896,7 +922,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def permanentlyDeleteItem(self, tHandle, askFirst=True, flush=True):
|
def permDeleteItem(self, tHandle: str, askFirst: bool = True, flush: bool = True) -> bool:
|
||||||
"""Permanently delete a tree item from the project and the map.
|
"""Permanently delete a tree item from the project and the map.
|
||||||
Root items are handled a little different than other items.
|
Root items are handled a little different than other items.
|
||||||
"""
|
"""
|
||||||
@@ -960,7 +986,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def setTreeItemValues(self, tHandle):
|
def setTreeItemValues(self, tHandle: str) -> None:
|
||||||
"""Set the name and flag values for a tree item from a handle in
|
"""Set the name and flag values for a tree item from a handle in
|
||||||
the project tree. Does not trigger a tree change as the data is
|
the project tree. Does not trigger a tree change as the data is
|
||||||
already coming from the project tree.
|
already coming from the project tree.
|
||||||
@@ -1053,7 +1079,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
logger.info("%d item(s) added to the project tree", count)
|
logger.info("%d item(s) added to the project tree", count)
|
||||||
return
|
return
|
||||||
|
|
||||||
def undoLastMove(self):
|
def undoLastMove(self) -> bool:
|
||||||
"""Attempt to undo the last action."""
|
"""Attempt to undo the last action."""
|
||||||
srcItem = self._lastMove.get("item", None)
|
srcItem = self._lastMove.get("item", None)
|
||||||
dstItem = self._lastMove.get("parent", None)
|
dstItem = self._lastMove.get("parent", None)
|
||||||
@@ -1093,19 +1119,17 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def getSelectedHandle(self):
|
def getSelectedHandle(self) -> str | None:
|
||||||
"""Get the currently selected handle. If multiple items are
|
"""Get the currently selected handle. If multiple items are
|
||||||
selected, return the first.
|
selected, return the first.
|
||||||
"""
|
"""
|
||||||
selItem = self.selectedItems()
|
selItem = self.selectedItems()
|
||||||
if selItem:
|
if selItem:
|
||||||
return selItem[0].data(self.C_DATA, self.D_HANDLE)
|
return selItem[0].data(self.C_DATA, self.D_HANDLE)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def setSelectedHandle(self, tHandle, doScroll=False):
|
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
|
||||||
"""Set a specific handle as the selected item.
|
"""Set a specific handle as the selected item."""
|
||||||
"""
|
|
||||||
tItem = self._getTreeItem(tHandle)
|
tItem = self._getTreeItem(tHandle)
|
||||||
if tItem is None:
|
if tItem is None:
|
||||||
return False
|
return False
|
||||||
@@ -1119,7 +1143,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def setExpandedFromHandle(self, tHandle, isExpanded):
|
def setExpandedFromHandle(self, tHandle: str | None, isExpanded: bool) -> None:
|
||||||
"""Iterate through items below tHandle and change expanded
|
"""Iterate through items below tHandle and change expanded
|
||||||
status for all child items. If tHandle is None, it affects the
|
status for all child items. If tHandle is None, it affects the
|
||||||
entire tree.
|
entire tree.
|
||||||
@@ -1128,18 +1152,16 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self._recursiveSetExpanded(trItem, isExpanded)
|
self._recursiveSetExpanded(trItem, isExpanded)
|
||||||
return
|
return
|
||||||
|
|
||||||
def openContextOnSelected(self):
|
def openContextOnSelected(self) -> bool:
|
||||||
"""Open the context menu on the current selected item.
|
"""Open the context menu on the current selected item."""
|
||||||
"""
|
|
||||||
selItem = self.selectedItems()
|
selItem = self.selectedItems()
|
||||||
if selItem:
|
if selItem:
|
||||||
pos = self.visualItemRect(selItem[0]).center()
|
pos = self.visualItemRect(selItem[0]).center()
|
||||||
return self._openContextMenu(pos)
|
return self._openContextMenu(pos)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def changedSince(self, checkTime):
|
def changedSince(self, checkTime: float) -> bool:
|
||||||
"""Check if the tree has changed since a given time.
|
"""Check if the tree has changed since a given time."""
|
||||||
"""
|
|
||||||
return self._timeChanged > checkTime
|
return self._timeChanged > checkTime
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -1147,16 +1169,15 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
##
|
##
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _treeSelectionChange(self):
|
def _treeSelectionChange(self) -> None:
|
||||||
"""The user changed which item is selected.
|
"""The user changed which item is selected."""
|
||||||
"""
|
|
||||||
tHandle = self.getSelectedHandle()
|
tHandle = self.getSelectedHandle()
|
||||||
if tHandle is not None:
|
if tHandle is not None:
|
||||||
self.projView.selectedItemChanged.emit(tHandle)
|
self.projView.selectedItemChanged.emit(tHandle)
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot("QTreeWidgetItem*", int)
|
@pyqtSlot("QTreeWidgetItem*", int)
|
||||||
def _treeDoubleClick(self, trItem, colNo):
|
def _treeDoubleClick(self, trItem: QTreeWidgetItem, colNo: int) -> None:
|
||||||
"""Capture a double-click event and either request the document
|
"""Capture a double-click event and either request the document
|
||||||
for editing if it is a file, or expand/close the node it is not.
|
for editing if it is a file, or expand/close the node it is not.
|
||||||
"""
|
"""
|
||||||
@@ -1176,7 +1197,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot("QPoint")
|
@pyqtSlot("QPoint")
|
||||||
def _openContextMenu(self, clickPos):
|
def _openContextMenu(self, clickPos: QPoint) -> bool:
|
||||||
"""The user right clicked an element in the project tree, so we
|
"""The user right clicked an element in the project tree, so we
|
||||||
open a context menu in-place.
|
open a context menu in-place.
|
||||||
"""
|
"""
|
||||||
@@ -1329,7 +1350,7 @@ 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):
|
||||||
aDelete = ctxMenu.addAction(self.tr("Delete Permanently"))
|
aDelete = ctxMenu.addAction(self.tr("Delete Permanently"))
|
||||||
aDelete.triggered.connect(lambda: self.permanentlyDeleteItem(tHandle))
|
aDelete.triggered.connect(lambda: self.permDeleteItem(tHandle))
|
||||||
else:
|
else:
|
||||||
aMoveTrash = ctxMenu.addAction(self.tr("Move to Trash"))
|
aMoveTrash = ctxMenu.addAction(self.tr("Move to Trash"))
|
||||||
aMoveTrash.triggered.connect(lambda: self.moveItemToTrash(tHandle))
|
aMoveTrash.triggered.connect(lambda: self.moveItemToTrash(tHandle))
|
||||||
@@ -1343,20 +1364,20 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
# Events
|
# Events
|
||||||
##
|
##
|
||||||
|
|
||||||
def mousePressEvent(self, theEvent):
|
def mousePressEvent(self, event: QMouseEvent) -> None:
|
||||||
"""Overload mousePressEvent to clear selection if clicking the
|
"""Overload mousePressEvent to clear selection if clicking the
|
||||||
mouse in a blank area of the tree view, and to load a document
|
mouse in a blank area of the tree view, and to load a document
|
||||||
for viewing if the user middle-clicked.
|
for viewing if the user middle-clicked.
|
||||||
"""
|
"""
|
||||||
super().mousePressEvent(theEvent)
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
if theEvent.button() == Qt.LeftButton:
|
if event.button() == Qt.LeftButton:
|
||||||
selItem = self.indexAt(theEvent.pos())
|
selItem = self.indexAt(event.pos())
|
||||||
if not selItem.isValid():
|
if not selItem.isValid():
|
||||||
self.clearSelection()
|
self.clearSelection()
|
||||||
|
|
||||||
elif theEvent.button() == Qt.MiddleButton:
|
elif event.button() == Qt.MiddleButton:
|
||||||
selItem = self.itemAt(theEvent.pos())
|
selItem = self.itemAt(event.pos())
|
||||||
if not isinstance(selItem, QTreeWidgetItem):
|
if not isinstance(selItem, QTreeWidgetItem):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1370,36 +1391,31 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def dropEvent(self, theEvent):
|
def dropEvent(self, event: QDropEvent) -> None:
|
||||||
"""Overload the drop item event to ensure relevant data has been
|
"""Overload the drop item event to ensure relevant data has been
|
||||||
updated.
|
updated.
|
||||||
"""
|
"""
|
||||||
sHandle = self.getSelectedHandle()
|
sHandle = self.getSelectedHandle()
|
||||||
if sHandle is None:
|
sItem = self._getTreeItem(sHandle) if sHandle else None
|
||||||
|
if sHandle is None or sItem is None:
|
||||||
logger.error("Invalid drag and drop event")
|
logger.error("Invalid drag and drop event")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
|
logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
|
||||||
|
|
||||||
sItem = self._getTreeItem(sHandle)
|
isExpanded = sItem.isExpanded()
|
||||||
isExpanded = False
|
|
||||||
if sItem is not None:
|
|
||||||
isExpanded = sItem.isExpanded()
|
|
||||||
|
|
||||||
pItem = sItem.parent()
|
pItem = sItem.parent()
|
||||||
pIndex = 0
|
pIndex = pItem.indexOfChild(sItem) if pItem else 0
|
||||||
if pItem is not None:
|
|
||||||
pIndex = pItem.indexOfChild(sItem)
|
|
||||||
|
|
||||||
wCount = self._getItemWordCount(sHandle)
|
wCount = self._getItemWordCount(sHandle)
|
||||||
self.propagateCount(sHandle, 0)
|
self.propagateCount(sHandle, 0)
|
||||||
|
|
||||||
super().dropEvent(theEvent)
|
super().dropEvent(event)
|
||||||
self._postItemMove(sHandle, wCount)
|
self._postItemMove(sHandle, wCount)
|
||||||
self._recordLastMove(sItem, pItem, pIndex)
|
self._recordLastMove(sItem, pItem, pIndex)
|
||||||
self._alertTreeChange(sHandle, flush=True)
|
self._alertTreeChange(sHandle, flush=True)
|
||||||
if sItem is not None:
|
|
||||||
sItem.setExpanded(isExpanded)
|
sItem.setExpanded(isExpanded)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1407,13 +1423,12 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _postItemMove(self, tHandle, wCount):
|
def _postItemMove(self, tHandle: str, wCount: int) -> bool:
|
||||||
"""Run various maintenance tasks for a moved item.
|
"""Run various maintenance tasks for a moved item."""
|
||||||
"""
|
|
||||||
trItemS = self._getTreeItem(tHandle)
|
trItemS = self._getTreeItem(tHandle)
|
||||||
nwItemS = self.theProject.tree[tHandle]
|
nwItemS = self.theProject.tree[tHandle]
|
||||||
trItemP = trItemS.parent()
|
trItemP = trItemS.parent() if trItemS else None
|
||||||
if trItemP is None:
|
if trItemP is None or nwItemS is None:
|
||||||
logger.error("Failed to find new parent item of '%s'", tHandle)
|
logger.error("Failed to find new parent item of '%s'", tHandle)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -1443,21 +1458,17 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _getItemWordCount(self, tHandle):
|
def _getItemWordCount(self, tHandle: str) -> int:
|
||||||
"""Retrun the word count of a given item handle.
|
"""Return the word count of a given item handle."""
|
||||||
"""
|
|
||||||
tItem = self._getTreeItem(tHandle)
|
tItem = self._getTreeItem(tHandle)
|
||||||
if tItem is None:
|
return int(tItem.data(self.C_DATA, self.D_WORDS)) if tItem else 0
|
||||||
return 0
|
|
||||||
return int(tItem.data(self.C_DATA, self.D_WORDS))
|
|
||||||
|
|
||||||
def _getTreeItem(self, tHandle: str | None) -> QTreeWidgetItem | None:
|
def _getTreeItem(self, tHandle: str | None) -> QTreeWidgetItem | None:
|
||||||
"""Return the QTreeWidgetItem of a given item handle."""
|
"""Return the QTreeWidgetItem of a given item handle."""
|
||||||
return self._treeMap.get(tHandle, None) if tHandle else None
|
return self._treeMap.get(tHandle, None) if tHandle else None
|
||||||
|
|
||||||
def _toggleItemActive(self, tHandle):
|
def _toggleItemActive(self, tHandle: str) -> None:
|
||||||
"""Toggle the active status of an item.
|
"""Toggle the active status of an item."""
|
||||||
"""
|
|
||||||
tItem = self.theProject.tree[tHandle]
|
tItem = self.theProject.tree[tHandle]
|
||||||
if tItem is not None:
|
if tItem is not None:
|
||||||
tItem.setActive(not tItem.isActive)
|
tItem.setActive(not tItem.isActive)
|
||||||
@@ -1465,7 +1476,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self._alertTreeChange(tHandle, flush=False)
|
self._alertTreeChange(tHandle, flush=False)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _recursiveSetExpanded(self, trItem, isExpanded):
|
def _recursiveSetExpanded(self, trItem: QTreeWidgetItem, isExpanded: bool) -> None:
|
||||||
"""Recursive function to set expanded status starting from (and
|
"""Recursive function to set expanded status starting from (and
|
||||||
not including) a given item.
|
not including) a given item.
|
||||||
"""
|
"""
|
||||||
@@ -1477,9 +1488,8 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self._recursiveSetExpanded(chItem, isExpanded)
|
self._recursiveSetExpanded(chItem, isExpanded)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _changeItemStatus(self, tHandle, tStatus):
|
def _changeItemStatus(self, tHandle: str, tStatus: str) -> None:
|
||||||
"""Set a new status value of an item.
|
"""Set a new status value of an item."""
|
||||||
"""
|
|
||||||
tItem = self.theProject.tree[tHandle]
|
tItem = self.theProject.tree[tHandle]
|
||||||
if tItem is not None:
|
if tItem is not None:
|
||||||
tItem.setStatus(tStatus)
|
tItem.setStatus(tStatus)
|
||||||
@@ -1487,9 +1497,8 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self._alertTreeChange(tHandle, flush=False)
|
self._alertTreeChange(tHandle, flush=False)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _changeItemImport(self, tHandle, tImport):
|
def _changeItemImport(self, tHandle: str, tImport: str) -> None:
|
||||||
"""Set a new importance value of an item.
|
"""Set a new importance value of an item."""
|
||||||
"""
|
|
||||||
tItem = self.theProject.tree[tHandle]
|
tItem = self.theProject.tree[tHandle]
|
||||||
if tItem is not None:
|
if tItem is not None:
|
||||||
tItem.setImport(tImport)
|
tItem.setImport(tImport)
|
||||||
@@ -1497,9 +1506,8 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self._alertTreeChange(tHandle, flush=False)
|
self._alertTreeChange(tHandle, flush=False)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _changeItemLayout(self, tHandle, itemLayout):
|
def _changeItemLayout(self, tHandle: str, itemLayout: nwItemLayout) -> None:
|
||||||
"""Set a new item layout value of an item.
|
"""Set a new item layout value of an item."""
|
||||||
"""
|
|
||||||
tItem = self.theProject.tree[tHandle]
|
tItem = self.theProject.tree[tHandle]
|
||||||
if tItem is not None:
|
if tItem is not None:
|
||||||
if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
|
if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
|
||||||
@@ -1512,9 +1520,8 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self._alertTreeChange(tHandle, flush=False)
|
self._alertTreeChange(tHandle, flush=False)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _covertFolderToFile(self, tHandle, itemLayout):
|
def _covertFolderToFile(self, tHandle: str, itemLayout: nwItemLayout) -> None:
|
||||||
"""Convert a folder to a note or document.
|
"""Convert a folder to a note or document."""
|
||||||
"""
|
|
||||||
tItem = self.theProject.tree[tHandle]
|
tItem = self.theProject.tree[tHandle]
|
||||||
if tItem is not None and tItem.isFolderType():
|
if tItem is not None and tItem.isFolderType():
|
||||||
msgYes = self.mainGui.askQuestion(
|
msgYes = self.mainGui.askQuestion(
|
||||||
@@ -1538,7 +1545,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
logger.info("Folder conversion cancelled")
|
logger.info("Folder conversion cancelled")
|
||||||
return
|
return
|
||||||
|
|
||||||
def _mergeDocuments(self, tHandle, newFile):
|
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)
|
||||||
itemList = self.getTreeFromHandle(tHandle)
|
itemList = self.getTreeFromHandle(tHandle)
|
||||||
@@ -1613,7 +1620,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _splitDocument(self, tHandle):
|
def _splitDocument(self, tHandle: str) -> bool:
|
||||||
"""Split a document into multiple documents."""
|
"""Split a document into multiple documents."""
|
||||||
logger.info("Request to split items with handle '%s'", tHandle)
|
logger.info("Request to split items with handle '%s'", tHandle)
|
||||||
|
|
||||||
@@ -1621,8 +1628,8 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
if tItem is None:
|
if tItem is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not tItem.isFileType():
|
if not tItem.isFileType() or tItem.itemParent is None:
|
||||||
logger.error("Only documents can be split")
|
logger.error("Only valid document items can be split")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
dlgSplit = GuiDocSplit(self.mainGui, tHandle)
|
dlgSplit = GuiDocSplit(self.mainGui, tHandle)
|
||||||
@@ -1715,9 +1722,8 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return itemList
|
return itemList
|
||||||
|
|
||||||
def _addTreeItem(
|
def _addTreeItem(self, nwItem: NWItem | None,
|
||||||
self, nwItem: NWItem | None, nHandle: str | None = None
|
nHandle: str | None = None) -> QTreeWidgetItem | None:
|
||||||
) -> QTreeWidgetItem | None:
|
|
||||||
"""Create a QTreeWidgetItem from an NWItem and add it to the
|
"""Create a QTreeWidgetItem from an NWItem and add it to the
|
||||||
project tree. Returns the widget if the item is valid, otherwise
|
project tree. Returns the widget if the item is valid, otherwise
|
||||||
a None is returned.
|
a None is returned.
|
||||||
@@ -1768,7 +1774,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return newItem
|
return newItem
|
||||||
|
|
||||||
def _addTrashRoot(self):
|
def _addTrashRoot(self) -> QTreeWidgetItem | None:
|
||||||
"""Adds the trash root folder if it doesn't already exist in the
|
"""Adds the trash root folder if it doesn't already exist in the
|
||||||
project tree.
|
project tree.
|
||||||
"""
|
"""
|
||||||
@@ -1785,7 +1791,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return trItem
|
return trItem
|
||||||
|
|
||||||
def _alertTreeChange(self, tHandle, flush=False):
|
def _alertTreeChange(self, tHandle: str | None, flush: bool = False) -> None:
|
||||||
"""Update information on tree change state, and emit necessary
|
"""Update information on tree change state, and emit necessary
|
||||||
signals. A flush is only needed if an item is moved, created or
|
signals. A flush is only needed if an item is moved, created or
|
||||||
deleted.
|
deleted.
|
||||||
@@ -1795,10 +1801,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
if flush:
|
if flush:
|
||||||
self.saveTreeOrder()
|
self.saveTreeOrder()
|
||||||
|
|
||||||
if tHandle is None:
|
if tHandle is None or tHandle not in self.theProject.tree:
|
||||||
return
|
|
||||||
|
|
||||||
if tHandle not in self.theProject.tree:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
tItem = self.theProject.tree[tHandle]
|
tItem = self.theProject.tree[tHandle]
|
||||||
@@ -1809,9 +1812,9 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _recordLastMove(self, srcItem, parItem, parIndex):
|
def _recordLastMove(self, srcItem: QTreeWidgetItem,
|
||||||
"""Record the last action so that it can be undone.
|
parItem: QTreeWidgetItem, parIndex: int) -> None:
|
||||||
"""
|
"""Record the last action so that it can be undone."""
|
||||||
prevItem = self._lastMove.get("item", None)
|
prevItem = self._lastMove.get("item", None)
|
||||||
if prevItem is None or srcItem != prevItem:
|
if prevItem is None or srcItem != prevItem:
|
||||||
self._lastMove = {
|
self._lastMove = {
|
||||||
@@ -1819,7 +1822,6 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
"parent": parItem,
|
"parent": parItem,
|
||||||
"index": parIndex,
|
"index": parIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# END Class GuiProjectTree
|
# END Class GuiProjectTree
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidgetItem, QDialog
|
|||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG
|
||||||
from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass
|
from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass
|
||||||
from novelwriter.guimain import GuiMain
|
from novelwriter.guimain import GuiMain
|
||||||
from novelwriter.gui.projtree import GuiProjectTree
|
from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView
|
||||||
from novelwriter.dialogs.docmerge import GuiDocMerge
|
from novelwriter.dialogs.docmerge import GuiDocMerge
|
||||||
from novelwriter.dialogs.docsplit import GuiDocSplit
|
from novelwriter.dialogs.docsplit import GuiDocSplit
|
||||||
from novelwriter.dialogs.editlabel import GuiEditLabel
|
from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||||
@@ -427,35 +428,35 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, pro
|
|||||||
|
|
||||||
# Invalid item
|
# Invalid item
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
assert projTree.permanentlyDeleteItem(C.hInvalid) is False
|
assert projTree.permDeleteItem(C.hInvalid) is False
|
||||||
assert "Could not find tree item for deletion" in caplog.text
|
assert "Could not find tree item for deletion" in caplog.text
|
||||||
|
|
||||||
# Not deleting root item in use
|
# Not deleting root item in use
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
assert projTree.permanentlyDeleteItem(C.hNovelRoot) is False
|
assert projTree.permDeleteItem(C.hNovelRoot) is False
|
||||||
assert "Root folders can only be deleted when they are empty" in caplog.text
|
assert "Root folders can only be deleted when they are empty" in caplog.text
|
||||||
assert C.hNovelRoot in theProject.tree
|
assert C.hNovelRoot in theProject.tree
|
||||||
|
|
||||||
# Deleting unused root item is allowed
|
# Deleting unused root item is allowed
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
assert projTree.permanentlyDeleteItem(C.hPlotRoot) is True
|
assert projTree.permDeleteItem(C.hPlotRoot) is True
|
||||||
assert C.hPlotRoot not in theProject.tree
|
assert C.hPlotRoot not in theProject.tree
|
||||||
|
|
||||||
# User cancels action
|
# User cancels action
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
|
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
|
||||||
assert projTree.permanentlyDeleteItem(C.hTitlePage) is False
|
assert projTree.permDeleteItem(C.hTitlePage) is False
|
||||||
assert C.hTitlePage in theProject.tree
|
assert C.hTitlePage in theProject.tree
|
||||||
|
|
||||||
# Deleting file is OK, and if it is open, it should close
|
# Deleting file is OK, and if it is open, it should close
|
||||||
assert nwGUI.openDocument(C.hTitlePage) is True
|
assert nwGUI.openDocument(C.hTitlePage) is True
|
||||||
assert nwGUI.docEditor.docHandle() == C.hTitlePage
|
assert nwGUI.docEditor.docHandle() == C.hTitlePage
|
||||||
assert projTree.permanentlyDeleteItem(C.hTitlePage) is True
|
assert projTree.permDeleteItem(C.hTitlePage) is True
|
||||||
assert C.hTitlePage not in theProject.tree
|
assert C.hTitlePage not in theProject.tree
|
||||||
assert nwGUI.docEditor.docHandle() is None
|
assert nwGUI.docEditor.docHandle() is None
|
||||||
|
|
||||||
# Deleting folder + files recursively is ok
|
# Deleting folder + files recursively is ok
|
||||||
assert projTree.permanentlyDeleteItem(C.hChapterDir) is True
|
assert projTree.permDeleteItem(C.hChapterDir) is True
|
||||||
assert C.hChapterDir not in theProject.tree
|
assert C.hChapterDir not in theProject.tree
|
||||||
assert C.hChapterDoc not in theProject.tree
|
assert C.hChapterDoc not in theProject.tree
|
||||||
assert C.hSceneDoc not in theProject.tree
|
assert C.hSceneDoc not in theProject.tree
|
||||||
@@ -904,15 +905,15 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mock
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd):
|
||||||
"""Test various parts of the project tree class not covered by
|
"""Test various parts of the project tree class not covered by
|
||||||
other tests.
|
other tests.
|
||||||
"""
|
"""
|
||||||
# Create a project
|
# Create a project
|
||||||
buildTestProject(nwGUI, projPath)
|
buildTestProject(nwGUI, projPath)
|
||||||
|
|
||||||
projView = nwGUI.projView
|
projView: GuiProjectView = nwGUI.projView
|
||||||
projTree = nwGUI.projView.projTree
|
projTree: GuiProjectTree = nwGUI.projView.projTree
|
||||||
|
|
||||||
# Method: initSettings
|
# Method: initSettings
|
||||||
# ====================
|
# ====================
|
||||||
@@ -938,12 +939,12 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
|||||||
|
|
||||||
# Try to add an orphaned file to the tree
|
# Try to add an orphaned file to the tree
|
||||||
nHandle = nwGUI.theProject.newFile("Test", C.hNovelRoot)
|
nHandle = nwGUI.theProject.newFile("Test", C.hNovelRoot)
|
||||||
nwGUI.theProject.tree[nHandle].setParent(None)
|
nwGUI.theProject.tree[nHandle].setParent(None) # type: ignore
|
||||||
assert projTree.revealNewTreeItem(nHandle) is False
|
assert projTree.revealNewTreeItem(nHandle) is False
|
||||||
|
|
||||||
# Try to add an item with unknown parent to the tree
|
# Try to add an item with unknown parent to the tree
|
||||||
nHandle = nwGUI.theProject.newFile("Test", C.hNovelRoot)
|
nHandle = nwGUI.theProject.newFile("Test", C.hNovelRoot)
|
||||||
nwGUI.theProject.tree[nHandle].setParent(C.hInvalid)
|
nwGUI.theProject.tree[nHandle].setParent(C.hInvalid) # type: ignore
|
||||||
assert projTree.revealNewTreeItem(nHandle) is False
|
assert projTree.revealNewTreeItem(nHandle) is False
|
||||||
|
|
||||||
# Method: undoLastMove
|
# Method: undoLastMove
|
||||||
@@ -971,7 +972,7 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
|||||||
assert nwGUI.docEditor.docHandle() is None
|
assert nwGUI.docEditor.docHandle() is None
|
||||||
|
|
||||||
# When the item cannot be found
|
# When the item cannot be found
|
||||||
projTree._getTreeItem(C.hTitlePage).setSelected(True)
|
projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None)
|
mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None)
|
||||||
projTree._treeDoubleClick(QTreeWidgetItem(), 0)
|
projTree._treeDoubleClick(QTreeWidgetItem(), 0)
|
||||||
@@ -980,14 +981,67 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
|||||||
# Successfully open a file
|
# Successfully open a file
|
||||||
projTree._treeDoubleClick(projTree._getTreeItem(C.hTitlePage), 0)
|
projTree._treeDoubleClick(projTree._getTreeItem(C.hTitlePage), 0)
|
||||||
assert nwGUI.docEditor.docHandle() == C.hTitlePage
|
assert nwGUI.docEditor.docHandle() == C.hTitlePage
|
||||||
projTree._getTreeItem(C.hTitlePage).setSelected(False)
|
projTree._getTreeItem(C.hTitlePage).setSelected(False) # type: ignore
|
||||||
|
|
||||||
# A non-file item should be expanded instead
|
# A non-file item should be expanded instead
|
||||||
projTree._getTreeItem(C.hNovelRoot).setExpanded(False)
|
projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # type: ignore
|
||||||
projTree._getTreeItem(C.hNovelRoot).setSelected(True)
|
projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore
|
||||||
projTree._treeDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1)
|
projTree._treeDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1)
|
||||||
assert nwGUI.docEditor.docHandle() == C.hTitlePage
|
assert nwGUI.docEditor.docHandle() == C.hTitlePage
|
||||||
assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True
|
assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True # type: ignore
|
||||||
|
|
||||||
|
# Navigate the Tree
|
||||||
|
# =================
|
||||||
|
|
||||||
|
# Expand handles
|
||||||
|
projTree.setExpandedFromHandle(C.hNovelRoot, True)
|
||||||
|
projTree.setSelectedHandle(C.hSceneDoc)
|
||||||
|
assert projTree.getSelectedHandle() == C.hSceneDoc
|
||||||
|
|
||||||
|
# Move between documents in that folder
|
||||||
|
projTree.moveToNextItem(-1)
|
||||||
|
assert projTree.getSelectedHandle() == C.hChapterDoc
|
||||||
|
projTree.moveToNextItem(-1) # Can't move further up
|
||||||
|
assert projTree.getSelectedHandle() == C.hChapterDoc
|
||||||
|
projTree.moveToNextItem(1)
|
||||||
|
assert projTree.getSelectedHandle() == C.hSceneDoc
|
||||||
|
projTree.moveToNextItem(1) # Can't move further down
|
||||||
|
assert projTree.getSelectedHandle() == C.hSceneDoc
|
||||||
|
|
||||||
|
# Move up/down the parent/child hierarchy
|
||||||
|
projTree.moveToLevel(-1)
|
||||||
|
assert projTree.getSelectedHandle() == C.hChapterDir
|
||||||
|
projTree.moveToLevel(-1)
|
||||||
|
assert projTree.getSelectedHandle() == C.hNovelRoot
|
||||||
|
projTree.moveToLevel(-1) # Can't move further up
|
||||||
|
assert projTree.getSelectedHandle() == C.hNovelRoot
|
||||||
|
projTree.moveToLevel(1)
|
||||||
|
assert projTree.getSelectedHandle() == C.hTitlePage
|
||||||
|
projTree.moveToLevel(1) # Can't move further down
|
||||||
|
assert projTree.getSelectedHandle() == C.hTitlePage
|
||||||
|
|
||||||
|
# Move between roots
|
||||||
|
projTree.setSelectedHandle(C.hNovelRoot)
|
||||||
|
projTree.moveToNextItem(1)
|
||||||
|
assert projTree.getSelectedHandle() == C.hPlotRoot
|
||||||
|
projTree.moveToNextItem(1)
|
||||||
|
assert projTree.getSelectedHandle() == C.hCharRoot
|
||||||
|
projTree.moveToNextItem(1)
|
||||||
|
assert projTree.getSelectedHandle() == C.hWorldRoot
|
||||||
|
projTree.moveToNextItem(1) # Can't move further down
|
||||||
|
assert projTree.getSelectedHandle() == C.hWorldRoot
|
||||||
|
|
||||||
|
# When nothing is selected, nothing happens
|
||||||
|
projTree.clearSelection()
|
||||||
|
assert projTree.getSelectedHandle() is None
|
||||||
|
projTree.moveToNextItem(-1)
|
||||||
|
assert projTree.getSelectedHandle() is None
|
||||||
|
projTree.moveToNextItem(1)
|
||||||
|
assert projTree.getSelectedHandle() is None
|
||||||
|
projTree.moveToLevel(-1)
|
||||||
|
assert projTree.getSelectedHandle() is None
|
||||||
|
projTree.moveToLevel(1)
|
||||||
|
assert projTree.getSelectedHandle() is None
|
||||||
|
|
||||||
# qtbot.stop()
|
# qtbot.stop()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user