From 1203dd639b73a11ef15ac03f66e1a486de0f8599 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 21 May 2024 18:12:08 +0200
Subject: [PATCH 01/13] Add a QTextDocument generator class
---
novelwriter/core/tokenizer.py | 1 +
novelwriter/core/toqdocument.py | 287 ++++++++++++++++++++++++++++++++
novelwriter/gui/docviewer.py | 31 ++--
3 files changed, 304 insertions(+), 15 deletions(-)
create mode 100644 novelwriter/core/toqdocument.py
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index 15cd80469..f0c586f03 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -152,6 +152,7 @@ def __init__(self, project: NWProject) -> None:
self._doComments = False # Also process comments
self._doKeywords = False # Also process keywords like tags and references
self._skipKeywords = set() # Keywords to ignore
+ self._keepBreaks = True # Keep line breaks in paragraphs
# Margins
self._marginTitle = (1.000, 0.500)
diff --git a/novelwriter/core/toqdocument.py b/novelwriter/core/toqdocument.py
new file mode 100644
index 000000000..08fcc179b
--- /dev/null
+++ b/novelwriter/core/toqdocument.py
@@ -0,0 +1,287 @@
+"""
+novelWriter – Markdown Text Converter
+=====================================
+
+File History:
+Created: 2021-02-06 [1.2b1] ToMarkdown
+
+This file is a part of novelWriter
+Copyright 2018–2024, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+from __future__ import annotations
+
+import logging
+
+from PyQt5.QtGui import (
+ QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat, QTextCursor,
+ QTextDocument
+)
+
+from novelwriter import SHARED
+from novelwriter.constants import nwHeaders
+from novelwriter.core.project import NWProject
+from novelwriter.core.tokenizer import T_Formats, Tokenizer
+from novelwriter.types import (
+ QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight, QtPageBreakAfter,
+ QtPageBreakBefore
+)
+
+logger = logging.getLogger(__name__)
+
+T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
+
+
+class ToQTextDocument(Tokenizer):
+ """Core: QTextDocument Writer
+
+ Extend the Tokenizer class to generate a QTextDocument output. This
+ is intended for usage in the document viewer and build tool preview.
+ """
+
+ def __init__(self, project: NWProject) -> None:
+ super().__init__(project)
+ self._document = QTextDocument()
+ self._document.setUndoRedoEnabled(False)
+
+ self._styles: dict[int, T_TextStyle] = {}
+ self._init = False
+
+ return
+
+ def initDocument(self, font: QFont | None = None) -> None:
+ """Initialise all computed values of the document."""
+ if not font:
+ font = QFont()
+ font.setFamily(self._textFont)
+ font.setPointSize(self._textSize)
+
+ self._document.setUndoRedoEnabled(False)
+ self._document.blockSignals(True)
+ self._document.clear()
+ self._document.setDefaultFont(font)
+
+ qMetric = QFontMetrics(font)
+ mScale = qMetric.height()
+ fPt = font.pointSizeF()
+
+ self._mHead = {
+ self.T_TITLE: (mScale * self._marginTitle[0], mScale * self._marginTitle[1]),
+ self.T_HEAD1: (mScale * self._marginHead1[0], mScale * self._marginHead1[1]),
+ self.T_HEAD2: (mScale * self._marginHead2[0], mScale * self._marginHead2[1]),
+ self.T_HEAD3: (mScale * self._marginHead3[0], mScale * self._marginHead3[1]),
+ self.T_HEAD4: (mScale * self._marginHead4[0], mScale * self._marginHead4[1]),
+ }
+
+ self._sHead = {
+ self.T_TITLE: nwHeaders.H_SIZES.get(1, 1.0) * fPt,
+ self.T_HEAD1: nwHeaders.H_SIZES.get(1, 1.0) * fPt,
+ self.T_HEAD2: nwHeaders.H_SIZES.get(2, 1.0) * fPt,
+ self.T_HEAD3: nwHeaders.H_SIZES.get(3, 1.0) * fPt,
+ self.T_HEAD4: nwHeaders.H_SIZES.get(4, 1.0) * fPt,
+ }
+
+ self._mText = (mScale * self._marginText[0], mScale * self._marginText[1])
+ self._mMeta = (mScale * self._marginMeta[0], mScale * self._marginMeta[1])
+
+ self._defaultChar = QTextCharFormat()
+ self._defaultChar.setForeground(SHARED.theme.colText)
+
+ self._defaultBlock = QTextBlockFormat()
+ self._defaultBlock.setTopMargin(self._mText[0])
+ self._defaultBlock.setBottomMargin(self._mText[1])
+
+ self._init = True
+
+ return
+
+ ##
+ # Properties
+ ##
+
+ @property
+ def document(self) -> QTextDocument:
+ """Return the document."""
+ return self._document
+
+ ##
+ # Class Methods
+ ##
+
+ def clearDocument(self) -> None:
+ """Clear the document so the class can be reused."""
+ self._document.clear()
+ return
+
+ def doConvert(self) -> None:
+ """Write text tokens into the document."""
+ if not self._init:
+ return
+
+ self._document.blockSignals(True)
+ cursor = QTextCursor(self._document)
+
+ def newBlock(bFmt: QTextBlockFormat) -> None:
+ if cursor.position() > 0:
+ cursor.insertBlock(bFmt)
+ else:
+ cursor.setBlockFormat(bFmt)
+
+ pText = []
+ pFmt = None
+ lineSep = "\n" if self._keepBreaks else " "
+
+ for tType, _, tText, tFormat, tStyle in self._tokens:
+
+ # Styles
+ bFmt = QTextBlockFormat(self._defaultBlock)
+ if tStyle is not None:
+ if tStyle & self.A_LEFT:
+ bFmt.setAlignment(QtAlignLeft)
+ elif tStyle & self.A_RIGHT:
+ bFmt.setAlignment(QtAlignRight)
+ elif tStyle & self.A_CENTRE:
+ bFmt.setAlignment(QtAlignCenter)
+ elif tStyle & self.A_JUSTIFY:
+ bFmt.setAlignment(QtAlignJustify)
+
+ if tStyle & self.A_PBB:
+ bFmt.setPageBreakPolicy(QtPageBreakBefore)
+ if tStyle & self.A_PBA:
+ bFmt.setPageBreakPolicy(QtPageBreakAfter)
+
+ if tStyle & self.A_Z_BTMMRG:
+ bFmt.setBottomMargin(0.0)
+ if tStyle & self.A_Z_TOPMRG:
+ bFmt.setTopMargin(0.0)
+
+ if tStyle & self.A_IND_L:
+ bFmt.setLeftMargin(self._blockIndent)
+ if tStyle & self.A_IND_R:
+ bFmt.setRightMargin(self._blockIndent)
+
+ if tType == self.T_EMPTY:
+ if pText and pFmt is not None:
+ tTemp = (lineSep.join(pText)).rstrip(" ")
+ newBlock(pFmt)
+ cursor.insertText(tTemp, self._defaultChar)
+ pText = []
+ pFmt = None
+
+ elif tType in self.L_HEADINGS:
+ bFmt, cFmt = self._genHeadStyle(tType, bFmt)
+ newBlock(bFmt)
+ cursor.insertText(tText, cFmt)
+
+ elif tType == self.T_SEP:
+ newBlock(bFmt)
+ cursor.insertText(tTemp, self._defaultChar)
+
+ elif tType == self.T_SKIP:
+ newBlock(bFmt)
+
+ elif tType == self.T_TEXT:
+ if pFmt is None:
+ pFmt = bFmt
+ pText.append(self._formatText(tText, tFormat).rstrip())
+
+ elif tType == self.T_SYNOPSIS and self._doSynopsis:
+ pass
+
+ elif tType == self.T_SHORT and self._doSynopsis:
+ pass
+
+ elif tType == self.T_COMMENT and self._doComments:
+ pass
+
+ elif tType == self.T_KEYWORD and self._doKeywords:
+ pass
+
+ self._document.blockSignals(False)
+
+ return
+
+ def appendFootnotes(self) -> None:
+ """Append the footnotes in the buffer."""
+ # if self._usedNotes:
+ # tags = STD_MD if self._genMode == self.M_STD else EXT_MD
+ # footnotes = self._localLookup("Footnotes")
+
+ # lines = []
+ # lines.append(f"### {footnotes}\n\n")
+ # for key, index in self._usedNotes.items():
+ # if content := self._footnotes.get(key):
+ # marker = f"{index}. "
+ # text = self._formatText(*content, tags)
+ # lines.append(f"{marker}{text}\n")
+ # lines.append("\n")
+
+ # result = "".join(lines)
+ # self._result += result
+ # self._fullMD.append(result)
+
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _formatText(self, text: str, tFmt: T_Formats) -> str:
+ """Apply formatting tags to text."""
+ temp = text
+ # for pos, fmt, data in reversed(tFmt):
+ # md = ""
+ # if fmt == self.FMT_FNOTE:
+ # if data in self._footnotes:
+ # index = len(self._usedNotes) + 1
+ # self._usedNotes[data] = index
+ # md = f"[{index}]"
+ # else:
+ # md = "[ERR]"
+ # else:
+ # md = tags.get(fmt, "")
+ # temp = f"{temp[:pos]}{md}{temp[pos:]}"
+ return temp
+
+ def _formatKeywords(self, text: str, style: int) -> str:
+ """Apply Markdown formatting to keywords."""
+ # valid, bits, _ = self._project.index.scanThis("@"+text)
+ # if not valid or not bits:
+ # return ""
+
+ result = ""
+ # if bits[0] in nwLabels.KEY_NAME:
+ # result += f"**{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:** "
+ # if len(bits) > 1:
+ # result += ", ".join(bits[1:])
+
+ # result += " \n" if style & self.A_Z_BTMMRG else "\n\n"
+
+ return result
+
+ def _genHeadStyle(self, level: int, rFmt: QTextBlockFormat) -> T_TextStyle:
+ """Generate a heading style set."""
+ mTop, mBottom = self._mHead.get(level, (0.0, 0.0))
+
+ bFmt = QTextBlockFormat(rFmt)
+ bFmt.setTopMargin(mTop)
+ bFmt.setBottomMargin(mBottom)
+
+ cFmt = QTextCharFormat(self._defaultChar)
+ cFmt.setForeground(SHARED.theme.colHead)
+ cFmt.setFontWeight(QFont.Weight.Bold)
+ cFmt.setFontPointSize(self._sHead.get(level, 1.0))
+
+ return bFmt, cFmt
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 917a4f7f3..082233bbc 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -40,7 +40,7 @@
from novelwriter import CONFIG, SHARED
from novelwriter.common import cssCol
from novelwriter.constants import nwHeaders, nwUnicode
-from novelwriter.core.tohtml import ToHtml
+from novelwriter.core.toqdocument import ToQTextDocument
from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
from novelwriter.error import logException
from novelwriter.extensions.eventfilters import WheelEventFilter
@@ -202,22 +202,22 @@ def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
sPos = self.verticalScrollBar().value()
- aDoc = ToHtml(SHARED.project)
- aDoc.setPreview(True)
- aDoc.setKeywords(True)
- aDoc.setComments(CONFIG.viewComments)
- aDoc.setSynopsis(CONFIG.viewSynopsis)
- aDoc.setLinkHeadings(True)
+ qDoc = ToQTextDocument(SHARED.project)
+ qDoc.initDocument(CONFIG.textFont)
+ qDoc.setKeywords(True)
+ qDoc.setComments(CONFIG.viewComments)
+ qDoc.setSynopsis(CONFIG.viewSynopsis)
+ qDoc.setLinkHeadings(True)
# Be extra careful here to prevent crashes when first opening a
# project as a crash here leaves no way of recovering.
# See issue #298
try:
- aDoc.setText(tHandle)
- aDoc.doPreProcessing()
- aDoc.tokenizeText()
- aDoc.doConvert()
- aDoc.appendFootnotes()
+ qDoc.setText(tHandle)
+ qDoc.doPreProcessing()
+ qDoc.tokenizeText()
+ qDoc.doConvert()
+ qDoc.appendFootnotes()
except Exception:
logger.error("Failed to generate preview for document with handle '%s'", tHandle)
logException()
@@ -235,9 +235,10 @@ def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
self.setDocumentTitle(tHandle)
# Replace tabs before setting the HTML, and then put them back in
- self.setHtml(aDoc.result.replace("\t", "!!tab!!"))
- while self.find("!!tab!!"):
- self.textCursor().insertText("\t")
+ # self.setHtml(qDoc.result.replace("\t", "!!tab!!"))
+ # while self.find("!!tab!!"):
+ # self.textCursor().insertText("\t")
+ self.setDocument(qDoc.document)
if self._docHandle == tHandle:
# This is a refresh, so we set the scrollbar back to where it was
From 6d849427466ac112199226521403846471ad338b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 23 May 2024 19:02:27 +0200
Subject: [PATCH 02/13] Update qdocument class to work with merged code from
main
---
novelwriter/core/toqdocument.py | 41 +++++++++++----------------------
novelwriter/gui/docviewer.py | 3 ++-
2 files changed, 15 insertions(+), 29 deletions(-)
diff --git a/novelwriter/core/toqdocument.py b/novelwriter/core/toqdocument.py
index 08fcc179b..9ed22ed32 100644
--- a/novelwriter/core/toqdocument.py
+++ b/novelwriter/core/toqdocument.py
@@ -42,6 +42,7 @@
logger = logging.getLogger(__name__)
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
+T_TextParFormat = tuple[str, QTextCharFormat]
class ToQTextDocument(Tokenizer):
@@ -61,21 +62,16 @@ def __init__(self, project: NWProject) -> None:
return
- def initDocument(self, font: QFont | None = None) -> None:
+ def initDocument(self) -> None:
"""Initialise all computed values of the document."""
- if not font:
- font = QFont()
- font.setFamily(self._textFont)
- font.setPointSize(self._textSize)
-
self._document.setUndoRedoEnabled(False)
self._document.blockSignals(True)
self._document.clear()
- self._document.setDefaultFont(font)
+ self._document.setDefaultFont(self._textFont)
- qMetric = QFontMetrics(font)
+ qMetric = QFontMetrics(self._textFont)
mScale = qMetric.height()
- fPt = font.pointSizeF()
+ fPt = self._textFont.pointSizeF()
self._mHead = {
self.T_TITLE: (mScale * self._marginTitle[0], mScale * self._marginTitle[1]),
@@ -139,10 +135,6 @@ def newBlock(bFmt: QTextBlockFormat) -> None:
else:
cursor.setBlockFormat(bFmt)
- pText = []
- pFmt = None
- lineSep = "\n" if self._keepBreaks else " "
-
for tType, _, tText, tFormat, tStyle in self._tokens:
# Styles
@@ -172,13 +164,10 @@ def newBlock(bFmt: QTextBlockFormat) -> None:
if tStyle & self.A_IND_R:
bFmt.setRightMargin(self._blockIndent)
- if tType == self.T_EMPTY:
- if pText and pFmt is not None:
- tTemp = (lineSep.join(pText)).rstrip(" ")
- newBlock(pFmt)
- cursor.insertText(tTemp, self._defaultChar)
- pText = []
- pFmt = None
+ if tType == self.T_TEXT:
+ newBlock(bFmt)
+ tTemp, cFmt = self._formatText(tText, tFormat, self._defaultChar)
+ cursor.insertText(tTemp, cFmt)
elif tType in self.L_HEADINGS:
bFmt, cFmt = self._genHeadStyle(tType, bFmt)
@@ -187,16 +176,11 @@ def newBlock(bFmt: QTextBlockFormat) -> None:
elif tType == self.T_SEP:
newBlock(bFmt)
- cursor.insertText(tTemp, self._defaultChar)
+ cursor.insertText(tText, self._defaultChar)
elif tType == self.T_SKIP:
newBlock(bFmt)
- elif tType == self.T_TEXT:
- if pFmt is None:
- pFmt = bFmt
- pText.append(self._formatText(tText, tFormat).rstrip())
-
elif tType == self.T_SYNOPSIS and self._doSynopsis:
pass
@@ -238,9 +222,10 @@ def appendFootnotes(self) -> None:
# Internal Functions
##
- def _formatText(self, text: str, tFmt: T_Formats) -> str:
+ def _formatText(self, text: str, tFmt: T_Formats, dFmt: QTextCharFormat) -> T_TextParFormat:
"""Apply formatting tags to text."""
temp = text
+ fmt = QTextCharFormat(dFmt)
# for pos, fmt, data in reversed(tFmt):
# md = ""
# if fmt == self.FMT_FNOTE:
@@ -253,7 +238,7 @@ def _formatText(self, text: str, tFmt: T_Formats) -> str:
# else:
# md = tags.get(fmt, "")
# temp = f"{temp[:pos]}{md}{temp[pos:]}"
- return temp
+ return temp, fmt
def _formatKeywords(self, text: str, style: int) -> str:
"""Apply Markdown formatting to keywords."""
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 082233bbc..093aac3a3 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -203,7 +203,8 @@ def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
sPos = self.verticalScrollBar().value()
qDoc = ToQTextDocument(SHARED.project)
- qDoc.initDocument(CONFIG.textFont)
+ qDoc.setFont(CONFIG.textFont)
+ qDoc.initDocument()
qDoc.setKeywords(True)
qDoc.setComments(CONFIG.viewComments)
qDoc.setSynopsis(CONFIG.viewSynopsis)
From c8017a1f42a997d613c6cc559d98fb9dcb3a9716 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 23 May 2024 19:43:32 +0200
Subject: [PATCH 03/13] Add text paragraph formatting
---
.../core/{toqdocument.py => toqdoc.py} | 88 ++++++++++++++-----
novelwriter/gui/docviewer.py | 2 +-
novelwriter/types.py | 6 +-
3 files changed, 72 insertions(+), 24 deletions(-)
rename novelwriter/core/{toqdocument.py => toqdoc.py} (75%)
diff --git a/novelwriter/core/toqdocument.py b/novelwriter/core/toqdoc.py
similarity index 75%
rename from novelwriter/core/toqdocument.py
rename to novelwriter/core/toqdoc.py
index 9ed22ed32..dac1b5324 100644
--- a/novelwriter/core/toqdocument.py
+++ b/novelwriter/core/toqdoc.py
@@ -26,8 +26,8 @@
import logging
from PyQt5.QtGui import (
- QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat, QTextCursor,
- QTextDocument
+ QColor, QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat,
+ QTextCursor, QTextDocument
)
from novelwriter import SHARED
@@ -36,13 +36,13 @@
from novelwriter.core.tokenizer import T_Formats, Tokenizer
from novelwriter.types import (
QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight, QtPageBreakAfter,
- QtPageBreakBefore
+ QtPageBreakBefore, QtTransparent, QtVAlignNormal, QtVAlignSub,
+ QtVAlignSuper
)
logger = logging.getLogger(__name__)
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
-T_TextParFormat = tuple[str, QTextCharFormat]
class ToQTextDocument(Tokenizer):
@@ -58,7 +58,11 @@ def __init__(self, project: NWProject) -> None:
self._document.setUndoRedoEnabled(False)
self._styles: dict[int, T_TextStyle] = {}
+ self._usedNotes: dict[str, int] = {}
+
self._init = False
+ self._bold = QFont.Weight.Bold
+ self._normal = QFont.Weight.Normal
return
@@ -166,8 +170,7 @@ def newBlock(bFmt: QTextBlockFormat) -> None:
if tType == self.T_TEXT:
newBlock(bFmt)
- tTemp, cFmt = self._formatText(tText, tFormat, self._defaultChar)
- cursor.insertText(tTemp, cFmt)
+ self._insertFragments(tText, tFormat, cursor, self._defaultChar)
elif tType in self.L_HEADINGS:
bFmt, cFmt = self._genHeadStyle(tType, bFmt)
@@ -222,23 +225,64 @@ def appendFootnotes(self) -> None:
# Internal Functions
##
- def _formatText(self, text: str, tFmt: T_Formats, dFmt: QTextCharFormat) -> T_TextParFormat:
+ def _insertFragments(
+ self, text: str, tFmt: T_Formats, cursor: QTextCursor,
+ dFmt: QTextCharFormat, bgCol: QColor = QtTransparent
+ ) -> None:
"""Apply formatting tags to text."""
- temp = text
- fmt = QTextCharFormat(dFmt)
- # for pos, fmt, data in reversed(tFmt):
- # md = ""
- # if fmt == self.FMT_FNOTE:
- # if data in self._footnotes:
- # index = len(self._usedNotes) + 1
- # self._usedNotes[data] = index
- # md = f"[{index}]"
- # else:
- # md = "[ERR]"
- # else:
- # md = tags.get(fmt, "")
- # temp = f"{temp[:pos]}{md}{temp[pos:]}"
- return temp, fmt
+ cFmt = QTextCharFormat(dFmt)
+ start = 0
+ for pos, fmt, data in tFmt:
+
+ # Insert buffer with previous format
+ cursor.insertText(text[start:pos], cFmt)
+
+ # Construct next format
+ if fmt == self.FMT_B_B:
+ cFmt.setFontWeight(self._bold)
+ elif fmt == self.FMT_B_E:
+ cFmt.setFontWeight(self._normal)
+ elif fmt == self.FMT_I_B:
+ cFmt.setFontItalic(True)
+ elif fmt == self.FMT_I_E:
+ cFmt.setFontItalic(False)
+ elif fmt == self.FMT_D_B:
+ cFmt.setFontStrikeOut(True)
+ elif fmt == self.FMT_D_E:
+ cFmt.setFontStrikeOut(False)
+ elif fmt == self.FMT_U_B:
+ cFmt.setFontUnderline(True)
+ elif fmt == self.FMT_U_E:
+ cFmt.setFontUnderline(False)
+ elif fmt == self.FMT_M_B:
+ cFmt.setBackground(SHARED.theme.colMark)
+ elif fmt == self.FMT_M_E:
+ cFmt.setBackground(bgCol)
+ elif fmt == self.FMT_SUP_B:
+ cFmt.setVerticalAlignment(QtVAlignSuper)
+ elif fmt == self.FMT_SUP_E:
+ cFmt.setVerticalAlignment(QtVAlignNormal)
+ elif fmt == self.FMT_SUB_B:
+ cFmt.setVerticalAlignment(QtVAlignSub)
+ elif fmt == self.FMT_SUB_E:
+ cFmt.setVerticalAlignment(QtVAlignNormal)
+ elif fmt == self.FMT_FNOTE:
+ cFmt.setVerticalAlignment(QtVAlignSuper)
+ if data in self._footnotes:
+ index = len(self._usedNotes) + 1
+ self._usedNotes[data] = index
+ cursor.insertText(f"[{index}]", cFmt)
+ else:
+ cursor.insertText("[ERR]", cFmt)
+ cFmt.setVerticalAlignment(QtVAlignNormal)
+
+ # Move pos for next pass
+ start = pos
+
+ # Insert whatever is left in the buffer
+ cursor.insertText(text[start:], cFmt)
+
+ return
def _formatKeywords(self, text: str, style: int) -> str:
"""Apply Markdown formatting to keywords."""
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 093aac3a3..e3eb68870 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -40,7 +40,7 @@
from novelwriter import CONFIG, SHARED
from novelwriter.common import cssCol
from novelwriter.constants import nwHeaders, nwUnicode
-from novelwriter.core.toqdocument import ToQTextDocument
+from novelwriter.core.toqdoc import ToQTextDocument
from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
from novelwriter.error import logException
from novelwriter.extensions.eventfilters import WheelEventFilter
diff --git a/novelwriter/types.py b/novelwriter/types.py
index c70d28934..c45d79e08 100644
--- a/novelwriter/types.py
+++ b/novelwriter/types.py
@@ -24,7 +24,7 @@
from __future__ import annotations
from PyQt5.QtCore import QRegularExpression, Qt
-from PyQt5.QtGui import QColor, QFont, QPainter, QTextCursor, QTextFormat
+from PyQt5.QtGui import QColor, QFont, QPainter, QTextCharFormat, QTextCursor, QTextFormat
from PyQt5.QtWidgets import QDialogButtonBox, QSizePolicy, QStyle
# Qt Alignment Flags
@@ -44,6 +44,10 @@
QtAlignRightTop = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
QtAlignTop = Qt.AlignmentFlag.AlignTop
+QtVAlignNormal = QTextCharFormat.VerticalAlignment.AlignNormal
+QtVAlignSub = QTextCharFormat.VerticalAlignment.AlignSubScript
+QtVAlignSuper = QTextCharFormat.VerticalAlignment.AlignSuperScript
+
# Qt Page Break
QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore
From e998cb634e6f4b70f73ffa2c7c461729cacdb298 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 23 May 2024 20:48:42 +0200
Subject: [PATCH 04/13] Add working meta data blocks
---
novelwriter/core/tokenizer.py | 5 +-
novelwriter/core/toqdoc.py | 98 +++++++++++++++++++---------
tests/test_gui/test_gui_docviewer.py | 14 ++--
3 files changed, 79 insertions(+), 38 deletions(-)
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index 72e684497..ba0046590 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -597,7 +597,10 @@ def tokenizeText(self) -> None:
# are automatically skipped.
valid, bits, _ = self._project.index.scanThis(aLine)
- if valid and bits and bits[0] not in self._skipKeywords:
+ if (
+ valid and bits and bits[0] in nwLabels.KEY_NAME
+ and bits[0] not in self._skipKeywords
+ ):
tokens.append((
self.T_KEYWORD, nHead, aLine[1:].strip(), [], sAlign
))
diff --git a/novelwriter/core/toqdoc.py b/novelwriter/core/toqdoc.py
index dac1b5324..411a2029c 100644
--- a/novelwriter/core/toqdoc.py
+++ b/novelwriter/core/toqdoc.py
@@ -26,12 +26,12 @@
import logging
from PyQt5.QtGui import (
- QColor, QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat,
- QTextCursor, QTextDocument
+ QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat, QTextCursor,
+ QTextDocument
)
from novelwriter import SHARED
-from novelwriter.constants import nwHeaders
+from novelwriter.constants import nwHeaders, nwHeadFmt, nwKeyWords, nwLabels, nwUnicode
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import T_Formats, Tokenizer
from novelwriter.types import (
@@ -99,6 +99,24 @@ def initDocument(self) -> None:
self._defaultChar = QTextCharFormat()
self._defaultChar.setForeground(SHARED.theme.colText)
+ self._comChar = QTextCharFormat()
+ self._comChar.setForeground(SHARED.theme.colHidden)
+
+ self._noteChar = QTextCharFormat()
+ self._noteChar.setForeground(SHARED.theme.colNote)
+
+ self._modChar = QTextCharFormat()
+ self._modChar.setForeground(SHARED.theme.colMod)
+
+ self._keyChar = QTextCharFormat()
+ self._keyChar.setForeground(SHARED.theme.colKey)
+
+ self._tagChar = QTextCharFormat()
+ self._tagChar.setForeground(SHARED.theme.colTag)
+
+ self._optChar = QTextCharFormat()
+ self._optChar.setForeground(SHARED.theme.colOpt)
+
self._defaultBlock = QTextBlockFormat()
self._defaultBlock.setTopMargin(self._mText[0])
self._defaultBlock.setBottomMargin(self._mText[1])
@@ -139,7 +157,7 @@ def newBlock(bFmt: QTextBlockFormat) -> None:
else:
cursor.setBlockFormat(bFmt)
- for tType, _, tText, tFormat, tStyle in self._tokens:
+ for tType, nHead, tText, tFormat, tStyle in self._tokens:
# Styles
bFmt = QTextBlockFormat(self._defaultBlock)
@@ -173,9 +191,9 @@ def newBlock(bFmt: QTextBlockFormat) -> None:
self._insertFragments(tText, tFormat, cursor, self._defaultChar)
elif tType in self.L_HEADINGS:
- bFmt, cFmt = self._genHeadStyle(tType, bFmt)
+ bFmt, cFmt = self._genHeadStyle(tType, nHead, bFmt)
newBlock(bFmt)
- cursor.insertText(tText, cFmt)
+ cursor.insertText(tText.replace(nwHeadFmt.BR, "\n"), cFmt)
elif tType == self.T_SEP:
newBlock(bFmt)
@@ -183,20 +201,30 @@ def newBlock(bFmt: QTextBlockFormat) -> None:
elif tType == self.T_SKIP:
newBlock(bFmt)
+ cursor.insertText(nwUnicode.U_NBSP, self._defaultChar)
elif tType == self.T_SYNOPSIS and self._doSynopsis:
- pass
+ newBlock(bFmt)
+ prefix = self._localLookup("Synopsis")
+ cursor.insertText(f"{prefix}: ", self._modChar)
+ self._insertFragments(tText, tFormat, cursor, self._noteChar)
elif tType == self.T_SHORT and self._doSynopsis:
- pass
+ newBlock(bFmt)
+ prefix = self._localLookup("Short Description")
+ cursor.insertText(f"{prefix}: ", self._modChar)
+ self._insertFragments(tText, tFormat, cursor, self._noteChar)
elif tType == self.T_COMMENT and self._doComments:
- pass
+ newBlock(bFmt)
+ self._insertFragments(tText, tFormat, cursor, self._comChar)
elif tType == self.T_KEYWORD and self._doKeywords:
- pass
+ newBlock(bFmt)
+ self._insertKeywords(tText, cursor)
self._document.blockSignals(False)
+ print(self._document.toHtml())
return
@@ -226,16 +254,16 @@ def appendFootnotes(self) -> None:
##
def _insertFragments(
- self, text: str, tFmt: T_Formats, cursor: QTextCursor,
- dFmt: QTextCharFormat, bgCol: QColor = QtTransparent
+ self, text: str, tFmt: T_Formats, cursor: QTextCursor, dFmt: QTextCharFormat
) -> None:
"""Apply formatting tags to text."""
cFmt = QTextCharFormat(dFmt)
start = 0
+ temp = text.replace("\n", nwUnicode.U_LSEP)
for pos, fmt, data in tFmt:
# Insert buffer with previous format
- cursor.insertText(text[start:pos], cFmt)
+ cursor.insertText(temp[start:pos], cFmt)
# Construct next format
if fmt == self.FMT_B_B:
@@ -257,7 +285,7 @@ def _insertFragments(
elif fmt == self.FMT_M_B:
cFmt.setBackground(SHARED.theme.colMark)
elif fmt == self.FMT_M_E:
- cFmt.setBackground(bgCol)
+ cFmt.setBackground(QtTransparent)
elif fmt == self.FMT_SUP_B:
cFmt.setVerticalAlignment(QtVAlignSuper)
elif fmt == self.FMT_SUP_E:
@@ -280,27 +308,35 @@ def _insertFragments(
start = pos
# Insert whatever is left in the buffer
- cursor.insertText(text[start:], cFmt)
+ cursor.insertText(temp[start:], cFmt)
return
- def _formatKeywords(self, text: str, style: int) -> str:
+ def _insertKeywords(self, text: str, cursor: QTextCursor) -> None:
"""Apply Markdown formatting to keywords."""
- # valid, bits, _ = self._project.index.scanThis("@"+text)
- # if not valid or not bits:
- # return ""
-
- result = ""
- # if bits[0] in nwLabels.KEY_NAME:
- # result += f"**{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:** "
- # if len(bits) > 1:
- # result += ", ".join(bits[1:])
-
- # result += " \n" if style & self.A_Z_BTMMRG else "\n\n"
-
- return result
+ valid, bits, _ = self._project.index.scanThis("@"+text)
+ if valid and bits:
+ key = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
+ cursor.insertText(key, self._keyChar)
+ if (num := len(bits)) > 1:
+ if bits[0] == nwKeyWords.TAG_KEY:
+ one, two = self._project.index.parseValue(bits[1])
+ cursor.insertText(one, self._tagChar)
+ if two:
+ cursor.insertText(" | ", self._defaultChar)
+ cursor.insertText(two, self._optChar)
+ else:
+ for n, bit in enumerate(bits[1:], 2):
+ cFmt = QTextCharFormat(self._tagChar)
+ cFmt.setFontUnderline(True)
+ cFmt.setAnchor(True)
+ cFmt.setAnchorHref(f"#{bits[0][1:]}={bit}")
+ cursor.insertText(bit, cFmt)
+ if n < num:
+ cursor.insertText(", ", self._defaultChar)
+ return
- def _genHeadStyle(self, level: int, rFmt: QTextBlockFormat) -> T_TextStyle:
+ def _genHeadStyle(self, level: int, nHead: int, rFmt: QTextBlockFormat) -> T_TextStyle:
"""Generate a heading style set."""
mTop, mBottom = self._mHead.get(level, (0.0, 0.0))
@@ -312,5 +348,7 @@ def _genHeadStyle(self, level: int, rFmt: QTextBlockFormat) -> T_TextStyle:
cFmt.setForeground(SHARED.theme.colHead)
cFmt.setFontWeight(QFont.Weight.Bold)
cFmt.setFontPointSize(self._sHead.get(level, 1.0))
+ cFmt.setAnchorNames([f"{self._handle}:T{nHead:04d}"])
+ cFmt.setAnchor(True)
return bFmt, cFmt
diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py
index b8d0bb4c6..2fa287f61 100644
--- a/tests/test_gui/test_gui_docviewer.py
+++ b/tests/test_gui/test_gui_docviewer.py
@@ -27,7 +27,7 @@
from PyQt5.QtWidgets import QAction, QApplication, QMenu
from novelwriter import CONFIG, SHARED
-from novelwriter.core.tohtml import ToHtml
+from novelwriter.core.toqdoc import ToQTextDocument
from novelwriter.enum import nwDocAction
from novelwriter.gui.docviewer import GuiDocViewer
from novelwriter.types import QtModeNone, QtMouseLeft
@@ -119,7 +119,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
# Select All
assert docViewer.docAction(nwDocAction.SEL_ALL) is True
cursor = docViewer.textCursor()
- assert len(cursor.selectedText()) == 3061
+ assert len(cursor.selectedText()) == 3060
# Other actions
assert docViewer.docAction(nwDocAction.NO_ACTION) is False
@@ -196,19 +196,19 @@ def mockExec(*a):
# Document footer show/hide synopsis
assert nwGUI.viewDocument("f96ec11c6a3da") is True
- assert len(docViewer.toPlainText()) == 4315
+ assert len(docViewer.toPlainText()) == 4314
docViewer.docFooter._doToggleSynopsis(False)
- assert len(docViewer.toPlainText()) == 4099
+ assert len(docViewer.toPlainText()) == 4098
# Document footer show/hide comments
assert nwGUI.viewDocument("846352075de7d") is True
- assert len(docViewer.toPlainText()) == 675
+ assert len(docViewer.toPlainText()) == 674
docViewer.docFooter._doToggleComments(False)
- assert len(docViewer.toPlainText()) == 635
+ assert len(docViewer.toPlainText()) == 634
# Crash the HTML rendering
with monkeypatch.context() as mp:
- mp.setattr(ToHtml, "doConvert", causeException)
+ mp.setattr(ToQTextDocument, "doConvert", causeException)
assert docViewer.loadText("846352075de7d") is False
assert docViewer.toPlainText() == "An error occurred while generating the preview."
From b4c0bf33227288e472fa5fd55acde3f7339be22e Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 23 May 2024 21:29:47 +0200
Subject: [PATCH 05/13] Complete all current document viewer features in the
qdoc class
---
novelwriter/core/tokenizer.py | 1 +
novelwriter/core/toqdoc.py | 108 ++++++++++++++++++----------------
novelwriter/gui/docviewer.py | 3 +-
3 files changed, 59 insertions(+), 53 deletions(-)
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index ba0046590..850af65c4 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -119,6 +119,7 @@ class Tokenizer(ABC):
# Lookups
L_HEADINGS = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD3, T_HEAD4]
L_SKIP_INDENT = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD2, T_HEAD3, T_HEAD4, T_SEP, T_SKIP]
+ L_SUMMARY = [T_SYNOPSIS, T_SHORT]
def __init__(self, project: NWProject) -> None:
diff --git a/novelwriter/core/toqdoc.py b/novelwriter/core/toqdoc.py
index 411a2029c..a485aa765 100644
--- a/novelwriter/core/toqdoc.py
+++ b/novelwriter/core/toqdoc.py
@@ -45,6 +45,13 @@
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
+def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
+ if cursor.position() > 0:
+ cursor.insertBlock(bFmt)
+ else:
+ cursor.setBlockFormat(bFmt)
+
+
class ToQTextDocument(Tokenizer):
"""Core: QTextDocument Writer
@@ -66,8 +73,10 @@ def __init__(self, project: NWProject) -> None:
return
- def initDocument(self) -> None:
+ def initDocument(self, font: QFont) -> None:
"""Initialise all computed values of the document."""
+ self._textFont = font
+
self._document.setUndoRedoEnabled(False)
self._document.blockSignals(True)
self._document.clear()
@@ -96,6 +105,8 @@ def initDocument(self) -> None:
self._mText = (mScale * self._marginText[0], mScale * self._marginText[1])
self._mMeta = (mScale * self._marginMeta[0], mScale * self._marginMeta[1])
+ self._mIndent = mScale * 2.0
+
self._defaultChar = QTextCharFormat()
self._defaultChar.setForeground(SHARED.theme.colText)
@@ -105,6 +116,9 @@ def initDocument(self) -> None:
self._noteChar = QTextCharFormat()
self._noteChar.setForeground(SHARED.theme.colNote)
+ self._codeChar = QTextCharFormat()
+ self._codeChar.setForeground(SHARED.theme.colCode)
+
self._modChar = QTextCharFormat()
self._modChar.setForeground(SHARED.theme.colMod)
@@ -138,11 +152,6 @@ def document(self) -> QTextDocument:
# Class Methods
##
- def clearDocument(self) -> None:
- """Clear the document so the class can be reused."""
- self._document.clear()
- return
-
def doConvert(self) -> None:
"""Write text tokens into the document."""
if not self._init:
@@ -151,12 +160,6 @@ def doConvert(self) -> None:
self._document.blockSignals(True)
cursor = QTextCursor(self._document)
- def newBlock(bFmt: QTextBlockFormat) -> None:
- if cursor.position() > 0:
- cursor.insertBlock(bFmt)
- else:
- cursor.setBlockFormat(bFmt)
-
for tType, nHead, tText, tFormat, tStyle in self._tokens:
# Styles
@@ -182,70 +185,69 @@ def newBlock(bFmt: QTextBlockFormat) -> None:
bFmt.setTopMargin(0.0)
if tStyle & self.A_IND_L:
- bFmt.setLeftMargin(self._blockIndent)
+ bFmt.setLeftMargin(self._mIndent)
if tStyle & self.A_IND_R:
- bFmt.setRightMargin(self._blockIndent)
+ bFmt.setRightMargin(self._mIndent)
if tType == self.T_TEXT:
- newBlock(bFmt)
+ newBlock(cursor, bFmt)
self._insertFragments(tText, tFormat, cursor, self._defaultChar)
elif tType in self.L_HEADINGS:
bFmt, cFmt = self._genHeadStyle(tType, nHead, bFmt)
- newBlock(bFmt)
+ newBlock(cursor, bFmt)
cursor.insertText(tText.replace(nwHeadFmt.BR, "\n"), cFmt)
elif tType == self.T_SEP:
- newBlock(bFmt)
+ newBlock(cursor, bFmt)
cursor.insertText(tText, self._defaultChar)
elif tType == self.T_SKIP:
- newBlock(bFmt)
+ newBlock(cursor, bFmt)
cursor.insertText(nwUnicode.U_NBSP, self._defaultChar)
- elif tType == self.T_SYNOPSIS and self._doSynopsis:
- newBlock(bFmt)
- prefix = self._localLookup("Synopsis")
- cursor.insertText(f"{prefix}: ", self._modChar)
- self._insertFragments(tText, tFormat, cursor, self._noteChar)
-
- elif tType == self.T_SHORT and self._doSynopsis:
- newBlock(bFmt)
- prefix = self._localLookup("Short Description")
+ elif tType in self.L_SUMMARY and self._doSynopsis:
+ newBlock(cursor, bFmt)
+ prefix = self._localLookup(
+ "Short Description" if tType == self.T_SHORT else "Synopsis"
+ )
cursor.insertText(f"{prefix}: ", self._modChar)
self._insertFragments(tText, tFormat, cursor, self._noteChar)
elif tType == self.T_COMMENT and self._doComments:
- newBlock(bFmt)
+ newBlock(cursor, bFmt)
self._insertFragments(tText, tFormat, cursor, self._comChar)
elif tType == self.T_KEYWORD and self._doKeywords:
- newBlock(bFmt)
+ newBlock(cursor, bFmt)
self._insertKeywords(tText, cursor)
self._document.blockSignals(False)
- print(self._document.toHtml())
return
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
- # if self._usedNotes:
- # tags = STD_MD if self._genMode == self.M_STD else EXT_MD
- # footnotes = self._localLookup("Footnotes")
-
- # lines = []
- # lines.append(f"### {footnotes}\n\n")
- # for key, index in self._usedNotes.items():
- # if content := self._footnotes.get(key):
- # marker = f"{index}. "
- # text = self._formatText(*content, tags)
- # lines.append(f"{marker}{text}\n")
- # lines.append("\n")
-
- # result = "".join(lines)
- # self._result += result
- # self._fullMD.append(result)
+ if self._usedNotes:
+ self._document.blockSignals(True)
+
+ cursor = QTextCursor(self._document)
+ cursor.movePosition(QTextCursor.MoveOperation.End)
+
+ bFmt, cFmt = self._genHeadStyle(self.T_HEAD3, -1, self._defaultBlock)
+ newBlock(cursor, bFmt)
+ cursor.insertText(self._localLookup("Footnotes"), cFmt)
+
+ for key, index in self._usedNotes.items():
+ if content := self._footnotes.get(key):
+ cFmt = QTextCharFormat(self._codeChar)
+ cFmt.setAnchor(True)
+ cFmt.setAnchorNames([f"footnote_{index}"])
+ newBlock(cursor, self._defaultBlock)
+ cursor.insertText(f"{index}. ", cFmt)
+ self._insertFragments(*content, cursor, self._defaultChar)
+
+ self._document.blockSignals(False)
return
@@ -295,14 +297,17 @@ def _insertFragments(
elif fmt == self.FMT_SUB_E:
cFmt.setVerticalAlignment(QtVAlignNormal)
elif fmt == self.FMT_FNOTE:
- cFmt.setVerticalAlignment(QtVAlignSuper)
+ xFmt = QTextCharFormat(self._codeChar)
+ xFmt.setVerticalAlignment(QtVAlignSuper)
if data in self._footnotes:
index = len(self._usedNotes) + 1
self._usedNotes[data] = index
- cursor.insertText(f"[{index}]", cFmt)
+ xFmt.setAnchor(True)
+ xFmt.setAnchorHref(f"#footnote_{index}")
+ xFmt.setFontUnderline(True)
+ cursor.insertText(f"[{index}]", xFmt)
else:
cursor.insertText("[ERR]", cFmt)
- cFmt.setVerticalAlignment(QtVAlignNormal)
# Move pos for next pass
start = pos
@@ -348,7 +353,8 @@ def _genHeadStyle(self, level: int, nHead: int, rFmt: QTextBlockFormat) -> T_Tex
cFmt.setForeground(SHARED.theme.colHead)
cFmt.setFontWeight(QFont.Weight.Bold)
cFmt.setFontPointSize(self._sHead.get(level, 1.0))
- cFmt.setAnchorNames([f"{self._handle}:T{nHead:04d}"])
- cFmt.setAnchor(True)
+ if nHead >= 0:
+ cFmt.setAnchorNames([f"{self._handle}:T{nHead:04d}"])
+ cFmt.setAnchor(True)
return bFmt, cFmt
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index e3eb68870..6c3569df0 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -203,8 +203,7 @@ def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
sPos = self.verticalScrollBar().value()
qDoc = ToQTextDocument(SHARED.project)
- qDoc.setFont(CONFIG.textFont)
- qDoc.initDocument()
+ qDoc.initDocument(CONFIG.textFont)
qDoc.setKeywords(True)
qDoc.setComments(CONFIG.viewComments)
qDoc.setSynopsis(CONFIG.viewSynopsis)
From c97a070e98abdf628d05b03af6dd3f0bdf4406ea Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 23 May 2024 22:04:19 +0200
Subject: [PATCH 06/13] Fix module header info
---
novelwriter/core/toqdoc.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/novelwriter/core/toqdoc.py b/novelwriter/core/toqdoc.py
index a485aa765..0e641168d 100644
--- a/novelwriter/core/toqdoc.py
+++ b/novelwriter/core/toqdoc.py
@@ -1,9 +1,9 @@
"""
-novelWriter – Markdown Text Converter
+novelWriter – QTextDocument Converter
=====================================
File History:
-Created: 2021-02-06 [1.2b1] ToMarkdown
+Created: 2024-05-21 [2.5b1] ToQTextDocument
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
From 6cc046c65cd36357390d4d90d81ee5ceb9dc3857 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 24 May 2024 18:52:18 +0200
Subject: [PATCH 07/13] Use preview document class for manuscript preview
---
novelwriter/constants.py | 2 +-
novelwriter/core/docbuild.py | 25 ++++++
novelwriter/core/toqdoc.py | 130 +++++++++++++++++++-------------
novelwriter/gui/docviewer.py | 17 ++++-
novelwriter/tools/manuscript.py | 86 +++++++++++----------
novelwriter/types.py | 1 +
6 files changed, 160 insertions(+), 101 deletions(-)
diff --git a/novelwriter/constants.py b/novelwriter/constants.py
index 9a871e4d8..ebaef80c0 100644
--- a/novelwriter/constants.py
+++ b/novelwriter/constants.py
@@ -99,7 +99,7 @@ class nwHeaders:
H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
- H_SIZES = {0: 1.00, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}
+ H_SIZES = {0: 2.50, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}
class nwFiles:
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index 1b5350a89..9c17e73e9 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -39,6 +39,7 @@
from novelwriter.core.tokenizer import Tokenizer
from novelwriter.core.tomarkdown import ToMarkdown
from novelwriter.core.toodt import ToOdt
+from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.enum import nwBuildFmt
from novelwriter.error import formatException, logException
@@ -134,6 +135,30 @@ def queueAll(self) -> None:
self._queue.append(item.itemHandle)
return
+ def iterBuildPreview(self, theme: TextDocumentTheme) -> Iterable[tuple[int, bool]]:
+ """Build a preview QTextDocument."""
+ makeObj = ToQTextDocument(self._project)
+ filtered = self._setupBuild(makeObj)
+
+ font = QFont()
+ font.fromString(self._build.getStr("format.textFont"))
+
+ makeObj.setLinkHeadings(self._preview)
+ makeObj.initDocument(font, theme)
+ for i, tHandle in enumerate(self._queue):
+ self._error = None
+ if filtered.get(tHandle, (False, 0))[0]:
+ yield i, self._doBuild(makeObj, tHandle)
+ else:
+ yield i, False
+
+ makeObj.appendFootnotes()
+
+ self._error = None
+ self._cache = makeObj
+
+ return
+
def iterBuild(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
"""Wrapper for builders based on format."""
if bFormat in (nwBuildFmt.ODT, nwBuildFmt.FODT):
diff --git a/novelwriter/core/toqdoc.py b/novelwriter/core/toqdoc.py
index 0e641168d..bfe4aa52c 100644
--- a/novelwriter/core/toqdoc.py
+++ b/novelwriter/core/toqdoc.py
@@ -26,18 +26,17 @@
import logging
from PyQt5.QtGui import (
- QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat, QTextCursor,
- QTextDocument
+ QColor, QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat,
+ QTextCursor, QTextDocument
)
-from novelwriter import SHARED
from novelwriter.constants import nwHeaders, nwHeadFmt, nwKeyWords, nwLabels, nwUnicode
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import T_Formats, Tokenizer
from novelwriter.types import (
- QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight, QtPageBreakAfter,
- QtPageBreakBefore, QtTransparent, QtVAlignNormal, QtVAlignSub,
- QtVAlignSuper
+ QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight, QtBlack,
+ QtPageBreakAfter, QtPageBreakBefore, QtTransparent, QtVAlignNormal,
+ QtVAlignSub, QtVAlignSuper
)
logger = logging.getLogger(__name__)
@@ -45,6 +44,19 @@
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
+class TextDocumentTheme:
+ text: QColor = QtBlack
+ highlight: QColor = QtTransparent
+ head: QColor = QtBlack
+ comment: QColor = QtBlack
+ note: QColor = QtBlack
+ code: QColor = QtBlack
+ modifier: QColor = QtBlack
+ keyword: QColor = QtBlack
+ tag: QColor = QtBlack
+ optional: QColor = QtBlack
+
+
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
if cursor.position() > 0:
cursor.insertBlock(bFmt)
@@ -64,6 +76,7 @@ def __init__(self, project: NWProject) -> None:
self._document = QTextDocument()
self._document.setUndoRedoEnabled(False)
+ self._theme = TextDocumentTheme()
self._styles: dict[int, T_TextStyle] = {}
self._usedNotes: dict[str, int] = {}
@@ -73,9 +86,10 @@ def __init__(self, project: NWProject) -> None:
return
- def initDocument(self, font: QFont) -> None:
+ def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
"""Initialise all computed values of the document."""
self._textFont = font
+ self._theme = theme
self._document.setUndoRedoEnabled(False)
self._document.blockSignals(True)
@@ -95,7 +109,7 @@ def initDocument(self, font: QFont) -> None:
}
self._sHead = {
- self.T_TITLE: nwHeaders.H_SIZES.get(1, 1.0) * fPt,
+ self.T_TITLE: nwHeaders.H_SIZES.get(0, 1.0) * fPt,
self.T_HEAD1: nwHeaders.H_SIZES.get(1, 1.0) * fPt,
self.T_HEAD2: nwHeaders.H_SIZES.get(2, 1.0) * fPt,
self.T_HEAD3: nwHeaders.H_SIZES.get(3, 1.0) * fPt,
@@ -107,33 +121,41 @@ def initDocument(self, font: QFont) -> None:
self._mIndent = mScale * 2.0
- self._defaultChar = QTextCharFormat()
- self._defaultChar.setForeground(SHARED.theme.colText)
+ self._cText = QTextCharFormat()
+ self._cText.setForeground(self._theme.text)
+
+ self._cHead = QTextCharFormat()
+ self._cHead.setForeground(self._theme.head)
+
+ self._cComment = QTextCharFormat()
+ self._cComment.setForeground(self._theme.comment)
- self._comChar = QTextCharFormat()
- self._comChar.setForeground(SHARED.theme.colHidden)
+ self._cCommentMod = QTextCharFormat()
+ self._cCommentMod.setForeground(self._theme.comment)
+ self._cCommentMod.setFontWeight(self._bold)
- self._noteChar = QTextCharFormat()
- self._noteChar.setForeground(SHARED.theme.colNote)
+ self._cNote = QTextCharFormat()
+ self._cNote.setForeground(self._theme.note)
- self._codeChar = QTextCharFormat()
- self._codeChar.setForeground(SHARED.theme.colCode)
+ self._cCode = QTextCharFormat()
+ self._cCode.setForeground(self._theme.code)
- self._modChar = QTextCharFormat()
- self._modChar.setForeground(SHARED.theme.colMod)
+ self._cModifier = QTextCharFormat()
+ self._cModifier.setForeground(self._theme.modifier)
+ self._cModifier.setFontWeight(self._bold)
- self._keyChar = QTextCharFormat()
- self._keyChar.setForeground(SHARED.theme.colKey)
+ self._cKeyword = QTextCharFormat()
+ self._cKeyword.setForeground(self._theme.keyword)
- self._tagChar = QTextCharFormat()
- self._tagChar.setForeground(SHARED.theme.colTag)
+ self._cTag = QTextCharFormat()
+ self._cTag.setForeground(self._theme.tag)
- self._optChar = QTextCharFormat()
- self._optChar.setForeground(SHARED.theme.colOpt)
+ self._cOptional = QTextCharFormat()
+ self._cOptional.setForeground(self._theme.optional)
- self._defaultBlock = QTextBlockFormat()
- self._defaultBlock.setTopMargin(self._mText[0])
- self._defaultBlock.setBottomMargin(self._mText[1])
+ self._blockFmt = QTextBlockFormat()
+ self._blockFmt.setTopMargin(self._mText[0])
+ self._blockFmt.setBottomMargin(self._mText[1])
self._init = True
@@ -159,11 +181,12 @@ def doConvert(self) -> None:
self._document.blockSignals(True)
cursor = QTextCursor(self._document)
+ cursor.movePosition(QTextCursor.MoveOperation.End)
for tType, nHead, tText, tFormat, tStyle in self._tokens:
# Styles
- bFmt = QTextBlockFormat(self._defaultBlock)
+ bFmt = QTextBlockFormat(self._blockFmt)
if tStyle is not None:
if tStyle & self.A_LEFT:
bFmt.setAlignment(QtAlignLeft)
@@ -191,7 +214,7 @@ def doConvert(self) -> None:
if tType == self.T_TEXT:
newBlock(cursor, bFmt)
- self._insertFragments(tText, tFormat, cursor, self._defaultChar)
+ self._insertFragments(tText, tFormat, cursor, self._cText)
elif tType in self.L_HEADINGS:
bFmt, cFmt = self._genHeadStyle(tType, nHead, bFmt)
@@ -200,23 +223,25 @@ def doConvert(self) -> None:
elif tType == self.T_SEP:
newBlock(cursor, bFmt)
- cursor.insertText(tText, self._defaultChar)
+ cursor.insertText(tText, self._cText)
elif tType == self.T_SKIP:
newBlock(cursor, bFmt)
- cursor.insertText(nwUnicode.U_NBSP, self._defaultChar)
+ cursor.insertText(nwUnicode.U_NBSP, self._cText)
elif tType in self.L_SUMMARY and self._doSynopsis:
newBlock(cursor, bFmt)
- prefix = self._localLookup(
+ modifier = self._localLookup(
"Short Description" if tType == self.T_SHORT else "Synopsis"
)
- cursor.insertText(f"{prefix}: ", self._modChar)
- self._insertFragments(tText, tFormat, cursor, self._noteChar)
+ cursor.insertText(f"{modifier}: ", self._cModifier)
+ self._insertFragments(tText, tFormat, cursor, self._cNote)
elif tType == self.T_COMMENT and self._doComments:
newBlock(cursor, bFmt)
- self._insertFragments(tText, tFormat, cursor, self._comChar)
+ modifier = self._localLookup("Comment")
+ cursor.insertText(f"{modifier}: ", self._cCommentMod)
+ self._insertFragments(tText, tFormat, cursor, self._cComment)
elif tType == self.T_KEYWORD and self._doKeywords:
newBlock(cursor, bFmt)
@@ -234,18 +259,18 @@ def appendFootnotes(self) -> None:
cursor = QTextCursor(self._document)
cursor.movePosition(QTextCursor.MoveOperation.End)
- bFmt, cFmt = self._genHeadStyle(self.T_HEAD3, -1, self._defaultBlock)
+ bFmt, cFmt = self._genHeadStyle(self.T_HEAD3, -1, self._blockFmt)
newBlock(cursor, bFmt)
cursor.insertText(self._localLookup("Footnotes"), cFmt)
for key, index in self._usedNotes.items():
if content := self._footnotes.get(key):
- cFmt = QTextCharFormat(self._codeChar)
+ cFmt = QTextCharFormat(self._cCode)
cFmt.setAnchor(True)
cFmt.setAnchorNames([f"footnote_{index}"])
- newBlock(cursor, self._defaultBlock)
+ newBlock(cursor, self._blockFmt)
cursor.insertText(f"{index}. ", cFmt)
- self._insertFragments(*content, cursor, self._defaultChar)
+ self._insertFragments(*content, cursor, self._cText)
self._document.blockSignals(False)
@@ -285,7 +310,7 @@ def _insertFragments(
elif fmt == self.FMT_U_E:
cFmt.setFontUnderline(False)
elif fmt == self.FMT_M_B:
- cFmt.setBackground(SHARED.theme.colMark)
+ cFmt.setBackground(self._theme.highlight)
elif fmt == self.FMT_M_E:
cFmt.setBackground(QtTransparent)
elif fmt == self.FMT_SUP_B:
@@ -297,7 +322,7 @@ def _insertFragments(
elif fmt == self.FMT_SUB_E:
cFmt.setVerticalAlignment(QtVAlignNormal)
elif fmt == self.FMT_FNOTE:
- xFmt = QTextCharFormat(self._codeChar)
+ xFmt = QTextCharFormat(self._cCode)
xFmt.setVerticalAlignment(QtVAlignSuper)
if data in self._footnotes:
index = len(self._usedNotes) + 1
@@ -322,37 +347,36 @@ def _insertKeywords(self, text: str, cursor: QTextCursor) -> None:
valid, bits, _ = self._project.index.scanThis("@"+text)
if valid and bits:
key = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
- cursor.insertText(key, self._keyChar)
+ cursor.insertText(key, self._cKeyword)
if (num := len(bits)) > 1:
if bits[0] == nwKeyWords.TAG_KEY:
one, two = self._project.index.parseValue(bits[1])
- cursor.insertText(one, self._tagChar)
+ cursor.insertText(one, self._cTag)
if two:
- cursor.insertText(" | ", self._defaultChar)
- cursor.insertText(two, self._optChar)
+ cursor.insertText(" | ", self._cText)
+ cursor.insertText(two, self._cOptional)
else:
for n, bit in enumerate(bits[1:], 2):
- cFmt = QTextCharFormat(self._tagChar)
+ cFmt = QTextCharFormat(self._cTag)
cFmt.setFontUnderline(True)
cFmt.setAnchor(True)
cFmt.setAnchorHref(f"#{bits[0][1:]}={bit}")
cursor.insertText(bit, cFmt)
if n < num:
- cursor.insertText(", ", self._defaultChar)
+ cursor.insertText(", ", self._cText)
return
- def _genHeadStyle(self, level: int, nHead: int, rFmt: QTextBlockFormat) -> T_TextStyle:
+ def _genHeadStyle(self, hType: int, nHead: int, rFmt: QTextBlockFormat) -> T_TextStyle:
"""Generate a heading style set."""
- mTop, mBottom = self._mHead.get(level, (0.0, 0.0))
+ mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
bFmt = QTextBlockFormat(rFmt)
bFmt.setTopMargin(mTop)
bFmt.setBottomMargin(mBottom)
- cFmt = QTextCharFormat(self._defaultChar)
- cFmt.setForeground(SHARED.theme.colHead)
- cFmt.setFontWeight(QFont.Weight.Bold)
- cFmt.setFontPointSize(self._sHead.get(level, 1.0))
+ cFmt = QTextCharFormat(self._cText if hType == self.T_TITLE else self._cHead)
+ cFmt.setFontWeight(self._bold)
+ cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
if nHead >= 0:
cFmt.setAnchorNames([f"{self._handle}:T{nHead:04d}"])
cFmt.setAnchor(True)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 6c3569df0..4eda952c9 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -40,7 +40,7 @@
from novelwriter import CONFIG, SHARED
from novelwriter.common import cssCol
from novelwriter.constants import nwHeaders, nwUnicode
-from novelwriter.core.toqdoc import ToQTextDocument
+from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
from novelwriter.error import logException
from novelwriter.extensions.eventfilters import WheelEventFilter
@@ -69,6 +69,7 @@ def __init__(self, parent: QWidget) -> None:
# Internal Variables
self._docHandle = None
+ self._docTheme = TextDocumentTheme()
# Settings
self.setMinimumWidth(CONFIG.pxInt(300))
@@ -152,6 +153,17 @@ def initViewer(self) -> None:
docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.viewport().setPalette(docPalette)
+ self._docTheme.text = SHARED.theme.colText
+ self._docTheme.highlight = SHARED.theme.colMark
+ self._docTheme.head = SHARED.theme.colHead
+ self._docTheme.comment = SHARED.theme.colHidden
+ self._docTheme.note = SHARED.theme.colNote
+ self._docTheme.code = SHARED.theme.colCode
+ self._docTheme.modifier = SHARED.theme.colMod
+ self._docTheme.keyword = SHARED.theme.colKey
+ self._docTheme.tag = SHARED.theme.colTag
+ self._docTheme.optional = SHARED.theme.colOpt
+
self.docHeader.matchColours()
self.docFooter.matchColours()
@@ -203,11 +215,10 @@ def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
sPos = self.verticalScrollBar().value()
qDoc = ToQTextDocument(SHARED.project)
- qDoc.initDocument(CONFIG.textFont)
+ qDoc.initDocument(CONFIG.textFont, self._docTheme)
qDoc.setKeywords(True)
qDoc.setComments(CONFIG.viewComments)
qDoc.setSynopsis(CONFIG.viewSynopsis)
- qDoc.setLinkHeadings(True)
# Be extra careful here to prevent crashes when first opening a
# project as a crash here leaves no way of recovering.
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 7e27b8679..84c043dcb 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -31,7 +31,7 @@
from typing import TYPE_CHECKING
from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal, pyqtSlot
-from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
+from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent, QTextDocument
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtWidgets import (
QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout,
@@ -44,8 +44,8 @@
from novelwriter.common import checkInt, fuzzyTime
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.core.docbuild import NWBuildDocument
-from novelwriter.core.tohtml import ToHtml
from novelwriter.core.tokenizer import HeadingFormatter
+from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.error import logException
from novelwriter.extensions.circularprogress import NProgressCircle
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
@@ -250,19 +250,19 @@ def loadContent(self) -> None:
if selected in self._buildMap:
self.buildList.setCurrentItem(self._buildMap[selected])
- logger.debug("Loading build cache")
- cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
- if cache.is_file():
- try:
- with open(cache, mode="r", encoding="utf-8") as fObj:
- data = json.load(fObj)
- build = self._builds.getBuild(data.get("uuid", ""))
- if isinstance(build, BuildSettings):
- self._updatePreview(data, build)
- except Exception:
- logger.error("Failed to load build cache")
- logException()
- return
+ # logger.debug("Loading build cache")
+ # cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
+ # if cache.is_file():
+ # try:
+ # with open(cache, mode="r", encoding="utf-8") as fObj:
+ # data = json.load(fObj)
+ # build = self._builds.getBuild(data.get("uuid", ""))
+ # if isinstance(build, BuildSettings):
+ # self._updatePreview(data, build)
+ # except Exception:
+ # logger.error("Failed to load build cache")
+ # logException()
+ # return
return
@@ -345,29 +345,38 @@ def _generatePreview(self) -> None:
docBuild.setPreviewMode(True)
docBuild.queueAll()
+ theme = TextDocumentTheme()
+ theme.text = QColor(0, 0, 0)
+ theme.highlight = QColor(255, 255, 166)
+ theme.head = QColor(66, 113, 174)
+ theme.comment = QColor(100, 100, 100)
+ theme.note = QColor(129, 55, 9)
+ theme.code = QColor(66, 113, 174)
+ theme.modifier = QColor(129, 55, 9)
+ theme.keyword = QColor(245, 135, 31)
+ theme.tag = QColor(66, 113, 174)
+ theme.optional = QColor(66, 113, 174)
+
self.docPreview.beginNewBuild(len(docBuild))
- for step, _ in docBuild.iterBuildHTML(None):
+ for step, _ in docBuild.iterBuildPreview(theme):
self.docPreview.buildStep(step + 1)
QApplication.processEvents()
buildObj = docBuild.lastBuild
- assert isinstance(buildObj, ToHtml)
- result = {
+ assert isinstance(buildObj, ToQTextDocument)
+ data = {
"uuid": build.buildID,
"time": int(time()),
"stats": buildObj.textStats,
"outline": buildObj.textOutline,
- "styles": buildObj.getStyleSheet(),
- "html": buildObj.fullHTML,
}
-
- self._updatePreview(result, build)
+ self._updatePreview(data, build, buildObj.document)
logger.debug("Saving build cache")
cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
try:
with open(cache, mode="w+", encoding="utf-8") as outFile:
- outFile.write(json.dumps(result, indent=2))
+ outFile.write(json.dumps(data, indent=2))
except Exception:
logger.error("Failed to save build cache")
logException()
@@ -400,17 +409,14 @@ def _printDocument(self) -> None:
# Internal Functions
##
- def _updatePreview(self, data: dict, build: BuildSettings) -> None:
+ def _updatePreview(self, data: dict, build: BuildSettings, document: QTextDocument) -> None:
"""Update the preview widget and set relevant values."""
- textFont = QFont()
- textFont.fromString(build.getStr("format.textFont"))
-
- self.docPreview.setContent(data)
+ font = QFont()
+ font.fromString(build.getStr("format.textFont"))
+ self.docPreview.setTextFont(font)
+ self.docPreview.setContent(data, document)
self.docPreview.setBuildName(build.name)
- self.docPreview.setTextFont(textFont)
- self.docPreview.setJustify(
- build.getBool("format.justifyText")
- )
+ self.docPreview.setJustify(build.getBool("format.justifyText"))
self.docStats.updateStats(data.get("stats", {}))
self.buildOutline.updateOutline(data.get("outline", {}))
return
@@ -848,30 +854,22 @@ def buildStep(self, value: int) -> None:
QApplication.processEvents()
return
- def setContent(self, data: dict) -> None:
+ def setContent(self, data: dict, doc: QTextDocument) -> None:
"""Set the content of the preview widget."""
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
self.buildProgress.setCentreText(self.tr("Processing ..."))
QApplication.processEvents()
- styles = "\n".join(data.get("styles", []))
- self.document().setDefaultStyleSheet(styles)
-
- html = "".join(data.get("html", []))
- html = html.replace("\t", "!!tab!!")
- self.setHtml(html)
- QApplication.processEvents()
- while self.find("!!tab!!"):
- cursor = self.textCursor()
- cursor.insertText("\t")
+ doc.setDocumentMargin(CONFIG.getTextMargin())
+ self.setDocument(doc)
self._docTime = checkInt(data.get("time"), 0)
self._updateBuildAge()
# Since we change the content while it may still be rendering, we mark
# the document as dirty again to make sure it's re-rendered properly.
- self.document().markContentsDirty(0, self.document().characterCount())
+ # self.document().markContentsDirty(0, self.document().characterCount())
self.buildProgress.setCentreText(self.tr("Done"))
QApplication.restoreOverrideCursor()
diff --git a/novelwriter/types.py b/novelwriter/types.py
index c45d79e08..8cdfb1db7 100644
--- a/novelwriter/types.py
+++ b/novelwriter/types.py
@@ -56,6 +56,7 @@
# Qt Painter Types
QtTransparent = QColor(0, 0, 0, 0)
+QtBlack = QColor(0, 0, 0)
QtNoBrush = Qt.BrushStyle.NoBrush
QtNoPen = Qt.PenStyle.NoPen
QtRoundCap = Qt.PenCapStyle.RoundCap
From e74c1057a4e08440d63ce49933503fd5fb65713d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 24 May 2024 20:12:13 +0200
Subject: [PATCH 08/13] Remove HTML preview mode and manuscript cache
---
novelwriter/core/docbuild.py | 20 ++-----
novelwriter/core/tohtml.py | 69 +++++-----------------
novelwriter/tools/manuscript.py | 89 ++++++++---------------------
tests/test_core/test_core_tohtml.py | 41 -------------
4 files changed, 42 insertions(+), 177 deletions(-)
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index 9c17e73e9..a6d47740b 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -55,7 +55,7 @@ class NWBuildDocument:
__slots__ = (
"_project", "_build", "_queue", "_error", "_cache", "_count",
- "_outline", "_preview"
+ "_outline",
)
def __init__(self, project: NWProject, build: BuildSettings) -> None:
@@ -66,7 +66,6 @@ def __init__(self, project: NWProject, build: BuildSettings) -> None:
self._cache = None
self._count = False
self._outline = False
- self._preview = False
return
##
@@ -100,15 +99,6 @@ def setBuildOutline(self, state: bool) -> None:
self._outline = state
return
- def setPreviewMode(self, state: bool) -> None:
- """Set the preview mode of the build. This also enables stats
- count and outline mode.
- """
- self._preview = state
- self._outline = state
- self._count = state
- return
-
##
# Special Methods
##
@@ -140,10 +130,12 @@ def iterBuildPreview(self, theme: TextDocumentTheme) -> Iterable[tuple[int, bool
makeObj = ToQTextDocument(self._project)
filtered = self._setupBuild(makeObj)
+ self._outline = True
+ self._count = True
+
font = QFont()
font.fromString(self._build.getStr("format.textFont"))
- makeObj.setLinkHeadings(self._preview)
makeObj.initDocument(font, theme)
for i, tHandle in enumerate(self._queue):
self._error = None
@@ -207,8 +199,6 @@ def iterBuildHTML(self, path: Path | None, asJson: bool = False) -> Iterable[tup
makeObj = ToHtml(self._project)
filtered = self._setupBuild(makeObj)
- makeObj.setPreview(self._preview)
- makeObj.setLinkHeadings(self._preview)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
@@ -218,7 +208,7 @@ def iterBuildHTML(self, path: Path | None, asJson: bool = False) -> Iterable[tup
makeObj.appendFootnotes()
- if not (self._build.getBool("html.preserveTabs") or self._preview):
+ if not self._build.getBool("html.preserveTabs"):
makeObj.replaceTabs()
self._error = None
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index f16cc515c..33e597891 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -38,24 +38,6 @@
logger = logging.getLogger(__name__)
-HTML4_TAGS = {
- Tokenizer.FMT_B_B: "",
- Tokenizer.FMT_B_E: "",
- Tokenizer.FMT_I_B: "",
- Tokenizer.FMT_I_E: "",
- Tokenizer.FMT_D_B: "",
- Tokenizer.FMT_D_E: "",
- Tokenizer.FMT_U_B: "",
- Tokenizer.FMT_U_E: "",
- Tokenizer.FMT_M_B: "",
- Tokenizer.FMT_M_E: "",
- Tokenizer.FMT_SUP_B: "",
- Tokenizer.FMT_SUP_E: "",
- Tokenizer.FMT_SUB_B: "",
- Tokenizer.FMT_SUB_E: "",
- Tokenizer.FMT_STRIP: "",
-}
-
HTML5_TAGS = {
Tokenizer.FMT_B_B: "",
Tokenizer.FMT_B_E: "",
@@ -82,14 +64,9 @@ class ToHtml(Tokenizer):
also used by the Document Viewer, and Manuscript Build Preview.
"""
- M_PREVIEW = 0 # Tweak output for the DocViewer
- M_EXPORT = 1 # Tweak output for saving to HTML or printing
- M_EBOOK = 2 # Tweak output for converting to epub
-
def __init__(self, project: NWProject) -> None:
super().__init__(project)
- self._genMode = self.M_EXPORT
self._cssStyles = True
self._fullHTML: list[str] = []
@@ -112,11 +89,6 @@ def fullHTML(self) -> list[str]:
# Setters
##
- def setPreview(self, state: bool) -> None:
- """Set to preview generator mode."""
- self._genMode = self.M_PREVIEW if state else self.M_EXPORT
- return
-
def setStyles(self, cssStyles: bool) -> None:
"""Enable or disable CSS styling. Some elements may still have
class tags.
@@ -157,8 +129,7 @@ def doConvert(self) -> None:
"""Convert the list of text tokens into an HTML document."""
self._result = ""
- hTags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS
- if self._isNovel and self._genMode != self.M_PREVIEW:
+ if self._isNovel:
# For story files, we bump the titles one level up
h1Cl = " class='title'"
h1 = "h1"
@@ -240,7 +211,7 @@ def doConvert(self) -> None:
# Process Text Type
if tType == self.T_TEXT:
- lines.append(f"
{self._formatText(tText, tFormat, hTags)}
\n")
+ lines.append(f"{self._formatText(tText, tFormat)}
\n")
elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "
")
@@ -269,13 +240,13 @@ def doConvert(self) -> None:
lines.append(f"
\n")
elif tType == self.T_SYNOPSIS and self._doSynopsis:
- lines.append(self._formatSynopsis(self._formatText(tText, tFormat, hTags), True))
+ lines.append(self._formatSynopsis(self._formatText(tText, tFormat), True))
elif tType == self.T_SHORT and self._doSynopsis:
- lines.append(self._formatSynopsis(self._formatText(tText, tFormat, hTags), False))
+ lines.append(self._formatSynopsis(self._formatText(tText, tFormat), False))
elif tType == self.T_COMMENT and self._doComments:
- lines.append(self._formatComments(self._formatText(tText, tFormat, hTags)))
+ lines.append(self._formatComments(self._formatText(tText, tFormat)))
elif tType == self.T_KEYWORD and self._doKeywords:
tag, text = self._formatKeywords(tText)
@@ -291,7 +262,6 @@ def doConvert(self) -> None:
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
if self._usedNotes:
- tags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS
footnotes = self._localLookup("Footnotes")
lines = []
@@ -299,7 +269,7 @@ def appendFootnotes(self) -> None:
lines.append("\n")
for key, index in self._usedNotes.items():
if content := self._footnotes.get(key):
- text = self._formatText(*content, tags)
+ text = self._formatText(*content)
lines.append(f"\n")
lines.append("
\n")
@@ -468,7 +438,7 @@ def getStyleSheet(self) -> list[str]:
# Internal Functions
##
- def _formatText(self, text: str, tFmt: T_Formats, tags: dict[int, str]) -> str:
+ def _formatText(self, text: str, tFmt: T_Formats) -> str:
"""Apply formatting tags to text."""
temp = text
for pos, fmt, data in reversed(tFmt):
@@ -481,7 +451,7 @@ def _formatText(self, text: str, tFmt: T_Formats, tags: dict[int, str]) -> str:
else:
html = "ERR"
else:
- html = tags.get(fmt, "ERR")
+ html = HTML5_TAGS.get(fmt, "ERR")
temp = f"{temp[:pos]}{html}{temp[pos:]}"
temp = temp.replace("\n", "
")
return stripEscape(temp)
@@ -492,18 +462,12 @@ def _formatSynopsis(self, text: str, synopsis: bool) -> str:
sSynop = self._localLookup("Synopsis")
else:
sSynop = self._localLookup("Short Description")
- if self._genMode == self.M_PREVIEW:
- return f"{sSynop}: {text}
\n"
- else:
- return f"{sSynop}: {text}
\n"
+ return f"{sSynop}: {text}
\n"
def _formatComments(self, text: str) -> str:
"""Apply HTML formatting to comments."""
- if self._genMode == self.M_PREVIEW:
- return f"\n"
- else:
- sComm = self._localLookup("Comment")
- return f"\n"
+ sComm = self._localLookup("Comment")
+ return f"\n"
def _formatKeywords(self, text: str) -> tuple[str, str]:
"""Apply HTML formatting to keywords."""
@@ -519,13 +483,8 @@ def _formatKeywords(self, text: str) -> tuple[str, str]:
if two:
result += f" | {two}"
else:
- if self._genMode == self.M_PREVIEW:
- result += ", ".join(
- f"{t}" for t in bits[1:]
- )
- else:
- result += ", ".join(
- f"{t}" for t in bits[1:]
- )
+ result += ", ".join(
+ f"{t}" for t in bits[1:]
+ )
return bits[0][1:], result
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 84c043dcb..d7da4cb08 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -23,10 +23,8 @@
"""
from __future__ import annotations
-import json
import logging
-from datetime import datetime
from time import time
from typing import TYPE_CHECKING
@@ -41,12 +39,11 @@
)
from novelwriter import CONFIG, SHARED
-from novelwriter.common import checkInt, fuzzyTime
+from novelwriter.common import fuzzyTime
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.tokenizer import HeadingFormatter
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
-from novelwriter.error import logException
from novelwriter.extensions.circularprogress import NProgressCircle
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
@@ -250,20 +247,6 @@ def loadContent(self) -> None:
if selected in self._buildMap:
self.buildList.setCurrentItem(self._buildMap[selected])
- # logger.debug("Loading build cache")
- # cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
- # if cache.is_file():
- # try:
- # with open(cache, mode="r", encoding="utf-8") as fObj:
- # data = json.load(fObj)
- # build = self._builds.getBuild(data.get("uuid", ""))
- # if isinstance(build, BuildSettings):
- # self._updatePreview(data, build)
- # except Exception:
- # logger.error("Failed to load build cache")
- # logException()
- # return
-
return
##
@@ -342,7 +325,6 @@ def _generatePreview(self) -> None:
SHARED.saveDocument()
docBuild = NWBuildDocument(SHARED.project, build)
- docBuild.setPreviewMode(True)
docBuild.queueAll()
theme = TextDocumentTheme()
@@ -364,23 +346,17 @@ def _generatePreview(self) -> None:
buildObj = docBuild.lastBuild
assert isinstance(buildObj, ToQTextDocument)
- data = {
- "uuid": build.buildID,
- "time": int(time()),
- "stats": buildObj.textStats,
- "outline": buildObj.textOutline,
- }
- self._updatePreview(data, build, buildObj.document)
-
- logger.debug("Saving build cache")
- cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
- try:
- with open(cache, mode="w+", encoding="utf-8") as outFile:
- outFile.write(json.dumps(data, indent=2))
- except Exception:
- logger.error("Failed to save build cache")
- logException()
- return
+
+ font = QFont()
+ font.fromString(build.getStr("format.textFont"))
+
+ self.docPreview.setTextFont(font)
+ self.docPreview.setContent(buildObj.document)
+ self.docPreview.setBuildName(build.name)
+ self.docPreview.setJustify(build.getBool("format.justifyText"))
+
+ self.docStats.updateStats(buildObj.textStats)
+ self.buildOutline.updateOutline(buildObj.textOutline)
return
@@ -409,18 +385,6 @@ def _printDocument(self) -> None:
# Internal Functions
##
- def _updatePreview(self, data: dict, build: BuildSettings, document: QTextDocument) -> None:
- """Update the preview widget and set relevant values."""
- font = QFont()
- font.fromString(build.getStr("format.textFont"))
- self.docPreview.setTextFont(font)
- self.docPreview.setContent(data, document)
- self.docPreview.setBuildName(build.name)
- self.docPreview.setJustify(build.getBool("format.justifyText"))
- self.docStats.updateStats(data.get("stats", {}))
- self.buildOutline.updateOutline(data.get("outline", {}))
- return
-
def _getSelectedBuild(self) -> BuildSettings | None:
"""Get the currently selected build. If none are selected,
automatically select the first one.
@@ -854,23 +818,19 @@ def buildStep(self, value: int) -> None:
QApplication.processEvents()
return
- def setContent(self, data: dict, doc: QTextDocument) -> None:
+ def setContent(self, document: QTextDocument) -> None:
"""Set the content of the preview widget."""
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
self.buildProgress.setCentreText(self.tr("Processing ..."))
QApplication.processEvents()
- doc.setDocumentMargin(CONFIG.getTextMargin())
- self.setDocument(doc)
+ document.setDocumentMargin(CONFIG.getTextMargin())
+ self.setDocument(document)
- self._docTime = checkInt(data.get("time"), 0)
+ self._docTime = int(time())
self._updateBuildAge()
- # Since we change the content while it may still be rendering, we mark
- # the document as dirty again to make sure it's re-rendered properly.
- # self.document().markContentsDirty(0, self.document().characterCount())
-
self.buildProgress.setCentreText(self.tr("Done"))
QApplication.restoreOverrideCursor()
QApplication.processEvents()
@@ -915,17 +875,14 @@ def navigateTo(self, anchor: str) -> None:
@pyqtSlot()
def _updateBuildAge(self) -> None:
"""Update the build time and the fuzzy age."""
- if self._docTime > 0:
- strBuildTime = "%s (%s)" % (
- CONFIG.localDateTime(datetime.fromtimestamp(self._docTime)),
- fuzzyTime(int(time()) - self._docTime)
- )
+ if self._buildName and self._docTime > 0:
+ self.ageLabel.setText("{0}
{1}: {2}".format(
+ self._buildName,
+ self.tr("Built"),
+ fuzzyTime(int(time()) - self._docTime),
+ ))
else:
- strBuildTime = self.tr("Unknown")
- text = "{0}: {1}".format(self.tr("Built"), strBuildTime)
- if self._buildName:
- text = "{0}
{1}".format(self._buildName, text)
- self.ageLabel.setText(text)
+ self.ageLabel.setText("{0}".format(self.tr("No Preview")))
return
@pyqtSlot()
diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py
index c02e833c7..d8abf99bb 100644
--- a/tests/test_core/test_core_tohtml.py
+++ b/tests/test_core/test_core_tohtml.py
@@ -284,21 +284,6 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
"\n"
)
- # Preview Mode
- # ============
-
- html.setPreview(True)
-
- # Text (HTML4)
- html._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
- html.tokenizeText()
- html.doConvert()
- assert html.result == (
- "Some nested bold and italic and "
- "strikethrough "
- "text here
\n"
- )
-
@pytest.mark.core
def testCoreToHtml_ConvertDirect(mockGUI):
@@ -682,29 +667,3 @@ def testCoreToHtml_Format(mockGUI):
"Bod, "
"Jane"
)
-
- # Preview Mode
- # ============
-
- html.setPreview(True)
-
- assert html._formatSynopsis("synopsis text", True) == (
- "Synopsis: synopsis text
\n"
- )
- assert html._formatSynopsis("short text", False) == (
- "Short Description: short text
\n"
- )
- assert html._formatComments("comment text") == (
- "\n"
- )
-
- assert html._formatKeywords("") == ("", "")
- assert html._formatKeywords("tag: Jane") == (
- "tag", "Tag: Jane"
- )
- assert html._formatKeywords("char: Bod, Jane") == (
- "char",
- "Characters: "
- "Bod, "
- "Jane"
- )
From a4e3bf848b2d2e89aa9787e66eabb41b512aa03d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 24 May 2024 20:38:34 +0200
Subject: [PATCH 09/13] Clean up no longer needed code, and fix text justify
setting
---
novelwriter/core/toqdoc.py | 25 ++++++---
novelwriter/gui/doceditor.py | 18 ++-----
novelwriter/gui/docviewer.py | 62 +++--------------------
novelwriter/tools/manuscript.py | 15 +-----
tests/test_gui/test_gui_docviewer.py | 2 +-
tests/test_tools/test_tools_manuscript.py | 55 +++++++-------------
6 files changed, 52 insertions(+), 125 deletions(-)
diff --git a/novelwriter/core/toqdoc.py b/novelwriter/core/toqdoc.py
index bfe4aa52c..ee530fd85 100644
--- a/novelwriter/core/toqdoc.py
+++ b/novelwriter/core/toqdoc.py
@@ -34,9 +34,9 @@
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import T_Formats, Tokenizer
from novelwriter.types import (
- QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight, QtBlack,
- QtPageBreakAfter, QtPageBreakBefore, QtTransparent, QtVAlignNormal,
- QtVAlignSub, QtVAlignSuper
+ QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
+ QtBlack, QtPageBreakAfter, QtPageBreakBefore, QtTransparent,
+ QtVAlignNormal, QtVAlignSub, QtVAlignSuper
)
logger = logging.getLogger(__name__)
@@ -75,6 +75,7 @@ def __init__(self, project: NWProject) -> None:
super().__init__(project)
self._document = QTextDocument()
self._document.setUndoRedoEnabled(False)
+ self._document.setDocumentMargin(0)
self._theme = TextDocumentTheme()
self._styles: dict[int, T_TextStyle] = {}
@@ -100,6 +101,9 @@ def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
mScale = qMetric.height()
fPt = self._textFont.pointSizeF()
+ # Scaled Sizes
+ # ============
+
self._mHead = {
self.T_TITLE: (mScale * self._marginTitle[0], mScale * self._marginTitle[1]),
self.T_HEAD1: (mScale * self._marginHead1[0], mScale * self._marginHead1[1]),
@@ -121,6 +125,17 @@ def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
self._mIndent = mScale * 2.0
+ # Block Format
+ # ============
+
+ self._blockFmt = QTextBlockFormat()
+ self._blockFmt.setTopMargin(self._mText[0])
+ self._blockFmt.setBottomMargin(self._mText[1])
+ self._blockFmt.setAlignment(QtAlignJustify if self._doJustify else QtAlignAbsolute)
+
+ # Character Formats
+ # =================
+
self._cText = QTextCharFormat()
self._cText.setForeground(self._theme.text)
@@ -153,10 +168,6 @@ def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
self._cOptional = QTextCharFormat()
self._cOptional.setForeground(self._theme.optional)
- self._blockFmt = QTextBlockFormat()
- self._blockFmt.setTopMargin(self._mText[0])
- self._blockFmt.setBottomMargin(self._mText[1])
-
self._init = True
return
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 7fdbfdf9e..6b16f021a 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -321,8 +321,11 @@ def initEditor(self) -> None:
# Reload spell check and dictionaries
SHARED.updateSpellCheckLanguage()
- # Set font
- self.initFont()
+ # Set the font. See issues #1862 and #1875.
+ self.setFont(CONFIG.textFont)
+ self.docHeader.updateFont()
+ self.docFooter.updateFont()
+ self.docSearch.updateFont()
# Update highlighter settings
self._qDocument.syntaxHighlighter.initHighlighter()
@@ -372,17 +375,6 @@ def initEditor(self) -> None:
return
- def initFont(self) -> None:
- """Set the font of the main widget and sub-widgets. This needs
- special attention since there appears to be a bug in Qt 5.15.3.
- See issues #1862 and #1875.
- """
- self.setFont(CONFIG.textFont)
- self.docHeader.updateFont()
- self.docFooter.updateFont()
- self.docSearch.updateFont()
- return
-
def loadText(self, tHandle: str, tLine: int | None = None) -> bool:
"""Load text from a document into the editor. If we have an I/O
error, we must handle this and clear the editor so that we don't
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 4eda952c9..cba386f58 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -31,14 +31,13 @@
from enum import Enum
from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot
-from PyQt5.QtGui import QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor, QTextOption
+from PyQt5.QtGui import QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor
from PyQt5.QtWidgets import (
QAction, QApplication, QFrame, QHBoxLayout, QLabel, QMenu, QTextBrowser,
QToolButton, QWidget
)
from novelwriter import CONFIG, SHARED
-from novelwriter.common import cssCol
from novelwriter.constants import nwHeaders, nwUnicode
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
@@ -46,9 +45,7 @@
from novelwriter.extensions.eventfilters import WheelEventFilter
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
-from novelwriter.types import (
- QtAlignCenterTop, QtAlignJustify, QtKeepAnchor, QtMouseLeft, QtMoveAnchor
-)
+from novelwriter.types import QtAlignCenterTop, QtKeepAnchor, QtMouseLeft, QtMoveAnchor
logger = logging.getLogger(__name__)
@@ -138,8 +135,10 @@ def updateTheme(self) -> None:
def initViewer(self) -> None:
"""Set editor settings from main config."""
- self._makeStyleSheet()
- self.initFont()
+ # Set the font. See issues #1862 and #1875.
+ self.setFont(CONFIG.textFont)
+ self.docHeader.updateFont()
+ self.docFooter.updateFont()
# Set the widget colours to match syntax theme
mainPalette = self.palette()
@@ -153,6 +152,7 @@ def initViewer(self) -> None:
docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.viewport().setPalette(docPalette)
+ # Update theme colours
self._docTheme.text = SHARED.theme.colText
self._docTheme.highlight = SHARED.theme.colMark
self._docTheme.head = SHARED.theme.colHead
@@ -169,10 +169,6 @@ def initViewer(self) -> None:
# Set default text margins
self.document().setDocumentMargin(0)
- options = QTextOption()
- if CONFIG.doJustify:
- options.setAlignment(QtAlignJustify)
- self.document().setDefaultTextOption(options)
# Scroll bars
if CONFIG.hideVScroll:
@@ -193,16 +189,6 @@ def initViewer(self) -> None:
return
- def initFont(self) -> None:
- """Set the font of the main widget and sub-widgets. This needs
- special attention since there appears to be a bug in Qt 5.15.3.
- See issues #1862 and #1875.
- """
- self.setFont(CONFIG.textFont)
- self.docHeader.updateFont()
- self.docFooter.updateFont()
- return
-
def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
"""Load text into the viewer from an item handle."""
if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
@@ -215,6 +201,7 @@ def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
sPos = self.verticalScrollBar().value()
qDoc = ToQTextDocument(SHARED.project)
+ qDoc.setJustify(CONFIG.doJustify)
qDoc.initDocument(CONFIG.textFont, self._docTheme)
qDoc.setKeywords(True)
qDoc.setComments(CONFIG.viewComments)
@@ -244,11 +231,6 @@ def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
self.docHistory.append(tHandle)
self.setDocumentTitle(tHandle)
-
- # Replace tabs before setting the HTML, and then put them back in
- # self.setHtml(qDoc.result.replace("\t", "!!tab!!"))
- # while self.find("!!tab!!"):
- # self.textCursor().insertText("\t")
self.setDocument(qDoc.document)
if self._docHandle == tHandle:
@@ -482,34 +464,6 @@ def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) ->
self._makeSelection(selType)
return
- def _makeStyleSheet(self) -> None:
- """Generate an appropriate style sheet for the document viewer,
- based on the current syntax highlighter theme.
- """
- colHead = cssCol(SHARED.theme.colHead)
- colHide = cssCol(SHARED.theme.colHidden)
- colKeys = cssCol(SHARED.theme.colKey)
- colMark = cssCol(SHARED.theme.colMark)
- colMods = cssCol(SHARED.theme.colMod)
- colNote = cssCol(SHARED.theme.colNote)
- colOpts = cssCol(SHARED.theme.colOpt)
- colTags = cssCol(SHARED.theme.colTag)
- colText = cssCol(SHARED.theme.colText)
- self.document().setDefaultStyleSheet(
- f"body {{color: {colText};}}\n"
- f"h1, h2, h3, h4 {{color: {colHead};}}\n"
- f"mark {{background-color: {colMark};}}\n"
- f".keyword {{color: {colKeys};}}\n"
- f".tag {{color: {colTags};}}\n"
- f".optional {{color: {colOpts};}}\n"
- f".comment {{color: {colHide};}}\n"
- f".note {{color: {colNote};}}\n"
- f".modifier {{color: {colMods};}}\n"
- ".title {text-align: center;}\n"
- )
-
- return
-
class GuiDocViewHistory:
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index d7da4cb08..263bf332d 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -50,8 +50,8 @@
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings
from novelwriter.types import (
- QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignRight, QtAlignTop,
- QtSizeExpanding, QtSizeIgnored, QtUserRole
+ QtAlignCenter, QtAlignRight, QtAlignTop, QtSizeExpanding, QtSizeIgnored,
+ QtUserRole
)
if TYPE_CHECKING: # pragma: no cover
@@ -353,7 +353,6 @@ def _generatePreview(self) -> None:
self.docPreview.setTextFont(font)
self.docPreview.setContent(buildObj.document)
self.docPreview.setBuildName(build.name)
- self.docPreview.setJustify(build.getBool("format.justifyText"))
self.docStats.updateStats(buildObj.textStats)
self.buildOutline.updateOutline(buildObj.textOutline)
@@ -777,16 +776,6 @@ def setBuildName(self, name: str) -> None:
self._updateBuildAge()
return
- def setJustify(self, state: bool) -> None:
- """Enable/disable the justify text option."""
- pOptions = self.document().defaultTextOption()
- if state:
- pOptions.setAlignment(QtAlignJustify)
- else:
- pOptions.setAlignment(QtAlignAbsolute)
- self.document().setDefaultTextOption(pOptions)
- return
-
def setTextFont(self, font: QFont) -> None:
"""Set the text font properties and then reset for sub-widgets.
This needs special attention since there appears to be a bug in
diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py
index 2fa287f61..72a71655b 100644
--- a/tests/test_gui/test_gui_docviewer.py
+++ b/tests/test_gui/test_gui_docviewer.py
@@ -202,7 +202,7 @@ def mockExec(*a):
# Document footer show/hide comments
assert nwGUI.viewDocument("846352075de7d") is True
- assert len(docViewer.toPlainText()) == 674
+ assert len(docViewer.toPlainText()) == 683
docViewer.docFooter._doToggleComments(False)
assert len(docViewer.toPlainText()) == 634
diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py
index 5d0c928cf..383ff098c 100644
--- a/tests/test_tools/test_tools_manuscript.py
+++ b/tests/test_tools/test_tools_manuscript.py
@@ -28,15 +28,14 @@
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from PyQt5.QtWidgets import QAction, QListWidgetItem
-from novelwriter import CONFIG, SHARED
+from novelwriter import SHARED
from novelwriter.constants import nwHeadFmt
from novelwriter.core.buildsettings import BuildSettings
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manuscript import GuiManuscript
from novelwriter.tools.manussettings import GuiBuildSettings
-from novelwriter.types import QtAlignAbsolute, QtAlignJustify, QtDialogApply, QtDialogSave
+from novelwriter.types import QtDialogApply, QtDialogSave
-from tests.mocked import causeOSError
from tests.tools import C, buildTestProject
@@ -64,23 +63,23 @@ def testManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.close()
- # A new dialog should load the old build
- manus = GuiManuscript(nwGUI)
- manus.show()
- manus.loadContent()
- assert manus.docPreview.toPlainText().strip() == allText
- manus.close()
+ # # A new dialog should load the old build
+ # manus = GuiManuscript(nwGUI)
+ # manus.show()
+ # manus.loadContent()
+ # assert manus.docPreview.toPlainText().strip() == allText
+ # manus.close()
- # But blocking the reload should leave it empty
- with monkeypatch.context() as mp:
- mp.setattr("builtins.open", lambda *a, **k: causeOSError)
- manus = GuiManuscript(nwGUI)
- manus.show()
- manus.loadContent()
- assert manus.docPreview.toPlainText().strip() == ""
+ # # But blocking the reload should leave it empty
+ # with monkeypatch.context() as mp:
+ # mp.setattr("builtins.open", lambda *a, **k: causeOSError)
+ # manus = GuiManuscript(nwGUI)
+ # manus.show()
+ # manus.loadContent()
+ # assert manus.docPreview.toPlainText().strip() == ""
- nwGUI.closeProject() # This should auto-close the manuscript tool
- assert manus.isHidden()
+ # nwGUI.closeProject() # This should auto-close the manuscript tool
+ # assert manus.isHidden()
# qtbot.stop()
@@ -186,7 +185,6 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.show()
manus.loadContent()
- cacheFile = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
manus.buildList.setCurrentRow(0)
build = manus._getSelectedBuild()
assert isinstance(build, BuildSettings)
@@ -199,17 +197,9 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.btnPreview.click()
qtbot.wait(200) # Should be enough to run the build
assert manus.docPreview.toPlainText().strip() == ""
- assert cacheFile.exists() is False
manus._updateBuildsList()
- # Preview the first, but fail to save cache
- manus.buildList.setCurrentRow(0)
- with monkeypatch.context() as mp:
- mp.setattr("builtins.open", lambda *a, **k: causeOSError)
- with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
- manus.btnPreview.click()
- assert cacheFile.exists() is False
-
+ # Preview the first
first = manus.buildList.item(0)
assert isinstance(first, QListWidgetItem)
build = manus._builds.getBuild(first.data(GuiManuscript.D_KEY))
@@ -218,12 +208,10 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
build.setValue("headings.fmtAltScene", nwHeadFmt.TITLE)
manus._builds.setBuild(build)
- # Preview again, and allow cache file to be created
manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() != ""
- assert cacheFile.exists() is True
# Check Outline
assert manus.buildOutline._outline == {
@@ -265,13 +253,6 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
assert manus.docStats.maxTotalWords.text() == "25"
assert manus.docStats.maxTotalChars.text() == "117"
- # Toggle justify
- assert manus.docPreview.document().defaultTextOption().alignment() == QtAlignAbsolute
- manus.docPreview.setJustify(True)
- assert manus.docPreview.document().defaultTextOption().alignment() == QtAlignJustify
- manus.docPreview.setJustify(False)
- assert manus.docPreview.document().defaultTextOption().alignment() == QtAlignAbsolute
-
# Tests are too fast to trigger this one, so we trigger it manually to ensure it isn't failing
manus.docPreview._postUpdate()
From f11ea8d07e83ad20add74187dc19acf2121ed2e5 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 24 May 2024 21:39:16 +0200
Subject: [PATCH 10/13] Make various fixes and tweaks, and update tests
---
novelwriter/core/tokenizer.py | 16 +++++++++++-----
novelwriter/core/toodt.py | 10 +++++++---
novelwriter/core/toqdoc.py | 15 +++++++++++----
novelwriter/gui/docviewer.py | 15 ++++++---------
.../reference/coreToOdt_SaveFlat_document.fodt | 18 +++++++++---------
tests/reference/coreToOdt_SaveFull_styles.xml | 14 +++++++-------
.../mBuildDocBuild_HTML5_Lorem_Ipsum.htm | 10 +++++-----
.../mBuildDocBuild_HTML5_Lorem_Ipsum.json | 14 +++++++-------
...BuildDocBuild_OpenDocument_Lorem_Ipsum.fodt | 18 +++++++++---------
tests/test_core/test_core_tokenizer.py | 13 ++++++++-----
tests/test_core/test_core_toodt.py | 4 ++--
tests/test_gui/test_gui_docviewer.py | 2 +-
12 files changed, 83 insertions(+), 66 deletions(-)
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index 850af65c4..d90cbf638 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -156,14 +156,15 @@ def __init__(self, project: NWProject) -> None:
self._keepBreaks = True # Keep line breaks in paragraphs
# Margins
- self._marginTitle = (1.000, 0.500)
- self._marginHead1 = (1.000, 0.500)
- self._marginHead2 = (0.834, 0.500)
- self._marginHead3 = (0.584, 0.500)
- self._marginHead4 = (0.584, 0.500)
+ self._marginTitle = (1.417, 0.500)
+ self._marginHead1 = (1.417, 0.500)
+ self._marginHead2 = (1.668, 0.500)
+ self._marginHead3 = (1.168, 0.500)
+ self._marginHead4 = (1.168, 0.500)
self._marginText = (0.000, 0.584)
self._marginMeta = (0.000, 0.584)
self._marginFoot = (1.417, 0.467)
+ self._marginSep = (1.168, 1.168)
# Title Formats
self._fmtTitle = nwHeadFmt.TITLE # Formatting for titles
@@ -379,6 +380,11 @@ def setMetaMargins(self, upper: float, lower: float) -> None:
self._marginMeta = (float(upper), float(lower))
return
+ def setSeparatorMargins(self, upper: float, lower: float) -> None:
+ """Set the upper and lower meta text margin."""
+ self._marginSep = (float(upper), float(lower))
+ return
+
def setLinkHeadings(self, state: bool) -> None:
"""Enable or disable adding an anchor before headings."""
self._linkHeadings = state
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index 9c631345b..be4a9fe87 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -192,6 +192,7 @@ def __init__(self, project: NWProject, isFlat: bool) -> None:
self._mTopHead = "0.423cm"
self._mTopText = "0.000cm"
self._mTopMeta = "0.000cm"
+ self._mTopSep = "0.247cm"
self._mBotTitle = "0.212cm"
self._mBotHead1 = "0.212cm"
@@ -201,6 +202,7 @@ def __init__(self, project: NWProject, isFlat: bool) -> None:
self._mBotHead = "0.212cm"
self._mBotText = "0.247cm"
self._mBotMeta = "0.106cm"
+ self._mBotSep = "0.247cm"
self._mBotFoot = "0.106cm"
self._mLeftFoot = "0.600cm"
@@ -299,6 +301,7 @@ def initDocument(self) -> None:
self._mTopHead = self._emToCm(mScale * self._marginHead4[0])
self._mTopText = self._emToCm(mScale * self._marginText[0])
self._mTopMeta = self._emToCm(mScale * self._marginMeta[0])
+ self._mTopSep = self._emToCm(mScale * self._marginSep[0])
self._mBotTitle = self._emToCm(mScale * self._marginTitle[1])
self._mBotHead1 = self._emToCm(mScale * self._marginHead1[1])
@@ -308,6 +311,7 @@ def initDocument(self) -> None:
self._mBotHead = self._emToCm(mScale * self._marginHead4[1])
self._mBotText = self._emToCm(mScale * self._marginText[1])
self._mBotMeta = self._emToCm(mScale * self._marginMeta[1])
+ self._mBotSep = self._emToCm(mScale * self._marginSep[1])
self._mLeftFoot = self._emToCm(self._marginFoot[0])
self._mBotFoot = self._emToCm(self._marginFoot[1])
@@ -501,7 +505,7 @@ def doConvert(self) -> None:
self._addTextPar(xText, S_SEP, oStyle, tText)
elif tType == self.T_SKIP:
- self._addTextPar(xText, S_SEP, oStyle, "")
+ self._addTextPar(xText, S_TEXT, oStyle, "")
elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, tFmt = self._formatSynopsis(tText, tFormat, True)
@@ -944,8 +948,8 @@ def _useableStyles(self) -> None:
style.setParentStyleName("Standard")
style.setNextStyleName(S_TEXT)
style.setClass("text")
- style.setMarginTop(self._mTopText)
- style.setMarginBottom(self._mBotText)
+ style.setMarginTop(self._mTopSep)
+ style.setMarginBottom(self._mBotSep)
style.setLineHeight(self._fLineHeight)
style.setTextAlign("center")
style.setFontName(self._fontFamily)
diff --git a/novelwriter/core/toqdoc.py b/novelwriter/core/toqdoc.py
index ee530fd85..f3e880a5e 100644
--- a/novelwriter/core/toqdoc.py
+++ b/novelwriter/core/toqdoc.py
@@ -122,6 +122,7 @@ def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
self._mText = (mScale * self._marginText[0], mScale * self._marginText[1])
self._mMeta = (mScale * self._marginMeta[0], mScale * self._marginMeta[1])
+ self._mSep = (mScale * self._marginSep[0], mScale * self._marginSep[1])
self._mIndent = mScale * 2.0
@@ -233,7 +234,10 @@ def doConvert(self) -> None:
cursor.insertText(tText.replace(nwHeadFmt.BR, "\n"), cFmt)
elif tType == self.T_SEP:
- newBlock(cursor, bFmt)
+ sFmt = QTextBlockFormat(bFmt)
+ sFmt.setTopMargin(self._mSep[0])
+ sFmt.setBottomMargin(self._mSep[1])
+ newBlock(cursor, sFmt)
cursor.insertText(tText, self._cText)
elif tType == self.T_SKIP:
@@ -270,7 +274,7 @@ def appendFootnotes(self) -> None:
cursor = QTextCursor(self._document)
cursor.movePosition(QTextCursor.MoveOperation.End)
- bFmt, cFmt = self._genHeadStyle(self.T_HEAD3, -1, self._blockFmt)
+ bFmt, cFmt = self._genHeadStyle(self.T_HEAD4, -1, self._blockFmt)
newBlock(cursor, bFmt)
cursor.insertText(self._localLookup("Footnotes"), cFmt)
@@ -362,7 +366,10 @@ def _insertKeywords(self, text: str, cursor: QTextCursor) -> None:
if (num := len(bits)) > 1:
if bits[0] == nwKeyWords.TAG_KEY:
one, two = self._project.index.parseValue(bits[1])
- cursor.insertText(one, self._cTag)
+ cFmt = QTextCharFormat(self._cTag)
+ cFmt.setAnchor(True)
+ cFmt.setAnchorNames([f"tag_{one}".lower()])
+ cursor.insertText(one, cFmt)
if two:
cursor.insertText(" | ", self._cText)
cursor.insertText(two, self._cOptional)
@@ -371,7 +378,7 @@ def _insertKeywords(self, text: str, cursor: QTextCursor) -> None:
cFmt = QTextCharFormat(self._cTag)
cFmt.setFontUnderline(True)
cFmt.setAnchor(True)
- cFmt.setAnchorHref(f"#{bits[0][1:]}={bit}")
+ cFmt.setAnchorHref(f"#tag_{bit}".lower())
cursor.insertText(bit, cFmt)
if n < num:
cursor.insertText(", ", self._cText)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index cba386f58..caee97b97 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -151,6 +151,8 @@ def initViewer(self) -> None:
docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.viewport().setPalette(docPalette)
+ self.docHeader.matchColours()
+ self.docFooter.matchColours()
# Update theme colours
self._docTheme.text = SHARED.theme.colText
@@ -164,9 +166,6 @@ def initViewer(self) -> None:
self._docTheme.tag = SHARED.theme.colTag
self._docTheme.optional = SHARED.theme.colOpt
- self.docHeader.matchColours()
- self.docFooter.matchColours()
-
# Set default text margins
self.document().setDocumentMargin(0)
@@ -369,12 +368,10 @@ def navForward(self) -> None:
@pyqtSlot("QUrl")
def _linkClicked(self, url: QUrl) -> None:
"""Process a clicked link in the document."""
- link = url.url()
- logger.debug("Clicked link: '%s'", link)
- if len(link) > 0:
- bits = link.split("=")
- if len(bits) == 2:
- self.loadDocumentTagRequest.emit(bits[1], nwDocMode.VIEW)
+ if link := url.url():
+ logger.debug("Clicked link: '%s'", link)
+ if (bits := link.partition("_")) and bits[2]:
+ self.loadDocumentTagRequest.emit(bits[2], nwDocMode.VIEW)
return
@pyqtSlot("QPoint")
diff --git a/tests/reference/coreToOdt_SaveFlat_document.fodt b/tests/reference/coreToOdt_SaveFlat_document.fodt
index cee2d9b59..53b1beb6b 100644
--- a/tests/reference/coreToOdt_SaveFlat_document.fodt
+++ b/tests/reference/coreToOdt_SaveFlat_document.fodt
@@ -1,13 +1,13 @@
- 2024-05-22T23:05:27
+ 2024-05-24T21:31:13
novelWriter/2.5a4
Jane Smith
1234
P42DT12H34M56S
Test Project
- 2024-05-22T23:05:27
+ 2024-05-24T21:31:13
Jane Smith
@@ -22,7 +22,7 @@
-
+
@@ -38,27 +38,27 @@
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/tests/reference/coreToOdt_SaveFull_styles.xml b/tests/reference/coreToOdt_SaveFull_styles.xml
index 809732e0e..9b7b65fb5 100644
--- a/tests/reference/coreToOdt_SaveFull_styles.xml
+++ b/tests/reference/coreToOdt_SaveFull_styles.xml
@@ -12,7 +12,7 @@
-
+
@@ -28,27 +28,27 @@
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
index e3e8da375..f38a985f9 100644
--- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
+++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
@@ -7,11 +7,11 @@
-
+
@@ -38,27 +38,27 @@
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index 39e37b891..814a27be9 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -56,13 +56,14 @@ def testCoreToken_Setters(mockGUI):
assert tokens._lineHeight == 1.15
assert tokens._blockIndent == 4.0
assert tokens._doJustify is False
- assert tokens._marginTitle == (1.000, 0.500)
- assert tokens._marginHead1 == (1.000, 0.500)
- assert tokens._marginHead2 == (0.834, 0.500)
- assert tokens._marginHead3 == (0.584, 0.500)
- assert tokens._marginHead4 == (0.584, 0.500)
+ assert tokens._marginTitle == (1.417, 0.500)
+ assert tokens._marginHead1 == (1.417, 0.500)
+ assert tokens._marginHead2 == (1.668, 0.500)
+ assert tokens._marginHead3 == (1.168, 0.500)
+ assert tokens._marginHead4 == (1.168, 0.500)
assert tokens._marginText == (0.000, 0.584)
assert tokens._marginMeta == (0.000, 0.584)
+ assert tokens._marginSep == (1.168, 1.168)
assert tokens._hideTitle is False
assert tokens._hideChapter is False
assert tokens._hideUnNum is False
@@ -93,6 +94,7 @@ def testCoreToken_Setters(mockGUI):
tokens.setHead4Margins(2.0, 2.0)
tokens.setTextMargins(2.0, 2.0)
tokens.setMetaMargins(2.0, 2.0)
+ tokens.setSeparatorMargins(2.0, 2.0)
tokens.setLinkHeadings(True)
tokens.setBodyText(False)
tokens.setSynopsis(True)
@@ -117,6 +119,7 @@ def testCoreToken_Setters(mockGUI):
assert tokens._marginHead4 == (2.0, 2.0)
assert tokens._marginText == (2.0, 2.0)
assert tokens._marginMeta == (2.0, 2.0)
+ assert tokens._marginSep == (2.0, 2.0)
assert tokens._hideTitle is True
assert tokens._hideChapter is True
assert tokens._hideUnNum is True
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index c924d762c..4a3411650 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -520,9 +520,9 @@ def getStyle(styleName):
assert odt.errData == []
assert xmlToText(odt._xText) == (
''
- ''
+ ''
'Text'
- ''
+ ''
'Text'
''
)
diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py
index 72a71655b..b5928209d 100644
--- a/tests/test_gui/test_gui_docviewer.py
+++ b/tests/test_gui/test_gui_docviewer.py
@@ -157,7 +157,7 @@ def mockExec(*a):
docViewer.setTextCursor(cursor)
docViewer._makeSelection(QTextCursor.WordUnderCursor)
rect = docViewer.cursorRect()
- docViewer._linkClicked(QUrl("#char=Bod"))
+ docViewer._linkClicked(QUrl("#tag_bod"))
assert docViewer.docHandle == "4c4f28287af27"
# Click mouse nav buttons
From aceb344856ae76a264a30ca1027afd03bb1dc153 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 25 May 2024 22:43:09 +0200
Subject: [PATCH 11/13] Fix a potential segfault issue in main gui and add
auto-build on open manuscript tool
---
novelwriter/guimain.py | 6 ++----
novelwriter/tools/manuscript.py | 5 +++--
2 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index af9ad75fe..eb9b4edab 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -800,8 +800,7 @@ def showNovelDetailsDialog(self) -> None:
def showBuildManuscriptDialog(self) -> None:
"""Open the build manuscript dialog."""
if SHARED.hasProject:
- if (dialog := SHARED.findTopLevelWidget(GuiManuscript)) is None:
- dialog = GuiManuscript(self)
+ dialog = GuiManuscript(self)
dialog.activateDialog()
dialog.loadContent()
return
@@ -819,8 +818,7 @@ def showProjectWordListDialog(self) -> None:
def showWritingStatsDialog(self) -> None:
"""Open the session stats dialog."""
if SHARED.hasProject:
- if (dialog := SHARED.findTopLevelWidget(GuiWritingStats)) is None:
- dialog = GuiWritingStats(self)
+ dialog = GuiWritingStats(self)
dialog.activateDialog()
dialog.populateGUI()
return
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 263bf332d..841eaa15a 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -246,6 +246,7 @@ def loadContent(self) -> None:
self._updateBuildsList()
if selected in self._buildMap:
self.buildList.setCurrentItem(self._buildMap[selected])
+ QTimer.singleShot(200, self._generatePreview)
return
@@ -363,8 +364,8 @@ def _generatePreview(self) -> None:
def _buildManuscript(self) -> None:
"""Open the build dialog and build the manuscript."""
if build := self._getSelectedBuild():
- dlgBuild = GuiManuscriptBuild(self, build)
- dlgBuild.exec()
+ dialog = GuiManuscriptBuild(self, build)
+ dialog.exec()
# After the build is done, save build settings changes
if build.changed:
From 91222f631f9ea34cd30f7c2050cc9d1b628c1593 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 25 May 2024 23:02:34 +0200
Subject: [PATCH 12/13] Update manuscript tests
---
tests/test_tools/test_tools_manusbuild.py | 2 +-
tests/test_tools/test_tools_manuscript.py | 37 +++++++-------------
tests/test_tools/test_tools_manussettings.py | 12 +++----
3 files changed, 20 insertions(+), 31 deletions(-)
diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py
index a1ca11751..eb1685aaa 100644
--- a/tests/test_tools/test_tools_manusbuild.py
+++ b/tests/test_tools/test_tools_manusbuild.py
@@ -40,7 +40,7 @@
@pytest.mark.gui
-def testManuscriptBuild_Main(
+def testToolManuscriptBuild_Main(
monkeypatch, qtbot: QtBot, nwGUI: GuiMain, fncPath: Path, projPath: Path, mockRnd
):
"""Test the GuiManuscriptBuild dialog."""
diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py
index 383ff098c..145fbfe57 100644
--- a/tests/test_tools/test_tools_manuscript.py
+++ b/tests/test_tools/test_tools_manuscript.py
@@ -40,7 +40,7 @@
@pytest.mark.gui
-def testManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
+def testToolManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
"""Test the init/main functionality of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
@@ -54,38 +54,27 @@ def testManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.show()
assert manus.docPreview.toPlainText().strip() == ""
- # Run the default build
+ # First load should have create a default build
+ assert manus.buildList.count() == 1
+
+ # Loading again should not add a new build
+ manus.loadContent()
+ assert manus.buildList.count() == 1
+
+ # Build a preview
manus.buildList.clearSelection()
manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() == allText
- manus.close()
-
- # # A new dialog should load the old build
- # manus = GuiManuscript(nwGUI)
- # manus.show()
- # manus.loadContent()
- # assert manus.docPreview.toPlainText().strip() == allText
- # manus.close()
-
- # # But blocking the reload should leave it empty
- # with monkeypatch.context() as mp:
- # mp.setattr("builtins.open", lambda *a, **k: causeOSError)
- # manus = GuiManuscript(nwGUI)
- # manus.show()
- # manus.loadContent()
- # assert manus.docPreview.toPlainText().strip() == ""
-
- # nwGUI.closeProject() # This should auto-close the manuscript tool
- # assert manus.isHidden()
+ nwGUI.closeProject() # This should auto-close the manuscript tool
# qtbot.stop()
@pytest.mark.gui
-def testManuscript_Builds(qtbot, nwGUI, projPath):
+def testToolManuscript_Builds(qtbot, nwGUI, projPath):
"""Test the handling of builds in the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
@@ -156,7 +145,7 @@ def _testNewSettingsReady(new: BuildSettings):
@pytest.mark.gui
-def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
+def testToolManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
"""Test other features of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
@@ -280,7 +269,7 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
@pytest.mark.gui
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
-def testManuscript_Print(monkeypatch, qtbot, nwGUI, projPath):
+def testToolManuscript_Print(monkeypatch, qtbot, nwGUI, projPath):
"""Test the print feature of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py
index 7d8dd2f35..76a41a481 100644
--- a/tests/test_tools/test_tools_manussettings.py
+++ b/tests/test_tools/test_tools_manussettings.py
@@ -40,7 +40,7 @@
@pytest.mark.gui
-def testBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
+def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
"""Test the initialisation of the GuiBuildSettings dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
@@ -110,7 +110,7 @@ def _testNewSettingsReady(new: BuildSettings):
@pytest.mark.gui
-def testBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
+def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
"""Test the Filter Tab of the GuiBuildSettings dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
@@ -314,7 +314,7 @@ def testBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
@pytest.mark.gui
-def testBuildSettings_Headings(qtbot, nwGUI):
+def testToolBuildSettings_Headings(qtbot, nwGUI):
"""Test the Headings Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
@@ -484,7 +484,7 @@ def testBuildSettings_Headings(qtbot, nwGUI):
@pytest.mark.gui
-def testBuildSettings_Content(qtbot, nwGUI):
+def testToolBuildSettings_Content(qtbot, nwGUI):
"""Test the Content Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
@@ -544,7 +544,7 @@ def testBuildSettings_Content(qtbot, nwGUI):
@pytest.mark.gui
-def testBuildSettings_Format(monkeypatch, qtbot, nwGUI):
+def testToolBuildSettings_Format(monkeypatch, qtbot, nwGUI):
"""Test the Format Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
@@ -650,7 +650,7 @@ def testBuildSettings_Format(monkeypatch, qtbot, nwGUI):
@pytest.mark.gui
-def testBuildSettings_Output(qtbot, nwGUI):
+def testToolBuildSettings_Output(qtbot, nwGUI):
"""Test the Output Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
From d3ea763d6bee805c1c0bd44acc5434d957c655d8 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 26 May 2024 00:51:27 +0200
Subject: [PATCH 13/13] Add test coverage of new ToQTextDocument class
---
novelwriter/core/toqdoc.py | 19 +-
tests/test_core/test_core_toqdoc.py | 571 ++++++++++++++++++++++++++++
2 files changed, 581 insertions(+), 9 deletions(-)
create mode 100644 tests/test_core/test_core_toqdoc.py
diff --git a/novelwriter/core/toqdoc.py b/novelwriter/core/toqdoc.py
index f3e880a5e..0f0940169 100644
--- a/novelwriter/core/toqdoc.py
+++ b/novelwriter/core/toqdoc.py
@@ -138,35 +138,36 @@ def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
# =================
self._cText = QTextCharFormat()
+ self._cText.setBackground(QtTransparent)
self._cText.setForeground(self._theme.text)
- self._cHead = QTextCharFormat()
+ self._cHead = QTextCharFormat(self._cText)
self._cHead.setForeground(self._theme.head)
- self._cComment = QTextCharFormat()
+ self._cComment = QTextCharFormat(self._cText)
self._cComment.setForeground(self._theme.comment)
- self._cCommentMod = QTextCharFormat()
+ self._cCommentMod = QTextCharFormat(self._cText)
self._cCommentMod.setForeground(self._theme.comment)
self._cCommentMod.setFontWeight(self._bold)
- self._cNote = QTextCharFormat()
+ self._cNote = QTextCharFormat(self._cText)
self._cNote.setForeground(self._theme.note)
- self._cCode = QTextCharFormat()
+ self._cCode = QTextCharFormat(self._cText)
self._cCode.setForeground(self._theme.code)
- self._cModifier = QTextCharFormat()
+ self._cModifier = QTextCharFormat(self._cText)
self._cModifier.setForeground(self._theme.modifier)
self._cModifier.setFontWeight(self._bold)
- self._cKeyword = QTextCharFormat()
+ self._cKeyword = QTextCharFormat(self._cText)
self._cKeyword.setForeground(self._theme.keyword)
- self._cTag = QTextCharFormat()
+ self._cTag = QTextCharFormat(self._cText)
self._cTag.setForeground(self._theme.tag)
- self._cOptional = QTextCharFormat()
+ self._cOptional = QTextCharFormat(self._cText)
self._cOptional.setForeground(self._theme.optional)
self._init = True
diff --git a/tests/test_core/test_core_toqdoc.py b/tests/test_core/test_core_toqdoc.py
new file mode 100644
index 000000000..13a5d28f0
--- /dev/null
+++ b/tests/test_core/test_core_toqdoc.py
@@ -0,0 +1,571 @@
+"""
+novelWriter – ToQTextDocument Class Tester
+==========================================
+
+This file is a part of novelWriter
+Copyright 2018–2024, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+from __future__ import annotations
+
+import pytest
+
+from PyQt5.QtGui import QColor, QTextBlock, QTextCharFormat, QTextCursor
+
+from novelwriter import CONFIG
+from novelwriter.constants import nwUnicode
+from novelwriter.core.project import NWProject
+from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
+from novelwriter.types import (
+ QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
+ QtPageBreakAfter, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
+)
+
+THEME = TextDocumentTheme()
+THEME.text = QColor(0, 0, 0)
+THEME.highlight = QColor(255, 255, 166)
+THEME.head = QColor(66, 113, 174)
+THEME.comment = QColor(100, 100, 100)
+THEME.note = QColor(129, 55, 9)
+THEME.code = QColor(66, 113, 174)
+THEME.modifier = QColor(129, 55, 9)
+THEME.keyword = QColor(245, 135, 31)
+THEME.tag = QColor(66, 113, 174)
+THEME.optional = QColor(66, 113, 174)
+
+
+def charFmtInBlock(block: QTextBlock, pos: int) -> QTextCharFormat:
+ """Get the character format at a given place in a block."""
+ cursor = QTextCursor(block)
+ cursor.setPosition(block.position() + pos)
+ return cursor.charFormat()
+
+
+@pytest.mark.core
+def testCoreToQTextDocument_ConvertHeaders(mockGUI):
+ """Test header formats in the ToQTextDocument class."""
+ project = NWProject()
+ qdoc = ToQTextDocument(project)
+ qdoc.initDocument(CONFIG.textFont, THEME)
+
+ qdoc._isNovel = True
+ qdoc._isFirst = True
+ qdoc._text = (
+ "#! Title\n"
+ "# Partition\n"
+ "## Chapter\n"
+ "### Scene\n"
+ "#### Section\n"
+ )
+ qdoc.tokenizeText()
+ qdoc.doConvert()
+ assert qdoc.document.blockCount() == 5
+
+ # Title
+ block = qdoc.document.findBlockByNumber(0)
+ assert block.text() == "Title"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mHead[qdoc.T_TITLE][0]
+ assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_TITLE][1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.fontWeight() == qdoc._bold
+ assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_TITLE]
+ assert cFmt.foreground().color() == THEME.text
+
+ # Partition
+ block = qdoc.document.findBlockByNumber(1)
+ assert block.text() == "Partition"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD1][0]
+ assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD1][1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.fontWeight() == qdoc._bold
+ assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD1]
+ assert cFmt.foreground().color() == THEME.head
+
+ # Chapter
+ block = qdoc.document.findBlockByNumber(2)
+ assert block.text() == "Chapter"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD2][0]
+ assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD2][1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.fontWeight() == qdoc._bold
+ assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD2]
+ assert cFmt.foreground().color() == THEME.head
+
+ # Scene
+ block = qdoc.document.findBlockByNumber(3)
+ assert block.text() == "Scene"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD3][0]
+ assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD3][1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.fontWeight() == qdoc._bold
+ assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD3]
+ assert cFmt.foreground().color() == THEME.head
+
+ # Section
+ block = qdoc.document.findBlockByNumber(4)
+ assert block.text() == "Section"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD4][0]
+ assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD4][1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.fontWeight() == qdoc._bold
+ assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD4]
+ assert cFmt.foreground().color() == THEME.head
+
+
+@pytest.mark.core
+def testCoreToQTextDocument_SeparatorSkip(mockGUI):
+ """Test separator and skip in the ToQTextDocument class."""
+ project = NWProject()
+ qdoc = ToQTextDocument(project)
+ qdoc.initDocument(CONFIG.textFont, THEME)
+
+ qdoc._isNovel = True
+ qdoc._isFirst = True
+ qdoc._text = (
+ "#! Title\n"
+ "## Chapter\n"
+ "### Scene 1\n"
+ "Text 1\n"
+ "### Scene 2\n"
+ "Text 2\n"
+ "#### Section\n"
+ "Text 3\n"
+ )
+ qdoc.setSceneFormat("* * *", False)
+ qdoc.setSectionFormat("", False)
+ qdoc.tokenizeText()
+ qdoc.doConvert()
+ assert qdoc.document.blockCount() == 7
+
+ # 0: Title
+ block = qdoc.document.findBlockByNumber(0)
+ assert block.text() == "Title"
+
+ # 1: Chapter
+ block = qdoc.document.findBlockByNumber(1)
+ assert block.text() == "Chapter"
+
+ # Hidden: Scene 1
+
+ # 2: Text 1
+ block = qdoc.document.findBlockByNumber(2)
+ assert block.text() == "Text 1"
+
+ # 3: Scene 2
+ block = qdoc.document.findBlockByNumber(3)
+ assert block.text() == "* * *"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mSep[0]
+ assert bFmt.bottomMargin() == qdoc._mSep[1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.foreground().color() == THEME.text
+
+ # 4: Text 2
+ block = qdoc.document.findBlockByNumber(4)
+ assert block.text() == "Text 2"
+
+ # 5: Section
+ block = qdoc.document.findBlockByNumber(5)
+ assert block.text() == nwUnicode.U_NBSP
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mText[0]
+ assert bFmt.bottomMargin() == qdoc._mText[1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.foreground().color() == THEME.text
+
+ # 6: Text 3
+ block = qdoc.document.findBlockByNumber(6)
+ assert block.text() == "Text 3"
+
+
+@pytest.mark.core
+def testCoreToQTextDocument_NovelMeta(mockGUI):
+ """Test novel meta formats in the ToQTextDocument class."""
+ project = NWProject()
+ qdoc = ToQTextDocument(project)
+ qdoc.initDocument(CONFIG.textFont, THEME)
+
+ qdoc._isNovel = True
+ qdoc._isFirst = True
+ qdoc.setComments(True)
+ qdoc.setSynopsis(True)
+ qdoc.setKeywords(True)
+ qdoc._text = (
+ "### Scene\n\n"
+ "@pov: Jane\n"
+ "@char: John, Bob\n\n"
+ "%Synopsis: Stuff that happened\n\n"
+ "% A regular comment\n\n"
+ "Text\n\n"
+ )
+ qdoc.tokenizeText()
+ qdoc.doConvert()
+ assert qdoc.document.blockCount() == 6
+
+ # 0: Scene
+ block = qdoc.document.findBlockByNumber(0)
+ assert block.text() == "Scene"
+
+ # 1: Jane
+ block = qdoc.document.findBlockByNumber(1)
+ assert block.text() == "Point of View: Jane"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mMeta[0]
+ assert bFmt.bottomMargin() == 0.0
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.foreground().color() == THEME.keyword
+ cFmt = charFmtInBlock(block, 16)
+ assert cFmt.foreground().color() == THEME.tag
+
+ # 2: John, Bob
+ block = qdoc.document.findBlockByNumber(2)
+ assert block.text() == "Characters: John, Bob"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == 0.0
+ assert bFmt.bottomMargin() == qdoc._mMeta[1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.foreground().color() == THEME.keyword
+ cFmt = charFmtInBlock(block, 13)
+ assert cFmt.foreground().color() == THEME.tag
+
+ # 3: Synopsis
+ block = qdoc.document.findBlockByNumber(3)
+ assert block.text() == "Synopsis: Stuff that happened"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mText[0]
+ assert bFmt.bottomMargin() == qdoc._mText[1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.foreground().color() == THEME.modifier
+ cFmt = charFmtInBlock(block, 11)
+ assert cFmt.foreground().color() == THEME.note
+
+ # 4: Comment
+ block = qdoc.document.findBlockByNumber(4)
+ assert block.text() == "Comment: A regular comment"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mText[0]
+ assert bFmt.bottomMargin() == qdoc._mText[1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.foreground().color() == THEME.comment
+ cFmt = charFmtInBlock(block, 10)
+ assert cFmt.foreground().color() == THEME.comment
+
+ # 5: Text
+ block = qdoc.document.findBlockByNumber(5)
+ assert block.text() == "Text"
+
+
+@pytest.mark.core
+def testCoreToQTextDocument_NoteMeta(mockGUI):
+ """Test note meta formats in the ToQTextDocument class."""
+ project = NWProject()
+ qdoc = ToQTextDocument(project)
+ qdoc.initDocument(CONFIG.textFont, THEME)
+
+ qdoc._isNovel = False
+ qdoc._isFirst = True
+ qdoc.setComments(True)
+ qdoc.setSynopsis(True)
+ qdoc.setKeywords(True)
+ qdoc._text = (
+ "# Jane Smith\n\n"
+ "@tag: Jane | Jane Smith\n"
+ "%Short: All about Jane\n\n"
+ "Text\n\n"
+ )
+ qdoc.tokenizeText()
+ qdoc.doConvert()
+ assert qdoc.document.blockCount() == 4
+
+ # 0: Title
+ block = qdoc.document.findBlockByNumber(0)
+ assert block.text() == "Jane Smith"
+
+ # 1: Tag
+ block = qdoc.document.findBlockByNumber(1)
+ assert block.text() == "Tag: Jane | Jane Smith"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mMeta[0]
+ assert bFmt.bottomMargin() == qdoc._mMeta[1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.foreground().color() == THEME.keyword
+ cFmt = charFmtInBlock(block, 6)
+ assert cFmt.foreground().color() == THEME.tag
+ cFmt = charFmtInBlock(block, 11)
+ assert cFmt.foreground().color() == THEME.text
+ cFmt = charFmtInBlock(block, 13)
+ assert cFmt.foreground().color() == THEME.optional
+
+ # 2: Short
+ block = qdoc.document.findBlockByNumber(2)
+ assert block.text() == "Short Description: All about Jane"
+ bFmt = block.blockFormat()
+ assert bFmt.topMargin() == qdoc._mText[0]
+ assert bFmt.bottomMargin() == qdoc._mText[1]
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.foreground().color() == THEME.modifier
+ cFmt = charFmtInBlock(block, 20)
+ assert cFmt.foreground().color() == THEME.note
+
+ # 3: Text
+ block = qdoc.document.findBlockByNumber(3)
+ assert block.text() == "Text"
+
+
+@pytest.mark.core
+def testCoreToQTextDocument_TextBlockFormats(mockGUI):
+ """Test text block formats in the ToQTextDocument class."""
+ project = NWProject()
+ qdoc = ToQTextDocument(project)
+ qdoc.initDocument(CONFIG.textFont, THEME)
+
+ qdoc._isNovel = True
+ qdoc._isFirst = True
+
+ # Alignment & Indent
+ # ==================
+ qdoc.document.clear()
+
+ qdoc._text = (
+ "### Scene\n\n"
+ "Left <<\n\n"
+ ">> Center <<\n\n"
+ ">> Right\n\n"
+ "> Left Indent\n\n"
+ "Right Indent <\n\n"
+ "> Double Indent <\n\n"
+ )
+ qdoc.tokenizeText()
+ qdoc.doConvert()
+ assert qdoc.document.blockCount() == 7
+
+ # 0: Scene
+ block = qdoc.document.findBlockByNumber(0)
+ assert block.text() == "Scene"
+
+ # 1: Left
+ block = qdoc.document.findBlockByNumber(1)
+ assert block.text() == "Left"
+ bFmt = block.blockFormat()
+ assert bFmt.alignment() == QtAlignLeft
+
+ # 2: Center
+ block = qdoc.document.findBlockByNumber(2)
+ assert block.text() == "Center"
+ bFmt = block.blockFormat()
+ assert bFmt.alignment() == QtAlignCenter
+
+ # 3: Right
+ block = qdoc.document.findBlockByNumber(3)
+ assert block.text() == "Right"
+ bFmt = block.blockFormat()
+ assert bFmt.alignment() == QtAlignRight
+
+ # 4: Left Indent
+ block = qdoc.document.findBlockByNumber(4)
+ assert block.text() == "Left Indent"
+ bFmt = block.blockFormat()
+ assert bFmt.alignment() == QtAlignAbsolute
+ assert bFmt.leftMargin() == qdoc._mIndent
+ assert bFmt.rightMargin() == 0.0
+
+ # 5: Right Indent
+ block = qdoc.document.findBlockByNumber(5)
+ assert block.text() == "Right Indent"
+ bFmt = block.blockFormat()
+ assert bFmt.alignment() == QtAlignAbsolute
+ assert bFmt.leftMargin() == 0.0
+ assert bFmt.rightMargin() == qdoc._mIndent
+
+ # 6: Double Indent
+ block = qdoc.document.findBlockByNumber(6)
+ assert block.text() == "Double Indent"
+ bFmt = block.blockFormat()
+ assert bFmt.alignment() == QtAlignAbsolute
+ assert bFmt.leftMargin() == qdoc._mIndent
+ assert bFmt.rightMargin() == qdoc._mIndent
+
+ # Unreachable
+ # ===========
+ # Some formatting markers are currently not reachable
+ qdoc.document.clear()
+
+ qdoc._tokens = [
+ (qdoc.T_TEXT, 1, "This is justified", [], qdoc.A_JUSTIFY),
+ (qdoc.T_TEXT, 1, "This has a page break", [], qdoc.A_PBA),
+ ]
+ qdoc.doConvert()
+ assert qdoc.document.blockCount() == 2
+
+ # 0: Justify
+ block = qdoc.document.findBlockByNumber(0)
+ assert block.text() == "This is justified"
+ bFmt = block.blockFormat()
+ assert bFmt.alignment() == QtAlignJustify
+
+ # 1: Page Break After
+ block = qdoc.document.findBlockByNumber(1)
+ assert block.text() == "This has a page break"
+ bFmt = block.blockFormat()
+ assert bFmt.pageBreakPolicy() == QtPageBreakAfter
+
+
+@pytest.mark.core
+def testCoreToQTextDocument_TextCharFormats(mockGUI):
+ """Test text char formats in the ToQTextDocument class."""
+ project = NWProject()
+ qdoc = ToQTextDocument(project)
+
+ # Convert before init
+ qdoc._text = "Blabla"
+ qdoc.doConvert()
+ qdoc.tokenizeText()
+ assert qdoc.document.toPlainText() == ""
+
+ # Init
+ qdoc.initDocument(CONFIG.textFont, THEME)
+
+ qdoc._isNovel = True
+ qdoc._isFirst = True
+
+ qdoc._text = (
+ "### Scene\n\n"
+ "With [b]bold[/b] text\n\n"
+ "With [i]italic[/i] text\n\n"
+ "With [s]deleted[/s] text\n\n"
+ "With [u]underlined[/u] text\n\n"
+ "With [m]highlighted[/m] text\n\n"
+ "With super[sup]script[/sup] text\n\n"
+ "With sub[sub]script[/sub] text\n\n"
+ )
+ qdoc.tokenizeText()
+ qdoc.doConvert()
+ assert qdoc.document.blockCount() == 8
+
+ # 0: Scene
+ block = qdoc.document.findBlockByNumber(0)
+ assert block.text() == "Scene"
+
+ # 1: Bold
+ block = qdoc.document.findBlockByNumber(1)
+ assert block.text() == "With bold text"
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.fontWeight() == qdoc._normal
+ cFmt = charFmtInBlock(block, 6)
+ assert cFmt.fontWeight() == qdoc._bold
+ cFmt = charFmtInBlock(block, 10)
+ assert cFmt.fontWeight() == qdoc._normal
+
+ # 2: Italic
+ block = qdoc.document.findBlockByNumber(2)
+ assert block.text() == "With italic text"
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.fontItalic() is False
+ cFmt = charFmtInBlock(block, 6)
+ assert cFmt.fontItalic() is True
+ cFmt = charFmtInBlock(block, 12)
+ assert cFmt.fontItalic() is False
+
+ # 3: Deleted
+ block = qdoc.document.findBlockByNumber(3)
+ assert block.text() == "With deleted text"
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.fontStrikeOut() is False
+ cFmt = charFmtInBlock(block, 6)
+ assert cFmt.fontStrikeOut() is True
+ cFmt = charFmtInBlock(block, 13)
+ assert cFmt.fontStrikeOut() is False
+
+ # 4: Underlined
+ block = qdoc.document.findBlockByNumber(4)
+ assert block.text() == "With underlined text"
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.fontUnderline() is False
+ cFmt = charFmtInBlock(block, 6)
+ assert cFmt.fontUnderline() is True
+ cFmt = charFmtInBlock(block, 16)
+ assert cFmt.fontUnderline() is False
+
+ # 5: Highlighted
+ block = qdoc.document.findBlockByNumber(5)
+ assert block.text() == "With highlighted text"
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.background() == QtTransparent
+ cFmt = charFmtInBlock(block, 6)
+ assert cFmt.background() == THEME.highlight
+ cFmt = charFmtInBlock(block, 17)
+ assert cFmt.background() == QtTransparent
+
+ # 6: Superscript
+ block = qdoc.document.findBlockByNumber(6)
+ assert block.text() == "With superscript text"
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.verticalAlignment() == QtVAlignNormal
+ cFmt = charFmtInBlock(block, 11)
+ assert cFmt.verticalAlignment() == QtVAlignSuper
+ cFmt = charFmtInBlock(block, 17)
+ assert cFmt.verticalAlignment() == QtVAlignNormal
+
+ # 7: Subscript
+ block = qdoc.document.findBlockByNumber(7)
+ assert block.text() == "With subscript text"
+ cFmt = charFmtInBlock(block, 1)
+ assert cFmt.verticalAlignment() == QtVAlignNormal
+ cFmt = charFmtInBlock(block, 9)
+ assert cFmt.verticalAlignment() == QtVAlignSub
+ cFmt = charFmtInBlock(block, 15)
+ assert cFmt.verticalAlignment() == QtVAlignNormal
+
+
+@pytest.mark.core
+def testCoreToQTextDocument_Footnotes(mockGUI):
+ """Test footnotes in the ToQTextDocument class."""
+ project = NWProject()
+ qdoc = ToQTextDocument(project)
+ qdoc.initDocument(CONFIG.textFont, THEME)
+
+ qdoc._isNovel = True
+ qdoc._isFirst = True
+
+ qdoc._text = (
+ "### Scene\n\n"
+ "Text with valid[footnote:fn1] and invalid[footnote:fn2] footnotes.\n\n"
+ "%Footnote.fn1: Here's the first note.\n\n"
+ )
+ qdoc.tokenizeText()
+ qdoc.doConvert()
+ qdoc.appendFootnotes()
+ assert qdoc.document.blockCount() == 4
+
+ # 0: Scene
+ block = qdoc.document.findBlockByNumber(0)
+ assert block.text() == "Scene"
+
+ # 1: Text
+ block = qdoc.document.findBlockByNumber(1)
+ assert block.text() == "Text with valid[1] and invalid[ERR] footnotes."
+
+ # 2: Footnotes
+ block = qdoc.document.findBlockByNumber(2)
+ assert block.text() == "Footnotes"
+
+ # 3: Footnote 1
+ block = qdoc.document.findBlockByNumber(3)
+ assert block.text() == "1. Here's the first note."