Added tags changed signal

This commit is contained in:
Veronica Berglyd Olsen
2023-11-15 18:11:37 +01:00
parent 3f8220f350
commit 2cb6020cac
5 changed files with 61 additions and 21 deletions
+41 -11
View File
@@ -34,8 +34,9 @@ import logging
from time import time from time import time
from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator
from pathlib import Path from pathlib import Path
from novelwriter import SHARED
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout, nwTrinary
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
@@ -254,7 +255,7 @@ class NWIndex:
return False return False
# Keep a record of existing tags, and create a new item entry # Keep a record of existing tags, and create a new item entry
itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), False) itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), nwTrinary.NEGATIVE)
self._itemIndex.add(tHandle, tItem) self._itemIndex.add(tHandle, tItem)
# Run word counter for the whole text # Run word counter for the whole text
@@ -289,7 +290,8 @@ class NWIndex:
# Internal Indexer Helpers # Internal Indexer Helpers
## ##
def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict) -> None: def _scanActive(self, tHandle: str, nwItem: NWItem, text: str,
tags: dict[str, nwTrinary]) -> None:
"""Scan an active document for meta data.""" """Scan an active document for meta data."""
nTitle = 0 # Line Number of the previous title nTitle = 0 # Line Number of the previous title
cTitle = TT_NONE # Tag of the current title cTitle = TT_NONE # Tag of the current title
@@ -345,10 +347,20 @@ class NWIndex:
self._indexWordCounts(tHandle, text, cTitle) self._indexWordCounts(tHandle, text, cTitle)
# Prune no longer used tags # Prune no longer used tags
for tTag, isActive in tags.items(): for tTag, tStatus in tags.items():
if not isActive: added = []
logger.debug("Deleting removed tag '%s'", tTag) deleted = []
if tStatus == nwTrinary.NEGATIVE:
logger.debug("Removed tag '%s'", tTag)
del self._tagsIndex[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)
return return
@@ -385,7 +397,7 @@ class NWIndex:
return return
def _indexKeyword(self, tHandle: str, line: str, sTitle: str, def _indexKeyword(self, tHandle: str, line: str, sTitle: str,
itemClass: nwItemClass, tags: dict) -> None: itemClass: nwItemClass, tags: dict[str, nwTrinary]) -> None:
"""Validate and save the information about a reference to a tag """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 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 of active tags is updated so that no longer used tags can be
@@ -402,9 +414,10 @@ class NWIndex:
if tBits[0] == nwKeyWords.TAG_KEY: if tBits[0] == nwKeyWords.TAG_KEY:
tagName = tBits[1] tagName = tBits[1]
tagKey = tagName.lower()
self._tagsIndex.add(tagName, tHandle, sTitle, itemClass) self._tagsIndex.add(tagName, tHandle, sTitle, itemClass)
self._itemIndex.setHeadingTag(tHandle, sTitle, tagName) self._itemIndex.setHeadingTag(tHandle, sTitle, tagName)
tags[tagName.lower()] = True tags[tagKey] = nwTrinary.NEUTRAL if tagKey in tags else nwTrinary.POSITIVE
else: else:
self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0]) self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0])
@@ -615,7 +628,7 @@ class NWIndex:
return tRefs return tRefs
def getTagSource(self, tagKey: str) -> tuple[str, str]: def getTagSource(self, tagKey: str) -> tuple[str | None, str]:
"""Return the source location of a given tag.""" """Return the source location of a given tag."""
tHandle = self._tagsIndex.tagHandle(tagKey) tHandle = self._tagsIndex.tagHandle(tagKey)
sTitle = self._tagsIndex.tagHeading(tagKey) sTitle = self._tagsIndex.tagHeading(tagKey)
@@ -625,6 +638,14 @@ class NWIndex:
"""Return all tags based on itemClass.""" """Return all tags based on itemClass."""
return self._tagsIndex.filterTagNames(itemClass.name) return self._tagsIndex.filterTagNames(itemClass.name)
def getTagsData(self) -> 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
return
# END Class NWIndex # END Class NWIndex
@@ -643,7 +664,7 @@ class TagsIndex:
__slots__ = ("_tags") __slots__ = ("_tags")
def __init__(self) -> None: def __init__(self) -> None:
self._tags: dict[str, dict] = {} self._tags: dict[str, dict[str, str]] = {}
return return
def __contains__(self, tagKey: str) -> bool: def __contains__(self, tagKey: str) -> bool:
@@ -665,6 +686,10 @@ class TagsIndex:
self._tags = {} self._tags = {}
return return
def items(self) -> ItemsView:
"""Return a dictionary view of all tags."""
return self._tags.items()
def add(self, tagKey: str, tHandle: str, sTitle: str, itemClass: nwItemClass) -> None: def add(self, tagKey: str, tHandle: str, sTitle: str, itemClass: nwItemClass) -> None:
"""Add a key to the index and set all values.""" """Add a key to the index and set all values."""
self._tags[tagKey.lower()] = { self._tags[tagKey.lower()] = {
@@ -676,7 +701,7 @@ class TagsIndex:
"""Get the display name of a given tag.""" """Get the display name of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("name", "") return self._tags.get(tagKey.lower(), {}).get("name", "")
def tagHandle(self, tagKey: str) -> str: def tagHandle(self, tagKey: str) -> str | None:
"""Get the handle of a given tag.""" """Get the handle of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("handle", None) return self._tags.get(tagKey.lower(), {}).get("handle", None)
@@ -937,6 +962,11 @@ class IndexItem:
# Properties # Properties
## ##
@property
def handle(self) -> str:
"""Return the item handle of the index item."""
return self._handle
@property @property
def item(self) -> NWItem: def item(self) -> NWItem:
"""Return the project item of the index item.""" """Return the project item of the index item."""
+1 -1
View File
@@ -64,7 +64,7 @@ class nwItemLayout(Enum):
class nwTrinary(Enum): class nwTrinary(Enum):
NEGATIVE = -1 NEGATIVE = -1
UNKNOWN = 0 NEUTRAL = 0
POSITIVE = 1 POSITIVE = 1
# END Enum nwTrinary # END Enum nwTrinary
+4 -4
View File
@@ -1783,13 +1783,13 @@ class GuiDocEditor(QPlainTextEdit):
block = cursor.block() block = cursor.block()
text = block.text() text = block.text()
if len(text) == 0: if len(text) == 0:
return nwTrinary.UNKNOWN return nwTrinary.NEUTRAL
if text.startswith("@") and isinstance(self._nwItem, NWItem): if text.startswith("@") and isinstance(self._nwItem, NWItem):
isGood, tBits, tPos = SHARED.project.index.scanThis(text) isGood, tBits, tPos = SHARED.project.index.scanThis(text)
if not isGood: if not isGood:
return nwTrinary.UNKNOWN return nwTrinary.NEUTRAL
tag = "" tag = ""
exist = False exist = False
@@ -1806,7 +1806,7 @@ class GuiDocEditor(QPlainTextEdit):
if not tag or tag.startswith("@"): if not tag or tag.startswith("@"):
# The keyword cannot be looked up, so we ignore that # The keyword cannot be looked up, so we ignore that
return nwTrinary.UNKNOWN return nwTrinary.NEUTRAL
if follow and exist: if follow and exist:
logger.debug("Attempting to follow tag '%s'", tag) logger.debug("Attempting to follow tag '%s'", tag)
@@ -1826,7 +1826,7 @@ class GuiDocEditor(QPlainTextEdit):
return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
return nwTrinary.UNKNOWN return nwTrinary.NEUTRAL
def _openContextFromCursor(self) -> None: def _openContextFromCursor(self) -> None:
"""Open the spell check context menu at the cursor.""" """Open the spell check context menu at the cursor."""
+11 -1
View File
@@ -52,6 +52,7 @@ class SharedData(QObject):
projectStatusChanged = pyqtSignal(bool) projectStatusChanged = pyqtSignal(bool)
projectStatusMessage = pyqtSignal(str) projectStatusMessage = pyqtSignal(str)
spellLanguageChanged = pyqtSignal(str, str) spellLanguageChanged = pyqtSignal(str, str)
indexChangedTags = pyqtSignal(list[str], list[str])
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@@ -171,7 +172,7 @@ class SharedData(QObject):
return return
def updateSpellCheckLanguage(self, reload: bool = False) -> None: def updateSpellCheckLanguage(self, reload: bool = False) -> None:
"""Update the active spell check langauge from settings.""" """Update the active spell check language from settings."""
from novelwriter import CONFIG from novelwriter import CONFIG
language = self.project.data.spellLang or CONFIG.spellLanguage language = self.project.data.spellLang or CONFIG.spellLanguage
if language != self.spelling.spellLanguage or reload: if language != self.spelling.spellLanguage or reload:
@@ -210,6 +211,15 @@ class SharedData(QObject):
QThreadPool.globalInstance().start(runnable, priority=priority) QThreadPool.globalInstance().start(runnable, priority=priority)
return return
##
# Call-Back Functions
##
def indexUpdatedTags(self, added: list[str], deleted: list[str]) -> None:
"""Emit the index changed tags signal."""
self.indexChangedTags.emit(added, deleted)
return
## ##
# Alert Boxes # Alert Boxes
## ##
+4 -4
View File
@@ -1194,15 +1194,15 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
# Empty Block # Empty Block
nwGUI.docEditor.setCursorLine(2) nwGUI.docEditor.setCursorLine(2)
assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN assert nwGUI.docEditor._processTag() is nwTrinary.NEUTRAL
# Not On Tag # Not On Tag
nwGUI.docEditor.setCursorLine(1) nwGUI.docEditor.setCursorLine(1)
assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN assert nwGUI.docEditor._processTag() is nwTrinary.NEUTRAL
# On Tag Keyword # On Tag Keyword
nwGUI.docEditor.setCursorPosition(15) nwGUI.docEditor.setCursorPosition(15)
assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN assert nwGUI.docEditor._processTag() is nwTrinary.NEUTRAL
# On Known Tag, No Follow # On Known Tag, No Follow
nwGUI.docEditor.setCursorPosition(22) nwGUI.docEditor.setCursorPosition(22)
@@ -1230,7 +1230,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert "0000000000012" not in SHARED.project.tree assert "0000000000012" not in SHARED.project.tree
nwGUI.docEditor.setCursorPosition(47) nwGUI.docEditor.setCursorPosition(47)
assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN assert nwGUI.docEditor._processTag() is nwTrinary.NEUTRAL
# qtbot.stop() # qtbot.stop()