Display headings with a line break on a single line

This commit is contained in:
Veronica Berglyd Olsen
2025-06-18 19:26:41 +02:00
parent 2176dd2020
commit 92ef020342
6 changed files with 68 additions and 68 deletions
+2 -2
View File
@@ -44,7 +44,7 @@ from novelwriter.common import (
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher, NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
formatTimeStamp, processDialogSymbols, simplified formatTimeStamp, processDialogSymbols, simplified
) )
from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode from novelwriter.constants import nwFiles, nwQuotes, nwUnicode
from novelwriter.error import formatException, logException from novelwriter.error import formatException, logException
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -892,7 +892,7 @@ class Config:
""" """
self.splashMessage(f"Initialising {kind} font: {font.family()}") self.splashMessage(f"Initialising {kind} font: {font.family()}")
metrics = QFontMetrics(font) metrics = QFontMetrics(font)
for char in nwHtmlUnicode.U_TO_H.keys(): for char in nwUnicode.UI_SYMBOLS:
if not metrics.inFont(char): # type: ignore if not metrics.inFont(char): # type: ignore
logger.warning("No glyph U+%04x in font", ord(char)) # pragma: no cover logger.warning("No glyph U+%04x in font", ord(char)) # pragma: no cover
return return
+12 -19
View File
@@ -586,21 +586,14 @@ class nwUnicode:
U_TIMES = "\u00d7" # Multiplication sign U_TIMES = "\u00d7" # Multiplication sign
U_DIVIDE = "\u00f7" # Division sign U_DIVIDE = "\u00f7" # Division sign
# Arrows
U_UTRI = "\u25b2" # Up-pointing triangle
U_UTRIS = "\u25b4" # Up-pointing triangle, small
U_RTRI = "\u25b6" # Right-pointing triangle
U_RTRIS = "\u25b8" # Right-pointing triangle, small
U_DTRI = "\u25bc" # Down-pointing triangle
U_DTRIS = "\u25be" # Down-pointing triangle, small
U_LTRI = "\u25c0" # Left-pointing triangle
U_LTRIS = "\u25c2" # Left-pointing triangle, small
# Special # Special
U_UNKN = "\ufffd" # Unknown character U_UNKN = "\ufffd" # Unknown character
U_NAC1 = "\ufffe" # Not a character U_NAC1 = "\ufffe" # Not a character
U_NAC2 = "\uffff" # Not a character U_NAC2 = "\uffff" # Not a character
# Placeholders
U_LBREAK = "\u21b2" # Downwards Arrow With Tip Leftwards
# HTML Equivalents # HTML Equivalents
# ================ # ================
@@ -655,15 +648,15 @@ class nwUnicode:
H_TIMES = "×" H_TIMES = "×"
H_DIVIDE = "÷" H_DIVIDE = "÷"
# Arrows # Unicode symbols expected to be used on the UI
H_UTRI = "▲" UI_SYMBOLS: Final[list[str]] = [
H_UTRIS = "▴" U_QUOT, U_APOS, U_LAQUO, U_RAQUO, U_LSQUO, U_RSQUO, U_SBQUO, U_SUQUO,
H_RTRI = "▶" U_LDQUO, U_RDQUO, U_BDQUO, U_UDQUO, U_LSAQUO, U_RSAQUO, U_BDRQUO,
H_RTRIS = "▸" U_LCQUO, U_RCQUO, U_LWCQUO, U_RWCQUO, U_FGDASH, U_ENDASH, U_EMDASH,
H_DTRI = "▼" U_HBAR, U_HELLIP, U_MAPOS, U_PRIME, U_DPRIME, U_NBSP, U_THSP, U_THNBSP,
H_DTRIS = "▾" U_ENSP, U_EMSP, U_MMSP, U_CHECK, U_CROSS, U_BULL, U_TRBULL, U_HYBULL,
H_LTRI = "◀" U_FLOWER, U_PERMIL, U_DEGREE, U_MINUS, U_TIMES, U_DIVIDE, U_LBREAK,
H_LTRIS = "◂" ]
class nwHtmlUnicode: class nwHtmlUnicode:
+23 -17
View File
@@ -1168,12 +1168,18 @@ class Tokenizer(ABC):
class HeadingFormatter: class HeadingFormatter:
def __init__(self, project: NWProject) -> None: def __init__(
self,
project: NWProject,
chapter: int = 0,
scene: int = 0,
absolute: int = 0,
) -> None:
self._project = project self._project = project
self._handle = None self._handle = None
self._chCount = 0 self._chapter = chapter
self._scChCount = 0 self._scene = scene
self._scAbsCount = 0 self._absolute = absolute
return return
def setHandle(self, tHandle: str | None) -> None: def setHandle(self, tHandle: str | None) -> None:
@@ -1183,42 +1189,42 @@ class HeadingFormatter:
def incChapter(self) -> None: def incChapter(self) -> None:
"""Increment the chapter counter.""" """Increment the chapter counter."""
self._chCount += 1 self._chapter += 1
return return
def incScene(self) -> None: def incScene(self) -> None:
"""Increment the scene counters.""" """Increment the scene counters."""
self._scChCount += 1 self._scene += 1
self._scAbsCount += 1 self._absolute += 1
return return
def resetAll(self) -> None: def resetAll(self) -> None:
"""Reset all counters.""" """Reset all counters."""
self._chCount = 0 self._chapter = 0
self._scChCount = 0 self._scene = 0
self._scAbsCount = 0 self._absolute = 0
return return
def resetScene(self) -> None: def resetScene(self) -> None:
"""Reset the chapter scene counter.""" """Reset the chapter scene counter."""
self._scChCount = 0 self._scene = 0
return return
def apply(self, hFormat: str, text: str, nHead: int) -> str: def apply(self, hFormat: str, text: str, nHead: int) -> str:
"""Apply formatting to a specific heading.""" """Apply formatting to a specific heading."""
hFormat = hFormat.replace(nwHeadFmt.TITLE, text) hFormat = hFormat.replace(nwHeadFmt.TITLE, text)
hFormat = hFormat.replace(nwHeadFmt.BR, "\n") hFormat = hFormat.replace(nwHeadFmt.BR, "\n")
hFormat = hFormat.replace(nwHeadFmt.CH_NUM, str(self._chCount)) hFormat = hFormat.replace(nwHeadFmt.CH_NUM, str(self._chapter))
hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scChCount)) hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scene))
hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._scAbsCount)) hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._absolute))
if nwHeadFmt.CH_WORD in hFormat: if nwHeadFmt.CH_WORD in hFormat:
chWord = self._project.localLookup(self._chCount) chWord = self._project.localLookup(self._chapter)
hFormat = hFormat.replace(nwHeadFmt.CH_WORD, chWord) hFormat = hFormat.replace(nwHeadFmt.CH_WORD, chWord)
if nwHeadFmt.CH_ROML in hFormat: if nwHeadFmt.CH_ROML in hFormat:
chRom = numberToRoman(self._chCount, toLower=True) chRom = numberToRoman(self._chapter, toLower=True)
hFormat = hFormat.replace(nwHeadFmt.CH_ROML, chRom) hFormat = hFormat.replace(nwHeadFmt.CH_ROML, chRom)
if nwHeadFmt.CH_ROMU in hFormat: if nwHeadFmt.CH_ROMU in hFormat:
chRom = numberToRoman(self._chCount, toLower=False) chRom = numberToRoman(self._chapter, toLower=False)
hFormat = hFormat.replace(nwHeadFmt.CH_ROMU, chRom) hFormat = hFormat.replace(nwHeadFmt.CH_ROMU, chRom)
if nwHeadFmt.CHAR_POV in hFormat or nwHeadFmt.CHAR_FOCUS in hFormat: if nwHeadFmt.CHAR_POV in hFormat or nwHeadFmt.CHAR_FOCUS in hFormat:
+4 -7
View File
@@ -43,7 +43,7 @@ from PyQt6.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import fuzzyTime, qtLambda from novelwriter.common import fuzzyTime, qtLambda
from novelwriter.constants import nwLabels, nwStats, trStats from novelwriter.constants import nwHeadFmt, nwLabels, nwStats, nwUnicode, trStats
from novelwriter.core.buildsettings import BuildCollection, BuildSettings from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
@@ -595,11 +595,7 @@ class _DetailsWidget(QWidget):
item.addChild(sub) item.addChild(sub)
# Headings # Headings
hFmt = HeadingFormatter(SHARED.project) hFmt = HeadingFormatter(SHARED.project, 7, 5, 23)
hFmt.incChapter()
hFmt.incScene()
hFmt.resetScene()
hFmt.incScene()
title = self.tr("Title") title = self.tr("Title")
hidden = self.tr("Hidden") hidden = self.tr("Hidden")
@@ -620,7 +616,8 @@ class _DetailsWidget(QWidget):
if build.getBool(hHide): if build.getBool(hHide):
sub.setText(1, f"[{hidden}]") sub.setText(1, f"[{hidden}]")
else: else:
sub.setText(1, hFmt.apply(build.getStr(hFormat), title, 0)) preview = build.getStr(hFormat).replace(nwHeadFmt.BR, nwUnicode.U_LBREAK)
sub.setText(1, hFmt.apply(preview, title, 0))
item.addChild(sub) item.addChild(sub)
# Text Content # Text Content
+19 -15
View File
@@ -38,7 +38,7 @@ from PyQt6.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import describeFont, fontMatcher, qtAddAction, qtLambda from novelwriter.common import describeFont, fontMatcher, qtAddAction, qtLambda
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, trConst from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwUnicode, trConst
from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.core.buildsettings import BuildSettings, FilterMode
from novelwriter.extensions.configlayout import ( from novelwriter.extensions.configlayout import (
NColorLabel, NFixedPage, NScrollableForm, NScrollablePage NColorLabel, NFixedPage, NScrollableForm, NScrollablePage
@@ -795,12 +795,15 @@ class _HeadingsTab(NScrollablePage):
def loadContent(self) -> None: def loadContent(self) -> None:
"""Populate the widgets.""" """Populate the widgets."""
self.fmtPart.setText(self._build.getStr("headings.fmtPart")) def fmtBreak(text: str) -> str:
self.fmtChapter.setText(self._build.getStr("headings.fmtChapter")) return text.replace(nwHeadFmt.BR, nwUnicode.U_LBREAK)
self.fmtUnnumbered.setText(self._build.getStr("headings.fmtUnnumbered"))
self.fmtScene.setText(self._build.getStr("headings.fmtScene")) self.fmtPart.setText(fmtBreak(self._build.getStr("headings.fmtPart")))
self.fmtAScene.setText(self._build.getStr("headings.fmtAltScene")) self.fmtChapter.setText(fmtBreak(self._build.getStr("headings.fmtChapter")))
self.fmtSection.setText(self._build.getStr("headings.fmtSection")) self.fmtUnnumbered.setText(fmtBreak(self._build.getStr("headings.fmtUnnumbered")))
self.fmtScene.setText(fmtBreak(self._build.getStr("headings.fmtScene")))
self.fmtAScene.setText(fmtBreak(self._build.getStr("headings.fmtAltScene")))
self.fmtSection.setText(fmtBreak(self._build.getStr("headings.fmtSection")))
self.swtPart.setChecked(self._build.getBool("headings.hidePart")) self.swtPart.setChecked(self._build.getBool("headings.hidePart"))
self.swtChapter.setChecked(self._build.getBool("headings.hideChapter")) self.swtChapter.setChecked(self._build.getBool("headings.hideChapter"))
@@ -880,7 +883,7 @@ class _HeadingsTab(NScrollablePage):
text = "" text = ""
label = self.tr("None") label = self.tr("None")
self.editTextBox.setPlainText(text.replace(nwHeadFmt.BR, "\n")) self.editTextBox.setPlainText(text.replace(nwUnicode.U_LBREAK, "\n"))
self.lblEditForm.setText(self.tr("Editing: {0}").format(label)) self.lblEditForm.setText(self.tr("Editing: {0}").format(label))
return return
@@ -893,25 +896,26 @@ class _HeadingsTab(NScrollablePage):
def _saveFormat(self) -> None: def _saveFormat(self) -> None:
"""Save the format from the edit text box.""" """Save the format from the edit text box."""
heading = self._editing heading = self._editing
text = self.editTextBox.toPlainText().strip().replace("\n", nwHeadFmt.BR) text = self.editTextBox.toPlainText().strip().replace("\n", nwUnicode.U_LBREAK)
value = text.replace(nwUnicode.U_LBREAK, nwHeadFmt.BR)
if heading == self.EDIT_TITLE: if heading == self.EDIT_TITLE:
self.fmtPart.setText(text) self.fmtPart.setText(text)
self._build.setValue("headings.fmtPart", text) self._build.setValue("headings.fmtPart", value)
elif heading == self.EDIT_CHAPTER: elif heading == self.EDIT_CHAPTER:
self.fmtChapter.setText(text) self.fmtChapter.setText(text)
self._build.setValue("headings.fmtChapter", text) self._build.setValue("headings.fmtChapter", value)
elif heading == self.EDIT_UNNUM: elif heading == self.EDIT_UNNUM:
self.fmtUnnumbered.setText(text) self.fmtUnnumbered.setText(text)
self._build.setValue("headings.fmtUnnumbered", text) self._build.setValue("headings.fmtUnnumbered", value)
elif heading == self.EDIT_SCENE: elif heading == self.EDIT_SCENE:
self.fmtScene.setText(text) self.fmtScene.setText(text)
self._build.setValue("headings.fmtScene", text) self._build.setValue("headings.fmtScene", value)
elif heading == self.EDIT_HSCENE: elif heading == self.EDIT_HSCENE:
self.fmtAScene.setText(text) self.fmtAScene.setText(text)
self._build.setValue("headings.fmtAltScene", text) self._build.setValue("headings.fmtAltScene", value)
elif heading == self.EDIT_SECTION: elif heading == self.EDIT_SECTION:
self.fmtSection.setText(text) self.fmtSection.setText(text)
self._build.setValue("headings.fmtSection", text) self._build.setValue("headings.fmtSection", value)
else: else:
return return
+8 -8
View File
@@ -1654,7 +1654,7 @@ def testFmtToken_ProcessHeaders(mockGUI):
# H2: Chapter Word Number # H2: Chapter Word Number
tokens._text = "## Chapter\n" tokens._text = "## Chapter\n"
tokens.setChapterFormat(f"Chapter {nwHeadFmt.CH_WORD}") tokens.setChapterFormat(f"Chapter {nwHeadFmt.CH_WORD}")
tokens._hFormatter._chCount = 0 tokens._hFormatter._chapter = 0
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._blocks == [ assert tokens._blocks == [
(BlockTyp.HEAD1, TM1, "Chapter One", [], BlockFmt.PBB), (BlockTyp.HEAD1, TM1, "Chapter One", [], BlockFmt.PBB),
@@ -1728,8 +1728,8 @@ def testFmtToken_ProcessHeaders(mockGUI):
# H3: Scene w/Absolute Number # H3: Scene w/Absolute Number
tokens._text = "### A Scene\n" tokens._text = "### A Scene\n"
tokens.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}", False) tokens.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}", False)
tokens._hFormatter._scAbsCount = 0 tokens._hFormatter._scene = 0
tokens._hFormatter._scChCount = 0 tokens._hFormatter._absolute = 0
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._blocks == [ assert tokens._blocks == [
(BlockTyp.HEAD2, TM1, "Scene 1", [], BlockFmt.NONE), (BlockTyp.HEAD2, TM1, "Scene 1", [], BlockFmt.NONE),
@@ -1738,8 +1738,8 @@ def testFmtToken_ProcessHeaders(mockGUI):
# H3: Scene w/Chapter Number # H3: Scene w/Chapter Number
tokens._text = "### A Scene\n" tokens._text = "### A Scene\n"
tokens.setSceneFormat(f"Scene {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM}", False) tokens.setSceneFormat(f"Scene {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM}", False)
tokens._hFormatter._scAbsCount = 0 tokens._hFormatter._scene = 1
tokens._hFormatter._scChCount = 1 tokens._hFormatter._absolute = 0
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._blocks == [ assert tokens._blocks == [
(BlockTyp.HEAD2, TM1, "Scene 3.2", [], BlockFmt.NONE), (BlockTyp.HEAD2, TM1, "Scene 3.2", [], BlockFmt.NONE),
@@ -2544,9 +2544,9 @@ def testFmtToken_HeadingFormatter(fncPath, mockGUI, mockRnd):
# Special Formats # Special Formats
# =============== # ===============
formatter._chCount = 2 formatter._chapter = 2
formatter._scChCount = 3 formatter._scene = 3
formatter._scAbsCount = 8 formatter._absolute = 8
# Chapter Number Word # Chapter Number Word
cFormat = f"Chapter {nwHeadFmt.CH_WORD}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.TITLE}" cFormat = f"Chapter {nwHeadFmt.CH_WORD}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.TITLE}"