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..a6d47740b 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 @@ -54,7 +55,7 @@ class NWBuildDocument: __slots__ = ( "_project", "_build", "_queue", "_error", "_cache", "_count", - "_outline", "_preview" + "_outline", ) def __init__(self, project: NWProject, build: BuildSettings) -> None: @@ -65,7 +66,6 @@ def __init__(self, project: NWProject, build: BuildSettings) -> None: self._cache = None self._count = False self._outline = False - self._preview = False return ## @@ -99,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 ## @@ -134,6 +125,32 @@ 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) + + self._outline = True + self._count = True + + font = QFont() + font.fromString(self._build.getStr("format.textFont")) + + 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): @@ -182,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]: @@ -193,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"
  1. {text}

  2. \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"

{text}

\n" - else: - sComm = self._localLookup("Comment") - return f"

{sComm}: {text}

\n" + sComm = self._localLookup("Comment") + return f"

{sComm}: {text}

\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/core/tokenizer.py b/novelwriter/core/tokenizer.py index 72e684497..d90cbf638 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: @@ -155,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 @@ -378,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 @@ -597,7 +604,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/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 new file mode 100644 index 000000000..0f0940169 --- /dev/null +++ b/novelwriter/core/toqdoc.py @@ -0,0 +1,403 @@ +""" +novelWriter – QTextDocument Converter +===================================== + +File History: +Created: 2024-05-21 [2.5b1] ToQTextDocument + +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 ( + QColor, QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat, + QTextCursor, QTextDocument +) + +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 ( + QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight, + QtBlack, QtPageBreakAfter, QtPageBreakBefore, QtTransparent, + QtVAlignNormal, QtVAlignSub, QtVAlignSuper +) + +logger = logging.getLogger(__name__) + +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) + else: + cursor.setBlockFormat(bFmt) + + +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._document.setDocumentMargin(0) + + self._theme = TextDocumentTheme() + 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 + + 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) + self._document.clear() + self._document.setDefaultFont(self._textFont) + + qMetric = QFontMetrics(self._textFont) + 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]), + 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(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, + 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._mSep = (mScale * self._marginSep[0], mScale * self._marginSep[1]) + + 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.setBackground(QtTransparent) + self._cText.setForeground(self._theme.text) + + self._cHead = QTextCharFormat(self._cText) + self._cHead.setForeground(self._theme.head) + + self._cComment = QTextCharFormat(self._cText) + self._cComment.setForeground(self._theme.comment) + + self._cCommentMod = QTextCharFormat(self._cText) + self._cCommentMod.setForeground(self._theme.comment) + self._cCommentMod.setFontWeight(self._bold) + + self._cNote = QTextCharFormat(self._cText) + self._cNote.setForeground(self._theme.note) + + self._cCode = QTextCharFormat(self._cText) + self._cCode.setForeground(self._theme.code) + + self._cModifier = QTextCharFormat(self._cText) + self._cModifier.setForeground(self._theme.modifier) + self._cModifier.setFontWeight(self._bold) + + self._cKeyword = QTextCharFormat(self._cText) + self._cKeyword.setForeground(self._theme.keyword) + + self._cTag = QTextCharFormat(self._cText) + self._cTag.setForeground(self._theme.tag) + + self._cOptional = QTextCharFormat(self._cText) + self._cOptional.setForeground(self._theme.optional) + + self._init = True + + return + + ## + # Properties + ## + + @property + def document(self) -> QTextDocument: + """Return the document.""" + return self._document + + ## + # Class Methods + ## + + def doConvert(self) -> None: + """Write text tokens into the document.""" + if not self._init: + return + + 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._blockFmt) + 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._mIndent) + if tStyle & self.A_IND_R: + bFmt.setRightMargin(self._mIndent) + + if tType == self.T_TEXT: + newBlock(cursor, bFmt) + self._insertFragments(tText, tFormat, cursor, self._cText) + + elif tType in self.L_HEADINGS: + bFmt, cFmt = self._genHeadStyle(tType, nHead, bFmt) + newBlock(cursor, bFmt) + cursor.insertText(tText.replace(nwHeadFmt.BR, "\n"), cFmt) + + elif tType == self.T_SEP: + 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: + newBlock(cursor, bFmt) + cursor.insertText(nwUnicode.U_NBSP, self._cText) + + elif tType in self.L_SUMMARY and self._doSynopsis: + newBlock(cursor, bFmt) + modifier = self._localLookup( + "Short Description" if tType == self.T_SHORT else "Synopsis" + ) + cursor.insertText(f"{modifier}: ", self._cModifier) + self._insertFragments(tText, tFormat, cursor, self._cNote) + + elif tType == self.T_COMMENT and self._doComments: + newBlock(cursor, bFmt) + 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) + self._insertKeywords(tText, cursor) + + self._document.blockSignals(False) + + return + + def appendFootnotes(self) -> None: + """Append the footnotes in the buffer.""" + if self._usedNotes: + self._document.blockSignals(True) + + cursor = QTextCursor(self._document) + cursor.movePosition(QTextCursor.MoveOperation.End) + + bFmt, cFmt = self._genHeadStyle(self.T_HEAD4, -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._cCode) + cFmt.setAnchor(True) + cFmt.setAnchorNames([f"footnote_{index}"]) + newBlock(cursor, self._blockFmt) + cursor.insertText(f"{index}. ", cFmt) + self._insertFragments(*content, cursor, self._cText) + + self._document.blockSignals(False) + + return + + ## + # Internal Functions + ## + + def _insertFragments( + 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(temp[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(self._theme.highlight) + elif fmt == self.FMT_M_E: + cFmt.setBackground(QtTransparent) + 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: + xFmt = QTextCharFormat(self._cCode) + xFmt.setVerticalAlignment(QtVAlignSuper) + if data in self._footnotes: + index = len(self._usedNotes) + 1 + self._usedNotes[data] = index + xFmt.setAnchor(True) + xFmt.setAnchorHref(f"#footnote_{index}") + xFmt.setFontUnderline(True) + cursor.insertText(f"[{index}]", xFmt) + else: + cursor.insertText("[ERR]", cFmt) + + # Move pos for next pass + start = pos + + # Insert whatever is left in the buffer + cursor.insertText(temp[start:], cFmt) + + return + + def _insertKeywords(self, text: str, cursor: QTextCursor) -> None: + """Apply Markdown formatting to keywords.""" + valid, bits, _ = self._project.index.scanThis("@"+text) + if valid and bits: + key = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: " + cursor.insertText(key, self._cKeyword) + if (num := len(bits)) > 1: + if bits[0] == nwKeyWords.TAG_KEY: + one, two = self._project.index.parseValue(bits[1]) + 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) + else: + for n, bit in enumerate(bits[1:], 2): + cFmt = QTextCharFormat(self._cTag) + cFmt.setFontUnderline(True) + cFmt.setAnchor(True) + cFmt.setAnchorHref(f"#tag_{bit}".lower()) + cursor.insertText(bit, cFmt) + if n < num: + cursor.insertText(", ", self._cText) + return + + def _genHeadStyle(self, hType: int, nHead: int, rFmt: QTextBlockFormat) -> T_TextStyle: + """Generate a heading style set.""" + mTop, mBottom = self._mHead.get(hType, (0.0, 0.0)) + + bFmt = QTextBlockFormat(rFmt) + bFmt.setTopMargin(mTop) + bFmt.setBottomMargin(mBottom) + + 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) + + return bFmt, cFmt 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 917a4f7f3..caee97b97 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -31,24 +31,21 @@ 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.tohtml import ToHtml +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 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__) @@ -69,6 +66,7 @@ def __init__(self, parent: QWidget) -> None: # Internal Variables self._docHandle = None + self._docTheme = TextDocumentTheme() # Settings self.setMinimumWidth(CONFIG.pxInt(300)) @@ -137,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() @@ -151,16 +151,23 @@ 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 + 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 + # 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: @@ -181,16 +188,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): @@ -202,22 +199,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.setJustify(CONFIG.doJustify) + qDoc.initDocument(CONFIG.textFont, self._docTheme) + qDoc.setKeywords(True) + qDoc.setComments(CONFIG.viewComments) + qDoc.setSynopsis(CONFIG.viewSynopsis) # 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() @@ -233,11 +230,7 @@ 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(aDoc.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 @@ -375,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") @@ -470,34 +461,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/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 7e27b8679..841eaa15a 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -23,15 +23,13 @@ """ from __future__ import annotations -import json import logging -from datetime import datetime from time import time 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, @@ -41,20 +39,19 @@ ) 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.tohtml import ToHtml from novelwriter.core.tokenizer import HeadingFormatter -from novelwriter.error import logException +from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument 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 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 @@ -249,20 +246,7 @@ def loadContent(self) -> None: self._updateBuildsList() 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 + QTimer.singleShot(200, self._generatePreview) return @@ -342,36 +326,37 @@ def _generatePreview(self) -> None: SHARED.saveDocument() docBuild = NWBuildDocument(SHARED.project, build) - 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 = { - "uuid": build.buildID, - "time": int(time()), - "stats": buildObj.textStats, - "outline": buildObj.textOutline, - "styles": buildObj.getStyleSheet(), - "html": buildObj.fullHTML, - } - - self._updatePreview(result, build) - - 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)) - except Exception: - logger.error("Failed to save build cache") - logException() - return + assert isinstance(buildObj, ToQTextDocument) + + font = QFont() + font.fromString(build.getStr("format.textFont")) + + self.docPreview.setTextFont(font) + self.docPreview.setContent(buildObj.document) + self.docPreview.setBuildName(build.name) + + self.docStats.updateStats(buildObj.textStats) + self.buildOutline.updateOutline(buildObj.textOutline) return @@ -379,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: @@ -400,21 +385,6 @@ def _printDocument(self) -> None: # Internal Functions ## - def _updatePreview(self, data: dict, build: BuildSettings) -> None: - """Update the preview widget and set relevant values.""" - textFont = QFont() - textFont.fromString(build.getStr("format.textFont")) - - self.docPreview.setContent(data) - self.docPreview.setBuildName(build.name) - self.docPreview.setTextFont(textFont) - 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. @@ -807,16 +777,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 @@ -848,31 +808,19 @@ def buildStep(self, value: int) -> None: QApplication.processEvents() return - def setContent(self, data: dict) -> 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() - 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") + 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() @@ -917,17 +865,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/novelwriter/types.py b/novelwriter/types.py index c70d28934..8cdfb1db7 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 @@ -52,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 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 @@