Add inactive tags filter to viewer panel (#1654)
This commit is contained in:
+15
-12
@@ -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
|
||||
|
||||
@@ -70,7 +70,7 @@ VALID_MAP: dict[str, set[str]] = {
|
||||
"winWidth", "winHeight", "fmtWidth", "sumWidth",
|
||||
},
|
||||
"GuiDocViewerPanel": {
|
||||
"colWidths",
|
||||
"colWidths", "hideInactive",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user