diff --git a/novelwriter/config.py b/novelwriter/config.py index ac403fdb..0c6d43bb 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -44,7 +44,7 @@ from novelwriter.common import ( NWConfigParser, checkInt, checkPath, describeFont, fontMatcher, formatTimeStamp, processDialogSymbols, simplified ) -from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode +from novelwriter.constants import nwFiles, nwQuotes, nwUnicode from novelwriter.error import formatException, logException if TYPE_CHECKING: @@ -892,7 +892,7 @@ class Config: """ self.splashMessage(f"Initialising {kind} font: {font.family()}") metrics = QFontMetrics(font) - for char in nwHtmlUnicode.U_TO_H.keys(): + for char in nwUnicode.UI_SYMBOLS: if not metrics.inFont(char): # type: ignore logger.warning("No glyph U+%04x in font", ord(char)) # pragma: no cover return diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 4fde6f38..191bcc1c 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -586,21 +586,14 @@ class nwUnicode: U_TIMES = "\u00d7" # Multiplication 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 U_UNKN = "\ufffd" # Unknown character U_NAC1 = "\ufffe" # Not a character U_NAC2 = "\uffff" # Not a character + # Placeholders + U_LBREAK = "\u21b2" # Downwards Arrow With Tip Leftwards + # HTML Equivalents # ================ @@ -655,15 +648,15 @@ class nwUnicode: H_TIMES = "×" H_DIVIDE = "÷" - # Arrows - H_UTRI = "▲" - H_UTRIS = "▴" - H_RTRI = "▶" - H_RTRIS = "▸" - H_DTRI = "▼" - H_DTRIS = "▾" - H_LTRI = "◀" - H_LTRIS = "◂" + # Unicode symbols expected to be used on the UI + UI_SYMBOLS: Final[list[str]] = [ + U_QUOT, U_APOS, U_LAQUO, U_RAQUO, U_LSQUO, U_RSQUO, U_SBQUO, U_SUQUO, + U_LDQUO, U_RDQUO, U_BDQUO, U_UDQUO, U_LSAQUO, U_RSAQUO, U_BDRQUO, + U_LCQUO, U_RCQUO, U_LWCQUO, U_RWCQUO, U_FGDASH, U_ENDASH, U_EMDASH, + U_HBAR, U_HELLIP, U_MAPOS, U_PRIME, U_DPRIME, U_NBSP, U_THSP, U_THNBSP, + U_ENSP, U_EMSP, U_MMSP, U_CHECK, U_CROSS, U_BULL, U_TRBULL, U_HYBULL, + U_FLOWER, U_PERMIL, U_DEGREE, U_MINUS, U_TIMES, U_DIVIDE, U_LBREAK, + ] class nwHtmlUnicode: diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 744c087b..e6d2d563 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -1168,12 +1168,18 @@ class Tokenizer(ABC): 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._handle = None - self._chCount = 0 - self._scChCount = 0 - self._scAbsCount = 0 + self._chapter = chapter + self._scene = scene + self._absolute = absolute return def setHandle(self, tHandle: str | None) -> None: @@ -1183,42 +1189,42 @@ class HeadingFormatter: def incChapter(self) -> None: """Increment the chapter counter.""" - self._chCount += 1 + self._chapter += 1 return def incScene(self) -> None: """Increment the scene counters.""" - self._scChCount += 1 - self._scAbsCount += 1 + self._scene += 1 + self._absolute += 1 return def resetAll(self) -> None: """Reset all counters.""" - self._chCount = 0 - self._scChCount = 0 - self._scAbsCount = 0 + self._chapter = 0 + self._scene = 0 + self._absolute = 0 return def resetScene(self) -> None: """Reset the chapter scene counter.""" - self._scChCount = 0 + self._scene = 0 return def apply(self, hFormat: str, text: str, nHead: int) -> str: """Apply formatting to a specific heading.""" hFormat = hFormat.replace(nwHeadFmt.TITLE, text) hFormat = hFormat.replace(nwHeadFmt.BR, "\n") - hFormat = hFormat.replace(nwHeadFmt.CH_NUM, str(self._chCount)) - hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scChCount)) - hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._scAbsCount)) + hFormat = hFormat.replace(nwHeadFmt.CH_NUM, str(self._chapter)) + hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scene)) + hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._absolute)) 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) 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) 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) if nwHeadFmt.CHAR_POV in hFormat or nwHeadFmt.CHAR_FOCUS in hFormat: diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 098a3f33..f1bfb2dc 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -43,7 +43,7 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED 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.docbuild import NWBuildDocument from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog @@ -595,11 +595,7 @@ class _DetailsWidget(QWidget): item.addChild(sub) # Headings - hFmt = HeadingFormatter(SHARED.project) - hFmt.incChapter() - hFmt.incScene() - hFmt.resetScene() - hFmt.incScene() + hFmt = HeadingFormatter(SHARED.project, 7, 5, 23) title = self.tr("Title") hidden = self.tr("Hidden") @@ -620,7 +616,8 @@ class _DetailsWidget(QWidget): if build.getBool(hHide): sub.setText(1, f"[{hidden}]") 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) # Text Content diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index e5ddc66b..6fd4baa5 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -38,7 +38,7 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED 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.extensions.configlayout import ( NColorLabel, NFixedPage, NScrollableForm, NScrollablePage @@ -795,12 +795,15 @@ class _HeadingsTab(NScrollablePage): def loadContent(self) -> None: """Populate the widgets.""" - self.fmtPart.setText(self._build.getStr("headings.fmtPart")) - 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.fmtAScene.setText(self._build.getStr("headings.fmtAltScene")) - self.fmtSection.setText(self._build.getStr("headings.fmtSection")) + def fmtBreak(text: str) -> str: + return text.replace(nwHeadFmt.BR, nwUnicode.U_LBREAK) + + self.fmtPart.setText(fmtBreak(self._build.getStr("headings.fmtPart"))) + self.fmtChapter.setText(fmtBreak(self._build.getStr("headings.fmtChapter"))) + 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.swtChapter.setChecked(self._build.getBool("headings.hideChapter")) @@ -880,7 +883,7 @@ class _HeadingsTab(NScrollablePage): text = "" 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)) return @@ -893,25 +896,26 @@ class _HeadingsTab(NScrollablePage): def _saveFormat(self) -> None: """Save the format from the edit text box.""" 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: self.fmtPart.setText(text) - self._build.setValue("headings.fmtPart", text) + self._build.setValue("headings.fmtPart", value) elif heading == self.EDIT_CHAPTER: self.fmtChapter.setText(text) - self._build.setValue("headings.fmtChapter", text) + self._build.setValue("headings.fmtChapter", value) elif heading == self.EDIT_UNNUM: self.fmtUnnumbered.setText(text) - self._build.setValue("headings.fmtUnnumbered", text) + self._build.setValue("headings.fmtUnnumbered", value) elif heading == self.EDIT_SCENE: self.fmtScene.setText(text) - self._build.setValue("headings.fmtScene", text) + self._build.setValue("headings.fmtScene", value) elif heading == self.EDIT_HSCENE: self.fmtAScene.setText(text) - self._build.setValue("headings.fmtAltScene", text) + self._build.setValue("headings.fmtAltScene", value) elif heading == self.EDIT_SECTION: self.fmtSection.setText(text) - self._build.setValue("headings.fmtSection", text) + self._build.setValue("headings.fmtSection", value) else: return diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index d141b6ef..7795601c 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -1654,7 +1654,7 @@ def testFmtToken_ProcessHeaders(mockGUI): # H2: Chapter Word Number tokens._text = "## Chapter\n" tokens.setChapterFormat(f"Chapter {nwHeadFmt.CH_WORD}") - tokens._hFormatter._chCount = 0 + tokens._hFormatter._chapter = 0 tokens.tokenizeText() assert tokens._blocks == [ (BlockTyp.HEAD1, TM1, "Chapter One", [], BlockFmt.PBB), @@ -1728,8 +1728,8 @@ def testFmtToken_ProcessHeaders(mockGUI): # H3: Scene w/Absolute Number tokens._text = "### A Scene\n" tokens.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}", False) - tokens._hFormatter._scAbsCount = 0 - tokens._hFormatter._scChCount = 0 + tokens._hFormatter._scene = 0 + tokens._hFormatter._absolute = 0 tokens.tokenizeText() assert tokens._blocks == [ (BlockTyp.HEAD2, TM1, "Scene 1", [], BlockFmt.NONE), @@ -1738,8 +1738,8 @@ def testFmtToken_ProcessHeaders(mockGUI): # H3: Scene w/Chapter Number tokens._text = "### A Scene\n" tokens.setSceneFormat(f"Scene {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM}", False) - tokens._hFormatter._scAbsCount = 0 - tokens._hFormatter._scChCount = 1 + tokens._hFormatter._scene = 1 + tokens._hFormatter._absolute = 0 tokens.tokenizeText() assert tokens._blocks == [ (BlockTyp.HEAD2, TM1, "Scene 3.2", [], BlockFmt.NONE), @@ -2544,9 +2544,9 @@ def testFmtToken_HeadingFormatter(fncPath, mockGUI, mockRnd): # Special Formats # =============== - formatter._chCount = 2 - formatter._scChCount = 3 - formatter._scAbsCount = 8 + formatter._chapter = 2 + formatter._scene = 3 + formatter._absolute = 8 # Chapter Number Word cFormat = f"Chapter {nwHeadFmt.CH_WORD}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.TITLE}"