Use preview document class for manuscript preview

This commit is contained in:
Veronica Berglyd Olsen
2024-05-24 18:52:18 +02:00
parent c97a070e98
commit 6cc046c65c
6 changed files with 160 additions and 101 deletions
+1 -1
View File
@@ -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:
+25
View File
@@ -39,6 +39,7 @@ from novelwriter.core.tohtml import ToHtml
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 @@ class NWBuildDocument:
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):
+77 -53
View File
@@ -26,18 +26,17 @@ from __future__ import annotations
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 @@ logger = logging.getLogger(__name__)
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
class TextDocumentTheme:
text: QColor = QtBlack
highlight: QColor = QtTransparent
head: QColor = QtBlack
comment: QColor = QtBlack
note: QColor = QtBlack
code: QColor = QtBlack
modifier: QColor = QtBlack
keyword: QColor = QtBlack
tag: QColor = QtBlack
optional: QColor = QtBlack
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
if cursor.position() > 0:
cursor.insertBlock(bFmt)
@@ -64,6 +76,7 @@ class ToQTextDocument(Tokenizer):
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 @@ class ToQTextDocument(Tokenizer):
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 @@ class ToQTextDocument(Tokenizer):
}
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 @@ class ToQTextDocument(Tokenizer):
self._mIndent = mScale * 2.0
self._defaultChar = QTextCharFormat()
self._defaultChar.setForeground(SHARED.theme.colText)
self._cText = QTextCharFormat()
self._cText.setForeground(self._theme.text)
self._comChar = QTextCharFormat()
self._comChar.setForeground(SHARED.theme.colHidden)
self._cHead = QTextCharFormat()
self._cHead.setForeground(self._theme.head)
self._noteChar = QTextCharFormat()
self._noteChar.setForeground(SHARED.theme.colNote)
self._cComment = QTextCharFormat()
self._cComment.setForeground(self._theme.comment)
self._codeChar = QTextCharFormat()
self._codeChar.setForeground(SHARED.theme.colCode)
self._cCommentMod = QTextCharFormat()
self._cCommentMod.setForeground(self._theme.comment)
self._cCommentMod.setFontWeight(self._bold)
self._modChar = QTextCharFormat()
self._modChar.setForeground(SHARED.theme.colMod)
self._cNote = QTextCharFormat()
self._cNote.setForeground(self._theme.note)
self._keyChar = QTextCharFormat()
self._keyChar.setForeground(SHARED.theme.colKey)
self._cCode = QTextCharFormat()
self._cCode.setForeground(self._theme.code)
self._tagChar = QTextCharFormat()
self._tagChar.setForeground(SHARED.theme.colTag)
self._cModifier = QTextCharFormat()
self._cModifier.setForeground(self._theme.modifier)
self._cModifier.setFontWeight(self._bold)
self._optChar = QTextCharFormat()
self._optChar.setForeground(SHARED.theme.colOpt)
self._cKeyword = QTextCharFormat()
self._cKeyword.setForeground(self._theme.keyword)
self._defaultBlock = QTextBlockFormat()
self._defaultBlock.setTopMargin(self._mText[0])
self._defaultBlock.setBottomMargin(self._mText[1])
self._cTag = QTextCharFormat()
self._cTag.setForeground(self._theme.tag)
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
@@ -159,11 +181,12 @@ class ToQTextDocument(Tokenizer):
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 @@ class ToQTextDocument(Tokenizer):
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 @@ class ToQTextDocument(Tokenizer):
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 @@ class ToQTextDocument(Tokenizer):
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 @@ class ToQTextDocument(Tokenizer):
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 @@ class ToQTextDocument(Tokenizer):
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 @@ class ToQTextDocument(Tokenizer):
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)
+14 -3
View File
@@ -40,7 +40,7 @@ from PyQt5.QtWidgets import (
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 @@ class GuiDocViewer(QTextBrowser):
# Internal Variables
self._docHandle = None
self._docTheme = TextDocumentTheme()
# Settings
self.setMinimumWidth(CONFIG.pxInt(300))
@@ -152,6 +153,17 @@ class GuiDocViewer(QTextBrowser):
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 @@ class GuiDocViewer(QTextBrowser):
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.
+42 -44
View File
@@ -31,7 +31,7 @@ from time import time
from typing import TYPE_CHECKING
from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent, QTextDocument
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtWidgets import (
QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout,
@@ -44,8 +44,8 @@ from novelwriter import CONFIG, SHARED
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 @@ class GuiManuscript(NToolDialog):
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 @@ class GuiManuscript(NToolDialog):
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 @@ class GuiManuscript(NToolDialog):
# 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 @@ class _PreviewWidget(QTextBrowser):
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()
+1
View File
@@ -56,6 +56,7 @@ QtPageBreakAfter = QTextFormat.PageBreakFlag.PageBreak_AlwaysAfter
# 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