Skip to content

Commit

Permalink
Add inactive tags filter to viewer panel (#1654)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo authored Jan 15, 2024
2 parents 427a910 + 3135bee commit ae7c19f
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 55 deletions.
27 changes: 15 additions & 12 deletions novelwriter/core/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,28 +517,28 @@ def getItemHeader(self, tHandle: str, sTitle: str) -> IndexHeading | None:
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
Expand All @@ -551,14 +551,14 @@ def getHandleHeaderCount(self, tHandle: str) -> int:
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)
Expand Down Expand Up @@ -646,12 +646,15 @@ def getClassTags(self, itemClass: nwItemClass) -> list[str]:
"""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]:
Expand Down Expand Up @@ -848,15 +851,15 @@ def iterAllHeaders(self) -> Iterable[tuple[str, str, IndexHeading]]:
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.
"""
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
Expand Down
2 changes: 1 addition & 1 deletion novelwriter/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"winWidth", "winHeight", "fmtWidth", "sumWidth",
},
"GuiDocViewerPanel": {
"colWidths",
"colWidths", "hideInactive",
}
}

Expand Down
96 changes: 72 additions & 24 deletions novelwriter/gui/docviewerpanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@

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
Expand All @@ -54,10 +54,24 @@ def __init__(self, parent: QWidget) -> None:

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] = {}
Expand Down Expand Up @@ -85,20 +99,27 @@ def __init__(self, parent: QWidget) -> None:

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)

Expand All @@ -111,20 +132,22 @@ def updateTheme(self, updateTabs: bool = True) -> None:

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

##
Expand All @@ -142,9 +165,7 @@ def indexWasCleared(self) -> None:
@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
Expand All @@ -153,10 +174,15 @@ def indexHasAppeared(self) -> None:
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)
Expand All @@ -182,6 +208,20 @@ def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
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
##
Expand All @@ -193,6 +233,14 @@ def _updateTabVisibility(self) -> None:
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


Expand Down
2 changes: 1 addition & 1 deletion novelwriter/gui/noveltree.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ def _populateTree(self, rootHandle: str | None) -> None:
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
Expand Down
2 changes: 1 addition & 1 deletion novelwriter/gui/outline.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ def _populateTree(self, rootHandle: str | None) -> None:
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)
Expand Down
32 changes: 16 additions & 16 deletions tests/test_core/test_core_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 == [
Expand All @@ -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 == [
Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit ae7c19f

Please sign in to comment.