Add hard scene break format (#1753)

This commit is contained in:
Veronica Berglyd Olsen
2024-03-13 11:11:59 +01:00
committed by GitHub
22 changed files with 270 additions and 125 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 70 KiB

+4
View File
@@ -75,6 +75,10 @@ Organising Your Text
Keep in mind that this is not what the section heading is intended for, so the app will not
understand the section heading as a scene, but it will be formatted correctly in the manuscript.
.. versionadded:: 2.4
You can now distinguish between soft and hard scene breaks with a modified scene header. See
:ref:`a_manuscript_settings_head_hard` for more details.
Other Tools
===========
+16
View File
@@ -63,6 +63,8 @@ In the figure, the green arrow icon and the blue pin icon indicates which docume
and the red forbidden icon indicates that a document is explicitly excluded.
.. _a_manuscript_settings_head:
Formatting Headings
-------------------
@@ -86,6 +88,7 @@ Any text you add that isn't highlighted in colours will remain in your formatted
You can preview the result of these format strings by clicking :guilabel:`Apply`, and then clicking
:guilabel:`Preview` in the :guilabel:`Manuscript Build` tool main window.
Scene Separators
^^^^^^^^^^^^^^^^
@@ -99,6 +102,19 @@ the desired separator text as the format. If the format is any piece of static t
be treated as a separator.
.. _a_manuscript_settings_head_hard:
Hard and Soft Scenes
^^^^^^^^^^^^^^^^^^^^
If you wish to distinguish between so-called soft and hard scene breaks, where a hard scene break
is understood as a scene break that changes point-of-view character, you can use the modified scene
heading format in your text. You can then give these headings a different formatting in the
:guilabel:`Headings` settings.
See :ref:`a_fmt_head` for more info on how to format headings in your text.
Output Settings
---------------
+6 -1
View File
@@ -68,7 +68,7 @@ correctly to produce the intended result. See :ref:`a_struct_heads` for more det
Heading level four. For novel documents, the level indicates the start of a new section. Section
titles can be replaced by separators or ignored completely when building the manuscript.
For headings level one and two, adding a ``!`` modifies its meaning:
For headings level one through three, adding a ``!`` modifies its meaning:
``#! Title Text``
This tells the build tool that the level one heading is intended to be used for the novel's or
@@ -81,6 +81,11 @@ For headings level one and two, adding a ``!`` modifies its meaning:
chapter numbers are being used. Such titles are useful for a prologue for instance. See
:ref:`a_struct_heads_unnum` for more details.
``###! Title Text``
This is an alternative scene heading that can be formatted differently in the
:guilabel:`Manuscript Build` tool. It is intended for separating "soft" and "hard" scene breaks.
Otherwise, it behaves identically to a regular scene heading.
.. note::
The space after the ``#`` or ``!`` character is mandatory. The syntax highlighter will change
colour and font size when the heading is correctly formatted.
+11 -7
View File
@@ -56,8 +56,10 @@ SETTINGS_TEMPLATE = {
"headings.fmtChapter": (str, nwHeadFmt.TITLE),
"headings.fmtUnnumbered": (str, nwHeadFmt.TITLE),
"headings.fmtScene": (str, "* * *"),
"headings.fmtHardScene": (str, ""),
"headings.fmtSection": (str, ""),
"headings.hideScene": (bool, False),
"headings.hideHardScene": (bool, False),
"headings.hideSection": (bool, True),
"headings.centerTitle": (bool, True),
"headings.centerChapter": (bool, False),
@@ -100,13 +102,15 @@ SETTINGS_LABELS = {
"filter.includeInactive": QT_TRANSLATE_NOOP("Builds", "Inactive Documents"),
"headings": QT_TRANSLATE_NOOP("Builds", "Headings"),
"headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Title Headings"),
"headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Headings"),
"headings.fmtUnnumbered": QT_TRANSLATE_NOOP("Builds", "Unnumbered Headings"),
"headings.fmtScene": QT_TRANSLATE_NOOP("Builds", "Scene Headings"),
"headings.fmtSection": QT_TRANSLATE_NOOP("Builds", "Section Headings"),
"headings.hideScene": QT_TRANSLATE_NOOP("Builds", "Hide Scene Headings"),
"headings.hideSection": QT_TRANSLATE_NOOP("Builds", "Hide Section Headings"),
"headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Partition Format"),
"headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Format"),
"headings.fmtUnnumbered": QT_TRANSLATE_NOOP("Builds", "Unnumbered Format"),
"headings.fmtScene": QT_TRANSLATE_NOOP("Builds", "Scene Format"),
"headings.fmtHardScene": QT_TRANSLATE_NOOP("Builds", "Hard Scene Format"),
"headings.fmtSection": QT_TRANSLATE_NOOP("Builds", "Section Format"),
"headings.hideScene": QT_TRANSLATE_NOOP("Builds", "Hide Scene Breaks"),
"headings.hideHardScene": QT_TRANSLATE_NOOP("Builds", "Hide Hard Scene Breaks"),
"headings.hideSection": QT_TRANSLATE_NOOP("Builds", "Hide Section Breaks"),
"text.grpContent": QT_TRANSLATE_NOOP("Builds", "Text Content"),
"text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"),
+4
View File
@@ -282,6 +282,10 @@ class NWBuildDocument:
self._build.getStr("headings.fmtScene"),
self._build.getBool("headings.hideScene")
)
bldObj.setHardSceneFormat(
self._build.getStr("headings.fmtHardScene"),
self._build.getBool("headings.hideHardScene")
)
bldObj.setSectionFormat(
self._build.getStr("headings.fmtSection"),
self._build.getBool("headings.hideSection")
+2
View File
@@ -396,6 +396,8 @@ class NWIndex:
return "H1", line[3:].strip()
elif line.startswith("##! "):
return "H2", line[4:].strip()
elif line.startswith("###! "):
return "H3", line[5:].strip()
return "H0", ""
def _indexWordCounts(self, tHandle: str, text: str, sTitle: str) -> None:
+51 -66
View File
@@ -156,9 +156,11 @@ class Tokenizer(ABC):
self._fmtChapter = nwHeadFmt.TITLE # Formatting for numbered chapters
self._fmtUnNum = nwHeadFmt.TITLE # Formatting for unnumbered chapters
self._fmtScene = nwHeadFmt.TITLE # Formatting for scenes
self._fmtHScene = nwHeadFmt.TITLE # Formatting for hard scenes
self._fmtSection = nwHeadFmt.TITLE # Formatting for sections
self._hideScene = False # Do not include scene headings
self._hideHScene = False # Do not include hard scene headings
self._hideSection = False # Do not include section headings
self._linkHeaders = False # Add an anchor before headings
@@ -254,6 +256,12 @@ class Tokenizer(ABC):
self._hideScene = hide
return
def setHardSceneFormat(self, hFormat: str, hide: bool) -> None:
"""Set the hard scene format pattern and hidden status."""
self._fmtHScene = hFormat.strip()
self._hideHScene = hide
return
def setSectionFormat(self, hFormat: str, hide: bool) -> None:
"""Set the section format pattern and hidden status."""
self._fmtSection = hFormat.strip()
@@ -484,7 +492,7 @@ class Tokenizer(ABC):
# Check Line Format
# =================
if aLine[0] == "[":
if aLine.startswith("["):
# Special Formats
# ===============
# Parse special formatting line. This must be a separate if
@@ -513,13 +521,13 @@ class Tokenizer(ABC):
]
continue
if aLine[0] == "%":
if aLine.startswith("%"):
# Comments
# ========
# All style comments are processed and the exact type exact
# style extracted. Ignored comments on the '%~' format are
# skipped completely.
if aLine[1] == "~":
if aLine.startswith("%~"):
continue
cStyle, cText, _ = processComment(aLine)
@@ -542,7 +550,7 @@ class Tokenizer(ABC):
if self._doComments and self._keepMarkdown:
tmpMarkdown.append(f"{aLine}\n")
elif aLine[0] == "@":
elif aLine.startswith("@"):
# Keywords
# ========
# Only valid keyword lines are parsed, and any ignored keywords
@@ -556,56 +564,70 @@ class Tokenizer(ABC):
if self._doKeywords and self._keepMarkdown:
tmpMarkdown.append(f"{aLine}\n")
elif aLine[:2] == "# ":
# Partition Headings
# ==================
elif aLine.startswith(("# ", "#! ")):
# Title or Partition Headings
# ===========================
# Main titles are allowed in any document, and they are always
# centred and start on a new page. For novel documents, we also
# reset all counters when such a title is encountered.
# Partition headings are only formatted in novel documents, and
# otherwise unchanged. Scene separators are disabled
# immediately after partitions, and scene numbers are reset.
isPlain = aLine.startswith("# ")
nHead += 1
tText = aLine[2:].strip()
tStyle = self.A_NONE
tType = self.T_HEAD1 if isPlain else self.T_TITLE
tStyle = self.A_NONE if isPlain else (self.A_PBB | self.A_CENTRE)
if self._isNovel:
tText = self._hFormatter.apply(self._fmtTitle, tText, nHead)
tStyle = self._titleStyle
if isPlain:
tText = self._hFormatter.apply(self._fmtTitle, tText, nHead)
tStyle = self._titleStyle
self._hFormatter.resetScene()
else:
self._hFormatter.resetAll()
self._noSep = True
self._hFormatter.resetScene()
self._tokens.append((
self.T_HEAD1, nHead, tText, [], tStyle
tType, nHead, tText, [], tStyle
))
if self._keepMarkdown:
tmpMarkdown.append(f"{aLine}\n")
elif aLine[:3] == "## ":
# Chapter Headings
# ================
elif aLine.startswith(("## ", "##! ")):
# (Unnumbered) Chapter Headings
# =============================
# Chapter headings are only formatted in novel documents, and
# otherwise unchanged. Chapter numbers are bumped before the
# heading is formatted. Scene separators are disabled
# immediately after chapter headings, and scene numbers are
# reset.
# reset. Unnumbered chapters are only meaningful in Novel docs,
# so if we're in a note, we keep them as level 2 headings.
isPlain = aLine.startswith("## ")
nHead += 1
tText = aLine[3:].strip()
tType = self.T_HEAD2
tStyle = self.A_NONE
tFormat = self._fmtChapter if isPlain else self._fmtUnNum
if self._isNovel:
self._hFormatter.incChapter()
tText = self._hFormatter.apply(self._fmtChapter, tText, nHead)
if isPlain:
self._hFormatter.incChapter()
tText = self._hFormatter.apply(tFormat, tText, nHead)
tType = self.T_HEAD2 if isPlain else self.T_UNNUM
tStyle = self._chapterStyle
self._noSep = True
self._hFormatter.resetScene()
self._tokens.append((
self.T_HEAD2, nHead, tText, [], tStyle
tType, nHead, tText, [], tStyle
))
if self._keepMarkdown:
tmpMarkdown.append(f"{aLine}\n")
elif aLine[:4] == "### ":
# Scene Headings
# ==============
elif aLine.startswith(("### ", "###! ")):
# (Hard) Scene Headings
# =====================
# Scene headings in novel documents are treated as centred
# separators if the formatting does not change the text. If the
# format is empty, the scene can be hidden or a blank paragraph
@@ -614,19 +636,22 @@ class Tokenizer(ABC):
# separators immediately after other titles. Scene numbers are
# always incremented before formatting. For notes, the heading
# is unchanged.
isPlain = aLine.startswith("### ")
nHead += 1
tText = aLine[4:].strip()
tType = self.T_HEAD3
tStyle = self.A_NONE
sHide = self._hideScene if isPlain else self._hideHScene
tFormat = self._fmtScene if isPlain else self._fmtHScene
if self._isNovel:
self._hFormatter.incScene()
tText = self._hFormatter.apply(self._fmtScene, tText, nHead)
tText = self._hFormatter.apply(tFormat, tText, nHead)
tStyle = self._sceneStyle
if tText == "":
tType = self.T_EMPTY if self._noSep or self._hideScene else self.T_SKIP
tType = self.T_EMPTY if self._noSep or sHide else self.T_SKIP
tStyle = self.A_NONE
elif tText == self._fmtScene:
elif tText == tFormat:
tText = "" if self._noSep else tText
tType = self.T_EMPTY if self._noSep else self.T_SEP
tStyle = self.A_NONE if self._noSep else self.A_CENTRE
@@ -638,7 +663,7 @@ class Tokenizer(ABC):
if self._keepMarkdown:
tmpMarkdown.append(f"{aLine}\n")
elif aLine[:5] == "#### ":
elif aLine.startswith("#### "):
# Section Headings
# =================
# Section headings in novel docs are treated as centred
@@ -664,46 +689,6 @@ class Tokenizer(ABC):
if self._keepMarkdown:
tmpMarkdown.append(f"{aLine}\n")
elif aLine[:3] == "#! ":
# Main Title
# ==========
# Main titles are allowed in any document, and they are always
# centred and start on a new page. For novel documents, we also
# reset all counters when such a title is encountered.
nHead += 1
self._tokens.append((
self.T_TITLE, nHead, aLine[3:].strip(), [], self.A_PBB | self.A_CENTRE
))
if self._isNovel:
self._noSep = True
self._hFormatter.resetAll()
if self._keepMarkdown:
tmpMarkdown.append(f"{aLine}\n")
elif aLine[:4] == "##! ":
# Unnumbered Chapter Headings
# ===========================
# Unnumbered chapters are only meaningful in Novel docs, so if
# we're in a note, we convert them to a plain level 2 heading.
nHead += 1
tText = aLine[4:].strip()
tType = self.T_HEAD2
tStyle = self.A_NONE
if self._isNovel:
tText = self._hFormatter.apply(self._fmtUnNum, tText, nHead)
tType = self.T_UNNUM
tStyle = self._chapterStyle
self._noSep = True
self._hFormatter.resetScene()
self._tokens.append((
tType, nHead, tText, [], tStyle
))
if self._keepMarkdown:
tmpMarkdown.append(f"{aLine}\n")
else:
# Text Lines
# ==========
+16 -15
View File
@@ -112,21 +112,22 @@ class nwDocAction(Enum):
BLOCK_TXT = 19
BLOCK_TTL = 20
BLOCK_UNN = 21
REPL_SNG = 22
REPL_DBL = 23
RM_BREAKS = 24
ALIGN_L = 25
ALIGN_C = 26
ALIGN_R = 27
INDENT_L = 28
INDENT_R = 29
SC_ITALIC = 30
SC_BOLD = 31
SC_STRIKE = 32
SC_ULINE = 33
SC_MARK = 34
SC_SUP = 35
SC_SUB = 36
BLOCK_HSC = 22
REPL_SNG = 23
REPL_DBL = 24
RM_BREAKS = 25
ALIGN_L = 26
ALIGN_C = 27
ALIGN_R = 28
INDENT_L = 29
INDENT_R = 30
SC_ITALIC = 31
SC_BOLD = 32
SC_STRIKE = 33
SC_ULINE = 34
SC_MARK = 35
SC_SUP = 36
SC_SUB = 37
# END Enum nwDocAction
+8
View File
@@ -744,6 +744,8 @@ class GuiDocEditor(QPlainTextEdit):
self._formatBlock(nwDocAction.BLOCK_TTL)
elif action == nwDocAction.BLOCK_UNN:
self._formatBlock(nwDocAction.BLOCK_UNN)
elif action == nwDocAction.BLOCK_HSC:
self._formatBlock(nwDocAction.BLOCK_HSC)
elif action == nwDocAction.REPL_SNG:
self._replaceQuotes("'", self._typSQuoteO, self._typSQuoteC)
elif action == nwDocAction.REPL_DBL:
@@ -1651,6 +1653,9 @@ class GuiDocEditor(QPlainTextEdit):
elif text.startswith("##! "):
temp = text[4:]
offset = 4
elif text.startswith("###! "):
temp = text[5:]
offset = 5
elif text.startswith(">> "):
temp = text[3:]
offset = 3
@@ -1702,6 +1707,9 @@ class GuiDocEditor(QPlainTextEdit):
elif action == nwDocAction.BLOCK_UNN:
text = f"##! {temp}"
offset -= 4
elif action == nwDocAction.BLOCK_HSC:
text = f"###! {temp}"
offset -= 5
elif action == nwDocAction.ALIGN_L:
text = f"{temp} <<"
elif action == nwDocAction.ALIGN_C:
+5 -1
View File
@@ -300,7 +300,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# so we force a return here
return
elif text.startswith(("# ", "#! ", "## ", "##! ", "### ", "#### ")):
elif text.startswith(("# ", "#! ", "## ", "##! ", "### ", "###! ", "#### ")):
self.setCurrentBlockState(self.BLOCK_TITLE)
if text.startswith("# "): # Heading 1
@@ -327,6 +327,10 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(0, 3, self._hStyles["head2h"])
self.setFormat(3, len(text), self._hStyles["header2"])
elif text.startswith("###! "): # Hard Scene
self.setFormat(0, 4, self._hStyles["head3h"])
self.setFormat(4, len(text), self._hStyles["header3"])
elif text.startswith("%"): # Comments
self.setCurrentBlockState(self.BLOCK_TEXT)
cStyle, _, cPos = processComment(text)
+6
View File
@@ -735,6 +735,12 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocAction.emit(nwDocAction.BLOCK_UNN)
)
# Format > Hard Scene
self.aFmtHardSc = self.fmtMenu.addAction(self.tr("Hard Scene"))
self.aFmtHardSc.triggered.connect(
lambda: self.requestDocAction.emit(nwDocAction.BLOCK_HSC)
)
# Format > Separator
self.fmtMenu.addSeparator()
+3
View File
@@ -106,6 +106,9 @@ def standardCounter(text: str) -> tuple[int, int, int]:
elif line[:4] == "##! ":
line = line[4:]
countPara = False
elif line[:5] == "###! ":
line = line[5:]
countPara = False
wCount += len(line.split())
cCount += len(line)
+6 -8
View File
@@ -610,16 +610,15 @@ class _DetailsWidget(QWidget):
item.setText(0, build.getLabel("headings"))
item.setText(1, "")
self.listView.addTopLevelItem(item)
entries = [
for key in [
"headings.fmtTitle", "headings.fmtChapter", "headings.fmtUnnumbered",
"headings.fmtScene", "headings.fmtSection"
]
for key in entries:
"headings.fmtScene", "headings.fmtHardScene", "headings.fmtSection"
]:
sub = QTreeWidgetItem()
sub.setText(0, build.getLabel(key))
sub.setText(1, hFmt.apply(build.getStr(key), title, 0))
item.addChild(sub)
for key in ["headings.hideScene", "headings.hideSection"]:
for key in ["headings.hideScene", "headings.hideHardScene", "headings.hideSection"]:
sub = QTreeWidgetItem()
sub.setText(0, build.getLabel(key))
sub.setIcon(1, on if build.getBool(key) else off)
@@ -630,11 +629,10 @@ class _DetailsWidget(QWidget):
item.setText(0, build.getLabel("text.grpContent"))
item.setText(1, "")
self.listView.addTopLevelItem(item)
entries = [
for key in [
"text.includeSynopsis", "text.includeComments",
"text.includeKeywords", "text.includeBodyText",
]
for key in entries:
]:
sub = QTreeWidgetItem()
sub.setText(0, build.getLabel(key))
sub.setIcon(1, on if build.getBool(key) else off)
+46 -9
View File
@@ -584,7 +584,8 @@ class _HeadingsTab(NScrollablePage):
EDIT_CHAPTER = 2
EDIT_UNNUM = 3
EDIT_SCENE = 4
EDIT_SECTION = 5
EDIT_HSCENE = 5
EDIT_SECTION = 6
def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None:
super().__init__(parent=buildMain)
@@ -603,7 +604,7 @@ class _HeadingsTab(NScrollablePage):
self.formatBox.setHorizontalSpacing(vSp)
# Title Heading
self.lblTitle = QLabel(self._build.getLabel("headings.fmtTitle"))
self.lblTitle = QLabel(self._build.getLabel("headings.fmtTitle"), self)
self.fmtTitle = QLineEdit("", self)
self.fmtTitle.setReadOnly(True)
self.btnTitle = QToolButton(self)
@@ -619,7 +620,7 @@ class _HeadingsTab(NScrollablePage):
self.formatBox.addLayout(wrapTitle, 0, 1)
# Chapter Heading
self.lblChapter = QLabel(self._build.getLabel("headings.fmtChapter"))
self.lblChapter = QLabel(self._build.getLabel("headings.fmtChapter"), self)
self.fmtChapter = QLineEdit("", self)
self.fmtChapter.setReadOnly(True)
self.btnChapter = QToolButton(self)
@@ -635,7 +636,7 @@ class _HeadingsTab(NScrollablePage):
self.formatBox.addLayout(wrapChapter, 1, 1)
# Unnumbered Chapter Heading
self.lblUnnumbered = QLabel(self._build.getLabel("headings.fmtUnnumbered"))
self.lblUnnumbered = QLabel(self._build.getLabel("headings.fmtUnnumbered"), self)
self.fmtUnnumbered = QLineEdit("", self)
self.fmtUnnumbered.setReadOnly(True)
self.btnUnnumbered = QToolButton(self)
@@ -652,7 +653,7 @@ class _HeadingsTab(NScrollablePage):
# Scene Heading
sceneHideTip = self._build.getLabel("headings.hideScene")
self.lblScene = QLabel(self._build.getLabel("headings.fmtScene"))
self.lblScene = QLabel(self._build.getLabel("headings.fmtScene"), self)
self.fmtScene = QLineEdit("", self)
self.fmtScene.setReadOnly(True)
self.btnScene = QToolButton(self)
@@ -677,9 +678,36 @@ class _HeadingsTab(NScrollablePage):
self.formatBox.addLayout(wrapScene, 3, 1)
self.formatBox.addLayout(wrapSceneHide, 3, 2)
# Alt Scene Heading
hardSceneHideTip = self._build.getLabel("headings.hideHardScene")
self.lblHScene = QLabel(self._build.getLabel("headings.fmtHardScene"), self)
self.fmtHScene = QLineEdit("", self)
self.fmtHScene.setReadOnly(True)
self.btnHScene = QToolButton(self)
self.btnHScene.setIcon(SHARED.theme.getIcon("edit"))
self.btnHScene.clicked.connect(lambda: self._editHeading(self.EDIT_HSCENE))
self.hdeHScene = QLabel(self.tr("Hide"))
self.hdeHScene.setToolTip(hardSceneHideTip)
self.swtHScene = NSwitch(self, height=iPx)
self.swtHScene.setToolTip(hardSceneHideTip)
wrapHScene = QHBoxLayout()
wrapHScene.addWidget(self.fmtHScene)
wrapHScene.addWidget(self.btnHScene)
wrapHScene.setSpacing(bSp)
wrapHSceneHide = QHBoxLayout()
wrapHSceneHide.addWidget(self.hdeHScene)
wrapHSceneHide.addWidget(self.swtHScene)
wrapHSceneHide.setSpacing(bSp)
self.formatBox.addWidget(self.lblHScene, 4, 0)
self.formatBox.addLayout(wrapHScene, 4, 1)
self.formatBox.addLayout(wrapHSceneHide, 4, 2)
# Section Heading
sectionHideTip = self._build.getLabel("headings.hideSection")
self.lblSection = QLabel(self._build.getLabel("headings.fmtSection"))
self.lblSection = QLabel(self._build.getLabel("headings.fmtSection"), self)
self.fmtSection = QLineEdit("", self)
self.fmtSection.setReadOnly(True)
self.btnSection = QToolButton(self)
@@ -700,9 +728,9 @@ class _HeadingsTab(NScrollablePage):
wrapSectionHide.addWidget(self.swtSection)
wrapSectionHide.setSpacing(bSp)
self.formatBox.addWidget(self.lblSection, 4, 0)
self.formatBox.addLayout(wrapSection, 4, 1)
self.formatBox.addLayout(wrapSectionHide, 4, 2)
self.formatBox.addWidget(self.lblSection, 5, 0)
self.formatBox.addLayout(wrapSection, 5, 1)
self.formatBox.addLayout(wrapSectionHide, 5, 2)
# Edit Form
# =========
@@ -830,8 +858,10 @@ class _HeadingsTab(NScrollablePage):
self.fmtChapter.setText(self._build.getStr("headings.fmtChapter"))
self.fmtUnnumbered.setText(self._build.getStr("headings.fmtUnnumbered"))
self.fmtScene.setText(self._build.getStr("headings.fmtScene"))
self.fmtHScene.setText(self._build.getStr("headings.fmtHardScene"))
self.fmtSection.setText(self._build.getStr("headings.fmtSection"))
self.swtScene.setChecked(self._build.getBool("headings.hideScene"))
self.swtHScene.setChecked(self._build.getBool("headings.hideHardScene"))
self.swtSection.setChecked(self._build.getBool("headings.hideSection"))
self.centerTitle.setChecked(self._build.getBool("headings.centerTitle"))
self.centerChapter.setChecked(self._build.getBool("headings.centerChapter"))
@@ -844,6 +874,7 @@ class _HeadingsTab(NScrollablePage):
def saveContent(self) -> None:
"""Save choices back into build object."""
self._build.setValue("headings.hideScene", self.swtScene.isChecked())
self._build.setValue("headings.hideHardScene", self.swtHScene.isChecked())
self._build.setValue("headings.hideSection", self.swtSection.isChecked())
self._build.setValue("headings.centerTitle", self.centerTitle.isChecked())
self._build.setValue("headings.centerChapter", self.centerChapter.isChecked())
@@ -881,6 +912,9 @@ class _HeadingsTab(NScrollablePage):
elif heading == self.EDIT_SCENE:
text = self.fmtScene.text()
label = self._build.getLabel("headings.fmtScene")
elif heading == self.EDIT_HSCENE:
text = self.fmtHScene.text()
label = self._build.getLabel("headings.fmtHardScene")
elif heading == self.EDIT_SECTION:
text = self.fmtSection.text()
label = self._build.getLabel("headings.fmtSection")
@@ -916,6 +950,9 @@ class _HeadingsTab(NScrollablePage):
elif heading == self.EDIT_SCENE:
self.fmtScene.setText(text)
self._build.setValue("headings.fmtScene", text)
elif heading == self.EDIT_HSCENE:
self.fmtHScene.setText(text)
self._build.setValue("headings.fmtHardScene", text)
elif heading == self.EDIT_SECTION:
self.fmtSection.setText(text)
self._build.setValue("headings.fmtSection", text)
+3 -3
View File
@@ -1,8 +1,8 @@
%%~name: Another Scene
%%~path: 6a2d6d5f4f401/bc0cbd2a407f3
%%~kind: NOVEL/DOCUMENT
%%~hash: b3f9e4b097f7041c77489876577014c435a8be0c
%%~date: Unknown/2023-08-25 16:51:57
%%~hash: d35e2629876e627b1a2418affa40d477022984f8
%%~date: Unknown/2024-03-11 22:56:28
### Another Scene
@pov: John
@@ -13,7 +13,7 @@ Adding more scenes to a chapter is as easy as adding more scene files with a lev
In fact, if you wish, you can add all the scenes in the chapter file too. All novelWriter cares about is the level of the headings and the order in which they appear.
### More Scenes
###! More Scenes
@pov: Jane
@focus: John
+2 -2
View File
@@ -196,8 +196,8 @@ def testCoreBuildSettings_BuildValues():
assert build.getFloat(floatSetting) == 2.5
# Check labels
assert build.getLabel(strSetting) == "Title Headings"
assert BuildSettings.getLabel(strSetting) == "Title Headings"
assert build.getLabel(strSetting) == "Partition Format"
assert BuildSettings.getLabel(strSetting) == "Partition Format"
# Pack the values
data = build.pack()
+13
View File
@@ -502,6 +502,19 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
assert index._itemIndex[tHandle]["T0001"].paraCount == 1 # type: ignore
assert index._itemIndex[tHandle]["T0001"].synopsis == "" # type: ignore
assert index.scanText(tHandle, (
"###! Hard Scene\n\n"
"We're no longer following Jane, but John!\n\n"
))
assert index._itemIndex[cHandle]["T0001"].references == {} # type: ignore
assert index._itemIndex[tHandle]["T0001"].level == "H3" # type: ignore
assert index._itemIndex[tHandle]["T0001"].line == 1 # type: ignore
assert index._itemIndex[tHandle]["T0001"].title == "Hard Scene" # type: ignore
assert index._itemIndex[tHandle]["T0001"].charCount == 51 # type: ignore
assert index._itemIndex[tHandle]["T0001"].wordCount == 9 # type: ignore
assert index._itemIndex[tHandle]["T0001"].paraCount == 1 # type: ignore
assert index._itemIndex[tHandle]["T0001"].synopsis == "" # type: ignore
# Page wo/Title
# =============
+32 -12
View File
@@ -47,6 +47,7 @@ def testCoreToken_Setters(mockGUI):
assert tokens._fmtChapter == nwHeadFmt.TITLE
assert tokens._fmtUnNum == nwHeadFmt.TITLE
assert tokens._fmtScene == nwHeadFmt.TITLE
assert tokens._fmtHScene == nwHeadFmt.TITLE
assert tokens._fmtSection == nwHeadFmt.TITLE
assert tokens._textFont == "Serif"
assert tokens._textSize == 11
@@ -74,6 +75,7 @@ def testCoreToken_Setters(mockGUI):
tokens.setChapterFormat(f"C: {nwHeadFmt.TITLE}")
tokens.setUnNumberedFormat(f"U: {nwHeadFmt.TITLE}")
tokens.setSceneFormat(f"S: {nwHeadFmt.TITLE}", True)
tokens.setHardSceneFormat(f"H: {nwHeadFmt.TITLE}", True)
tokens.setSectionFormat(f"X: {nwHeadFmt.TITLE}", True)
tokens.setFont("Monospace", 10, True)
tokens.setLineHeight(2.0)
@@ -97,6 +99,7 @@ def testCoreToken_Setters(mockGUI):
assert tokens._fmtChapter == f"C: {nwHeadFmt.TITLE}"
assert tokens._fmtUnNum == f"U: {nwHeadFmt.TITLE}"
assert tokens._fmtScene == f"S: {nwHeadFmt.TITLE}"
assert tokens._fmtHScene == f"H: {nwHeadFmt.TITLE}"
assert tokens._fmtSection == f"X: {nwHeadFmt.TITLE}"
assert tokens._textFont == "Monospace"
assert tokens._textSize == 10
@@ -1741,7 +1744,7 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
"# Title Two\n\n"
"### Scene Three\n\n"
"Text\n\n"
"### Scene Four\n\n"
"###! Scene Four\n\n"
"Text\n\n"
)
md.setTitleFormat(f"T: {nwHeadFmt.TITLE}")
@@ -1749,13 +1752,14 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
md.setSectionFormat("", True)
# Static Separator
md.setSceneFormat("* * *", False)
md.setSceneFormat("~", False)
md.setHardSceneFormat("* * *", False)
md.tokenizeText()
md.doConvert()
assert md.result == (
"# T: Title One\n\n"
"Text\n\n"
"* * *\n\n"
"~\n\n"
"Text\n\n"
"# T: Title Two\n\n"
"Text\n\n"
@@ -1765,6 +1769,7 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
# Scene Title Formatted
md.setSceneFormat(f"S: {nwHeadFmt.TITLE}", False)
md.setHardSceneFormat(f"H: {nwHeadFmt.TITLE}", False)
md.tokenizeText()
md.doConvert()
assert md.result == (
@@ -1776,7 +1781,7 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
"# T: Title Two\n\n"
"### S: Scene Three\n\n"
"Text\n\n"
"### S: Scene Four\n\n"
"### H: Scene Four\n\n"
"Text\n\n"
)
@@ -1793,7 +1798,7 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
"## Chapter Two\n\n"
"### Scene Three\n\n"
"Text\n\n"
"### Scene Four\n\n"
"###! Scene Four\n\n"
"Text\n\n"
)
md.setTitleFormat(f"T: {nwHeadFmt.TITLE}")
@@ -1802,6 +1807,7 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
# Static Separator
md.setSceneFormat("* * *", False)
md.setHardSceneFormat("* * *", False)
md.tokenizeText()
md.doConvert()
assert md.result == (
@@ -1818,6 +1824,7 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
# Scene Title Formatted
md.setSceneFormat(f"S: {nwHeadFmt.TITLE}", False)
md.setHardSceneFormat(f"H: {nwHeadFmt.TITLE}", False)
md.tokenizeText()
md.doConvert()
assert md.result == (
@@ -1830,15 +1837,18 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
"## C: Chapter Two\n\n"
"### S: Scene Three\n\n"
"Text\n\n"
"### S: Scene Four\n\n"
"### H: Scene Four\n\n"
"Text\n\n"
)
# Counter Handling, Novel Titles
# ==============================
# This also checks that only numbered chapters bump the counter
md._text = (
"#! Novel One\n\n"
"##! Prologue\n\n"
"Text\n\n"
"## Chapter One\n\n"
"### Scene One\n\n"
"Text\n\n"
@@ -1847,9 +1857,11 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
"## Chapter Two\n\n"
"### Scene Three\n\n"
"Text\n\n"
"### Scene Four\n\n"
"###! Scene Four\n\n"
"Text\n\n"
"#! Novel Two\n\n"
"##! Prologue\n\n"
"Text\n\n"
"## Chapter One\n\n"
"### Scene One\n\n"
"Text\n\n"
@@ -1858,13 +1870,17 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
"## Chapter Two\n\n"
"### Scene Three\n\n"
"Text\n\n"
"### Scene Four\n\n"
"###! Scene Four\n\n"
"Text\n\n"
)
md.setTitleFormat(f"T: {nwHeadFmt.TITLE}")
md.setChapterFormat(f"C {nwHeadFmt.CH_NUM}: {nwHeadFmt.TITLE}")
md.setSceneFormat(f"S {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM} ({nwHeadFmt.SC_ABS}): "
f"{nwHeadFmt.TITLE}", False)
md.setSceneFormat(
f"S {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM} ({nwHeadFmt.SC_ABS}): {nwHeadFmt.TITLE}", False
)
md.setHardSceneFormat(
f"H {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM} ({nwHeadFmt.SC_ABS}): {nwHeadFmt.TITLE}", False
)
md.setSectionFormat("", True)
# Two Novel Format
@@ -1872,6 +1888,8 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
md.doConvert()
assert md.result == (
"# Novel One\n\n"
"## Prologue\n\n"
"Text\n\n"
"## C 1: Chapter One\n\n"
"### S 1.1 (1): Scene One\n\n"
"Text\n\n"
@@ -1880,9 +1898,11 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
"## C 2: Chapter Two\n\n"
"### S 2.1 (3): Scene Three\n\n"
"Text\n\n"
"### S 2.2 (4): Scene Four\n\n"
"### H 2.2 (4): Scene Four\n\n"
"Text\n\n"
"# Novel Two\n\n"
"## Prologue\n\n"
"Text\n\n"
"## C 1: Chapter One\n\n"
"### S 1.1 (1): Scene One\n\n"
"Text\n\n"
@@ -1891,7 +1911,7 @@ def testCoreToken_HeaderCounterAndVisibility(mockGUI):
"## C 2: Chapter Two\n\n"
"### S 2.1 (3): Scene Three\n\n"
"Text\n\n"
"### S 2.2 (4): Scene Four\n\n"
"### H 2.2 (4): Scene Four\n\n"
"Text\n\n"
)
+15 -1
View File
@@ -1134,13 +1134,20 @@ def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, projPath, ipsumText
assert nwGUI.docEditor.getText() == "Title\n\n"
assert nwGUI.docEditor.getCursorPosition() == 2
# Strip Unnumbered CHapter
# Strip Unnumbered Chapter
nwGUI.docEditor.replaceText("##! Title\n\n")
nwGUI.docEditor.setCursorPosition(5)
assert nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True
assert nwGUI.docEditor.getText() == "Title\n\n"
assert nwGUI.docEditor.getCursorPosition() == 1
# Strip Hard Scene
nwGUI.docEditor.replaceText("###! Title\n\n")
nwGUI.docEditor.setCursorPosition(6)
assert nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True
assert nwGUI.docEditor.getText() == "Title\n\n"
assert nwGUI.docEditor.getCursorPosition() == 1
# Strip Text
nwGUI.docEditor.replaceText("Generic text\n\n")
nwGUI.docEditor.setCursorPosition(5)
@@ -1302,6 +1309,13 @@ def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, projPath, ipsumText
assert nwGUI.docEditor.getText() == "##! Some text\n\n"
assert nwGUI.docEditor.getCursorPosition() == 9
# Hard Scene
nwGUI.docEditor.replaceText("Some text\n\n")
nwGUI.docEditor.setCursorPosition(5)
assert nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_HSC) is True
assert nwGUI.docEditor.getText() == "###! Some text\n\n"
assert nwGUI.docEditor.getCursorPosition() == 10
# Left Indent
nwGUI.docEditor.replaceText("Some text\n\n")
nwGUI.docEditor.setCursorPosition(5)
+5
View File
@@ -126,6 +126,11 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
fmtStr = "##! Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:105] == fmtStr
# Hard Scene
nwGUI.mainMenu.aFmtHardSc.activate(QAction.Trigger)
fmtStr = "###! Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:106] == fmtStr
# Clear Format
nwGUI.mainMenu.aFmtNoFormat.activate(QAction.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
@@ -327,12 +327,14 @@ def testBuildSettings_Headings(qtbot: QtBot, nwGUI: GuiMain):
chTitle = f"Chapter: {nwHeadFmt.TITLE}"
unTitle = f"Interlude: {nwHeadFmt.TITLE}"
scTitle = f"Scene: {nwHeadFmt.TITLE}"
shTitle = f"Hard Scene: {nwHeadFmt.TITLE}"
sxTitle = f"Section: {nwHeadFmt.TITLE}"
build.setValue("headings.fmtTitle", ttTitle)
build.setValue("headings.fmtChapter", chTitle)
build.setValue("headings.fmtUnnumbered", unTitle)
build.setValue("headings.fmtScene", scTitle)
build.setValue("headings.fmtHardScene", shTitle)
build.setValue("headings.fmtSection", sxTitle)
build.setValue("headings.hideScene", False)
build.setValue("headings.hideSection", False)
@@ -351,6 +353,7 @@ def testBuildSettings_Headings(qtbot: QtBot, nwGUI: GuiMain):
assert headTab.fmtChapter.text() == chTitle
assert headTab.fmtUnnumbered.text() == unTitle
assert headTab.fmtScene.text() == scTitle
assert headTab.fmtHScene.text() == shTitle
assert headTab.fmtSection.text() == sxTitle
assert headTab.swtScene.isChecked() is False
assert headTab.swtSection.isChecked() is False
@@ -396,6 +399,14 @@ def testBuildSettings_Headings(qtbot: QtBot, nwGUI: GuiMain):
headTab.btnApply.click()
assert headTab.editTextBox.isEnabled() is False
# Hard Scene
headTab.btnHScene.click()
assert headTab._editing == headTab.EDIT_HSCENE
assert headTab.editTextBox.isEnabled() is True
assert headTab.editTextBox.toPlainText() == shTitle
headTab.btnApply.click()
assert headTab.editTextBox.isEnabled() is False
# Section
headTab.btnSection.click()
assert headTab._editing == headTab.EDIT_SECTION
@@ -455,6 +466,11 @@ def testBuildSettings_Headings(qtbot: QtBot, nwGUI: GuiMain):
headTab.btnApply.click()
assert build.getStr("headings.fmtScene") == nwHeadFmt.TITLE
headTab.btnHScene.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtHardScene") == nwHeadFmt.TITLE
headTab.btnSection.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()