Case insensitive tags (#1522)

This commit is contained in:
Veronica Berglyd Olsen
2023-09-18 08:16:54 +01:00
committed by GitHub
5 changed files with 700 additions and 667 deletions
+152 -144
View File
@@ -151,11 +151,13 @@ class NWIndex:
"""Check if the index has changed since a given time."""
return self._indexChange > float(checkTime)
def rootChangedSince(self, rootHandle: str, checkTime: int | float) -> bool:
def rootChangedSince(self, rootHandle: str | None, checkTime: int | float) -> bool:
"""Check if the index has changed since a given time for a
given root item.
"""
return self._rootChange.get(rootHandle, self._indexChange) > float(checkTime)
if isinstance(rootHandle, str):
return self._rootChange.get(rootHandle, self._indexChange) > float(checkTime)
return False
##
# Load and Save Index to/from File
@@ -167,15 +169,13 @@ class NWIndex:
if not isinstance(indexFile, Path):
return False
theData = {}
tStart = time()
self._indexBroken = False
if indexFile.exists():
logger.debug("Loading index file")
try:
with open(indexFile, mode="r", encoding="utf-8") as inFile:
theData = json.load(inFile)
data = json.load(inFile)
except Exception:
logger.error("Failed to load index file")
logException()
@@ -183,8 +183,8 @@ class NWIndex:
return False
try:
self._tagsIndex.unpackData(theData["novelWriter.tagsIndex"])
self._itemIndex.unpackData(theData["novelWriter.itemIndex"])
self._tagsIndex.unpackData(data["novelWriter.tagsIndex"])
self._itemIndex.unpackData(data["novelWriter.itemIndex"])
except Exception:
logger.error("The index content is invalid")
logException()
@@ -238,50 +238,50 @@ class NWIndex:
# Index Building
##
def scanText(self, tHandle: str, theText: str) -> bool:
def scanText(self, tHandle: str, text: str) -> bool:
"""Scan a piece of text associated with a handle. This will
update the indices accordingly. This function takes the handle
and text as separate inputs as we want to primarily scan the
files before we save them, in which case we already have the
text.
"""
theItem = self._project.tree[tHandle]
if theItem is None:
tItem = self._project.tree[tHandle]
if tItem is None:
logger.info("Not indexing unknown item '%s'", tHandle)
return False
if not theItem.isFileType():
if not tItem.isFileType():
logger.info("Not indexing non-file item '%s'", tHandle)
return False
# Keep a record of existing tags, and create a new item entry
itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), False)
self._itemIndex.add(tHandle, theItem)
self._itemIndex.add(tHandle, tItem)
# Run word counter for the whole text
cC, wC, pC = countWords(theText)
theItem.setCharCount(cC)
theItem.setWordCount(wC)
theItem.setParaCount(pC)
cC, wC, pC = countWords(text)
tItem.setCharCount(cC)
tItem.setWordCount(wC)
tItem.setParaCount(pC)
# If the file's meta data is missing, or the file is out of the
# main project, we don't index the content
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
if tItem.itemLayout == nwItemLayout.NO_LAYOUT:
logger.info("Not indexing no-layout item '%s'", tHandle)
return False
if theItem.itemParent is None:
if tItem.itemParent is None:
logger.info("Not indexing orphaned item '%s'", tHandle)
return False
logger.debug("Indexing item with handle '%s'", tHandle)
if theItem.isInactiveClass():
self._scanInactive(theItem, theText)
if tItem.isInactiveClass():
self._scanInactive(tItem, text)
else:
self._scanActive(tHandle, theItem, theText, itemTags)
self._scanActive(tHandle, tItem, text, itemTags)
# Update timestamps for index changes
nowTime = time()
self._indexChange = nowTime
self._rootChange[theItem.itemRoot] = nowTime
self._rootChange[tItem.itemRoot] = nowTime
return True
@@ -296,14 +296,14 @@ class NWIndex:
pTitle = TT_NONE # Tag of the previous title
canSetHeader = True # First header has not yet been set
theLines = text.splitlines()
for nLine, aLine in enumerate(theLines, start=1):
lines = text.splitlines()
for n, line in enumerate(lines, start=1):
if aLine.strip() == "":
if line.strip() == "":
continue
if aLine.startswith("#"):
hDepth, hText = self._splitHeading(aLine)
if line.startswith("#"):
hDepth, hText = self._splitHeading(line)
if hDepth == "H0":
continue
@@ -311,33 +311,33 @@ class NWIndex:
nwItem.setMainHeading(hDepth)
canSetHeader = False
cTitle = self._itemIndex.addItemHeading(tHandle, nLine, hDepth, hText)
cTitle = self._itemIndex.addItemHeading(tHandle, n, hDepth, hText)
if cTitle != TT_NONE:
if nTitle > 0:
# We have a new title, so we need to count the words of the previous one
lastText = "\n".join(theLines[nTitle-1:nLine-1])
lastText = "\n".join(lines[nTitle-1:n-1])
self._indexWordCounts(tHandle, lastText, pTitle)
nTitle = nLine
nTitle = n
pTitle = cTitle
elif aLine.startswith("@"):
elif line.startswith("@"):
if cTitle != TT_NONE:
self._indexKeyword(tHandle, aLine, cTitle, nwItem.itemClass, tags)
self._indexKeyword(tHandle, line, cTitle, nwItem.itemClass, tags)
elif aLine.startswith("%"):
elif line.startswith("%"):
if cTitle != TT_NONE:
toCheck = aLine[1:].lstrip()
toCheck = line[1:].lstrip()
synTag = toCheck[:9].lower()
tLen = len(aLine)
tLen = len(line)
cLen = len(toCheck)
cOff = tLen - cLen
if synTag == "synopsis:":
sText = aLine[cOff+9:].strip()
sText = line[cOff+9:].strip()
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, sText)
# Count words for remaining text after last heading
if pTitle != TT_NONE:
lastText = "\n".join(theLines[nTitle-1:])
lastText = "\n".join(lines[nTitle-1:])
self._indexWordCounts(tHandle, lastText, pTitle)
# Also count words on a page with no titles
@@ -354,9 +354,9 @@ class NWIndex:
def _scanInactive(self, nwItem: NWItem, text: str) -> None:
"""Scan an inactive document for meta data."""
for aLine in text.splitlines():
if aLine.startswith("#"):
hDepth, _ = self._splitHeading(aLine)
for line in text.splitlines():
if line.startswith("#"):
hDepth, _ = self._splitHeading(line)
if hDepth != "H0":
nwItem.setMainHeading(hDepth)
break
@@ -378,7 +378,7 @@ class NWIndex:
return "H2", line[4:].strip()
return "H0", ""
def _indexWordCounts(self, tHandle: str, text: str, sTitle: str):
def _indexWordCounts(self, tHandle: str, text: str, sTitle: str) -> None:
"""Count text stats and save the counts to the index."""
cC, wC, pC = countWords(text)
self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC)
@@ -391,22 +391,22 @@ class NWIndex:
of active tags is updated so that no longer used tags can be
pruned later.
"""
isValid, theBits, _ = self.scanThis(line)
if not isValid or len(theBits) < 2:
logger.warning("Skipping keyword with %d value(s) in '%s'", len(theBits), tHandle)
isValid, tBits, _ = self.scanThis(line)
if not isValid or len(tBits) < 2:
logger.warning("Skipping keyword with %d value(s) in '%s'", len(tBits), tHandle)
return
if theBits[0] not in nwKeyWords.VALID_KEYS:
logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle)
if tBits[0] not in nwKeyWords.VALID_KEYS:
logger.warning("Skipping invalid keyword '%s' in '%s'", tBits[0], tHandle)
return
if theBits[0] == nwKeyWords.TAG_KEY:
tagName = theBits[1]
if tBits[0] == nwKeyWords.TAG_KEY:
tagName = tBits[1]
self._tagsIndex.add(tagName, tHandle, sTitle, itemClass)
self._itemIndex.setHeadingTag(tHandle, sTitle, tagName)
tags[tagName] = True
else:
self._itemIndex.addHeadingReferences(tHandle, sTitle, theBits[1:], theBits[0])
self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0])
return
@@ -475,10 +475,10 @@ class NWIndex:
return isGood
# If we're still here, we check that the references exist
theKey = nwKeyWords.KEY_CLASS[tBits[0]].name
refKey = nwKeyWords.KEY_CLASS[tBits[0]].name
for n in range(1, nBits):
if tBits[n] in self._tagsIndex:
isGood[n] = self._tagsIndex.tagClass(tBits[n]) == theKey
isGood[n] = self._tagsIndex.tagClass(tBits[n]) == refKey
return isGood
@@ -504,8 +504,8 @@ class NWIndex:
they appear in the tree view and in the respective document
files, but skipping all note files.
"""
novStruct = self._itemIndex.iterNovelStructure(rHandle=rootHandle, skipExcl=skipExcl)
for tHandle, sTitle, hItem in novStruct:
structure = self._itemIndex.iterNovelStructure(rHandle=rootHandle, skipExcl=skipExcl)
for tHandle, sTitle, hItem in structure:
yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem
return
@@ -555,14 +555,14 @@ class NWIndex:
"words": hItem.wordCount,
}
theToC = [(
result = [(
tKey,
tData[tKey]["level"],
tData[tKey]["title"],
tData[tKey]["words"]
) for tKey in tOrder]
return theToC
return result
def getCounts(self, tHandle: str, sTitle: str | None = None) -> tuple[int, int, int]:
"""Return the counts for a file, or a section of a file,
@@ -586,15 +586,15 @@ class NWIndex:
"""Extract all references made in a file, and optionally title
section.
"""
theRefs = {x: [] for x in nwKeyWords.KEY_CLASS}
tRefs = {x: [] for x in nwKeyWords.KEY_CLASS}
for rTitle, hItem in self._itemIndex.iterItemHeaders(tHandle):
if sTitle is None or sTitle == rTitle:
for aTag, refTypes in hItem.references.items():
for refType in refTypes:
if refType in theRefs:
theRefs[refType].append(aTag)
if refType in tRefs:
tRefs[refType].append(self._tagsIndex.tagName(aTag))
return theRefs
return tRefs
def getBackReferenceList(self, tHandle: str) -> dict[str, str]:
"""Build a list of files referring back to our file, specified
@@ -603,17 +603,17 @@ class NWIndex:
if tHandle is None or tHandle not in self._itemIndex:
return {}
theRefs = {}
theTags = self._itemIndex.allItemTags(tHandle)
if not theTags:
return theRefs
tRefs = {}
tTags = self._itemIndex.allItemTags(tHandle)
if not tTags:
return tRefs
for aHandle, sTitle, hItem in self._itemIndex.iterAllHeaders():
for aTag in hItem.references:
if aTag in theTags and aHandle not in theRefs:
theRefs[aHandle] = sTitle
if aTag in tTags and aHandle not in tRefs:
tRefs[aHandle] = sTitle
return theRefs
return tRefs
def getTagSource(self, tagKey: str) -> tuple[str, str]:
"""Return the source location of a given tag."""
@@ -638,47 +638,51 @@ class TagsIndex:
__slots__ = ("_tags")
def __init__(self):
def __init__(self) -> None:
self._tags: dict[str, dict] = {}
return
def __contains__(self, tagKey):
return tagKey in self._tags
def __contains__(self, tagKey: str) -> bool:
return tagKey.lower() in self._tags
def __delitem__(self, tagKey):
self._tags.pop(tagKey, None)
def __delitem__(self, tagKey: str) -> None:
self._tags.pop(tagKey.lower(), None)
return
def __getitem__(self, tagKey):
return self._tags.get(tagKey, None)
def __getitem__(self, tagKey: str) -> dict | None:
return self._tags.get(tagKey.lower(), None)
##
# Methods
##
def clear(self):
def clear(self) -> None:
"""Clear the index."""
self._tags = {}
return
def add(self, tagKey: str, tHandle: str, sTitle: str, itemClass: nwItemClass):
def add(self, tagKey: str, tHandle: str, sTitle: str, itemClass: nwItemClass) -> None:
"""Add a key to the index and set all values."""
self._tags[tagKey] = {
"handle": tHandle, "heading": sTitle, "class": itemClass.name
self._tags[tagKey.lower()] = {
"name": tagKey, "handle": tHandle, "heading": sTitle, "class": itemClass.name
}
return
def tagName(self, tagKey: str) -> str:
"""Get the display name of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("name", "")
def tagHandle(self, tagKey: str) -> str:
"""Get the handle of a given tag."""
return self._tags.get(tagKey, {}).get("handle", None)
return self._tags.get(tagKey.lower(), {}).get("handle", None)
def tagHeading(self, tagKey: str) -> str:
"""Get the heading of a given tag."""
return self._tags.get(tagKey, {}).get("heading", TT_NONE)
return self._tags.get(tagKey.lower(), {}).get("heading", TT_NONE)
def tagClass(self, tagKey: str) -> str | None:
"""Get the class of a given tag."""
return self._tags.get(tagKey, {}).get("class", None)
return self._tags.get(tagKey.lower(), {}).get("class", None)
##
# Pack/Unpack
@@ -688,7 +692,7 @@ class TagsIndex:
"""Pack all the data of the tags into a single dictionary."""
return self._tags
def unpackData(self, data: dict):
def unpackData(self, data: dict) -> None:
"""Iterate through the tagsIndex loaded from cache and check
that it's valid.
"""
@@ -699,12 +703,16 @@ class TagsIndex:
for tagKey, tagData in data.items():
if not isinstance(tagKey, str):
raise ValueError("tagsIndex keys must be a strings")
if "name" not in tagData:
raise KeyError("A tagIndex item is missing a name entry")
if "handle" not in tagData:
raise KeyError("A tagIndex item is missing a handle entry")
if "heading" not in tagData:
raise KeyError("A tagIndex item is missing a heading entry")
if "class" not in tagData:
raise KeyError("A tagIndex item is missing a class entry")
if tagData["name"].lower() != tagKey:
raise ValueError("tagsIndex name must match key")
if not isHandle(tagData["handle"]):
raise ValueError("tagsIndex handle must be a handle")
if not isTitleTag(tagData["heading"]):
@@ -735,7 +743,7 @@ class ItemIndex:
__slots__ = ("_project", "_items")
def __init__(self, project: NWProject):
def __init__(self, project: NWProject) -> None:
self._project = project
self._items: dict[str, IndexItem] = {}
return
@@ -743,7 +751,7 @@ class ItemIndex:
def __contains__(self, tHandle: str) -> bool:
return tHandle in self._items
def __delitem__(self, tHandle: str):
def __delitem__(self, tHandle: str) -> None:
self._items.pop(tHandle, None)
return
@@ -754,12 +762,12 @@ class ItemIndex:
# Methods
##
def clear(self):
def clear(self) -> None:
"""Clear the index."""
self._items = {}
return
def add(self, tHandle: str, nwItem: NWItem):
def add(self, tHandle: str, nwItem: NWItem) -> None:
"""Add a new item to the index. This will overwrite the item if
it already exists.
"""
@@ -827,7 +835,7 @@ class ItemIndex:
return sTitle
return TT_NONE
def setHeadingCounts(self, tHandle: str, sTitle: str, cC: int, wC: int, pC: int):
def setHeadingCounts(self, tHandle: str, sTitle: str, cC: int, wC: int, pC: int) -> None:
"""Set the character, word and paragraph counts of a heading
on a given item.
"""
@@ -835,22 +843,22 @@ class ItemIndex:
self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC)
return
def setHeadingSynopsis(self, tHandle: str, sTitle: str, text: str):
def setHeadingSynopsis(self, tHandle: str, sTitle: str, text: str) -> None:
"""Set the synopsis text for a heading on a given item."""
if tHandle in self._items:
self._items[tHandle].setHeadingSynopsis(sTitle, text)
return
def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str):
def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str) -> None:
"""Set the main tag for a heading on a given item."""
if tHandle in self._items:
self._items[tHandle].setHeadingTag(sTitle, tagKey)
return
def addHeadingReferences(self, tHandle: str, sTitle: str, tagKeys: list[str], refType: str):
def addHeadingRef(self, tHandle: str, sTitle: str, tagKeys: list[str], refType: str) -> None:
"""Set the reference tags for a heading on a given item."""
if tHandle in self._items:
self._items[tHandle].addHeadingReferences(sTitle, tagKeys, refType)
self._items[tHandle].addHeadingRef(sTitle, tagKeys, refType)
return
##
@@ -861,7 +869,7 @@ class ItemIndex:
"""Pack all the data of the index into a single dictionary."""
return {handle: item.packData() for handle, item in self._items.items()}
def unpackData(self, data: dict):
def unpackData(self, data: dict) -> None:
"""Iterate through the itemIndex loaded from cache and check
that it's valid. This will raise errors if there is a problem.
"""
@@ -894,17 +902,13 @@ class IndexItem:
must be reset each time the item is re-indexed.
"""
__slots__ = ("_handle", "_item", "_headings", "_headings", "_count")
__slots__ = ("_handle", "_item", "_headings", "_count")
def __init__(self, tHandle: str, nwItem: NWItem):
def __init__(self, tHandle: str, nwItem: NWItem) -> None:
self._handle = tHandle
self._item = nwItem
self._headings: dict[str, IndexHeading] = {}
self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(TT_NONE)}
self._count = 0
# Add a placeholder heading
self._headings[TT_NONE] = IndexHeading(TT_NONE)
return
def __repr__(self) -> str:
@@ -925,13 +929,14 @@ class IndexItem:
@property
def item(self) -> NWItem:
"""Return the project item of the index item."""
return self._item
##
# Setters
##
def addHeading(self, tHeading: IndexHeading):
def addHeading(self, tHeading: IndexHeading) -> None:
"""Add a heading to the item. Also remove the placeholder entry
if it exists.
"""
@@ -940,25 +945,25 @@ class IndexItem:
self._headings[tHeading.key] = tHeading
return
def setHeadingCounts(self, sTitle: str, cCount: int, wCount: int, pCount: int):
def setHeadingCounts(self, sTitle: str, cCount: int, wCount: int, pCount: int) -> None:
"""Set the character, word and paragraph count of a heading."""
if sTitle in self._headings:
self._headings[sTitle].setCounts(cCount, wCount, pCount)
return
def setHeadingSynopsis(self, sTitle: str, text: str):
def setHeadingSynopsis(self, sTitle: str, text: str) -> None:
"""Set the synopsis text of a heading."""
if sTitle in self._headings:
self._headings[sTitle].setSynopsis(text)
return
def setHeadingTag(self, sTitle: str, tagKey: str):
def setHeadingTag(self, sTitle: str, tagKey: str) -> None:
"""Set the tag of a heading."""
if sTitle in self._headings:
self._headings[sTitle].setTag(tagKey)
return
def addHeadingReferences(self, sTitle: str, tagKeys: list[str], refType: str):
def addHeadingRef(self, sTitle: str, tagKeys: list[str], refType: str) -> None:
"""Add a reference key and all its types to a heading."""
if sTitle in self._headings:
for tagKey in tagKeys:
@@ -970,9 +975,11 @@ class IndexItem:
##
def items(self) -> ItemsView[str, IndexHeading]:
"""Return IndexHeading items."""
return self._headings.items()
def headings(self) -> list[str]:
"""Return heading keys in sorted order."""
return sorted(self._headings.keys())
def allTags(self) -> list[str]:
@@ -1005,7 +1012,7 @@ class IndexItem:
return data
def unpackData(self, data: dict):
def unpackData(self, data: dict) -> None:
"""Unpack an item entry from the data."""
references = data.get("references", {})
for sTitle, hData in data.get("headings", {}).items():
@@ -1033,7 +1040,7 @@ class IndexHeading:
"_paraCount", "_synopsis", "_tag", "_refs",
)
def __init__(self, key: str, line: int = 0, level: str = "H0", title: str = ""):
def __init__(self, key: str, line: int = 0, level: str = "H0", title: str = "") -> None:
self._key = key
self._line = line
self._level = level
@@ -1100,18 +1107,18 @@ class IndexHeading:
# Setters
##
def setLevel(self, level: str):
def setLevel(self, level: str) -> None:
"""Set the level of the header if it's a valid value."""
if level in nwHeaders.H_VALID:
self._level = level
return
def setLine(self, line: int):
def setLine(self, line: int) -> None:
"""Set the line number of a heading."""
self._line = max(0, checkInt(line, 0))
return
def setCounts(self, charCount: int, wordCount: int, paraCount: int):
def setCounts(self, charCount: int, wordCount: int, paraCount: int) -> None:
"""Set the character, word and paragraph count. Make sure the
value is an integer and is not smaller than 0.
"""
@@ -1120,21 +1127,22 @@ class IndexHeading:
self._paraCount = max(0, checkInt(paraCount, 0))
return
def setSynopsis(self, text: str):
def setSynopsis(self, text: str) -> None:
"""Set the synopsis text and make sure it is a string."""
self._synopsis = str(text)
return
def setTag(self, tagKey: str):
def setTag(self, tagKey: str) -> None:
"""Set the tag for references, and make sure it is a string."""
self._tag = str(tagKey)
self._tag = str(tagKey).lower()
return
def addReference(self, tagKey: str, refType: str):
def addReference(self, tagKey: str, refType: str) -> None:
"""Add a record of a reference tag, and what keyword types it is
associated with.
"""
if refType in nwKeyWords.VALID_KEYS:
tagKey = tagKey.lower()
if tagKey not in self._refs:
self._refs[tagKey] = set()
self._refs[tagKey].add(refType)
@@ -1165,7 +1173,7 @@ class IndexHeading:
"""
return {key: ",".join(sorted(list(value))) for key, value in self._refs.items()}
def unpackData(self, data: dict):
def unpackData(self, data: dict) -> None:
"""Unpack a heading entry from a dictionary."""
self.setLevel(data.get("level", "H0"))
self._title = str(data.get("title", ""))
@@ -1179,7 +1187,7 @@ class IndexHeading:
self._synopsis = str(data.get("synopsis", ""))
return
def unpackReferences(self, data: dict):
def unpackReferences(self, data: dict) -> None:
"""Unpack a set of references from a dictionary."""
for tagKey, refTypes in data.items():
if not isinstance(tagKey, str):
@@ -1221,55 +1229,55 @@ def countWords(text: str) -> tuple[int, int, int]:
if nwUnicode.U_EMDASH in text:
text = text.replace(nwUnicode.U_EMDASH, " ")
for aLine in text.splitlines():
for line in text.splitlines():
countPara = True
if not aLine:
if not line:
prevEmpty = True
continue
if aLine[0] == "@" or aLine[0] == "%":
if line[0] == "@" or line[0] == "%":
continue
if aLine[0] == "[":
if aLine.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")):
if line[0] == "[":
if line.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")):
continue
elif aLine.startswith("[VSPACE:") and aLine.endswith("]"):
elif line.startswith("[VSPACE:") and line.endswith("]"):
continue
elif aLine[0] == "#":
if aLine[:5] == "#### ":
aLine = aLine[5:]
elif line[0] == "#":
if line[:5] == "#### ":
line = line[5:]
countPara = False
elif aLine[:4] == "### ":
aLine = aLine[4:]
elif line[:4] == "### ":
line = line[4:]
countPara = False
elif aLine[:3] == "## ":
aLine = aLine[3:]
elif line[:3] == "## ":
line = line[3:]
countPara = False
elif aLine[:2] == "# ":
aLine = aLine[2:]
elif line[:2] == "# ":
line = line[2:]
countPara = False
elif aLine[:3] == "#! ":
aLine = aLine[3:]
elif line[:3] == "#! ":
line = line[3:]
countPara = False
elif aLine[:4] == "##! ":
aLine = aLine[4:]
elif line[:4] == "##! ":
line = line[4:]
countPara = False
elif aLine[0] == ">" or aLine[-1] == "<":
if aLine[:2] == ">>":
aLine = aLine[2:].lstrip(" ")
elif aLine[:1] == ">":
aLine = aLine[1:].lstrip(" ")
if aLine[-2:] == "<<":
aLine = aLine[:-2].rstrip(" ")
elif aLine[-1:] == "<":
aLine = aLine[:-1].rstrip(" ")
elif line[0] == ">" or line[-1] == "<":
if line[:2] == ">>":
line = line[2:].lstrip(" ")
elif line[:1] == ">":
line = line[1:].lstrip(" ")
if line[-2:] == "<<":
line = line[:-2].rstrip(" ")
elif line[-1:] == "<":
line = line[:-1].rstrip(" ")
wordCount += len(aLine.split())
charCount += len(aLine)
wordCount += len(line.split())
charCount += len(line)
if countPara and prevEmpty:
paraCount += 1
+1 -1
View File
@@ -62,7 +62,7 @@ class NovelSelector(QComboBox):
# Methods
##
def setHandle(self, tHandle: str, blockSignal: bool = True) -> None:
def setHandle(self, tHandle: str | None, blockSignal: bool = True) -> None:
"""Set the currently selected handle."""
self._blockSignal = blockSignal
if tHandle is None:
+143 -165
View File
@@ -59,8 +59,8 @@ class GuiOutlineView(QWidget):
loadDocumentTagRequest = pyqtSignal(str, Enum)
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
def __init__(self, mainGui):
super().__init__(parent=mainGui)
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
# Build GUI
self.outlineTree = GuiOutlineTree(self)
@@ -98,38 +98,33 @@ class GuiOutlineView(QWidget):
# Methods
##
def updateTheme(self):
"""Update theme elements.
"""
def updateTheme(self) -> None:
"""Update theme elements."""
self.outlineBar.updateTheme()
self.refreshTree()
return
def initSettings(self):
"""Initialise GUI elements that depend on specific settings.
"""
def initSettings(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
self.outlineTree.initSettings()
self.outlineData.initSettings()
return
def refreshTree(self):
"""Refresh the current tree.
"""
def refreshTree(self) -> None:
"""Refresh the current tree."""
self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline"))
return
def clearOutline(self):
"""Clear project-related GUI content.
"""
def clearOutline(self) -> None:
"""Clear project-related GUI content."""
self.outlineData.clearDetails()
self.outlineBar.setEnabled(False)
return
def openProjectTasks(self):
"""Run open project tasks.
"""
def openProjectTasks(self) -> None:
"""Run open project tasks."""
lastOutline = SHARED.project.data.getLastHandle("outline")
if not (lastOutline in SHARED.project.tree or lastOutline is None):
if not (lastOutline is None or lastOutline in SHARED.project.tree):
lastOutline = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
logger.debug("Setting outline tree to root item '%s'", lastOutline)
@@ -141,23 +136,23 @@ class GuiOutlineView(QWidget):
return
def closeProjectTasks(self):
def closeProjectTasks(self) -> None:
"""Run closing project tasks."""
self.outlineTree.closeProjectTasks()
self.outlineData.updateClasses()
self.clearOutline()
return
def splitSizes(self):
def splitSizes(self) -> list[int]:
"""Get the sizes of the splitter widget."""
return self.splitOutline.sizes()
def setTreeFocus(self):
"""Set the focus to the tree widget.
"""
def setTreeFocus(self) -> None:
"""Set the focus to the tree widget."""
return self.outlineTree.setFocus()
def treeHasFocus(self):
"""Check if the outline tree has focus.
"""
def treeHasFocus(self) -> bool:
"""Check if the outline tree has focus."""
return self.outlineTree.hasFocus()
##
@@ -165,9 +160,8 @@ class GuiOutlineView(QWidget):
##
@pyqtSlot(str)
def updateRootItem(self, tHandle):
"""Should be called whenever a root folders changes.
"""
def updateRootItem(self, tHandle: str) -> None:
"""Handle tasks whenever a root folders changes."""
self.outlineBar.populateNovelList()
self.outlineData.updateClasses()
return
@@ -177,7 +171,7 @@ class GuiOutlineView(QWidget):
##
@pyqtSlot()
def _updateMenuColumns(self):
def _updateMenuColumns(self) -> None:
"""Trigger an update of the toggled state of the column menu
checkboxes whenever a signal is received that the hidden state
of columns has changed.
@@ -186,18 +180,16 @@ class GuiOutlineView(QWidget):
return
@pyqtSlot(str)
def _tagClicked(self, link):
"""Capture the click of a tag in the details panel.
"""
def _tagClicked(self, link: str) -> None:
"""Capture the click of a tag in the details panel."""
if link:
self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW)
return
@pyqtSlot(str)
def _rootItemChanged(self, handle):
"""The root novel handle has changed or needs to be refreshed.
"""
self.outlineTree.refreshTree(rootHandle=(handle or None), overRide=True)
def _rootItemChanged(self, tHandle) -> None:
"""Handle root novel changed or needs to be refreshed."""
self.outlineTree.refreshTree(rootHandle=(tHandle or None), overRide=True)
return
# END Class GuiOutlineView
@@ -208,8 +200,8 @@ class GuiOutlineToolBar(QToolBar):
loadNovelRootRequest = pyqtSignal(str)
viewColumnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
super().__init__(parent=theOutline)
def __init__(self, outlineView: GuiOutlineView) -> None:
super().__init__(parent=outlineView)
logger.debug("Create: GuiOutlineToolBar")
@@ -221,7 +213,7 @@ class GuiOutlineToolBar(QToolBar):
self.setContentsMargins(0, 0, 0, 0)
stretch = QWidget(self)
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
stretch.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Novel Selector
self.novelLabel = QLabel(self.tr("Outline of"))
@@ -243,7 +235,7 @@ class GuiOutlineToolBar(QToolBar):
self.tbColumns = QToolButton(self)
self.tbColumns.setMenu(self.mColumns)
self.tbColumns.setPopupMode(QToolButton.InstantPopup)
self.tbColumns.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
# Assemble
self.addWidget(self.novelLabel)
@@ -263,32 +255,26 @@ class GuiOutlineToolBar(QToolBar):
# Methods
##
def updateTheme(self):
"""Update theme elements.
"""
def updateTheme(self) -> None:
"""Update theme elements."""
self.setStyleSheet("QToolBar {border: 0px;}")
self.novelValue.updateList(includeAll=True)
self.aRefresh.setIcon(SHARED.theme.getIcon("refresh"))
self.tbColumns.setIcon(SHARED.theme.getIcon("menu"))
return
def populateNovelList(self):
"""Reload the content of the novel list.
"""
def populateNovelList(self) -> None:
"""Reload the content of the novel list."""
self.novelValue.updateList(includeAll=True)
return
def setCurrentRoot(self, rootHandle):
"""Set the current active root handle.
"""
def setCurrentRoot(self, rootHandle: str | None) -> None:
"""Set the current active root handle."""
self.novelValue.setHandle(rootHandle)
return
def setColumnHiddenState(self, hiddenState):
"""Forward the change of column hidden states to the menu.
"""
def setColumnHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None:
"""Forward the change of column hidden states to the menu."""
self.mColumns.setHiddenState(hiddenState)
return
@@ -297,16 +283,14 @@ class GuiOutlineToolBar(QToolBar):
##
@pyqtSlot(str)
def _novelValueChanged(self, tHandle):
"""Emit a signal containing the handle of the selected item.
"""
def _novelValueChanged(self, tHandle: str) -> None:
"""Emit a signal containing the handle of the selected item."""
self.loadNovelRootRequest.emit(tHandle)
return
@pyqtSlot()
def _refreshRequested(self):
"""Emit a signal containing the handle of the selected item.
"""
def _refreshRequested(self) -> None:
"""Emit a signal containing the handle of the selected item."""
self.loadNovelRootRequest.emit(self.novelValue.handle)
return
@@ -361,7 +345,7 @@ class GuiOutlineTree(QTreeWidget):
hiddenStateChanged = pyqtSignal()
activeItemChanged = pyqtSignal(str, str)
def __init__(self, outlineView):
def __init__(self, outlineView: GuiOutlineView) -> None:
super().__init__(parent=outlineView)
logger.debug("Create: GuiOutlineTree")
@@ -369,9 +353,9 @@ class GuiOutlineTree(QTreeWidget):
self.outlineView = outlineView
self.setUniformRowHeights(True)
self.setFrameStyle(QFrame.NoFrame)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setFrameStyle(QFrame.Shape.NoFrame)
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.setExpandsOnDoubleClick(False)
self.setDragEnabled(False)
self.itemDoubleClicked.connect(self._treeDoubleClick)
@@ -431,23 +415,19 @@ class GuiOutlineTree(QTreeWidget):
# Methods
##
def initSettings(self):
"""Set or update outline settings.
"""
# Scroll bars
def initSettings(self) -> None:
"""Set or update outline settings."""
if CONFIG.hideVScroll:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
else:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
if CONFIG.hideHScroll:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
return
def clearContent(self):
def clearContent(self) -> None:
"""Clear the tree and header and set the default values for the
columns arrays.
"""
@@ -455,10 +435,10 @@ class GuiOutlineTree(QTreeWidget):
self.setColumnCount(1)
self.setHeaderLabel(trConst(nwLabels.OUTLINE_COLS[nwOutline.TITLE]))
self._treeOrder = []
self._colWidth = {}
self._colHidden = {}
self._colIdx = {}
self._treeOrder: list[nwOutline] = []
self._colWidth: dict[nwOutline, int] = {}
self._colHidden: dict[nwOutline, bool] = {}
self._colIdx: dict[nwOutline, int] = {}
self._treeNCols = 0
for hItem in nwOutline:
@@ -470,7 +450,8 @@ class GuiOutlineTree(QTreeWidget):
return
def refreshTree(self, rootHandle=None, overRide=False, novelChanged=False):
def refreshTree(self, rootHandle: str | None = None,
overRide: bool = False, novelChanged: bool = False) -> None:
"""Called whenever the Outline tab is activated and controls
what data to load, and if necessary, force a rebuild of the
tree.
@@ -494,15 +475,14 @@ class GuiOutlineTree(QTreeWidget):
return
def closeProjectTasks(self):
"""Called before a project is closed.
"""
def closeProjectTasks(self) -> None:
"""Called before a project is closed."""
self._saveHeaderState()
self.clearContent()
self._firstView = True
return
def getSelectedHandle(self):
def getSelectedHandle(self) -> tuple[str | None, str | None]:
"""Get the currently selected handle. If multiple items are
selected, return the first.
"""
@@ -518,7 +498,7 @@ class GuiOutlineTree(QTreeWidget):
##
@pyqtSlot("QTreeWidgetItem*", int)
def _treeDoubleClick(self, tItem, tCol):
def _treeDoubleClick(self, tItem: QTreeWidgetItem, tCol: int) -> None:
"""Extract the handle and line number of the title double-
clicked, and send it to the main gui class for opening in the
document editor.
@@ -530,7 +510,7 @@ class GuiOutlineTree(QTreeWidget):
return
@pyqtSlot()
def _itemSelected(self):
def _itemSelected(self) -> None:
"""Extract the handle and line number of the currently selected
title, and send it to the details panel.
"""
@@ -542,7 +522,7 @@ class GuiOutlineTree(QTreeWidget):
return
@pyqtSlot(int, int, int)
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
def _columnMoved(self, logIdx: int, oldVisualIdx: int, newVisualIdx: int) -> None:
"""Make sure the order array is up to date with the actual order
of the columns.
"""
@@ -551,12 +531,12 @@ class GuiOutlineTree(QTreeWidget):
return
@pyqtSlot(bool, Enum)
def menuColumnToggled(self, isChecked, theItem):
def menuColumnToggled(self, isChecked: bool, hItem: nwOutline) -> None:
"""Receive the changes to column visibility forwarded by the
column selection menu.
"""
if theItem in self._colIdx:
self.setColumnHidden(self._colIdx[theItem], not isChecked)
if hItem in self._colIdx:
self.setColumnHidden(self._colIdx[hItem], not isChecked)
self._saveHeaderState()
return
@@ -600,7 +580,7 @@ class GuiOutlineTree(QTreeWidget):
return
def _saveHeaderState(self):
def _saveHeaderState(self) -> None:
"""Save the state of the main tree header, that is, column
order, column width and column hidden state. We don't want to
save the current width of hidden columns though. This preserves
@@ -627,7 +607,7 @@ class GuiOutlineTree(QTreeWidget):
return
def _populateTree(self, rootHandle):
def _populateTree(self, rootHandle: str | None) -> None:
"""Build the tree based on the project index, and the header
based on the defined constants, default values and user selected
width, order and hidden state. All columns are populated, even
@@ -653,22 +633,25 @@ class GuiOutlineTree(QTreeWidget):
headItem = self.headerItem()
if isinstance(headItem, QTreeWidgetItem):
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
headItem.setTextAlignment(
self._colIdx[nwOutline.CCOUNT], Qt.AlignmentFlag.AlignRight)
headItem.setTextAlignment(
self._colIdx[nwOutline.WCOUNT], Qt.AlignmentFlag.AlignRight)
headItem.setTextAlignment(
self._colIdx[nwOutline.PCOUNT], Qt.AlignmentFlag.AlignRight)
novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for _, tHandle, sTitle, novIdx in novStruct:
iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
if iLevel == 0:
nwItem = SHARED.project.tree[tHandle]
if iLevel == 0 or nwItem is None:
continue
trItem = QTreeWidgetItem()
nwItem = SHARED.project.tree[tHandle]
hDec = SHARED.theme.getHeaderDecoration(iLevel)
trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, hDec)
trItem.setData(self._colIdx[nwOutline.TITLE], Qt.ItemDataRole.DecorationRole, hDec)
trItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
trItem.setData(self._colIdx[nwOutline.TITLE], self.D_HANDLE, tHandle)
trItem.setData(self._colIdx[nwOutline.TITLE], self.D_TITLE, sTitle)
@@ -681,9 +664,9 @@ class GuiOutlineTree(QTreeWidget):
trItem.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}")
trItem.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}")
trItem.setText(self._colIdx[nwOutline.PCOUNT], f"{novIdx.paraCount:n}")
trItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
trItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
trItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
trItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignmentFlag.AlignRight)
trItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignmentFlag.AlignRight)
trItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignmentFlag.AlignRight)
refs = SHARED.project.index.getReferences(tHandle, sTitle)
trItem.setText(self._colIdx[nwOutline.POV], ", ".join(refs[nwKeyWords.POV_KEY]))
@@ -709,8 +692,8 @@ class GuiOutlineHeaderMenu(QMenu):
columnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
super().__init__(parent=theOutline)
def __init__(self, outlineToolBar: GuiOutlineToolBar) -> None:
super().__init__(parent=outlineToolBar)
self.acceptToggle = True
@@ -731,7 +714,7 @@ class GuiOutlineHeaderMenu(QMenu):
return
def setHiddenState(self, hiddenState):
def setHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None:
"""Overwrite the checked state of the columns as the inverse of
the hidden state. Skip the TITLE column as it cannot be hidden.
"""
@@ -760,12 +743,12 @@ class GuiOutlineDetails(QScrollArea):
itemTagClicked = pyqtSignal(str)
def __init__(self, theOutline):
super().__init__(parent=theOutline)
def __init__(self, outlineView: GuiOutlineView) -> None:
super().__init__(parent=outlineView)
logger.debug("Create: GuiOutlineDetails")
self.theOutline = theOutline
self.outlineView = outlineView
# Sizes
minTitle = 30*SHARED.theme.textNWidth
@@ -878,23 +861,26 @@ class GuiOutlineDetails(QScrollArea):
# Selected Item Details
self.mainGroup = QGroupBox(self.tr("Title Details"), self)
self.mainForm = QGridLayout()
self.mainForm = QGridLayout()
self.mainGroup.setLayout(self.mainForm)
self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
topLeft = Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
topRight = Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight
self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, topLeft)
self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, topLeft)
self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, topLeft)
self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, topRight)
self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, topLeft)
self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, topLeft)
self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, topLeft)
self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, topRight)
self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, topLeft)
self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, topLeft)
self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, topLeft)
self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, topRight)
self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, topLeft)
self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, topLeft)
self.mainForm.setColumnStretch(1, 1)
self.mainForm.setRowStretch(4, 1)
@@ -906,24 +892,24 @@ class GuiOutlineDetails(QScrollArea):
self.tagsForm = QGridLayout()
self.tagsGroup.setLayout(self.tagsForm)
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, topLeft)
self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, topLeft)
self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, topLeft)
self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, topLeft)
self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, topLeft)
self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, topLeft)
self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, topLeft)
self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, topLeft)
self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, topLeft)
self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, topLeft)
self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, topLeft)
self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, topLeft)
self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, topLeft)
self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, topLeft)
self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, topLeft)
self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, topLeft)
self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, topLeft)
self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, topLeft)
self.tagsForm.setColumnStretch(1, 1)
self.tagsForm.setRowStretch(8, 1)
@@ -939,10 +925,10 @@ class GuiOutlineDetails(QScrollArea):
self.outerWidget.setLayout(self.outerBox)
self.setWidget(self.outerWidget)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setWidgetResizable(True)
self.setFrameStyle(QFrame.NoFrame)
self.setFrameStyle(QFrame.Shape.NoFrame)
self.initSettings()
@@ -950,27 +936,21 @@ class GuiOutlineDetails(QScrollArea):
return
def initSettings(self):
"""Set or update outline settings.
"""
# Scroll bars
def initSettings(self) -> None:
"""Set or update outline settings."""
if CONFIG.hideVScroll:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
else:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
if CONFIG.hideHScroll:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.updateClasses()
return
def clearDetails(self):
"""Clear all the data labels.
"""
def clearDetails(self) -> None:
"""Clear all the data labels."""
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText("")
self.fileValue.setText("")
@@ -996,7 +976,7 @@ class GuiOutlineDetails(QScrollArea):
##
@pyqtSlot(str, str)
def showItem(self, tHandle, sTitle):
def showItem(self, tHandle: str, sTitle: str) -> bool:
"""Update the content of the tree with the given handle and line
number pointing to a header.
"""
@@ -1041,9 +1021,8 @@ class GuiOutlineDetails(QScrollArea):
return True
@pyqtSlot()
def updateClasses(self):
"""Update the visibility status of class details.
"""
def updateClasses(self) -> None:
"""Update the visibility status of class details."""
usedClasses = SHARED.project.tree.rootClasses()
pltVisible = nwItemClass.PLOT in usedClasses
@@ -1069,9 +1048,8 @@ class GuiOutlineDetails(QScrollArea):
return
@staticmethod
def _formatTags(refs, key):
"""Convert a list of tags into a list of clickable tag links.
"""
def _formatTags(refs: dict[str, list[str]], key: str) -> str:
"""Convert a list of tags into a list of clickable tag links."""
return ", ".join(
[f"<a href='{tag}'>{tag}</a>" for tag in refs.get(key, [])]
)
@@ -1,8 +1,8 @@
{
"novelWriter.tagsIndex": {
"Bod": {"handle": "4c4f28287af27", "heading": "T0001", "class": "CHARACTER"},
"Main": {"handle": "2426c6f0ca922", "heading": "T0001", "class": "PLOT"},
"Europe": {"handle": "04468803b92e1", "heading": "T0001", "class": "WORLD"}
"bod": {"name": "Bod", "handle": "4c4f28287af27", "heading": "T0001", "class": "CHARACTER"},
"main": {"name": "Main", "handle": "2426c6f0ca922", "heading": "T0001", "class": "PLOT"},
"europe": {"name": "Europe", "handle": "04468803b92e1", "heading": "T0001", "class": "WORLD"}
},
"novelWriter.itemIndex": {
"7a992350f3eb6": {
@@ -30,7 +30,7 @@
"T0001": {"level": "H2", "title": "Chapter One", "line": 1, "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."}
},
"references": {
"T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
}
},
"88243afbe5ed8": {
@@ -39,7 +39,7 @@
"T0002": {"level": "H4", "title": "Scene One, Section Two", "line": 13, "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
},
"references": {
"T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
}
},
"f96ec11c6a3da": {
@@ -48,7 +48,7 @@
"T0002": {"level": "H4", "title": "Scene Two, Section Two", "line": 15, "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}
},
"references": {
"T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
}
},
"846352075de7d": {
@@ -61,7 +61,7 @@
"T0001": {"level": "H2", "title": "Chapter Two", "line": 1, "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."}
},
"references": {
"T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
}
},
"eb103bc70c90c": {
@@ -69,7 +69,7 @@
"T0001": {"level": "H3", "title": "Scene Three", "line": 1, "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."}
},
"references": {
"T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
}
},
"f8c0562e50f1b": {
@@ -77,7 +77,7 @@
"T0001": {"level": "H3", "title": "Scene Four", "line": 1, "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."}
},
"references": {
"T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
}
},
"47666c91c7ccf": {
@@ -85,25 +85,25 @@
"T0001": {"level": "H3", "title": "Scene Five", "line": 1, "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."}
},
"references": {
"T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
}
},
"4c4f28287af27": {
"headings": {
"T0001": {"level": "H1", "title": "Nobody Owens", "line": 1, "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
"T0001": {"level": "H1", "title": "Nobody Owens", "line": 1, "tag": "bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
},
"references": {
"T0001": {"Main": "@plot"}
"T0001": {"main": "@plot"}
}
},
"2426c6f0ca922": {
"headings": {
"T0001": {"level": "H1", "title": "Main Plot", "line": 1, "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
"T0001": {"level": "H1", "title": "Main Plot", "line": 1, "tag": "main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
}
},
"04468803b92e1": {
"headings": {
"T0001": {"level": "H1", "title": "Ancient Europe", "line": 1, "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""}
"T0001": {"level": "H1", "title": "Ancient Europe", "line": 1, "tag": "europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""}
}
}
}
File diff suppressed because it is too large Load Diff