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"
  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/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") == ( - "

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 @@