From ca4aa75880c946635b62b8140bb91e076f3da8c3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 15 Jan 2024 15:48:45 +0100 Subject: [PATCH 1/4] Rename outdated function parameter in index class --- novelwriter/core/index.py | 27 ++++++++++++++----------- novelwriter/gui/noveltree.py | 2 +- novelwriter/gui/outline.py | 2 +- tests/test_core/test_core_index.py | 32 +++++++++++++++--------------- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 40936c3c..f58145f9 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -517,28 +517,28 @@ class NWIndex: return None def novelStructure( - self, rootHandle: str | None = None, skipExcl: bool = True + self, rootHandle: str | None = None, activeOnly: bool = True ) -> Iterator[tuple[str, str, str, IndexHeading]]: """Iterate over all titles in the novel, in the correct order as they appear in the tree view and in the respective document files, but skipping all note files. """ - structure = self._itemIndex.iterNovelStructure(rHandle=rootHandle, skipExcl=skipExcl) + structure = self._itemIndex.iterNovelStructure(rHandle=rootHandle, activeOnly=activeOnly) for tHandle, sTitle, hItem in structure: yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem return - def getNovelWordCount(self, skipExcl: bool = True) -> int: + def getNovelWordCount(self, activeOnly: bool = True) -> int: """Count the number of words in the novel project.""" wCount = 0 - for _, _, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): + for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly): wCount += hItem.wordCount return wCount - def getNovelTitleCounts(self, skipExcl: bool = True) -> list[int]: + def getNovelTitleCounts(self, activeOnly: bool = True) -> list[int]: """Count the number of titles in the novel project.""" hCount = [0, 0, 0, 0, 0] - for _, _, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): + for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly): iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) hCount[iLevel] += 1 return hCount @@ -551,14 +551,14 @@ class NWIndex: return 0 def getTableOfContents( - self, rHandle: str | None, maxDepth: int, skipExcl: bool = True + self, rHandle: str | None, maxDepth: int, activeOnly: bool = True ) -> list[tuple[str, int, str, int]]: """Generate a table of contents up to a maximum depth.""" tOrder = [] tData = {} pKey = None for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure( - rHandle=rHandle, skipExcl=skipExcl + rHandle=rHandle, activeOnly=activeOnly ): tKey = f"{tHandle}:{sTitle}" iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) @@ -646,12 +646,15 @@ class NWIndex: """Return all tags based on itemClass.""" return self._tagsIndex.filterTagNames(itemClass.name) - def getTagsData(self) -> Iterator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]: + def getTagsData( + self, activeOnly: bool = True + ) -> Iterator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]: """Return all known tags.""" for tag, data in self._tagsIndex.items(): iItem = self._itemIndex[data.get("handle")] hItem = None if iItem is None else iItem[data.get("heading")] - yield tag, data.get("name", ""), data.get("class", ""), iItem, hItem + if not activeOnly or (iItem and iItem.item.isActive): + yield tag, data.get("name", ""), data.get("class", ""), iItem, hItem return def getSingleTag(self, tagKey: str) -> tuple[str, str, IndexItem | None, IndexHeading | None]: @@ -848,7 +851,7 @@ class ItemIndex: return def iterNovelStructure( - self, rHandle: str | None = None, skipExcl: bool = False + self, rHandle: str | None = None, activeOnly: bool = False ) -> Iterable[tuple[str, str, IndexHeading]]: """Iterate over all items and headers in the novel structure for a given root handle, or for all if root handle is None. @@ -856,7 +859,7 @@ class ItemIndex: for tItem in self._project.tree: if tItem.isNoteLayout(): continue - if skipExcl and not tItem.isActive: + if activeOnly and not tItem.isActive: continue tHandle = tItem.itemHandle diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 8398092b..1569262b 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -674,7 +674,7 @@ class GuiNovelTree(QTreeWidget): tStart = time() logger.debug("Building novel tree for root item '%s'", rootHandle) - novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, skipExcl=True) + novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, activeOnly=True) for tKey, tHandle, sTitle, novIdx in novStruct: if novIdx.level == "H0": continue diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index bc1a6497..db066cee 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -646,7 +646,7 @@ class GuiOutlineTree(QTreeWidget): headItem.setTextAlignment( self._colIdx[nwOutline.PCOUNT], Qt.AlignmentFlag.AlignRight) - novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, skipExcl=True) + novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, activeOnly=True) for _, tHandle, sTitle, novIdx in novStruct: iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0) diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 920c4553..5e9dee3f 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -579,7 +579,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): project.tree[nHandle].setActive(False) # type: ignore keys = [] - for aKey, _, _, _ in index.novelStructure(skipExcl=False): + for aKey, _, _, _ in index.novelStructure(activeOnly=False): keys.append(aKey) assert keys == [ @@ -590,7 +590,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): ] keys = [] - for aKey, _, _, _ in index.novelStructure(skipExcl=True): + for aKey, _, _, _ in index.novelStructure(activeOnly=True): keys.append(aKey) assert keys == [ @@ -761,7 +761,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): assert index.scanText(sHandle, "### Scene One\n\n") # type: ignore assert index.scanText(tHandle, "### Scene Two\n\n") # type: ignore - assert [(h, t) for h, t, _ in index._itemIndex.iterNovelStructure(skipExcl=False)] == [ + assert [(h, t) for h, t, _ in index._itemIndex.iterNovelStructure(activeOnly=False)] == [ (C.hTitlePage, "T0001"), (C.hChapterDoc, "T0001"), (C.hSceneDoc, "T0001"), @@ -772,7 +772,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): (tHandle, "T0001"), ] - assert [(h, t) for h, t, _ in index._itemIndex.iterNovelStructure(skipExcl=True)] == [ + assert [(h, t) for h, t, _ in index._itemIndex.iterNovelStructure(activeOnly=True)] == [ (C.hTitlePage, "T0001"), (C.hChapterDoc, "T0001"), (C.hSceneDoc, "T0001"), @@ -783,7 +783,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): # Add a fake handle to the tree and check that it's ignored project.tree._order.append("0000000000000") - assert [(h, t) for h, t, _ in index._itemIndex.iterNovelStructure(skipExcl=False)] == [ + assert [(h, t) for h, t, _ in index._itemIndex.iterNovelStructure(activeOnly=False)] == [ (C.hTitlePage, "T0001"), (C.hChapterDoc, "T0001"), (C.hSceneDoc, "T0001"), @@ -796,22 +796,22 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): project.tree._order.remove("0000000000000") # Extract stats - assert index.getNovelWordCount(skipExcl=False) == 43 - assert index.getNovelWordCount(skipExcl=True) == 15 - assert index.getNovelTitleCounts(skipExcl=False) == [0, 3, 2, 3, 0] - assert index.getNovelTitleCounts(skipExcl=True) == [0, 1, 2, 3, 0] + assert index.getNovelWordCount(activeOnly=False) == 43 + assert index.getNovelWordCount(activeOnly=True) == 15 + assert index.getNovelTitleCounts(activeOnly=False) == [0, 3, 2, 3, 0] + assert index.getNovelTitleCounts(activeOnly=True) == [0, 1, 2, 3, 0] # Table of Contents - assert index.getTableOfContents(C.hNovelRoot, 0, skipExcl=True) == [] - assert index.getTableOfContents(C.hNovelRoot, 1, skipExcl=True) == [ + assert index.getTableOfContents(C.hNovelRoot, 0, activeOnly=True) == [] + assert index.getTableOfContents(C.hNovelRoot, 1, activeOnly=True) == [ (f"{C.hTitlePage}:T0001", 1, "New Novel", 15), ] - assert index.getTableOfContents(C.hNovelRoot, 2, skipExcl=True) == [ + assert index.getTableOfContents(C.hNovelRoot, 2, activeOnly=True) == [ (f"{C.hTitlePage}:T0001", 1, "New Novel", 5), (f"{C.hChapterDoc}:T0001", 2, "New Chapter", 4), (f"{hHandle}:T0001", 2, "Chapter One", 6), ] - assert index.getTableOfContents(C.hNovelRoot, 3, skipExcl=True) == [ + assert index.getTableOfContents(C.hNovelRoot, 3, activeOnly=True) == [ (f"{C.hTitlePage}:T0001", 1, "New Novel", 5), (f"{C.hChapterDoc}:T0001", 2, "New Chapter", 2), (f"{C.hSceneDoc}:T0001", 3, "New Scene", 2), @@ -820,8 +820,8 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): (f"{tHandle}:T0001", 3, "Scene Two", 2), ] - assert index.getTableOfContents(C.hNovelRoot, 0, skipExcl=False) == [] - assert index.getTableOfContents(C.hNovelRoot, 1, skipExcl=False) == [ + assert index.getTableOfContents(C.hNovelRoot, 0, activeOnly=False) == [] + assert index.getTableOfContents(C.hNovelRoot, 1, activeOnly=False) == [ (f"{C.hTitlePage}:T0001", 1, "New Novel", 9), (f"{nHandle}:T0001", 1, "Hello World!", 12), (f"{nHandle}:T0002", 1, "Hello World!", 22), @@ -1173,7 +1173,7 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): # Skip excluded project.tree[sHandle].setActive(False) # type: ignore - nStruct = list(itemIndex.iterNovelStructure(skipExcl=True)) + nStruct = list(itemIndex.iterNovelStructure(activeOnly=True)) assert len(nStruct) == 3 assert nStruct[0][0] == nHandle assert nStruct[1][0] == cHandle From a2a62f8b26baf6856e14de4de694a0fc10dbaf6c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 15 Jan 2024 15:50:19 +0100 Subject: [PATCH 2/4] Allow hiding inactive tags in document viewer panel --- novelwriter/core/options.py | 2 +- novelwriter/gui/docviewerpanel.py | 96 +++++++++++++++++++++++-------- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 5db203f2..8e50c30e 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -70,7 +70,7 @@ VALID_MAP: dict[str, set[str]] = { "winWidth", "winHeight", "fmtWidth", "sumWidth", }, "GuiDocViewerPanel": { - "colWidths", + "colWidths", "hideInactive", } } diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py index 5de091a3..310e81c7 100644 --- a/novelwriter/gui/docviewerpanel.py +++ b/novelwriter/gui/docviewerpanel.py @@ -29,8 +29,8 @@ from enum import Enum from PyQt5.QtCore import QModelIndex, QSize, Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QAbstractItemView, QFrame, QHeaderView, QTabWidget, QTreeWidget, - QTreeWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QFrame, QHeaderView, QMenu, QTabWidget, QToolButton, + QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED @@ -54,10 +54,24 @@ class GuiDocViewerPanel(QWidget): self._lastHandle = None + iPx = int(1.0*SHARED.theme.baseIconSize) + self.tabBackRefs = _ViewPanelBackRefs(self) + self.optsMenu = QMenu(self) + + self.aInactive = self.optsMenu.addAction(self.tr("Hide Inactive")) + self.aInactive.setCheckable(True) + self.aInactive.toggled.connect(self._toggleHideInactive) + + self.optsButton = QToolButton(self) + self.optsButton.setIconSize(QSize(iPx, iPx)) + self.optsButton.setMenu(self.optsMenu) + self.optsButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + self.mainTabs = QTabWidget(self) self.mainTabs.addTab(self.tabBackRefs, self.tr("References")) + self.mainTabs.setCornerWidget(self.optsButton, Qt.Corner.TopLeftCorner) self.kwTabs: dict[str, _ViewPanelKeyWords] = {} self.idTabs: dict[str, int] = {} @@ -85,20 +99,27 @@ class GuiDocViewerPanel(QWidget): def updateTheme(self, updateTabs: bool = True) -> None: """Update theme elements.""" + qPalette = self.palette() + mPx = CONFIG.pxInt(2) vPx = CONFIG.pxInt(4) - lPx = CONFIG.pxInt(2) - rPx = CONFIG.pxInt(14) - hCol = self.palette().highlight().color() + hPx = CONFIG.pxInt(8) + hCol = qPalette.highlight().color() + fCol = qPalette.text().color() + + buttonStyle = ( + "QToolButton {{padding: {0}px; margin: 0 0 {1}px 0; border: none; " + "background: transparent;}} " + "QToolButton:hover {{border: none; background: rgba({2}, {3}, {4}, 0.2);}} " + "QToolButton::menu-indicator {{image: none;}} " + ).format(mPx, mPx, fCol.red(), fCol.green(), fCol.blue()) + self.optsButton.setIcon(SHARED.theme.getIcon("menu")) + self.optsButton.setStyleSheet(buttonStyle) styleSheet = ( - "QTabWidget::pane {border: 0;} " - "QTabWidget QTabBar::tab {" - f"border: 0; padding: {vPx}px {rPx}px {vPx}px {lPx}px;" - "} " - "QTabWidget QTabBar::tab:selected {" - f"color: rgb({hCol.red()}, {hCol.green()}, {hCol.blue()});" - "} " - ) + "QTabWidget::pane {{border: 0;}} " + "QTabWidget QTabBar::tab {{border: 0; padding: {0}px {1}px;}} " + "QTabWidget QTabBar::tab:selected {{color: rgb({2}, {3}, {4});}} " + ).format(vPx, hPx, hCol.red(), hCol.green(), hCol.blue()) self.mainTabs.setStyleSheet(styleSheet) self.updateHandle(self._lastHandle) @@ -111,20 +132,22 @@ class GuiDocViewerPanel(QWidget): def openProjectTasks(self) -> None: """Run open project tasks.""" - widths = SHARED.project.options.getValue("GuiDocViewerPanel", "colWidths", {}) - if isinstance(widths, dict): - for key, value in widths.items(): + colWidths = SHARED.project.options.getValue("GuiDocViewerPanel", "colWidths", {}) + hideInactive = SHARED.project.options.getBool("GuiDocViewerPanel", "hideInactive", False) + self.aInactive.setChecked(hideInactive) + if isinstance(colWidths, dict): + for key, value in colWidths.items(): if key in self.kwTabs and isinstance(value, list): self.kwTabs[key].setColumnWidths(value) return def closeProjectTasks(self) -> None: """Run close project tasks.""" - widths = {} - for key, tab in self.kwTabs.items(): - widths[key] = tab.getColumnWidths() logger.debug("Saving State: GuiDocViewerPanel") - SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", widths) + colWidths = {k: t.getColumnWidths() for k, t in self.kwTabs.items()} + hideInactive = self.aInactive.isChecked() + SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", colWidths) + SHARED.project.options.setValue("GuiDocViewerPanel", "hideInactive", hideInactive) return ## @@ -142,9 +165,7 @@ class GuiDocViewerPanel(QWidget): @pyqtSlot() def indexHasAppeared(self) -> None: """Handle event when the index has appeared.""" - for key, name, tClass, iItem, hItem in SHARED.project.index.getTagsData(): - if tClass in self.kwTabs and iItem and hItem: - self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem) + self._loadAllTags() self._updateTabVisibility() self.updateHandle(self._lastHandle) return @@ -153,10 +174,15 @@ class GuiDocViewerPanel(QWidget): def projectItemChanged(self, tHandle: str) -> None: """Update meta data for project item.""" self.tabBackRefs.refreshDocument(tHandle) + activeOnly = self.aInactive.isChecked() for key in SHARED.project.index.getDocumentTags(tHandle): name, tClass, iItem, hItem = SHARED.project.index.getSingleTag(key) if tClass in self.kwTabs and iItem and hItem: - self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem) + if not activeOnly or (iItem and iItem.item.isActive): + self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem) + else: + self.kwTabs[tClass].removeEntry(key) + self._updateTabVisibility() return @pyqtSlot(str) @@ -182,6 +208,20 @@ class GuiDocViewerPanel(QWidget): self._updateTabVisibility() return + ## + # Private Slots + ## + + @pyqtSlot(bool) + def _toggleHideInactive(self, state: bool) -> None: + """Process toggling of active/inactive visibility.""" + logger.debug("Setting inactive items to %s", "hidden" if state else "visible") + for cTab in self.kwTabs.values(): + cTab.clearContent() + self._loadAllTags() + self._updateTabVisibility() + return + ## # Internal Functions ## @@ -193,6 +233,14 @@ class GuiDocViewerPanel(QWidget): self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0) return + def _loadAllTags(self) -> None: + """Load all tags into the tabs.""" + data = SHARED.project.index.getTagsData(activeOnly=self.aInactive.isChecked()) + for key, name, tClass, iItem, hItem in data: + if tClass in self.kwTabs and iItem and hItem: + self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem) + return + # END Class GuiDocViewerPanel From c15ffb0813e8a1b8439a3d57c6c7d01197673397 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 15 Jan 2024 15:59:46 +0100 Subject: [PATCH 3/4] Update test coverage --- tests/test_gui/test_gui_docviewerpanel.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_gui/test_gui_docviewerpanel.py b/tests/test_gui/test_gui_docviewerpanel.py index 764bc3b6..b194a52f 100644 --- a/tests/test_gui/test_gui_docviewerpanel.py +++ b/tests/test_gui/test_gui_docviewerpanel.py @@ -28,6 +28,7 @@ from PyQt5.QtGui import QIcon from novelwriter import CONFIG, SHARED from novelwriter.constants import nwLists +from novelwriter.core.item import NWItem from novelwriter.dialogs.editlabel import GuiEditLabel @@ -216,6 +217,17 @@ def testGuiViewerPanel_Tags(qtbot, monkeypatch, caplog, nwGUI, projPath, mockRnd charTab._treeItemDoubleClicked(charTab.model().index(0, charTab.C_NAME)) assert nwGUI.docViewer.docHandle == hJane + # Hide Inactive Tags + viewPanel.aInactive.setChecked(True) + assert charTab.topLevelItemCount() == 2 + + nwJohn = SHARED.project.tree[hJohn] + assert isinstance(nwJohn, NWItem) + nwJohn.setActive(False) + projTree.setTreeItemValues(hJohn) + projTree._alertTreeChange(hJohn, flush=False) + assert charTab.topLevelItemCount() == 1 + # qtbot.stop() # END Test testGuiViewerPanel_Tags From 3135bee47dc5354b0a3b7014a870869cdc12423f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 15 Jan 2024 16:02:02 +0100 Subject: [PATCH 4/4] Changes the option text --- novelwriter/gui/docviewerpanel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py index 310e81c7..a5a5695e 100644 --- a/novelwriter/gui/docviewerpanel.py +++ b/novelwriter/gui/docviewerpanel.py @@ -60,7 +60,7 @@ class GuiDocViewerPanel(QWidget): self.optsMenu = QMenu(self) - self.aInactive = self.optsMenu.addAction(self.tr("Hide Inactive")) + self.aInactive = self.optsMenu.addAction(self.tr("Hide Inactive Tags")) self.aInactive.setCheckable(True) self.aInactive.toggled.connect(self._toggleHideInactive)