diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 0b7ae8ca..14012193 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -36,7 +36,7 @@ from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator from pathlib import Path from novelwriter import SHARED -from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout, nwTrinary +from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.error import logException from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders @@ -255,7 +255,7 @@ class NWIndex: return False # Keep a record of existing tags, and create a new item entry - itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), nwTrinary.NEGATIVE) + itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), False) self._itemIndex.add(tHandle, tItem) # Run word counter for the whole text @@ -290,8 +290,7 @@ class NWIndex: # Internal Indexer Helpers ## - def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, - tags: dict[str, nwTrinary]) -> None: + def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict[str, bool]) -> None: """Scan an active document for meta data.""" nTitle = 0 # Line Number of the previous title cTitle = TT_NONE # Tag of the current title @@ -347,20 +346,18 @@ class NWIndex: self._indexWordCounts(tHandle, text, cTitle) # Prune no longer used tags - for tTag, tStatus in tags.items(): - added = [] + for tTag, isActive in tags.items(): + updated = [] deleted = [] - if tStatus == nwTrinary.NEGATIVE: + if isActive: + logger.debug("Added/updated tag '%s'", tTag) + updated.append(tTag) + else: logger.debug("Removed tag '%s'", tTag) del self._tagsIndex[tTag] deleted.append(tTag) - elif tStatus == nwTrinary.POSITIVE: - logger.debug("Added new tag '%s'", tTag) - added.append(tTag) - else: - logger.debug("Unchanged tag '%s'", tTag) - if added or deleted: - SHARED.indexUpdatedTags(added, deleted) + if updated or deleted: + SHARED.indexUpdatedTags(updated, deleted) return @@ -397,7 +394,7 @@ class NWIndex: return def _indexKeyword(self, tHandle: str, line: str, sTitle: str, - itemClass: nwItemClass, tags: dict[str, nwTrinary]) -> None: + itemClass: nwItemClass, tags: dict[str, bool]) -> 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 @@ -414,10 +411,9 @@ class NWIndex: if tBits[0] == nwKeyWords.TAG_KEY: tagName = tBits[1] - tagKey = tagName.lower() self._tagsIndex.add(tagName, tHandle, sTitle, itemClass) self._itemIndex.setHeadingTag(tHandle, sTitle, tagName) - tags[tagKey] = nwTrinary.NEUTRAL if tagKey in tags else nwTrinary.POSITIVE + tags[tagName.lower()] = True else: self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0]) @@ -634,7 +630,11 @@ class NWIndex: sTitle = self._tagsIndex.tagHeading(tagKey) return tHandle, sTitle - def getTags(self, itemClass: nwItemClass) -> list[str]: + def getDocumentTags(self, tHandle: str | None) -> list[str]: + """Return all tags used by a specific document.""" + return self._itemIndex.allItemTags(tHandle) if tHandle else [] + + def getClassTags(self, itemClass: nwItemClass) -> list[str]: """Return all tags based on itemClass.""" return self._tagsIndex.filterTagNames(itemClass.name) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 00e3a093..a83dae05 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2054,7 +2054,7 @@ class MetaCompleter(QMenu): length = len(lookup) suffix = "" options = list(filter( - lambda x: lookup in x.lower(), SHARED.project.index.getTags( + lambda x: lookup in x.lower(), SHARED.project.index.getClassTags( nwKeyWords.KEY_CLASS.get(kw.strip(), nwItemClass.NO_CLASS) ) ))[:15] diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py index 0326bb0c..4673043e 100644 --- a/novelwriter/gui/docviewerpanel.py +++ b/novelwriter/gui/docviewerpanel.py @@ -102,8 +102,8 @@ class GuiDocViewerPanel(QWidget): """Run open project tasks.""" self.clearClassTabs() for key, name, tClass, iItem, hItem in SHARED.project.index.getTagsData(): - if tClass in self.kwTabs: - self.kwTabs[tClass].addEntry(key, name, iItem, hItem) + if tClass in self.kwTabs and iItem and hItem: + self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem) self._updateTabVisibility() return @@ -123,6 +123,16 @@ class GuiDocViewerPanel(QWidget): # Public Slots ## + @pyqtSlot(str) + def projectItemChanged(self, tHandle: str) -> None: + """Update meta data for project item.""" + self.tabBackRefs.refreshDocument(tHandle) + 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) + return + @pyqtSlot(str) def updateHandle(self, tHandle: str | None) -> None: """Update the document handle.""" @@ -131,22 +141,19 @@ class GuiDocViewerPanel(QWidget): return @pyqtSlot(list, list) - def updateChangedTags(self, added: list[str], deleted: list[str]) -> None: + def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None: """Forward tags changes to the lists.""" - for key in added: + for key in updated: name, tClass, iItem, hItem = SHARED.project.index.getSingleTag(key) - if tClass in self.kwTabs: - self.kwTabs[tClass].addEntry(key, name, iItem, hItem) - + if tClass in self.kwTabs and iItem and hItem: + self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem) for key in deleted: for cTab in self.kwTabs.values(): if cTab.removeEntry(key): break else: logger.warning("Could not remove tag '%s' from view panel", key) - self._updateTabVisibility() - return ## @@ -180,9 +187,7 @@ class _ViewPanelBackRefs(QTreeWidget): cMg = CONFIG.pxInt(6) # Content - self.setHeaderLabels([ - self.tr("Heading"), "", "", self.tr("Document") - ]) + self.setHeaderLabels([self.tr("Heading"), "", "", self.tr("Document")]) self.setIndentation(0) self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) self.setIconSize(QSize(iPx, iPx)) @@ -233,6 +238,21 @@ class _ViewPanelBackRefs(QTreeWidget): self.addTopLevelItem(trItem) return + def refreshDocument(self, tHandle: str) -> None: + """Refresh document meta data.""" + nwItem = SHARED.project.tree[tHandle] + if nwItem: + docIcon = SHARED.theme.getItemIcon( + nwItem.itemType, nwItem.itemClass, + nwItem.itemLayout, nwItem.mainHeading + ) + for i in range(self.topLevelItemCount()): + trItem = self.topLevelItem(i) + if trItem and trItem.data(self.C_DATA, self.D_HANDLE) == tHandle: + trItem.setIcon(self.C_DOCUMENT, docIcon) + trItem.setText(self.C_DOCUMENT, nwItem.itemName) + return + # END Class _ViewPanelBackRefs @@ -250,15 +270,13 @@ class _ViewPanelKeyWords(QTreeWidget): def __init__(self, parent: QWidget, itemClass: nwItemClass) -> None: super().__init__(parent=parent) - self._itemClass = nwItemClass + self._tagMap: dict[str, QTreeWidgetItem] = {} iPx = SHARED.theme.baseIconSize cMg = CONFIG.pxInt(6) - self.setHeaderLabels([ - self.tr("Tag"), "", "", self.tr("Heading"), self.tr("Document") - ]) + self.setHeaderLabels([self.tr("Tag"), "", "", self.tr("Heading"), self.tr("Document")]) self.setIndentation(0) self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) self.setIconSize(QSize(iPx, iPx)) @@ -266,6 +284,7 @@ class _ViewPanelKeyWords(QTreeWidget): self.setSortingEnabled(True) self.sortByColumn(self.C_NAME, Qt.SortOrder.AscendingOrder) + # Set Header Sizes treeHeader = self.header() treeHeader.setStretchLastSection(True) treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents) @@ -274,6 +293,7 @@ class _ViewPanelKeyWords(QTreeWidget): treeHeader.resizeSection(self.C_EDIT, iPx + cMg) treeHeader.resizeSection(self.C_VIEW, iPx + cMg) + # Cache Icons Locally self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass]) self._editIcon = SHARED.theme.getIcon("edit") self._viewIcon = SHARED.theme.getIcon("view") @@ -281,14 +301,11 @@ class _ViewPanelKeyWords(QTreeWidget): return def count(self) -> int: + """Return the number of items in the list.""" return self.topLevelItemCount() - def addEntry(self, tag: str, name: str, iItem: IndexItem | None, - hItem: IndexHeading | None) -> None: - """Add a tag entry to the list.""" - if not iItem or not hItem: - return - + def addUpdateEntry(self, tag: str, name: str, iItem: IndexItem, hItem: IndexHeading) -> None: + """Add a new entry, or update an existing one.""" nwItem = iItem.item docIcon = SHARED.theme.getItemIcon( nwItem.itemType, nwItem.itemClass, @@ -297,7 +314,13 @@ class _ViewPanelKeyWords(QTreeWidget): iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5 hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel) - trItem = QTreeWidgetItem() + # This can not use a get call to the dictionary as that creates + # some weird issue with Qt, so we need to do this with an if + if tag in self._tagMap: + trItem = self._tagMap[tag] + else: + trItem = QTreeWidgetItem() + trItem.setText(self.C_NAME, name) trItem.setIcon(self.C_NAME, self._classIcon) trItem.setIcon(self.C_EDIT, self._editIcon) @@ -308,8 +331,10 @@ class _ViewPanelKeyWords(QTreeWidget): trItem.setText(self.C_DOCUMENT, nwItem.itemName) trItem.setData(self.C_DATA, self.D_TAG, tag) trItem.setData(self.C_DATA, self.D_HANDLE, iItem.handle) - self.addTopLevelItem(trItem) - self._tagMap[tag] = trItem + + if tag not in self._tagMap: + self.addTopLevelItem(trItem) + self._tagMap[tag] = trItem return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index bff6fb47..ad08c920 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -259,6 +259,7 @@ class GuiMain(QMainWindow): self.projView.treeItemChanged.connect(self.docEditor.updateDocInfo) self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo) self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox) + self.projView.treeItemChanged.connect(self.docViewerPanel.projectItemChanged) self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem) self.projView.rootFolderChanged.connect(self.novelView.updateRootItem) self.projView.rootFolderChanged.connect(self.projView.updateRootItem) @@ -788,11 +789,9 @@ class GuiMain(QMainWindow): if not SHARED.hasProject: logger.error("No project open") return False - if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode): tHandle = self.docEditor.docHandle self.projView.renameTreeItem(tHandle) - return True def rebuildTrees(self) -> None: diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 059a92d7..8ed333d6 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -215,9 +215,9 @@ class SharedData(QObject): # Call-Back Functions ## - def indexUpdatedTags(self, added: list[str], deleted: list[str]) -> None: + def indexUpdatedTags(self, updated: list[str], deleted: list[str]) -> None: """Emit the index changed tags signal.""" - self.indexChangedTags.emit(added, deleted) + self.indexChangedTags.emit(updated, deleted) return ##