From aa64fdbfc3fbe71e542ca15abc8c2a45950ed4d7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 29 Jul 2023 20:45:12 +0200 Subject: [PATCH] Annotate and clean up project tree source --- novelwriter/core/index.py | 32 +++--- novelwriter/gui/projtree.py | 224 +++++++++++++++--------------------- 2 files changed, 106 insertions(+), 150 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 5aa6b7fe..4e815ed8 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -76,7 +76,7 @@ class NWIndex: a rebuild of the index data. """ - def __init__(self, project: NWProject): + def __init__(self, project: NWProject) -> None: self._project = project @@ -91,7 +91,7 @@ class NWIndex: return - def __repr__(self): + def __repr__(self) -> str: return f"" ## @@ -99,14 +99,14 @@ class NWIndex: ## @property - def indexBroken(self): + def indexBroken(self) -> bool: return self._indexBroken ## # Public Methods ## - def clearIndex(self): + def clearIndex(self) -> None: """Clear the index dictionaries and time stamps.""" self._tagsIndex.clear() self._itemIndex.clear() @@ -114,7 +114,7 @@ class NWIndex: self._rootChange = {} return - def rebuildIndex(self): + def rebuildIndex(self) -> None: """Rebuild the entire index from scratch.""" self.clearIndex() for nwItem in self._project.tree: @@ -125,22 +125,20 @@ class NWIndex: self._indexBroken = False return - def deleteHandle(self, tHandle: str): + def deleteHandle(self, tHandle: str) -> None: """Delete all entries of a given document handle.""" logger.debug("Removing item '%s' from the index", tHandle) for tTag in self._itemIndex.allItemTags(tHandle): del self._tagsIndex[tTag] - del self._itemIndex[tHandle] - 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 moved from the archive or trash folders back into the active 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 logger.debug("Re-indexing item '%s'", tHandle) @@ -163,9 +161,8 @@ class NWIndex: # Load and Save Index to/from File ## - def loadIndex(self): - """Load index from last session from the project meta folder. - """ + def loadIndex(self) -> bool: + """Load index from last session from the project meta folder.""" indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE) if not isinstance(indexFile, Path): return False @@ -292,7 +289,7 @@ class NWIndex: # 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.""" nTitle = 0 # Line Number of the previous title cTitle = TT_NONE # Tag of the current title @@ -355,7 +352,7 @@ class NWIndex: return - def _scanInactive(self, nwItem: NWItem, text: str): + def _scanInactive(self, nwItem: NWItem, text: str) -> None: """Scan an inactive document for meta data.""" for aLine in text.splitlines(): if aLine.startswith("#"): @@ -387,9 +384,8 @@ class NWIndex: self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC) return - def _indexKeyword( - self, tHandle: str, line: str, sTitle: str, itemClass: nwItemClass, tags: dict - ): + def _indexKeyword(self, tHandle: str, line: str, sTitle: str, + itemClass: nwItemClass, tags: dict) -> None: """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 of active tags is updated so that no longer used tags can be diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index e21f509d..8c6d1f9c 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -32,8 +32,8 @@ from enum import Enum from time import time from typing import TYPE_CHECKING -from PyQt5.QtGui import QPalette -from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot +from PyQt5.QtGui import QDropEvent, QMouseEvent, QPalette +from PyQt5.QtCore import QPoint, Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, @@ -74,7 +74,7 @@ class GuiProjectView(QWidget): # Requests for the main GUI projectSettingsRequest = pyqtSignal(int) - def __init__(self, mainGui: GuiMain): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) self.mainGui = mainGui @@ -132,83 +132,71 @@ class GuiProjectView(QWidget): # Methods ## - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" self.projBar.updateTheme() self.populateTree() return - def initSettings(self): - """Initialise GUI elements that depend on specific settings. - """ + def initSettings(self) -> None: + """Initialise GUI elements that depend on specific settings.""" self.projTree.initSettings() return - def clearProject(self): - """Clear project-related GUI content. - """ + def clearProject(self) -> None: + """Clear project-related GUI content.""" self.projBar.clearContent() self.projBar.setEnabled(False) self.projTree.clearTree() return - def openProjectTasks(self): - """Run open project tasks. - """ + def openProjectTasks(self) -> None: + """Run open project tasks.""" self.projBar.buildQuickLinkMenu() self.projBar.setEnabled(True) return - def saveProjectTasks(self): - """Run save project tasks. - """ + def saveProjectTasks(self) -> None: + """Run save project tasks.""" self.projTree.saveTreeOrder() return - def populateTree(self): - """Build the tree structure from project data. - """ + def populateTree(self) -> None: + """Build the tree structure from project data.""" self.projTree.buildTree() return - def setFocus(self): - """Forward the set focus call to the tree widget. - """ + def setFocus(self) -> None: + """Forward the set focus call to the tree widget.""" self.projTree.setFocus() return - def treeHasFocus(self): - """Check if the project tree has focus. - """ + def treeHasFocus(self) -> bool: + """Check if the project tree has focus.""" 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 item. This is triggered by the global menu or keyboard shortcut. """ if tHandle is None: tHandle = self.projTree.getSelectedHandle() - if tHandle: - return self.projTree.renameTreeItem(tHandle) - return + return self.projTree.renameTreeItem(tHandle) if tHandle else False ## # Public Slots ## @pyqtSlot(str, int, int, int) - def updateCounts(self, tHandle, cCount, wCount, pCount): - """Slot for updating the word count of a specific item. - """ + def updateCounts(self, tHandle: str, cCount: int, wCount: int, pCount: int) -> None: + """Slot for updating the word count of a specific item.""" self.projTree.propagateCount(tHandle, wCount, countChildren=True) self.wordCountsChanged.emit() return @pyqtSlot(str) - def updateRootItem(self, tHandle): - """If any root item changes, rebuild the quick link root menu. - """ + def updateRootItem(self, tHandle: str) -> None: + """If any root item changes, rebuild the quick link menu.""" self.projBar.buildQuickLinkMenu() return @@ -217,7 +205,7 @@ class GuiProjectView(QWidget): class GuiProjectToolBar(QWidget): - def __init__(self, projView): + def __init__(self, projView: GuiProjectView) -> None: super().__init__(parent=projView) logger.debug("Create: GuiProjectToolBar") @@ -237,7 +225,7 @@ class GuiProjectToolBar(QWidget): # Widget Label self.viewLabel = QLabel("%s" % self.tr("Project Content")) 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 self.mQuick = QMenu() @@ -341,9 +329,8 @@ class GuiProjectToolBar(QWidget): # Methods ## - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" qPalette = self.palette() qPalette.setBrush(QPalette.Window, qPalette.base()) self.setPalette(qPalette) @@ -376,17 +363,14 @@ class GuiProjectToolBar(QWidget): return - def clearContent(self): - """Clear dynamic content on the tool bar. - """ + def clearContent(self) -> None: + """Clear dynamic content on the tool bar.""" self.mQuick.clear() return - def buildQuickLinkMenu(self): - """Build the quick link menu. - """ + def buildQuickLinkMenu(self) -> None: + """Build the quick link menu.""" logger.debug("Rebuilding quick links menu") - self.mQuick.clear() for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(None)): aRoot = self.mQuick.addAction(nwItem.itemName) @@ -395,16 +379,14 @@ class GuiProjectToolBar(QWidget): aRoot.triggered.connect( lambda n, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True) ) - return ## # Internal Functions ## - def _buildRootMenu(self): - """Build the rood folder menu. - """ + def _buildRootMenu(self) -> None: + """Build the rood folder menu.""" def addClass(itemClass): aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass])) aNew.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[itemClass])) @@ -431,7 +413,7 @@ class GuiProjectToolBar(QWidget): ## @pyqtSlot(str) - def _treeSelectionChanged(self, tHandle): + def _treeSelectionChanged(self, tHandle: str) -> None: """Toggle the visibility of the new item entries for novel documents. They should only be visible if novel documents can actually be added. @@ -470,7 +452,7 @@ class GuiProjectTree(QTreeWidget): # Internal Variables self._treeMap = {} self._lastMove = {} - self._timeChanged = 0 + self._timeChanged = 0.0 # Build GUI # ========= @@ -553,7 +535,7 @@ class GuiProjectTree(QTreeWidget): self.clear() self._treeMap = {} self._lastMove = {} - self._timeChanged = 0 + self._timeChanged = 0.0 return def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None, @@ -658,11 +640,11 @@ class GuiProjectTree(QTreeWidget): 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: """Reveal a newly added project item in the project tree.""" - nwItem = self.theProject.tree[tHandle] - if not nwItem: + nwItem = self.theProject.tree[tHandle] if tHandle else None + if tHandle is None or nwItem is None: return False trItem = self._addTreeItem(nwItem, nHandle) @@ -1129,17 +1111,15 @@ class GuiProjectTree(QTreeWidget): return def openContextOnSelected(self): - """Open the context menu on the current selected item. - """ + """Open the context menu on the current selected item.""" selItem = self.selectedItems() if selItem: pos = self.visualItemRect(selItem[0]).center() return self._openContextMenu(pos) return False - def changedSince(self, checkTime): - """Check if the tree has changed since a given time. - """ + def changedSince(self, checkTime: float) -> bool: + """Check if the tree has changed since a given time.""" return self._timeChanged > checkTime ## @@ -1147,16 +1127,15 @@ class GuiProjectTree(QTreeWidget): ## @pyqtSlot() - def _treeSelectionChange(self): - """The user changed which item is selected. - """ + def _treeSelectionChange(self) -> None: + """The user changed which item is selected.""" tHandle = self.getSelectedHandle() if tHandle is not None: self.projView.selectedItemChanged.emit(tHandle) return @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 for editing if it is a file, or expand/close the node it is not. """ @@ -1176,7 +1155,7 @@ class GuiProjectTree(QTreeWidget): return @pyqtSlot("QPoint") - def _openContextMenu(self, clickPos): + def _openContextMenu(self, clickPos: QPoint) -> bool: """The user right clicked an element in the project tree, so we open a context menu in-place. """ @@ -1343,20 +1322,20 @@ class GuiProjectTree(QTreeWidget): # Events ## - def mousePressEvent(self, theEvent): + 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(theEvent) + super().mousePressEvent(event) - if theEvent.button() == Qt.LeftButton: - selItem = self.indexAt(theEvent.pos()) + if event.button() == Qt.LeftButton: + selItem = self.indexAt(event.pos()) if not selItem.isValid(): self.clearSelection() - elif theEvent.button() == Qt.MiddleButton: - selItem = self.itemAt(theEvent.pos()) + elif event.button() == Qt.MiddleButton: + selItem = self.itemAt(event.pos()) if not isinstance(selItem, QTreeWidgetItem): return @@ -1370,36 +1349,31 @@ class GuiProjectTree(QTreeWidget): return - def dropEvent(self, theEvent): + def dropEvent(self, event: QDropEvent) -> None: """Overload the drop item event to ensure relevant data has been updated. """ 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") return logger.debug("Drag'n'drop of item '%s' accepted", sHandle) - sItem = self._getTreeItem(sHandle) - isExpanded = False - if sItem is not None: - isExpanded = sItem.isExpanded() - + isExpanded = sItem.isExpanded() pItem = sItem.parent() - pIndex = 0 - if pItem is not None: - pIndex = pItem.indexOfChild(sItem) + pIndex = pItem.indexOfChild(sItem) if pItem else 0 wCount = self._getItemWordCount(sHandle) self.propagateCount(sHandle, 0) - super().dropEvent(theEvent) + super().dropEvent(event) self._postItemMove(sHandle, wCount) self._recordLastMove(sItem, pItem, pIndex) self._alertTreeChange(sHandle, flush=True) - if sItem is not None: - sItem.setExpanded(isExpanded) + + sItem.setExpanded(isExpanded) return @@ -1407,13 +1381,12 @@ class GuiProjectTree(QTreeWidget): # Internal Functions ## - def _postItemMove(self, tHandle, wCount): - """Run various maintenance tasks for a moved item. - """ + def _postItemMove(self, tHandle: str, wCount: int) -> bool: + """Run various maintenance tasks for a moved item.""" trItemS = self._getTreeItem(tHandle) nwItemS = self.theProject.tree[tHandle] - trItemP = trItemS.parent() - if trItemP is None: + 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 False @@ -1443,21 +1416,17 @@ class GuiProjectTree(QTreeWidget): return True - def _getItemWordCount(self, tHandle): - """Retrun the word count of a given item handle. - """ + def _getItemWordCount(self, tHandle: str) -> int: + """Return the word count of a given item handle.""" tItem = self._getTreeItem(tHandle) - if tItem is None: - return 0 - return int(tItem.data(self.C_DATA, self.D_WORDS)) + return int(tItem.data(self.C_DATA, self.D_WORDS)) if tItem else 0 def _getTreeItem(self, tHandle: str | None) -> QTreeWidgetItem | None: """Return the QTreeWidgetItem of a given item handle.""" return self._treeMap.get(tHandle, None) if tHandle else None - def _toggleItemActive(self, tHandle): - """Toggle the active status of an item. - """ + def _toggleItemActive(self, tHandle: str) -> None: + """Toggle the active status of an item.""" tItem = self.theProject.tree[tHandle] if tItem is not None: tItem.setActive(not tItem.isActive) @@ -1465,7 +1434,7 @@ class GuiProjectTree(QTreeWidget): self._alertTreeChange(tHandle, flush=False) return - def _recursiveSetExpanded(self, trItem, isExpanded): + def _recursiveSetExpanded(self, trItem: QTreeWidgetItem, isExpanded: bool) -> None: """Recursive function to set expanded status starting from (and not including) a given item. """ @@ -1477,9 +1446,8 @@ class GuiProjectTree(QTreeWidget): self._recursiveSetExpanded(chItem, isExpanded) return - def _changeItemStatus(self, tHandle, tStatus): - """Set a new status value of an item. - """ + def _changeItemStatus(self, tHandle: str, tStatus: str) -> None: + """Set a new status value of an item.""" tItem = self.theProject.tree[tHandle] if tItem is not None: tItem.setStatus(tStatus) @@ -1487,9 +1455,8 @@ class GuiProjectTree(QTreeWidget): self._alertTreeChange(tHandle, flush=False) return - def _changeItemImport(self, tHandle, tImport): - """Set a new importance value of an item. - """ + def _changeItemImport(self, tHandle: str, tImport: str) -> None: + """Set a new importance value of an item.""" tItem = self.theProject.tree[tHandle] if tItem is not None: tItem.setImport(tImport) @@ -1497,9 +1464,8 @@ class GuiProjectTree(QTreeWidget): self._alertTreeChange(tHandle, flush=False) return - def _changeItemLayout(self, tHandle, itemLayout): - """Set a new item layout value of an item. - """ + def _changeItemLayout(self, tHandle: str, itemLayout: nwItemLayout) -> None: + """Set a new item layout value of an item.""" tItem = self.theProject.tree[tHandle] if tItem is not None: if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed(): @@ -1512,9 +1478,8 @@ class GuiProjectTree(QTreeWidget): self._alertTreeChange(tHandle, flush=False) return - def _covertFolderToFile(self, tHandle, itemLayout): - """Convert a folder to a note or document. - """ + def _covertFolderToFile(self, tHandle: str, itemLayout: nwItemLayout) -> None: + """Convert a folder to a note or document.""" tItem = self.theProject.tree[tHandle] if tItem is not None and tItem.isFolderType(): msgYes = self.mainGui.askQuestion( @@ -1538,7 +1503,7 @@ class GuiProjectTree(QTreeWidget): logger.info("Folder conversion cancelled") return - def _mergeDocuments(self, tHandle, newFile): + 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) itemList = self.getTreeFromHandle(tHandle) @@ -1613,7 +1578,7 @@ class GuiProjectTree(QTreeWidget): return True - def _splitDocument(self, tHandle): + def _splitDocument(self, tHandle: str) -> bool: """Split a document into multiple documents.""" logger.info("Request to split items with handle '%s'", tHandle) @@ -1621,8 +1586,8 @@ class GuiProjectTree(QTreeWidget): if tItem is None: return False - if not tItem.isFileType(): - logger.error("Only documents can be split") + if not tItem.isFileType() or tItem.itemParent is None: + logger.error("Only valid document items can be split") return False dlgSplit = GuiDocSplit(self.mainGui, tHandle) @@ -1715,9 +1680,8 @@ class GuiProjectTree(QTreeWidget): return itemList - def _addTreeItem( - self, nwItem: NWItem | None, nHandle: str | None = None - ) -> QTreeWidgetItem | None: + 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. @@ -1768,7 +1732,7 @@ class GuiProjectTree(QTreeWidget): return newItem - def _addTrashRoot(self): + def _addTrashRoot(self) -> QTreeWidgetItem | None: """Adds the trash root folder if it doesn't already exist in the project tree. """ @@ -1785,7 +1749,7 @@ class GuiProjectTree(QTreeWidget): 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 signals. A flush is only needed if an item is moved, created or deleted. @@ -1795,10 +1759,7 @@ class GuiProjectTree(QTreeWidget): if flush: self.saveTreeOrder() - if tHandle is None: - return - - if tHandle not in self.theProject.tree: + if tHandle is None or tHandle not in self.theProject.tree: return tItem = self.theProject.tree[tHandle] @@ -1809,9 +1770,9 @@ class GuiProjectTree(QTreeWidget): return - def _recordLastMove(self, srcItem, parItem, parIndex): - """Record the last action so that it can be undone. - """ + def _recordLastMove(self, srcItem: QTreeWidgetItem, + parItem: QTreeWidgetItem, parIndex: int) -> None: + """Record the last action so that it can be undone.""" prevItem = self._lastMove.get("item", None) if prevItem is None or srcItem != prevItem: self._lastMove = { @@ -1819,7 +1780,6 @@ class GuiProjectTree(QTreeWidget): "parent": parItem, "index": parIndex, } - return # END Class GuiProjectTree