Add story notes and comment auto-complete (#2346)

This commit is contained in:
Veronica Berglyd Olsen
2025-05-18 22:54:34 +02:00
committed by GitHub
15 changed files with 183 additions and 31 deletions
@@ -4,6 +4,7 @@
"Footnotes": "Footnotes", "Footnotes": "Footnotes",
"Comment": "Comment", "Comment": "Comment",
"Story Structure": "Story Structure", "Story Structure": "Story Structure",
"Note": "Note",
"Notes": "Notes", "Notes": "Notes",
"Tag": "Tag", "Tag": "Tag",
"Point of View": "Point of View", "Point of View": "Point of View",
+2
View File
@@ -79,6 +79,7 @@ SETTINGS_TEMPLATE: dict[str, tuple[type, T_BuildValue]] = {
"text.includeSynopsis": (bool, False), "text.includeSynopsis": (bool, False),
"text.includeComments": (bool, False), "text.includeComments": (bool, False),
"text.includeStory": (bool, False), "text.includeStory": (bool, False),
"text.includeNotes": (bool, False),
"text.includeKeywords": (bool, False), "text.includeKeywords": (bool, False),
"text.includeBodyText": (bool, True), "text.includeBodyText": (bool, True),
"text.ignoredKeywords": (str, ""), "text.ignoredKeywords": (str, ""),
@@ -146,6 +147,7 @@ SETTINGS_LABELS = {
"text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"), "text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"),
"text.includeComments": QT_TRANSLATE_NOOP("Builds", "Include Comments"), "text.includeComments": QT_TRANSLATE_NOOP("Builds", "Include Comments"),
"text.includeStory": QT_TRANSLATE_NOOP("Builds", "Include Story Structure"), "text.includeStory": QT_TRANSLATE_NOOP("Builds", "Include Story Structure"),
"text.includeNotes": QT_TRANSLATE_NOOP("Builds", "Include Manuscript Notes"),
"text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"), "text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"),
"text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"), "text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"),
"text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"), "text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"),
+1
View File
@@ -317,6 +317,7 @@ class NWBuildDocument:
bldObj.setCommentType(nwComment.SYNOPSIS, self._build.getBool("text.includeSynopsis")) bldObj.setCommentType(nwComment.SYNOPSIS, self._build.getBool("text.includeSynopsis"))
bldObj.setCommentType(nwComment.SHORT, self._build.getBool("text.includeSynopsis")) bldObj.setCommentType(nwComment.SHORT, self._build.getBool("text.includeSynopsis"))
bldObj.setCommentType(nwComment.STORY, self._build.getBool("text.includeStory")) bldObj.setCommentType(nwComment.STORY, self._build.getBool("text.includeStory"))
bldObj.setCommentType(nwComment.NOTE, self._build.getBool("text.includeNotes"))
if isinstance(bldObj, ToHtml): if isinstance(bldObj, ToHtml):
bldObj.setStyles(self._build.getBool("html.addStyles")) bldObj.setStyles(self._build.getBool("html.addStyles"))
+10 -1
View File
@@ -618,6 +618,10 @@ class Index:
"""Return all story structure keys.""" """Return all story structure keys."""
return self._itemIndex.allStoryKeys() return self._itemIndex.allStoryKeys()
def getNoteKeys(self) -> set[str]:
"""Return all note comment keys."""
return self._itemIndex.allNoteKeys()
def novelStructure( def novelStructure(
self, rootHandle: str | None = None, activeOnly: bool = True self, rootHandle: str | None = None, activeOnly: bool = True
) -> Iterable[tuple[str, str, str, IndexHeading]]: ) -> Iterable[tuple[str, str, str, IndexHeading]]:
@@ -920,11 +924,12 @@ class IndexCache:
which provides lookup capabilities and caching for shared data. which provides lookup capabilities and caching for shared data.
""" """
__slots__ = ("story", "tags") __slots__ = ("note", "story", "tags")
def __init__(self, tagsIndex: TagsIndex) -> None: def __init__(self, tagsIndex: TagsIndex) -> None:
self.tags: TagsIndex = tagsIndex self.tags: TagsIndex = tagsIndex
self.story: set[str] = set() self.story: set[str] = set()
self.note: set[str] = set()
return return
@@ -979,6 +984,10 @@ class ItemIndex:
"""Return all story structure keys.""" """Return all story structure keys."""
return self._cache.story.copy() return self._cache.story.copy()
def allNoteKeys(self) -> set[str]:
"""Return all note comment keys."""
return self._cache.note.copy()
def allItemTags(self, tHandle: str) -> list[str]: def allItemTags(self, tHandle: str) -> list[str]:
"""Get all tags set for headings of an item.""" """Get all tags set for headings of an item."""
if tHandle in self._items: if tHandle in self._items:
+4 -1
View File
@@ -316,6 +316,9 @@ class IndexHeading:
case "story" if key: case "story" if key:
self._cache.story.add(key) self._cache.story.add(key)
self._comments[f"story.{key}"] = str(text) self._comments[f"story.{key}"] = str(text)
case "note" if key:
self._cache.note.add(key)
self._comments[f"note.{key}"] = str(text)
return return
def setTag(self, tag: str) -> None: def setTag(self, tag: str) -> None:
@@ -395,7 +398,7 @@ class IndexHeading:
self.addReference(tag, keyword) self.addReference(tag, keyword)
else: else:
raise ValueError("Heading reference contains an invalid keyword") raise ValueError("Heading reference contains an invalid keyword")
elif key == "summary" or key.startswith("story"): elif key == "summary" or key.startswith(("story", "note")):
comment, _, kind = str(key).partition(".") comment, _, kind = str(key).partition(".")
self.setComment(comment, compact(kind), str(entry)) self.setComment(comment, compact(kind), str(entry))
else: else:
+2 -1
View File
@@ -614,7 +614,8 @@ class Tokenizer(ABC):
tStyle |= BlockFmt.JUSTIFY tStyle |= BlockFmt.JUSTIFY
if cStyle in ( if cStyle in (
nwComment.SYNOPSIS, nwComment.SHORT, nwComment.PLAIN, nwComment.STORY nwComment.SYNOPSIS, nwComment.SHORT, nwComment.PLAIN,
nwComment.STORY, nwComment.NOTE,
): ):
bStyle = COMMENT_STYLE[cStyle] bStyle = COMMENT_STYLE[cStyle]
tLine, tFmt = self._formatComment(bStyle, cKey, cText) tLine, tFmt = self._formatComment(bStyle, cKey, cText)
+69 -21
View File
@@ -3,15 +3,16 @@ novelWriter GUI Document Editor
================================= =================================
File History: File History:
Created: 2018-09-29 [0.0.1] GuiDocEditor Created: 2018-09-29 [0.0.1] GuiDocEditor
Created: 2019-04-22 [0.0.1] BackgroundWordCounter Created: 2019-04-22 [0.0.1] BackgroundWordCounter
Created: 2019-09-29 [0.2.1] GuiDocEditSearch Created: 2019-09-29 [0.2.1] GuiDocEditSearch
Created: 2020-04-25 [0.4.5] GuiDocEditHeader Created: 2020-04-25 [0.4.5] GuiDocEditHeader
Rewritten: 2020-06-15 [0.9] GuiDocEditSearch Rewritten: 2020-06-15 [0.9] GuiDocEditSearch
Created: 2020-06-27 [0.10] GuiDocEditFooter Created: 2020-06-27 [0.10] GuiDocEditFooter
Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
Created: 2023-11-06 [2.2b1] MetaCompleter Created: 2023-11-06 [2.2b1] MetaCompleter
Created: 2023-11-07 [2.2b1] GuiDocToolBar Created: 2023-11-07 [2.2b1] GuiDocToolBar
Extended: 2025-05-18 [2.7rc1] CommandCompleter
This file is a part of novelWriter This file is a part of novelWriter
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
@@ -149,7 +150,7 @@ class GuiDocEditor(QPlainTextEdit):
self._autoReplace = TextAutoReplace() self._autoReplace = TextAutoReplace()
# Completer # Completer
self._completer = MetaCompleter(self) self._completer = CommandCompleter(self)
self._completer.complete.connect(self._insertCompletion) self._completer.complete.connect(self._insertCompletion)
# Create Custom Document # Create Custom Document
@@ -1079,13 +1080,16 @@ class GuiDocEditor(QPlainTextEdit):
if (block := self._qDocument.findBlock(pos)).isValid(): if (block := self._qDocument.findBlock(pos)).isValid():
text = block.text() text = block.text()
if text.startswith("@") and added + removed == 1: if text and text[0] in "@%" and added + removed == 1:
# Only run on single character changes, or it will trigger # Only run on single character changes, or it will trigger
# at unwanted times when other changes are made to the document # at unwanted times when other changes are made to the document
cursor = self.textCursor() cursor = self.textCursor()
bPos = cursor.positionInBlock() bPos = cursor.positionInBlock()
if bPos > 0 and (viewport := self.viewport()): if bPos > 0 and (viewport := self.viewport()):
show = self._completer.updateText(text, bPos) if text[0] == "@":
show = self._completer.updateMetaText(text, bPos)
else:
show = self._completer.updateCommentText(text, bPos)
point = self.cursorRect().bottomRight() point = self.cursorRect().bottomRight()
self._completer.move(viewport.mapToGlobal(point)) self._completer.move(viewport.mapToGlobal(point))
self._completer.setVisible(show) self._completer.setVisible(show)
@@ -2073,13 +2077,13 @@ class GuiDocEditor(QPlainTextEdit):
return return
class MetaCompleter(QMenu): class CommandCompleter(QMenu):
"""GuiWidget: Meta Completer Menu """GuiWidget: Command Completer Menu
This is a context menu with options populated from the user's This is a context menu with options populated from the user's
defined tags. It also helps to type the meta data keyword on a new defined tags and keys. It also helps to type the meta data keyword
line starting with an @. The updateText function should be called on on a new line starting with @ or %. The update functions should be
every keystroke on a line starting with @. called on every keystroke on a line starting with @ or %.
""" """
complete = pyqtSignal(int, int, str) complete = pyqtSignal(int, int, str)
@@ -2088,7 +2092,7 @@ class MetaCompleter(QMenu):
super().__init__(parent=parent) super().__init__(parent=parent)
return return
def updateText(self, text: str, pos: int) -> bool: def updateMetaText(self, text: str, pos: int) -> bool:
"""Update the menu options based on the line of text.""" """Update the menu options based on the line of text."""
self.clear() self.clear()
kw, sep, _ = text.partition(":") kw, sep, _ = text.partition(":")
@@ -2096,7 +2100,7 @@ class MetaCompleter(QMenu):
offset = 0 offset = 0
length = len(kw.rstrip()) length = len(kw.rstrip())
suffix = "" if sep else ":" suffix = "" if sep else ":"
options = list(filter( options = sorted(filter(
lambda x: x.startswith(kw.rstrip()), nwKeyWords.VALID_KEYS lambda x: x.startswith(kw.rstrip()), nwKeyWords.VALID_KEYS
)) ))
else: else:
@@ -2108,7 +2112,7 @@ class MetaCompleter(QMenu):
offset = tPos[index] if lookup else pos offset = tPos[index] if lookup else pos
length = len(lookup) length = len(lookup)
suffix = "" suffix = ""
options = list(filter( options = sorted(filter(
lambda x: lookup in x.lower(), SHARED.project.index.getClassTags( lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
nwKeyWords.KEY_CLASS.get(kw.strip()) nwKeyWords.KEY_CLASS.get(kw.strip())
) )
@@ -2117,13 +2121,57 @@ class MetaCompleter(QMenu):
if not options: if not options:
return False return False
for value in sorted(options): for value in options:
rep = value + suffix rep = value + suffix
action = qtAddAction(self, value) action = qtAddAction(self, value)
action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep)) action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
return True return True
def updateCommentText(self, text: str, pos: int) -> bool:
"""Update the menu options based on the line of text."""
self.clear()
cmd, sep, _ = text.partition(":")
if pos <= len(cmd):
clean = text[1:].lstrip()[:6].lower()
if clean[:6] == "story.":
pre, _, key = cmd.partition(".")
offset = len(pre) + 1
length = len(key)
suffix = "" if sep else ": "
options = sorted(filter(
lambda x: x.startswith(key.rstrip()),
SHARED.project.index.getStoryKeys(),
))
elif clean[:5] == "note.":
pre, _, key = cmd.partition(".")
offset = len(pre) + 1
length = len(key)
suffix = "" if sep else ": "
options = sorted(filter(
lambda x: x.startswith(key.rstrip()),
SHARED.project.index.getNoteKeys(),
))
elif pos < 12:
offset = 0
length = len(cmd.rstrip())
suffix = ""
options = list(filter(
lambda x: x.startswith(cmd.rstrip()),
["%Synopsis: ", "%Short: ", "%Story", "%Note"],
))
else:
return False
if options:
for value in options:
rep = value + suffix
action = qtAddAction(self, rep.rstrip(":. "))
action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
return True
return False
## ##
# Events # Events
## ##
+7
View File
@@ -751,9 +751,13 @@ class GuiOutlineTree(QTreeWidget):
def _dumpNovelData(self, rootHandle: str | None) -> list[list[str | int]]: def _dumpNovelData(self, rootHandle: str | None) -> list[list[str | int]]:
"""Dump all novel data into a table.""" """Dump all novel data into a table."""
sLabel = SHARED.project.localLookup("Story Structure") sLabel = SHARED.project.localLookup("Story Structure")
nLabel = SHARED.project.localLookup("Note")
sKeys = sorted(SHARED.project.index.getStoryKeys()) sKeys = sorted(SHARED.project.index.getStoryKeys())
nKeys = sorted(SHARED.project.index.getNoteKeys())
sMatch = [f"story.{k}" for k in sKeys] sMatch = [f"story.{k}" for k in sKeys]
nMatch = [f"note.{k}" for k in nKeys]
sHeaders = [f"{sLabel} ({k})" for k in sKeys] sHeaders = [f"{sLabel} ({k})" for k in sKeys]
nHeaders = [f"{nLabel} ({k})" for k in nKeys]
data: list[list[str | int]] = [[ data: list[list[str | int]] = [[
"H", "H",
@@ -777,6 +781,7 @@ class GuiOutlineTree(QTreeWidget):
trConst(nwLabels.OUTLINE_COLS[nwOutline.MENTION]), trConst(nwLabels.OUTLINE_COLS[nwOutline.MENTION]),
trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP]), trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP]),
*sHeaders, *sHeaders,
*nHeaders,
]] ]]
for _, tHandle, sTitle, novIdx in SHARED.project.index.novelStructure( for _, tHandle, sTitle, novIdx in SHARED.project.index.novelStructure(
@@ -786,6 +791,7 @@ class GuiOutlineTree(QTreeWidget):
refs = SHARED.project.index.getReferences(tHandle, sTitle) refs = SHARED.project.index.getReferences(tHandle, sTitle)
comments = dict(novIdx.comments.items()) comments = dict(novIdx.comments.items())
story = [comments.get(k, "") for k in sMatch] story = [comments.get(k, "") for k in sMatch]
notes = [comments.get(k, "") for k in nMatch]
data.append([ data.append([
novIdx.level, novIdx.level,
novIdx.title, novIdx.title,
@@ -808,6 +814,7 @@ class GuiOutlineTree(QTreeWidget):
", ".join(refs[nwKeyWords.MENTION_KEY]), ", ".join(refs[nwKeyWords.MENTION_KEY]),
novIdx.synopsis, novIdx.synopsis,
*story, *story,
*notes,
]) ])
return data return data
+1 -1
View File
@@ -630,7 +630,7 @@ class _DetailsWidget(QWidget):
self.listView.addTopLevelItem(item) self.listView.addTopLevelItem(item)
for key in [ for key in [
"text.includeSynopsis", "text.includeComments", "text.includeStory", "text.includeSynopsis", "text.includeComments", "text.includeStory",
"text.includeKeywords", "text.includeBodyText", "text.includeNotes", "text.includeKeywords", "text.includeBodyText",
]: ]:
sub = QTreeWidgetItem() sub = QTreeWidgetItem()
sub.setText(0, build.getLabel(key)) sub.setText(0, build.getLabel(key))
+4
View File
@@ -974,12 +974,14 @@ class _FormattingTab(NScrollableForm):
self.incSynopsis = NSwitch(self, height=iPx) self.incSynopsis = NSwitch(self, height=iPx)
self.incComments = NSwitch(self, height=iPx) self.incComments = NSwitch(self, height=iPx)
self.incStory = NSwitch(self, height=iPx) self.incStory = NSwitch(self, height=iPx)
self.incNotes = NSwitch(self, height=iPx)
self.incKeywords = NSwitch(self, height=iPx) self.incKeywords = NSwitch(self, height=iPx)
self.addRow(self._build.getLabel("text.includeBodyText"), self.incBodyText) self.addRow(self._build.getLabel("text.includeBodyText"), self.incBodyText)
self.addRow(self._build.getLabel("text.includeSynopsis"), self.incSynopsis) self.addRow(self._build.getLabel("text.includeSynopsis"), self.incSynopsis)
self.addRow(self._build.getLabel("text.includeComments"), self.incComments) self.addRow(self._build.getLabel("text.includeComments"), self.incComments)
self.addRow(self._build.getLabel("text.includeStory"), self.incStory) self.addRow(self._build.getLabel("text.includeStory"), self.incStory)
self.addRow(self._build.getLabel("text.includeNotes"), self.incNotes)
self.addRow(self._build.getLabel("text.includeKeywords"), self.incKeywords) self.addRow(self._build.getLabel("text.includeKeywords"), self.incKeywords)
# Ignored Keywords # Ignored Keywords
@@ -1288,6 +1290,7 @@ class _FormattingTab(NScrollableForm):
self.incSynopsis.setChecked(self._build.getBool("text.includeSynopsis")) self.incSynopsis.setChecked(self._build.getBool("text.includeSynopsis"))
self.incComments.setChecked(self._build.getBool("text.includeComments")) self.incComments.setChecked(self._build.getBool("text.includeComments"))
self.incStory.setChecked(self._build.getBool("text.includeStory")) self.incStory.setChecked(self._build.getBool("text.includeStory"))
self.incNotes.setChecked(self._build.getBool("text.includeNotes"))
self.incKeywords.setChecked(self._build.getBool("text.includeKeywords")) self.incKeywords.setChecked(self._build.getBool("text.includeKeywords"))
self.ignoredKeywords.setText(self._build.getStr("text.ignoredKeywords")) self.ignoredKeywords.setText(self._build.getStr("text.ignoredKeywords"))
self.addNoteHead.setChecked(self._build.getBool("text.addNoteHeadings")) self.addNoteHead.setChecked(self._build.getBool("text.addNoteHeadings"))
@@ -1387,6 +1390,7 @@ class _FormattingTab(NScrollableForm):
self._build.setValue("text.includeSynopsis", self.incSynopsis.isChecked()) self._build.setValue("text.includeSynopsis", self.incSynopsis.isChecked())
self._build.setValue("text.includeComments", self.incComments.isChecked()) self._build.setValue("text.includeComments", self.incComments.isChecked())
self._build.setValue("text.includeStory", self.incStory.isChecked()) self._build.setValue("text.includeStory", self.incStory.isChecked())
self._build.setValue("text.includeNotes", self.incNotes.isChecked())
self._build.setValue("text.includeKeywords", self.incKeywords.isChecked()) self._build.setValue("text.includeKeywords", self.incKeywords.isChecked())
self._build.setValue("text.ignoredKeywords", self.ignoredKeywords.text()) self._build.setValue("text.ignoredKeywords", self.ignoredKeywords.text())
self._build.setValue("text.addNoteHeadings", self.addNoteHead.isChecked()) self._build.setValue("text.addNoteHeadings", self.addNoteHead.isChecked())
+3 -2
View File
@@ -1,8 +1,8 @@
%%~name: Making a Scene %%~name: Making a Scene
%%~path: 6a2d6d5f4f401/636b6aa9b697b %%~path: 6a2d6d5f4f401/636b6aa9b697b
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
%%~hash: c057a5e9309b0e764c367b0fe9ab0607e3308622 %%~hash: 1f3d98a6a27b4f9a7f2239fcd78f9e2263cd3b97
%%~date: Unknown/2025-04-08 20:10:33 %%~date: Unknown/2025-05-18 22:36:59
### Making a Scene ### Making a Scene
@pov: Jane @pov: Jane
@@ -11,6 +11,7 @@
@mention: Space @mention: Space
%Story.Resolution: You can describe the scene structure with story comments. %Story.Resolution: You can describe the scene structure with story comments.
%Note.Consistency: You can also make notes about things like consistency of the story.
A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. The scene document can be sorted after the chapter document, or as a child of the chapter. Both result in the same output in the end, so it is a matter of preference. A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. The scene document can be sorted after the chapter document, or as a child of the chapter. Both result in the same output in the end, so it is a matter of preference.
+3 -3
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:07:59"> <novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-05-18 22:37:05">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2179" autoCount="285" editTime="96588"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2189" autoCount="286" editTime="96864">
<name>Sample Project</name> <name>Sample Project</name>
<author>Jane Smith</author> <author>Jane Smith</author>
</project> </project>
@@ -58,7 +58,7 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name> <name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item> </item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2999" wordCount="530" paraCount="16" cursorPos="159" /> <meta expanded="no" heading="H3" charCount="2999" wordCount="530" paraCount="16" cursorPos="718" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name> <name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item> </item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
+9
View File
@@ -224,6 +224,14 @@ def testCoreIndexData_IndexHeading():
"story.crisis": "It exploded!", "story.crisis": "It exploded!",
} }
# Set Note Comment
head.setComment(nwComment.NOTE.name, "consitency", "Only explode once")
assert head.comments == {
"summary": "In the beginning ...",
"story.crisis": "It exploded!",
"note.consitency": "Only explode once",
}
# Set Tag # Set Tag
head.setTag("Stuff") head.setTag("Stuff")
assert head.tag == "stuff" # Case insensitive assert head.tag == "stuff" # Case insensitive
@@ -240,6 +248,7 @@ def testCoreIndexData_IndexHeading():
"refs": {"stuff": "@object"}, "refs": {"stuff": "@object"},
"summary": "In the beginning ...", "summary": "In the beginning ...",
"story.crisis": "It exploded!", "story.crisis": "It exploded!",
"note.consitency": "Only explode once",
} }
# Unpack KeyError # Unpack KeyError
+59 -1
View File
@@ -1877,10 +1877,68 @@ def testGuiEditor_Completer(qtbot, nwGUI, projPath, mockRnd):
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY) qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY) qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Escape, delay=KEY_DELAY) qtbot.keyClick(completer, Qt.Key.Key_Escape, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == ( assert docEditor.getText() == (
"### Scene One\n\n" "### Scene One\n\n"
"@char: Jane\n" "@char: Jane\n"
"@focus: John" "@focus: John\n"
)
# Send keypresses to the completer object for a comment
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
assert len(completer.actions()) == 4
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John\n"
"%Synopsis: \n"
)
# Auto-complete story comment
SHARED.project.index._itemIndex._cache.story.add("Resolution")
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
assert len(completer.actions()) == 4
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(completer, ".", delay=KEY_DELAY)
assert len(completer.actions()) == 1
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John\n"
"%Synopsis: \n"
"%Story.Resolution: \n"
)
# Auto-complete note comment
SHARED.project.index._itemIndex._cache.note.add("Consistency")
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
assert len(completer.actions()) == 4
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(completer, ".", delay=KEY_DELAY)
assert len(completer.actions()) == 1
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John\n"
"%Synopsis: \n"
"%Story.Resolution: \n"
"%Note.Consistency: \n"
) )
# qtbot.stop() # qtbot.stop()
@@ -510,6 +510,8 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI):
build.setValue("text.includeBodyText", False) build.setValue("text.includeBodyText", False)
build.setValue("text.includeSynopsis", False) build.setValue("text.includeSynopsis", False)
build.setValue("text.includeComments", False) build.setValue("text.includeComments", False)
build.setValue("text.includeStory", False)
build.setValue("text.includeNotes", False)
build.setValue("text.includeKeywords", False) build.setValue("text.includeKeywords", False)
build.setValue("text.ignoredKeywords", "") build.setValue("text.ignoredKeywords", "")
@@ -530,6 +532,8 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI):
assert fmtTab.incBodyText.isChecked() is False assert fmtTab.incBodyText.isChecked() is False
assert fmtTab.incSynopsis.isChecked() is False assert fmtTab.incSynopsis.isChecked() is False
assert fmtTab.incComments.isChecked() is False assert fmtTab.incComments.isChecked() is False
assert fmtTab.incStory.isChecked() is False
assert fmtTab.incNotes.isChecked() is False
assert fmtTab.incKeywords.isChecked() is False assert fmtTab.incKeywords.isChecked() is False
assert fmtTab.ignoredKeywords.text() == "" assert fmtTab.ignoredKeywords.text() == ""
@@ -539,6 +543,8 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI):
fmtTab.incBodyText.setChecked(True) fmtTab.incBodyText.setChecked(True)
fmtTab.incSynopsis.setChecked(True) fmtTab.incSynopsis.setChecked(True)
fmtTab.incComments.setChecked(True) fmtTab.incComments.setChecked(True)
fmtTab.incStory.setChecked(True)
fmtTab.incNotes.setChecked(True)
fmtTab.incKeywords.setChecked(True) fmtTab.incKeywords.setChecked(True)
fmtTab.addNoteHead.setChecked(True) fmtTab.addNoteHead.setChecked(True)
@@ -554,6 +560,8 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI):
assert build.getBool("text.includeBodyText") is True assert build.getBool("text.includeBodyText") is True
assert build.getBool("text.includeSynopsis") is True assert build.getBool("text.includeSynopsis") is True
assert build.getBool("text.includeComments") is True assert build.getBool("text.includeComments") is True
assert build.getBool("text.includeStory") is True
assert build.getBool("text.includeNotes") is True
assert build.getBool("text.includeKeywords") is True assert build.getBool("text.includeKeywords") is True
assert build.getStr("text.ignoredKeywords") in ("@custom, @object", "@object, @custom") assert build.getStr("text.ignoredKeywords") in ("@custom, @object", "@object, @custom")