Add story notes and comment auto-complete (#2346)
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user