Add inactive tags filter to viewer panel (#1654)

This commit is contained in:
Veronica Berglyd Olsen
2024-01-15 16:05:58 +01:00
committed by GitHub
7 changed files with 118 additions and 55 deletions
+15 -12
View File
@@ -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
+1 -1
View File
@@ -70,7 +70,7 @@ VALID_MAP: dict[str, set[str]] = {
"winWidth", "winHeight", "fmtWidth", "sumWidth",
},
"GuiDocViewerPanel": {
"colWidths",
"colWidths", "hideInactive",
}
}
+72 -24
View File
@@ -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 Tags"))
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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+16 -16
View File
@@ -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
+12
View File
@@ -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