Add a QTextDocument format class for previews (#1892)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,7 +55,7 @@ class NWBuildDocument:
|
||||
|
||||
__slots__ = (
|
||||
"_project", "_build", "_queue", "_error", "_cache", "_count",
|
||||
"_outline", "_preview"
|
||||
"_outline",
|
||||
)
|
||||
|
||||
def __init__(self, project: NWProject, build: BuildSettings) -> None:
|
||||
@@ -65,7 +66,6 @@ class NWBuildDocument:
|
||||
self._cache = None
|
||||
self._count = False
|
||||
self._outline = False
|
||||
self._preview = False
|
||||
return
|
||||
|
||||
##
|
||||
@@ -99,15 +99,6 @@ class NWBuildDocument:
|
||||
self._outline = state
|
||||
return
|
||||
|
||||
def setPreviewMode(self, state: bool) -> None:
|
||||
"""Set the preview mode of the build. This also enables stats
|
||||
count and outline mode.
|
||||
"""
|
||||
self._preview = state
|
||||
self._outline = state
|
||||
self._count = state
|
||||
return
|
||||
|
||||
##
|
||||
# Special Methods
|
||||
##
|
||||
@@ -134,6 +125,32 @@ 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)
|
||||
|
||||
self._outline = True
|
||||
self._count = True
|
||||
|
||||
font = QFont()
|
||||
font.fromString(self._build.getStr("format.textFont"))
|
||||
|
||||
makeObj.initDocument(font, theme)
|
||||
for i, tHandle in enumerate(self._queue):
|
||||
self._error = None
|
||||
if filtered.get(tHandle, (False, 0))[0]:
|
||||
yield i, self._doBuild(makeObj, tHandle)
|
||||
else:
|
||||
yield i, False
|
||||
|
||||
makeObj.appendFootnotes()
|
||||
|
||||
self._error = None
|
||||
self._cache = makeObj
|
||||
|
||||
return
|
||||
|
||||
def iterBuild(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
|
||||
"""Wrapper for builders based on format."""
|
||||
if bFormat in (nwBuildFmt.ODT, nwBuildFmt.FODT):
|
||||
@@ -182,8 +199,6 @@ class NWBuildDocument:
|
||||
makeObj = ToHtml(self._project)
|
||||
filtered = self._setupBuild(makeObj)
|
||||
|
||||
makeObj.setPreview(self._preview)
|
||||
makeObj.setLinkHeadings(self._preview)
|
||||
for i, tHandle in enumerate(self._queue):
|
||||
self._error = None
|
||||
if filtered.get(tHandle, (False, 0))[0]:
|
||||
@@ -193,7 +208,7 @@ class NWBuildDocument:
|
||||
|
||||
makeObj.appendFootnotes()
|
||||
|
||||
if not (self._build.getBool("html.preserveTabs") or self._preview):
|
||||
if not self._build.getBool("html.preserveTabs"):
|
||||
makeObj.replaceTabs()
|
||||
|
||||
self._error = None
|
||||
|
||||
+14
-55
@@ -38,24 +38,6 @@ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HTML4_TAGS = {
|
||||
Tokenizer.FMT_B_B: "<b>",
|
||||
Tokenizer.FMT_B_E: "</b>",
|
||||
Tokenizer.FMT_I_B: "<i>",
|
||||
Tokenizer.FMT_I_E: "</i>",
|
||||
Tokenizer.FMT_D_B: "<span style='text-decoration: line-through;'>",
|
||||
Tokenizer.FMT_D_E: "</span>",
|
||||
Tokenizer.FMT_U_B: "<u>",
|
||||
Tokenizer.FMT_U_E: "</u>",
|
||||
Tokenizer.FMT_M_B: "<mark>",
|
||||
Tokenizer.FMT_M_E: "</mark>",
|
||||
Tokenizer.FMT_SUP_B: "<sup>",
|
||||
Tokenizer.FMT_SUP_E: "</sup>",
|
||||
Tokenizer.FMT_SUB_B: "<sub>",
|
||||
Tokenizer.FMT_SUB_E: "</sub>",
|
||||
Tokenizer.FMT_STRIP: "",
|
||||
}
|
||||
|
||||
HTML5_TAGS = {
|
||||
Tokenizer.FMT_B_B: "<strong>",
|
||||
Tokenizer.FMT_B_E: "</strong>",
|
||||
@@ -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 @@ class ToHtml(Tokenizer):
|
||||
# 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 @@ class ToHtml(Tokenizer):
|
||||
"""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 @@ class ToHtml(Tokenizer):
|
||||
|
||||
# Process Text Type
|
||||
if tType == self.T_TEXT:
|
||||
lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat, hTags)}</p>\n")
|
||||
lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat)}</p>\n")
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
||||
@@ -269,13 +240,13 @@ class ToHtml(Tokenizer):
|
||||
lines.append(f"<p class='skip'{hStyle}> </p>\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 @@ class ToHtml(Tokenizer):
|
||||
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 @@ class ToHtml(Tokenizer):
|
||||
lines.append("<ol>\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"<li id='footnote_{index}'><p>{text}</p></li>\n")
|
||||
lines.append("</ol>\n")
|
||||
|
||||
@@ -468,7 +438,7 @@ class ToHtml(Tokenizer):
|
||||
# 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 @@ class ToHtml(Tokenizer):
|
||||
else:
|
||||
html = "<sup>ERR</sup>"
|
||||
else:
|
||||
html = tags.get(fmt, "ERR")
|
||||
html = HTML5_TAGS.get(fmt, "ERR")
|
||||
temp = f"{temp[:pos]}{html}{temp[pos:]}"
|
||||
temp = temp.replace("\n", "<br>")
|
||||
return stripEscape(temp)
|
||||
@@ -492,18 +462,12 @@ class ToHtml(Tokenizer):
|
||||
sSynop = self._localLookup("Synopsis")
|
||||
else:
|
||||
sSynop = self._localLookup("Short Description")
|
||||
if self._genMode == self.M_PREVIEW:
|
||||
return f"<p class='note'><span class='modifier'>{sSynop}:</span> {text}</p>\n"
|
||||
else:
|
||||
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
|
||||
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
|
||||
|
||||
def _formatComments(self, text: str) -> str:
|
||||
"""Apply HTML formatting to comments."""
|
||||
if self._genMode == self.M_PREVIEW:
|
||||
return f"<p class='comment'>{text}</p>\n"
|
||||
else:
|
||||
sComm = self._localLookup("Comment")
|
||||
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
|
||||
sComm = self._localLookup("Comment")
|
||||
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
|
||||
|
||||
def _formatKeywords(self, text: str) -> tuple[str, str]:
|
||||
"""Apply HTML formatting to keywords."""
|
||||
@@ -519,13 +483,8 @@ class ToHtml(Tokenizer):
|
||||
if two:
|
||||
result += f" | <span class='optional'>{two}</a>"
|
||||
else:
|
||||
if self._genMode == self.M_PREVIEW:
|
||||
result += ", ".join(
|
||||
f"<a class='tag' href='#{bits[0][1:]}={t}'>{t}</a>" for t in bits[1:]
|
||||
)
|
||||
else:
|
||||
result += ", ".join(
|
||||
f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
|
||||
)
|
||||
result += ", ".join(
|
||||
f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
|
||||
)
|
||||
|
||||
return bits[0][1:], result
|
||||
|
||||
@@ -119,6 +119,7 @@ class Tokenizer(ABC):
|
||||
# Lookups
|
||||
L_HEADINGS = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD3, T_HEAD4]
|
||||
L_SKIP_INDENT = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD2, T_HEAD3, T_HEAD4, T_SEP, T_SKIP]
|
||||
L_SUMMARY = [T_SYNOPSIS, T_SHORT]
|
||||
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
|
||||
@@ -155,14 +156,15 @@ class Tokenizer(ABC):
|
||||
self._keepBreaks = True # Keep line breaks in paragraphs
|
||||
|
||||
# Margins
|
||||
self._marginTitle = (1.000, 0.500)
|
||||
self._marginHead1 = (1.000, 0.500)
|
||||
self._marginHead2 = (0.834, 0.500)
|
||||
self._marginHead3 = (0.584, 0.500)
|
||||
self._marginHead4 = (0.584, 0.500)
|
||||
self._marginTitle = (1.417, 0.500)
|
||||
self._marginHead1 = (1.417, 0.500)
|
||||
self._marginHead2 = (1.668, 0.500)
|
||||
self._marginHead3 = (1.168, 0.500)
|
||||
self._marginHead4 = (1.168, 0.500)
|
||||
self._marginText = (0.000, 0.584)
|
||||
self._marginMeta = (0.000, 0.584)
|
||||
self._marginFoot = (1.417, 0.467)
|
||||
self._marginSep = (1.168, 1.168)
|
||||
|
||||
# Title Formats
|
||||
self._fmtTitle = nwHeadFmt.TITLE # Formatting for titles
|
||||
@@ -378,6 +380,11 @@ class Tokenizer(ABC):
|
||||
self._marginMeta = (float(upper), float(lower))
|
||||
return
|
||||
|
||||
def setSeparatorMargins(self, upper: float, lower: float) -> None:
|
||||
"""Set the upper and lower meta text margin."""
|
||||
self._marginSep = (float(upper), float(lower))
|
||||
return
|
||||
|
||||
def setLinkHeadings(self, state: bool) -> None:
|
||||
"""Enable or disable adding an anchor before headings."""
|
||||
self._linkHeadings = state
|
||||
@@ -597,7 +604,10 @@ class Tokenizer(ABC):
|
||||
# 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
|
||||
))
|
||||
|
||||
@@ -192,6 +192,7 @@ class ToOdt(Tokenizer):
|
||||
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 @@ class ToOdt(Tokenizer):
|
||||
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 @@ class ToOdt(Tokenizer):
|
||||
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 @@ class ToOdt(Tokenizer):
|
||||
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 @@ class ToOdt(Tokenizer):
|
||||
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 @@ class ToOdt(Tokenizer):
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
"""
|
||||
novelWriter – QTextDocument Converter
|
||||
=====================================
|
||||
|
||||
File History:
|
||||
Created: 2024-05-21 [2.5b1] ToQTextDocument
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5.QtGui import (
|
||||
QColor, QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat,
|
||||
QTextCursor, QTextDocument
|
||||
)
|
||||
|
||||
from novelwriter.constants import nwHeaders, nwHeadFmt, nwKeyWords, nwLabels, nwUnicode
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.tokenizer import T_Formats, Tokenizer
|
||||
from novelwriter.types import (
|
||||
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
|
||||
QtBlack, QtPageBreakAfter, QtPageBreakBefore, QtTransparent,
|
||||
QtVAlignNormal, QtVAlignSub, QtVAlignSuper
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
|
||||
|
||||
|
||||
class TextDocumentTheme:
|
||||
text: QColor = QtBlack
|
||||
highlight: QColor = QtTransparent
|
||||
head: QColor = QtBlack
|
||||
comment: QColor = QtBlack
|
||||
note: QColor = QtBlack
|
||||
code: QColor = QtBlack
|
||||
modifier: QColor = QtBlack
|
||||
keyword: QColor = QtBlack
|
||||
tag: QColor = QtBlack
|
||||
optional: QColor = QtBlack
|
||||
|
||||
|
||||
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
|
||||
if cursor.position() > 0:
|
||||
cursor.insertBlock(bFmt)
|
||||
else:
|
||||
cursor.setBlockFormat(bFmt)
|
||||
|
||||
|
||||
class ToQTextDocument(Tokenizer):
|
||||
"""Core: QTextDocument Writer
|
||||
|
||||
Extend the Tokenizer class to generate a QTextDocument output. This
|
||||
is intended for usage in the document viewer and build tool preview.
|
||||
"""
|
||||
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
super().__init__(project)
|
||||
self._document = QTextDocument()
|
||||
self._document.setUndoRedoEnabled(False)
|
||||
self._document.setDocumentMargin(0)
|
||||
|
||||
self._theme = TextDocumentTheme()
|
||||
self._styles: dict[int, T_TextStyle] = {}
|
||||
self._usedNotes: dict[str, int] = {}
|
||||
|
||||
self._init = False
|
||||
self._bold = QFont.Weight.Bold
|
||||
self._normal = QFont.Weight.Normal
|
||||
|
||||
return
|
||||
|
||||
def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
|
||||
"""Initialise all computed values of the document."""
|
||||
self._textFont = font
|
||||
self._theme = theme
|
||||
|
||||
self._document.setUndoRedoEnabled(False)
|
||||
self._document.blockSignals(True)
|
||||
self._document.clear()
|
||||
self._document.setDefaultFont(self._textFont)
|
||||
|
||||
qMetric = QFontMetrics(self._textFont)
|
||||
mScale = qMetric.height()
|
||||
fPt = self._textFont.pointSizeF()
|
||||
|
||||
# Scaled Sizes
|
||||
# ============
|
||||
|
||||
self._mHead = {
|
||||
self.T_TITLE: (mScale * self._marginTitle[0], mScale * self._marginTitle[1]),
|
||||
self.T_HEAD1: (mScale * self._marginHead1[0], mScale * self._marginHead1[1]),
|
||||
self.T_HEAD2: (mScale * self._marginHead2[0], mScale * self._marginHead2[1]),
|
||||
self.T_HEAD3: (mScale * self._marginHead3[0], mScale * self._marginHead3[1]),
|
||||
self.T_HEAD4: (mScale * self._marginHead4[0], mScale * self._marginHead4[1]),
|
||||
}
|
||||
|
||||
self._sHead = {
|
||||
self.T_TITLE: nwHeaders.H_SIZES.get(0, 1.0) * fPt,
|
||||
self.T_HEAD1: nwHeaders.H_SIZES.get(1, 1.0) * fPt,
|
||||
self.T_HEAD2: nwHeaders.H_SIZES.get(2, 1.0) * fPt,
|
||||
self.T_HEAD3: nwHeaders.H_SIZES.get(3, 1.0) * fPt,
|
||||
self.T_HEAD4: nwHeaders.H_SIZES.get(4, 1.0) * fPt,
|
||||
}
|
||||
|
||||
self._mText = (mScale * self._marginText[0], mScale * self._marginText[1])
|
||||
self._mMeta = (mScale * self._marginMeta[0], mScale * self._marginMeta[1])
|
||||
self._mSep = (mScale * self._marginSep[0], mScale * self._marginSep[1])
|
||||
|
||||
self._mIndent = mScale * 2.0
|
||||
|
||||
# Block Format
|
||||
# ============
|
||||
|
||||
self._blockFmt = QTextBlockFormat()
|
||||
self._blockFmt.setTopMargin(self._mText[0])
|
||||
self._blockFmt.setBottomMargin(self._mText[1])
|
||||
self._blockFmt.setAlignment(QtAlignJustify if self._doJustify else QtAlignAbsolute)
|
||||
|
||||
# Character Formats
|
||||
# =================
|
||||
|
||||
self._cText = QTextCharFormat()
|
||||
self._cText.setBackground(QtTransparent)
|
||||
self._cText.setForeground(self._theme.text)
|
||||
|
||||
self._cHead = QTextCharFormat(self._cText)
|
||||
self._cHead.setForeground(self._theme.head)
|
||||
|
||||
self._cComment = QTextCharFormat(self._cText)
|
||||
self._cComment.setForeground(self._theme.comment)
|
||||
|
||||
self._cCommentMod = QTextCharFormat(self._cText)
|
||||
self._cCommentMod.setForeground(self._theme.comment)
|
||||
self._cCommentMod.setFontWeight(self._bold)
|
||||
|
||||
self._cNote = QTextCharFormat(self._cText)
|
||||
self._cNote.setForeground(self._theme.note)
|
||||
|
||||
self._cCode = QTextCharFormat(self._cText)
|
||||
self._cCode.setForeground(self._theme.code)
|
||||
|
||||
self._cModifier = QTextCharFormat(self._cText)
|
||||
self._cModifier.setForeground(self._theme.modifier)
|
||||
self._cModifier.setFontWeight(self._bold)
|
||||
|
||||
self._cKeyword = QTextCharFormat(self._cText)
|
||||
self._cKeyword.setForeground(self._theme.keyword)
|
||||
|
||||
self._cTag = QTextCharFormat(self._cText)
|
||||
self._cTag.setForeground(self._theme.tag)
|
||||
|
||||
self._cOptional = QTextCharFormat(self._cText)
|
||||
self._cOptional.setForeground(self._theme.optional)
|
||||
|
||||
self._init = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def document(self) -> QTextDocument:
|
||||
"""Return the document."""
|
||||
return self._document
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def doConvert(self) -> None:
|
||||
"""Write text tokens into the document."""
|
||||
if not self._init:
|
||||
return
|
||||
|
||||
self._document.blockSignals(True)
|
||||
cursor = QTextCursor(self._document)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.End)
|
||||
|
||||
for tType, nHead, tText, tFormat, tStyle in self._tokens:
|
||||
|
||||
# Styles
|
||||
bFmt = QTextBlockFormat(self._blockFmt)
|
||||
if tStyle is not None:
|
||||
if tStyle & self.A_LEFT:
|
||||
bFmt.setAlignment(QtAlignLeft)
|
||||
elif tStyle & self.A_RIGHT:
|
||||
bFmt.setAlignment(QtAlignRight)
|
||||
elif tStyle & self.A_CENTRE:
|
||||
bFmt.setAlignment(QtAlignCenter)
|
||||
elif tStyle & self.A_JUSTIFY:
|
||||
bFmt.setAlignment(QtAlignJustify)
|
||||
|
||||
if tStyle & self.A_PBB:
|
||||
bFmt.setPageBreakPolicy(QtPageBreakBefore)
|
||||
if tStyle & self.A_PBA:
|
||||
bFmt.setPageBreakPolicy(QtPageBreakAfter)
|
||||
|
||||
if tStyle & self.A_Z_BTMMRG:
|
||||
bFmt.setBottomMargin(0.0)
|
||||
if tStyle & self.A_Z_TOPMRG:
|
||||
bFmt.setTopMargin(0.0)
|
||||
|
||||
if tStyle & self.A_IND_L:
|
||||
bFmt.setLeftMargin(self._mIndent)
|
||||
if tStyle & self.A_IND_R:
|
||||
bFmt.setRightMargin(self._mIndent)
|
||||
|
||||
if tType == self.T_TEXT:
|
||||
newBlock(cursor, bFmt)
|
||||
self._insertFragments(tText, tFormat, cursor, self._cText)
|
||||
|
||||
elif tType in self.L_HEADINGS:
|
||||
bFmt, cFmt = self._genHeadStyle(tType, nHead, bFmt)
|
||||
newBlock(cursor, bFmt)
|
||||
cursor.insertText(tText.replace(nwHeadFmt.BR, "\n"), cFmt)
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
sFmt = QTextBlockFormat(bFmt)
|
||||
sFmt.setTopMargin(self._mSep[0])
|
||||
sFmt.setBottomMargin(self._mSep[1])
|
||||
newBlock(cursor, sFmt)
|
||||
cursor.insertText(tText, self._cText)
|
||||
|
||||
elif tType == self.T_SKIP:
|
||||
newBlock(cursor, bFmt)
|
||||
cursor.insertText(nwUnicode.U_NBSP, self._cText)
|
||||
|
||||
elif tType in self.L_SUMMARY and self._doSynopsis:
|
||||
newBlock(cursor, bFmt)
|
||||
modifier = self._localLookup(
|
||||
"Short Description" if tType == self.T_SHORT else "Synopsis"
|
||||
)
|
||||
cursor.insertText(f"{modifier}: ", self._cModifier)
|
||||
self._insertFragments(tText, tFormat, cursor, self._cNote)
|
||||
|
||||
elif tType == self.T_COMMENT and self._doComments:
|
||||
newBlock(cursor, bFmt)
|
||||
modifier = self._localLookup("Comment")
|
||||
cursor.insertText(f"{modifier}: ", self._cCommentMod)
|
||||
self._insertFragments(tText, tFormat, cursor, self._cComment)
|
||||
|
||||
elif tType == self.T_KEYWORD and self._doKeywords:
|
||||
newBlock(cursor, bFmt)
|
||||
self._insertKeywords(tText, cursor)
|
||||
|
||||
self._document.blockSignals(False)
|
||||
|
||||
return
|
||||
|
||||
def appendFootnotes(self) -> None:
|
||||
"""Append the footnotes in the buffer."""
|
||||
if self._usedNotes:
|
||||
self._document.blockSignals(True)
|
||||
|
||||
cursor = QTextCursor(self._document)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.End)
|
||||
|
||||
bFmt, cFmt = self._genHeadStyle(self.T_HEAD4, -1, self._blockFmt)
|
||||
newBlock(cursor, bFmt)
|
||||
cursor.insertText(self._localLookup("Footnotes"), cFmt)
|
||||
|
||||
for key, index in self._usedNotes.items():
|
||||
if content := self._footnotes.get(key):
|
||||
cFmt = QTextCharFormat(self._cCode)
|
||||
cFmt.setAnchor(True)
|
||||
cFmt.setAnchorNames([f"footnote_{index}"])
|
||||
newBlock(cursor, self._blockFmt)
|
||||
cursor.insertText(f"{index}. ", cFmt)
|
||||
self._insertFragments(*content, cursor, self._cText)
|
||||
|
||||
self._document.blockSignals(False)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _insertFragments(
|
||||
self, text: str, tFmt: T_Formats, cursor: QTextCursor, dFmt: QTextCharFormat
|
||||
) -> None:
|
||||
"""Apply formatting tags to text."""
|
||||
cFmt = QTextCharFormat(dFmt)
|
||||
start = 0
|
||||
temp = text.replace("\n", nwUnicode.U_LSEP)
|
||||
for pos, fmt, data in tFmt:
|
||||
|
||||
# Insert buffer with previous format
|
||||
cursor.insertText(temp[start:pos], cFmt)
|
||||
|
||||
# Construct next format
|
||||
if fmt == self.FMT_B_B:
|
||||
cFmt.setFontWeight(self._bold)
|
||||
elif fmt == self.FMT_B_E:
|
||||
cFmt.setFontWeight(self._normal)
|
||||
elif fmt == self.FMT_I_B:
|
||||
cFmt.setFontItalic(True)
|
||||
elif fmt == self.FMT_I_E:
|
||||
cFmt.setFontItalic(False)
|
||||
elif fmt == self.FMT_D_B:
|
||||
cFmt.setFontStrikeOut(True)
|
||||
elif fmt == self.FMT_D_E:
|
||||
cFmt.setFontStrikeOut(False)
|
||||
elif fmt == self.FMT_U_B:
|
||||
cFmt.setFontUnderline(True)
|
||||
elif fmt == self.FMT_U_E:
|
||||
cFmt.setFontUnderline(False)
|
||||
elif fmt == self.FMT_M_B:
|
||||
cFmt.setBackground(self._theme.highlight)
|
||||
elif fmt == self.FMT_M_E:
|
||||
cFmt.setBackground(QtTransparent)
|
||||
elif fmt == self.FMT_SUP_B:
|
||||
cFmt.setVerticalAlignment(QtVAlignSuper)
|
||||
elif fmt == self.FMT_SUP_E:
|
||||
cFmt.setVerticalAlignment(QtVAlignNormal)
|
||||
elif fmt == self.FMT_SUB_B:
|
||||
cFmt.setVerticalAlignment(QtVAlignSub)
|
||||
elif fmt == self.FMT_SUB_E:
|
||||
cFmt.setVerticalAlignment(QtVAlignNormal)
|
||||
elif fmt == self.FMT_FNOTE:
|
||||
xFmt = QTextCharFormat(self._cCode)
|
||||
xFmt.setVerticalAlignment(QtVAlignSuper)
|
||||
if data in self._footnotes:
|
||||
index = len(self._usedNotes) + 1
|
||||
self._usedNotes[data] = index
|
||||
xFmt.setAnchor(True)
|
||||
xFmt.setAnchorHref(f"#footnote_{index}")
|
||||
xFmt.setFontUnderline(True)
|
||||
cursor.insertText(f"[{index}]", xFmt)
|
||||
else:
|
||||
cursor.insertText("[ERR]", cFmt)
|
||||
|
||||
# Move pos for next pass
|
||||
start = pos
|
||||
|
||||
# Insert whatever is left in the buffer
|
||||
cursor.insertText(temp[start:], cFmt)
|
||||
|
||||
return
|
||||
|
||||
def _insertKeywords(self, text: str, cursor: QTextCursor) -> None:
|
||||
"""Apply Markdown formatting to keywords."""
|
||||
valid, bits, _ = self._project.index.scanThis("@"+text)
|
||||
if valid and bits:
|
||||
key = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
|
||||
cursor.insertText(key, self._cKeyword)
|
||||
if (num := len(bits)) > 1:
|
||||
if bits[0] == nwKeyWords.TAG_KEY:
|
||||
one, two = self._project.index.parseValue(bits[1])
|
||||
cFmt = QTextCharFormat(self._cTag)
|
||||
cFmt.setAnchor(True)
|
||||
cFmt.setAnchorNames([f"tag_{one}".lower()])
|
||||
cursor.insertText(one, cFmt)
|
||||
if two:
|
||||
cursor.insertText(" | ", self._cText)
|
||||
cursor.insertText(two, self._cOptional)
|
||||
else:
|
||||
for n, bit in enumerate(bits[1:], 2):
|
||||
cFmt = QTextCharFormat(self._cTag)
|
||||
cFmt.setFontUnderline(True)
|
||||
cFmt.setAnchor(True)
|
||||
cFmt.setAnchorHref(f"#tag_{bit}".lower())
|
||||
cursor.insertText(bit, cFmt)
|
||||
if n < num:
|
||||
cursor.insertText(", ", self._cText)
|
||||
return
|
||||
|
||||
def _genHeadStyle(self, hType: int, nHead: int, rFmt: QTextBlockFormat) -> T_TextStyle:
|
||||
"""Generate a heading style set."""
|
||||
mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
|
||||
|
||||
bFmt = QTextBlockFormat(rFmt)
|
||||
bFmt.setTopMargin(mTop)
|
||||
bFmt.setBottomMargin(mBottom)
|
||||
|
||||
cFmt = QTextCharFormat(self._cText if hType == self.T_TITLE else self._cHead)
|
||||
cFmt.setFontWeight(self._bold)
|
||||
cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
|
||||
if nHead >= 0:
|
||||
cFmt.setAnchorNames([f"{self._handle}:T{nHead:04d}"])
|
||||
cFmt.setAnchor(True)
|
||||
|
||||
return bFmt, cFmt
|
||||
@@ -321,8 +321,11 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
# 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 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
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
|
||||
|
||||
@@ -31,24 +31,21 @@ import logging
|
||||
from enum import Enum
|
||||
|
||||
from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor, QTextOption
|
||||
from PyQt5.QtGui import QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor
|
||||
from PyQt5.QtWidgets import (
|
||||
QAction, QApplication, QFrame, QHBoxLayout, QLabel, QMenu, QTextBrowser,
|
||||
QToolButton, QWidget
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import cssCol
|
||||
from novelwriter.constants import nwHeaders, nwUnicode
|
||||
from novelwriter.core.tohtml import ToHtml
|
||||
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
|
||||
from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.extensions.eventfilters import WheelEventFilter
|
||||
from novelwriter.extensions.modified import NIconToolButton
|
||||
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
|
||||
from novelwriter.types import (
|
||||
QtAlignCenterTop, QtAlignJustify, QtKeepAnchor, QtMouseLeft, QtMoveAnchor
|
||||
)
|
||||
from novelwriter.types import QtAlignCenterTop, QtKeepAnchor, QtMouseLeft, QtMoveAnchor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -69,6 +66,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
# Internal Variables
|
||||
self._docHandle = None
|
||||
self._docTheme = TextDocumentTheme()
|
||||
|
||||
# Settings
|
||||
self.setMinimumWidth(CONFIG.pxInt(300))
|
||||
@@ -137,8 +135,10 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
def initViewer(self) -> None:
|
||||
"""Set editor settings from main config."""
|
||||
self._makeStyleSheet()
|
||||
self.initFont()
|
||||
# Set the font. See issues #1862 and #1875.
|
||||
self.setFont(CONFIG.textFont)
|
||||
self.docHeader.updateFont()
|
||||
self.docFooter.updateFont()
|
||||
|
||||
# Set the widget colours to match syntax theme
|
||||
mainPalette = self.palette()
|
||||
@@ -151,16 +151,23 @@ class GuiDocViewer(QTextBrowser):
|
||||
docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
|
||||
docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
|
||||
self.viewport().setPalette(docPalette)
|
||||
|
||||
self.docHeader.matchColours()
|
||||
self.docFooter.matchColours()
|
||||
|
||||
# Update theme colours
|
||||
self._docTheme.text = SHARED.theme.colText
|
||||
self._docTheme.highlight = SHARED.theme.colMark
|
||||
self._docTheme.head = SHARED.theme.colHead
|
||||
self._docTheme.comment = SHARED.theme.colHidden
|
||||
self._docTheme.note = SHARED.theme.colNote
|
||||
self._docTheme.code = SHARED.theme.colCode
|
||||
self._docTheme.modifier = SHARED.theme.colMod
|
||||
self._docTheme.keyword = SHARED.theme.colKey
|
||||
self._docTheme.tag = SHARED.theme.colTag
|
||||
self._docTheme.optional = SHARED.theme.colOpt
|
||||
|
||||
# Set default text margins
|
||||
self.document().setDocumentMargin(0)
|
||||
options = QTextOption()
|
||||
if CONFIG.doJustify:
|
||||
options.setAlignment(QtAlignJustify)
|
||||
self.document().setDefaultTextOption(options)
|
||||
|
||||
# Scroll bars
|
||||
if CONFIG.hideVScroll:
|
||||
@@ -181,16 +188,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
return
|
||||
|
||||
def initFont(self) -> None:
|
||||
"""Set the font of the main widget and sub-widgets. This needs
|
||||
special attention since there appears to be a bug in Qt 5.15.3.
|
||||
See issues #1862 and #1875.
|
||||
"""
|
||||
self.setFont(CONFIG.textFont)
|
||||
self.docHeader.updateFont()
|
||||
self.docFooter.updateFont()
|
||||
return
|
||||
|
||||
def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
|
||||
"""Load text into the viewer from an item handle."""
|
||||
if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
|
||||
@@ -202,22 +199,22 @@ class GuiDocViewer(QTextBrowser):
|
||||
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
||||
|
||||
sPos = self.verticalScrollBar().value()
|
||||
aDoc = ToHtml(SHARED.project)
|
||||
aDoc.setPreview(True)
|
||||
aDoc.setKeywords(True)
|
||||
aDoc.setComments(CONFIG.viewComments)
|
||||
aDoc.setSynopsis(CONFIG.viewSynopsis)
|
||||
aDoc.setLinkHeadings(True)
|
||||
qDoc = ToQTextDocument(SHARED.project)
|
||||
qDoc.setJustify(CONFIG.doJustify)
|
||||
qDoc.initDocument(CONFIG.textFont, self._docTheme)
|
||||
qDoc.setKeywords(True)
|
||||
qDoc.setComments(CONFIG.viewComments)
|
||||
qDoc.setSynopsis(CONFIG.viewSynopsis)
|
||||
|
||||
# Be extra careful here to prevent crashes when first opening a
|
||||
# project as a crash here leaves no way of recovering.
|
||||
# See issue #298
|
||||
try:
|
||||
aDoc.setText(tHandle)
|
||||
aDoc.doPreProcessing()
|
||||
aDoc.tokenizeText()
|
||||
aDoc.doConvert()
|
||||
aDoc.appendFootnotes()
|
||||
qDoc.setText(tHandle)
|
||||
qDoc.doPreProcessing()
|
||||
qDoc.tokenizeText()
|
||||
qDoc.doConvert()
|
||||
qDoc.appendFootnotes()
|
||||
except Exception:
|
||||
logger.error("Failed to generate preview for document with handle '%s'", tHandle)
|
||||
logException()
|
||||
@@ -233,11 +230,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.docHistory.append(tHandle)
|
||||
|
||||
self.setDocumentTitle(tHandle)
|
||||
|
||||
# Replace tabs before setting the HTML, and then put them back in
|
||||
self.setHtml(aDoc.result.replace("\t", "!!tab!!"))
|
||||
while self.find("!!tab!!"):
|
||||
self.textCursor().insertText("\t")
|
||||
self.setDocument(qDoc.document)
|
||||
|
||||
if self._docHandle == tHandle:
|
||||
# This is a refresh, so we set the scrollbar back to where it was
|
||||
@@ -375,12 +368,10 @@ class GuiDocViewer(QTextBrowser):
|
||||
@pyqtSlot("QUrl")
|
||||
def _linkClicked(self, url: QUrl) -> None:
|
||||
"""Process a clicked link in the document."""
|
||||
link = url.url()
|
||||
logger.debug("Clicked link: '%s'", link)
|
||||
if len(link) > 0:
|
||||
bits = link.split("=")
|
||||
if len(bits) == 2:
|
||||
self.loadDocumentTagRequest.emit(bits[1], nwDocMode.VIEW)
|
||||
if link := url.url():
|
||||
logger.debug("Clicked link: '%s'", link)
|
||||
if (bits := link.partition("_")) and bits[2]:
|
||||
self.loadDocumentTagRequest.emit(bits[2], nwDocMode.VIEW)
|
||||
return
|
||||
|
||||
@pyqtSlot("QPoint")
|
||||
@@ -470,34 +461,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
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:
|
||||
|
||||
|
||||
@@ -800,8 +800,7 @@ class GuiMain(QMainWindow):
|
||||
def showBuildManuscriptDialog(self) -> None:
|
||||
"""Open the build manuscript dialog."""
|
||||
if SHARED.hasProject:
|
||||
if (dialog := SHARED.findTopLevelWidget(GuiManuscript)) is None:
|
||||
dialog = GuiManuscript(self)
|
||||
dialog = GuiManuscript(self)
|
||||
dialog.activateDialog()
|
||||
dialog.loadContent()
|
||||
return
|
||||
@@ -819,8 +818,7 @@ class GuiMain(QMainWindow):
|
||||
def showWritingStatsDialog(self) -> None:
|
||||
"""Open the session stats dialog."""
|
||||
if SHARED.hasProject:
|
||||
if (dialog := SHARED.findTopLevelWidget(GuiWritingStats)) is None:
|
||||
dialog = GuiWritingStats(self)
|
||||
dialog = GuiWritingStats(self)
|
||||
dialog.activateDialog()
|
||||
dialog.populateGUI()
|
||||
return
|
||||
|
||||
@@ -23,15 +23,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from datetime import datetime
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
|
||||
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent, QTextDocument
|
||||
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout,
|
||||
@@ -41,20 +39,19 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import checkInt, fuzzyTime
|
||||
from novelwriter.common import fuzzyTime
|
||||
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
|
||||
from novelwriter.core.docbuild import NWBuildDocument
|
||||
from novelwriter.core.tohtml import ToHtml
|
||||
from novelwriter.core.tokenizer import HeadingFormatter
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
|
||||
from novelwriter.extensions.circularprogress import NProgressCircle
|
||||
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
|
||||
from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
|
||||
from novelwriter.tools.manusbuild import GuiManuscriptBuild
|
||||
from novelwriter.tools.manussettings import GuiBuildSettings
|
||||
from novelwriter.types import (
|
||||
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignRight, QtAlignTop,
|
||||
QtSizeExpanding, QtSizeIgnored, QtUserRole
|
||||
QtAlignCenter, QtAlignRight, QtAlignTop, QtSizeExpanding, QtSizeIgnored,
|
||||
QtUserRole
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
@@ -249,20 +246,7 @@ class GuiManuscript(NToolDialog):
|
||||
self._updateBuildsList()
|
||||
if selected in self._buildMap:
|
||||
self.buildList.setCurrentItem(self._buildMap[selected])
|
||||
|
||||
logger.debug("Loading build cache")
|
||||
cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
|
||||
if cache.is_file():
|
||||
try:
|
||||
with open(cache, mode="r", encoding="utf-8") as fObj:
|
||||
data = json.load(fObj)
|
||||
build = self._builds.getBuild(data.get("uuid", ""))
|
||||
if isinstance(build, BuildSettings):
|
||||
self._updatePreview(data, build)
|
||||
except Exception:
|
||||
logger.error("Failed to load build cache")
|
||||
logException()
|
||||
return
|
||||
QTimer.singleShot(200, self._generatePreview)
|
||||
|
||||
return
|
||||
|
||||
@@ -342,36 +326,37 @@ class GuiManuscript(NToolDialog):
|
||||
SHARED.saveDocument()
|
||||
|
||||
docBuild = NWBuildDocument(SHARED.project, build)
|
||||
docBuild.setPreviewMode(True)
|
||||
docBuild.queueAll()
|
||||
|
||||
theme = TextDocumentTheme()
|
||||
theme.text = QColor(0, 0, 0)
|
||||
theme.highlight = QColor(255, 255, 166)
|
||||
theme.head = QColor(66, 113, 174)
|
||||
theme.comment = QColor(100, 100, 100)
|
||||
theme.note = QColor(129, 55, 9)
|
||||
theme.code = QColor(66, 113, 174)
|
||||
theme.modifier = QColor(129, 55, 9)
|
||||
theme.keyword = QColor(245, 135, 31)
|
||||
theme.tag = QColor(66, 113, 174)
|
||||
theme.optional = QColor(66, 113, 174)
|
||||
|
||||
self.docPreview.beginNewBuild(len(docBuild))
|
||||
for step, _ in docBuild.iterBuildHTML(None):
|
||||
for step, _ in docBuild.iterBuildPreview(theme):
|
||||
self.docPreview.buildStep(step + 1)
|
||||
QApplication.processEvents()
|
||||
|
||||
buildObj = docBuild.lastBuild
|
||||
assert isinstance(buildObj, ToHtml)
|
||||
result = {
|
||||
"uuid": build.buildID,
|
||||
"time": int(time()),
|
||||
"stats": buildObj.textStats,
|
||||
"outline": buildObj.textOutline,
|
||||
"styles": buildObj.getStyleSheet(),
|
||||
"html": buildObj.fullHTML,
|
||||
}
|
||||
assert isinstance(buildObj, ToQTextDocument)
|
||||
|
||||
self._updatePreview(result, build)
|
||||
font = QFont()
|
||||
font.fromString(build.getStr("format.textFont"))
|
||||
|
||||
logger.debug("Saving build cache")
|
||||
cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
|
||||
try:
|
||||
with open(cache, mode="w+", encoding="utf-8") as outFile:
|
||||
outFile.write(json.dumps(result, indent=2))
|
||||
except Exception:
|
||||
logger.error("Failed to save build cache")
|
||||
logException()
|
||||
return
|
||||
self.docPreview.setTextFont(font)
|
||||
self.docPreview.setContent(buildObj.document)
|
||||
self.docPreview.setBuildName(build.name)
|
||||
|
||||
self.docStats.updateStats(buildObj.textStats)
|
||||
self.buildOutline.updateOutline(buildObj.textOutline)
|
||||
|
||||
return
|
||||
|
||||
@@ -379,8 +364,8 @@ class GuiManuscript(NToolDialog):
|
||||
def _buildManuscript(self) -> None:
|
||||
"""Open the build dialog and build the manuscript."""
|
||||
if build := self._getSelectedBuild():
|
||||
dlgBuild = GuiManuscriptBuild(self, build)
|
||||
dlgBuild.exec()
|
||||
dialog = GuiManuscriptBuild(self, build)
|
||||
dialog.exec()
|
||||
|
||||
# After the build is done, save build settings changes
|
||||
if build.changed:
|
||||
@@ -400,21 +385,6 @@ class GuiManuscript(NToolDialog):
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _updatePreview(self, data: dict, build: BuildSettings) -> None:
|
||||
"""Update the preview widget and set relevant values."""
|
||||
textFont = QFont()
|
||||
textFont.fromString(build.getStr("format.textFont"))
|
||||
|
||||
self.docPreview.setContent(data)
|
||||
self.docPreview.setBuildName(build.name)
|
||||
self.docPreview.setTextFont(textFont)
|
||||
self.docPreview.setJustify(
|
||||
build.getBool("format.justifyText")
|
||||
)
|
||||
self.docStats.updateStats(data.get("stats", {}))
|
||||
self.buildOutline.updateOutline(data.get("outline", {}))
|
||||
return
|
||||
|
||||
def _getSelectedBuild(self) -> BuildSettings | None:
|
||||
"""Get the currently selected build. If none are selected,
|
||||
automatically select the first one.
|
||||
@@ -807,16 +777,6 @@ class _PreviewWidget(QTextBrowser):
|
||||
self._updateBuildAge()
|
||||
return
|
||||
|
||||
def setJustify(self, state: bool) -> None:
|
||||
"""Enable/disable the justify text option."""
|
||||
pOptions = self.document().defaultTextOption()
|
||||
if state:
|
||||
pOptions.setAlignment(QtAlignJustify)
|
||||
else:
|
||||
pOptions.setAlignment(QtAlignAbsolute)
|
||||
self.document().setDefaultTextOption(pOptions)
|
||||
return
|
||||
|
||||
def setTextFont(self, font: QFont) -> None:
|
||||
"""Set the text font properties and then reset for sub-widgets.
|
||||
This needs special attention since there appears to be a bug in
|
||||
@@ -848,31 +808,19 @@ class _PreviewWidget(QTextBrowser):
|
||||
QApplication.processEvents()
|
||||
return
|
||||
|
||||
def setContent(self, data: dict) -> None:
|
||||
def setContent(self, document: QTextDocument) -> None:
|
||||
"""Set the content of the preview widget."""
|
||||
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
||||
|
||||
self.buildProgress.setCentreText(self.tr("Processing ..."))
|
||||
QApplication.processEvents()
|
||||
|
||||
styles = "\n".join(data.get("styles", []))
|
||||
self.document().setDefaultStyleSheet(styles)
|
||||
document.setDocumentMargin(CONFIG.getTextMargin())
|
||||
self.setDocument(document)
|
||||
|
||||
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")
|
||||
|
||||
self._docTime = checkInt(data.get("time"), 0)
|
||||
self._docTime = int(time())
|
||||
self._updateBuildAge()
|
||||
|
||||
# Since we change the content while it may still be rendering, we mark
|
||||
# the document as dirty again to make sure it's re-rendered properly.
|
||||
self.document().markContentsDirty(0, self.document().characterCount())
|
||||
|
||||
self.buildProgress.setCentreText(self.tr("Done"))
|
||||
QApplication.restoreOverrideCursor()
|
||||
QApplication.processEvents()
|
||||
@@ -917,17 +865,14 @@ class _PreviewWidget(QTextBrowser):
|
||||
@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("<b>{0}</b><br>{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 = "<b>{0}</b><br>{1}".format(self._buildName, text)
|
||||
self.ageLabel.setText(text)
|
||||
self.ageLabel.setText("<b>{0}</b>".format(self.tr("No Preview")))
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
|
||||
@@ -24,7 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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 @@ QtAlignRightMiddle = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
QtAlignRightTop = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
|
||||
QtAlignTop = Qt.AlignmentFlag.AlignTop
|
||||
|
||||
QtVAlignNormal = QTextCharFormat.VerticalAlignment.AlignNormal
|
||||
QtVAlignSub = QTextCharFormat.VerticalAlignment.AlignSubScript
|
||||
QtVAlignSuper = QTextCharFormat.VerticalAlignment.AlignSuperScript
|
||||
|
||||
# Qt Page Break
|
||||
|
||||
QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore
|
||||
@@ -52,6 +56,7 @@ 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
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||
<office:meta>
|
||||
<meta:creation-date>2024-05-22T23:05:27</meta:creation-date>
|
||||
<meta:creation-date>2024-05-24T21:31:13</meta:creation-date>
|
||||
<meta:generator>novelWriter/2.5a4</meta:generator>
|
||||
<meta:initial-creator>Jane Smith</meta:initial-creator>
|
||||
<meta:editing-cycles>1234</meta:editing-cycles>
|
||||
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
|
||||
<dc:title>Test Project</dc:title>
|
||||
<dc:date>2024-05-22T23:05:27</dc:date>
|
||||
<dc:date>2024-05-24T21:31:13</dc:date>
|
||||
<dc:creator>Jane Smith</dc:creator>
|
||||
</office:meta>
|
||||
<office:font-face-decls>
|
||||
@@ -22,7 +22,7 @@
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="12pt" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" fo:keep-with-next="always" />
|
||||
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" fo:keep-with-next="always" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="14pt" />
|
||||
</style:style>
|
||||
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
|
||||
@@ -38,27 +38,27 @@
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" fo:color="#813709" loext:opacity="100%" />
|
||||
</style:style>
|
||||
<style:style style:name="Title" style:family="paragraph" style:display-name="Title" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:class="chapter">
|
||||
<style:paragraph-properties fo:margin-top="0.423cm" fo:margin-bottom="0.212cm" fo:text-align="center" />
|
||||
<style:paragraph-properties fo:margin-top="0.600cm" fo:margin-bottom="0.212cm" fo:text-align="center" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="30pt" fo:font-weight="bold" />
|
||||
</style:style>
|
||||
<style:style style:name="Separator" style:family="paragraph" style:display-name="Separator" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.247cm" fo:line-height="115%" fo:text-align="center" />
|
||||
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.494cm" fo:line-height="115%" fo:text-align="center" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_1" style:family="paragraph" style:display-name="Heading 1" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="1" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.423cm" fo:margin-bottom="0.212cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.600cm" fo:margin-bottom="0.212cm" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="24pt" fo:font-weight="bold" fo:color="#2a6099" loext:opacity="100%" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_2" style:family="paragraph" style:display-name="Heading 2" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="2" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.353cm" fo:margin-bottom="0.212cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.706cm" fo:margin-bottom="0.212cm" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="19pt" fo:font-weight="bold" fo:color="#2a6099" loext:opacity="100%" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_3" style:family="paragraph" style:display-name="Heading 3" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="3" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="16pt" fo:font-weight="bold" fo:color="#444444" loext:opacity="100%" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_4" style:family="paragraph" style:display-name="Heading 4" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="4" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="14pt" fo:font-weight="bold" fo:color="#444444" loext:opacity="100%" />
|
||||
</style:style>
|
||||
<style:style style:name="Header" style:family="paragraph" style:display-name="Header" style:parent-style-name="Header_20_and_20_Footer">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="12pt" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" fo:keep-with-next="always" />
|
||||
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" fo:keep-with-next="always" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="14pt" />
|
||||
</style:style>
|
||||
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
|
||||
@@ -28,27 +28,27 @@
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
|
||||
</style:style>
|
||||
<style:style style:name="Title" style:family="paragraph" style:display-name="Title" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:class="chapter">
|
||||
<style:paragraph-properties fo:margin-top="0.423cm" fo:margin-bottom="0.212cm" fo:text-align="center" />
|
||||
<style:paragraph-properties fo:margin-top="0.600cm" fo:margin-bottom="0.212cm" fo:text-align="center" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="30pt" fo:font-weight="bold" />
|
||||
</style:style>
|
||||
<style:style style:name="Separator" style:family="paragraph" style:display-name="Separator" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.247cm" fo:line-height="115%" fo:text-align="center" />
|
||||
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.494cm" fo:line-height="115%" fo:text-align="center" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_1" style:family="paragraph" style:display-name="Heading 1" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="1" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.423cm" fo:margin-bottom="0.212cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.600cm" fo:margin-bottom="0.212cm" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="24pt" fo:font-weight="bold" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_2" style:family="paragraph" style:display-name="Heading 2" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="2" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.353cm" fo:margin-bottom="0.212cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.706cm" fo:margin-bottom="0.212cm" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="19pt" fo:font-weight="bold" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_3" style:family="paragraph" style:display-name="Heading 3" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="3" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="16pt" fo:font-weight="bold" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_4" style:family="paragraph" style:display-name="Heading 4" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="4" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" />
|
||||
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="14pt" fo:font-weight="bold" />
|
||||
</style:style>
|
||||
<style:style style:name="Header" style:family="paragraph" style:display-name="Header" style:parent-style-name="Header_20_and_20_Footer">
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<style>
|
||||
body {font-family: 'Arial'; font-size: 12pt; font-weight: 400; font-style: normal;}
|
||||
p {text-align: justify; line-height: 150%; margin-top: 0.00em; margin-bottom: 0.76em;}
|
||||
h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.30em; margin-bottom: 0.65em;}
|
||||
h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.09em; margin-bottom: 0.65em;}
|
||||
h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.76em; margin-bottom: 0.65em;}
|
||||
h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.76em; margin-bottom: 0.65em;}
|
||||
.title {font-size: 2.5em; margin-top: 1.30em; margin-bottom: 0.65em;}
|
||||
h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.85em; margin-bottom: 0.65em;}
|
||||
h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 2.18em; margin-bottom: 0.65em;}
|
||||
h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 1.52em; margin-bottom: 0.65em;}
|
||||
h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 1.52em; margin-bottom: 0.65em;}
|
||||
.title {font-size: 2.5em; margin-top: 1.85em; margin-bottom: 0.65em;}
|
||||
.sep, .skip {text-align: center; margin-top: 1.30em; margin-bottom: 1.30em;}
|
||||
a {color: rgb(66, 113, 174);}
|
||||
mark {background: rgb(255, 255, 166);}
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
"meta": {
|
||||
"projectName": "Lorem Ipsum",
|
||||
"novelAuthor": "lipsum.com",
|
||||
"buildTime": 1716412100,
|
||||
"buildTimeStr": "2024-05-22 23:08:20"
|
||||
"buildTime": 1716579341,
|
||||
"buildTimeStr": "2024-05-24 21:35:41"
|
||||
},
|
||||
"text": {
|
||||
"css": [
|
||||
"body {font-family: 'Arial'; font-size: 12pt; font-weight: 400; font-style: normal;}",
|
||||
"p {text-align: justify; line-height: 150%; margin-top: 0.00em; margin-bottom: 0.76em;}",
|
||||
"h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.30em; margin-bottom: 0.65em;}",
|
||||
"h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.09em; margin-bottom: 0.65em;}",
|
||||
"h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.76em; margin-bottom: 0.65em;}",
|
||||
"h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.76em; margin-bottom: 0.65em;}",
|
||||
".title {font-size: 2.5em; margin-top: 1.30em; margin-bottom: 0.65em;}",
|
||||
"h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.85em; margin-bottom: 0.65em;}",
|
||||
"h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 2.18em; margin-bottom: 0.65em;}",
|
||||
"h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 1.52em; margin-bottom: 0.65em;}",
|
||||
"h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 1.52em; margin-bottom: 0.65em;}",
|
||||
".title {font-size: 2.5em; margin-top: 1.85em; margin-bottom: 0.65em;}",
|
||||
".sep, .skip {text-align: center; margin-top: 1.30em; margin-bottom: 1.30em;}",
|
||||
"a {color: rgb(66, 113, 174);}",
|
||||
"mark {background: rgb(255, 255, 166);}",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||
<office:meta>
|
||||
<meta:creation-date>2024-05-22T23:05:26</meta:creation-date>
|
||||
<meta:creation-date>2024-05-24T21:31:12</meta:creation-date>
|
||||
<meta:generator>novelWriter/2.5a4</meta:generator>
|
||||
<meta:initial-creator>lipsum.com</meta:initial-creator>
|
||||
<meta:editing-cycles>45</meta:editing-cycles>
|
||||
<meta:editing-duration>P0DT0H36M8S</meta:editing-duration>
|
||||
<dc:title>Lorem Ipsum</dc:title>
|
||||
<dc:date>2024-05-22T23:05:26</dc:date>
|
||||
<dc:date>2024-05-24T21:31:12</dc:date>
|
||||
<dc:creator>lipsum.com</dc:creator>
|
||||
</office:meta>
|
||||
<office:font-face-decls>
|
||||
@@ -22,7 +22,7 @@
|
||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-weight="normal" fo:font-style="normal" fo:font-size="12pt" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.322cm" fo:margin-bottom="0.276cm" fo:keep-with-next="always" />
|
||||
<style:paragraph-properties fo:margin-top="0.645cm" fo:margin-bottom="0.276cm" fo:keep-with-next="always" />
|
||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-weight="normal" fo:font-style="normal" fo:font-size="14pt" />
|
||||
</style:style>
|
||||
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
|
||||
@@ -38,27 +38,27 @@
|
||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="12pt" fo:font-weight="normal" fo:color="#813709" loext:opacity="100%" />
|
||||
</style:style>
|
||||
<style:style style:name="Title" style:family="paragraph" style:display-name="Title" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:class="chapter">
|
||||
<style:paragraph-properties fo:margin-top="0.552cm" fo:margin-bottom="0.276cm" fo:text-align="center" />
|
||||
<style:paragraph-properties fo:margin-top="0.782cm" fo:margin-bottom="0.276cm" fo:text-align="center" />
|
||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="30pt" fo:font-weight="bold" />
|
||||
</style:style>
|
||||
<style:style style:name="Separator" style:family="paragraph" style:display-name="Separator" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.322cm" fo:line-height="150%" fo:text-align="center" />
|
||||
<style:paragraph-properties fo:margin-top="0.645cm" fo:margin-bottom="0.645cm" fo:line-height="150%" fo:text-align="center" />
|
||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="12pt" fo:font-weight="normal" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_1" style:family="paragraph" style:display-name="Heading 1" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="1" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.552cm" fo:margin-bottom="0.276cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.782cm" fo:margin-bottom="0.276cm" />
|
||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="24pt" fo:font-weight="bold" fo:color="#2a6099" loext:opacity="100%" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_2" style:family="paragraph" style:display-name="Heading 2" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="2" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.461cm" fo:margin-bottom="0.276cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.921cm" fo:margin-bottom="0.276cm" />
|
||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="19pt" fo:font-weight="bold" fo:color="#2a6099" loext:opacity="100%" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_3" style:family="paragraph" style:display-name="Heading 3" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="3" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.322cm" fo:margin-bottom="0.276cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.645cm" fo:margin-bottom="0.276cm" />
|
||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="16pt" fo:font-weight="bold" fo:color="#444444" loext:opacity="100%" />
|
||||
</style:style>
|
||||
<style:style style:name="Heading_20_4" style:family="paragraph" style:display-name="Heading 4" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="4" style:class="text">
|
||||
<style:paragraph-properties fo:margin-top="0.322cm" fo:margin-bottom="0.276cm" />
|
||||
<style:paragraph-properties fo:margin-top="0.645cm" fo:margin-bottom="0.276cm" />
|
||||
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="14pt" fo:font-weight="bold" fo:color="#444444" loext:opacity="100%" />
|
||||
</style:style>
|
||||
<style:style style:name="Header" style:family="paragraph" style:display-name="Header" style:parent-style-name="Header_20_and_20_Footer">
|
||||
|
||||
@@ -284,21 +284,6 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
|
||||
"</ol>\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 == (
|
||||
"<p>Some <b>nested bold and <i>italic</i> and "
|
||||
"<span style='text-decoration: line-through;'>strikethrough</span> "
|
||||
"text</b> here</p>\n"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToHtml_ConvertDirect(mockGUI):
|
||||
@@ -682,29 +667,3 @@ def testCoreToHtml_Format(mockGUI):
|
||||
"<a class='tag' href='#tag_Bod'>Bod</a>, "
|
||||
"<a class='tag' href='#tag_Jane'>Jane</a>"
|
||||
)
|
||||
|
||||
# Preview Mode
|
||||
# ============
|
||||
|
||||
html.setPreview(True)
|
||||
|
||||
assert html._formatSynopsis("synopsis text", True) == (
|
||||
"<p class='note'><span class='modifier'>Synopsis:</span> synopsis text</p>\n"
|
||||
)
|
||||
assert html._formatSynopsis("short text", False) == (
|
||||
"<p class='note'><span class='modifier'>Short Description:</span> short text</p>\n"
|
||||
)
|
||||
assert html._formatComments("comment text") == (
|
||||
"<p class='comment'>comment text</p>\n"
|
||||
)
|
||||
|
||||
assert html._formatKeywords("") == ("", "")
|
||||
assert html._formatKeywords("tag: Jane") == (
|
||||
"tag", "<span class='keyword'>Tag:</span> <a class='tag' name='tag_Jane'>Jane</a>"
|
||||
)
|
||||
assert html._formatKeywords("char: Bod, Jane") == (
|
||||
"char",
|
||||
"<span class='keyword'>Characters:</span> "
|
||||
"<a class='tag' href='#char=Bod'>Bod</a>, "
|
||||
"<a class='tag' href='#char=Jane'>Jane</a>"
|
||||
)
|
||||
|
||||
@@ -56,13 +56,14 @@ def testCoreToken_Setters(mockGUI):
|
||||
assert tokens._lineHeight == 1.15
|
||||
assert tokens._blockIndent == 4.0
|
||||
assert tokens._doJustify is False
|
||||
assert tokens._marginTitle == (1.000, 0.500)
|
||||
assert tokens._marginHead1 == (1.000, 0.500)
|
||||
assert tokens._marginHead2 == (0.834, 0.500)
|
||||
assert tokens._marginHead3 == (0.584, 0.500)
|
||||
assert tokens._marginHead4 == (0.584, 0.500)
|
||||
assert tokens._marginTitle == (1.417, 0.500)
|
||||
assert tokens._marginHead1 == (1.417, 0.500)
|
||||
assert tokens._marginHead2 == (1.668, 0.500)
|
||||
assert tokens._marginHead3 == (1.168, 0.500)
|
||||
assert tokens._marginHead4 == (1.168, 0.500)
|
||||
assert tokens._marginText == (0.000, 0.584)
|
||||
assert tokens._marginMeta == (0.000, 0.584)
|
||||
assert tokens._marginSep == (1.168, 1.168)
|
||||
assert tokens._hideTitle is False
|
||||
assert tokens._hideChapter is False
|
||||
assert tokens._hideUnNum is False
|
||||
@@ -93,6 +94,7 @@ def testCoreToken_Setters(mockGUI):
|
||||
tokens.setHead4Margins(2.0, 2.0)
|
||||
tokens.setTextMargins(2.0, 2.0)
|
||||
tokens.setMetaMargins(2.0, 2.0)
|
||||
tokens.setSeparatorMargins(2.0, 2.0)
|
||||
tokens.setLinkHeadings(True)
|
||||
tokens.setBodyText(False)
|
||||
tokens.setSynopsis(True)
|
||||
@@ -117,6 +119,7 @@ def testCoreToken_Setters(mockGUI):
|
||||
assert tokens._marginHead4 == (2.0, 2.0)
|
||||
assert tokens._marginText == (2.0, 2.0)
|
||||
assert tokens._marginMeta == (2.0, 2.0)
|
||||
assert tokens._marginSep == (2.0, 2.0)
|
||||
assert tokens._hideTitle is True
|
||||
assert tokens._hideChapter is True
|
||||
assert tokens._hideUnNum is True
|
||||
|
||||
@@ -520,9 +520,9 @@ def testCoreToOdt_ConvertParagraphs(mockGUI):
|
||||
assert odt.errData == []
|
||||
assert xmlToText(odt._xText) == (
|
||||
'<office:text>'
|
||||
'<text:p text:style-name="Separator" />'
|
||||
'<text:p text:style-name="Text_20_body" />'
|
||||
'<text:p text:style-name="Text_20_body">Text</text:p>'
|
||||
'<text:p text:style-name="Separator" />'
|
||||
'<text:p text:style-name="Text_20_body" />'
|
||||
'<text:p text:style-name="Text_20_body">Text</text:p>'
|
||||
'</office:text>'
|
||||
)
|
||||
|
||||
@@ -0,0 +1,571 @@
|
||||
"""
|
||||
novelWriter – ToQTextDocument Class Tester
|
||||
==========================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtGui import QColor, QTextBlock, QTextCharFormat, QTextCursor
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwUnicode
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
|
||||
from novelwriter.types import (
|
||||
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
|
||||
QtPageBreakAfter, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
|
||||
)
|
||||
|
||||
THEME = TextDocumentTheme()
|
||||
THEME.text = QColor(0, 0, 0)
|
||||
THEME.highlight = QColor(255, 255, 166)
|
||||
THEME.head = QColor(66, 113, 174)
|
||||
THEME.comment = QColor(100, 100, 100)
|
||||
THEME.note = QColor(129, 55, 9)
|
||||
THEME.code = QColor(66, 113, 174)
|
||||
THEME.modifier = QColor(129, 55, 9)
|
||||
THEME.keyword = QColor(245, 135, 31)
|
||||
THEME.tag = QColor(66, 113, 174)
|
||||
THEME.optional = QColor(66, 113, 174)
|
||||
|
||||
|
||||
def charFmtInBlock(block: QTextBlock, pos: int) -> QTextCharFormat:
|
||||
"""Get the character format at a given place in a block."""
|
||||
cursor = QTextCursor(block)
|
||||
cursor.setPosition(block.position() + pos)
|
||||
return cursor.charFormat()
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToQTextDocument_ConvertHeaders(mockGUI):
|
||||
"""Test header formats in the ToQTextDocument class."""
|
||||
project = NWProject()
|
||||
qdoc = ToQTextDocument(project)
|
||||
qdoc.initDocument(CONFIG.textFont, THEME)
|
||||
|
||||
qdoc._isNovel = True
|
||||
qdoc._isFirst = True
|
||||
qdoc._text = (
|
||||
"#! Title\n"
|
||||
"# Partition\n"
|
||||
"## Chapter\n"
|
||||
"### Scene\n"
|
||||
"#### Section\n"
|
||||
)
|
||||
qdoc.tokenizeText()
|
||||
qdoc.doConvert()
|
||||
assert qdoc.document.blockCount() == 5
|
||||
|
||||
# Title
|
||||
block = qdoc.document.findBlockByNumber(0)
|
||||
assert block.text() == "Title"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mHead[qdoc.T_TITLE][0]
|
||||
assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_TITLE][1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.fontWeight() == qdoc._bold
|
||||
assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_TITLE]
|
||||
assert cFmt.foreground().color() == THEME.text
|
||||
|
||||
# Partition
|
||||
block = qdoc.document.findBlockByNumber(1)
|
||||
assert block.text() == "Partition"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD1][0]
|
||||
assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD1][1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.fontWeight() == qdoc._bold
|
||||
assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD1]
|
||||
assert cFmt.foreground().color() == THEME.head
|
||||
|
||||
# Chapter
|
||||
block = qdoc.document.findBlockByNumber(2)
|
||||
assert block.text() == "Chapter"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD2][0]
|
||||
assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD2][1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.fontWeight() == qdoc._bold
|
||||
assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD2]
|
||||
assert cFmt.foreground().color() == THEME.head
|
||||
|
||||
# Scene
|
||||
block = qdoc.document.findBlockByNumber(3)
|
||||
assert block.text() == "Scene"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD3][0]
|
||||
assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD3][1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.fontWeight() == qdoc._bold
|
||||
assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD3]
|
||||
assert cFmt.foreground().color() == THEME.head
|
||||
|
||||
# Section
|
||||
block = qdoc.document.findBlockByNumber(4)
|
||||
assert block.text() == "Section"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD4][0]
|
||||
assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD4][1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.fontWeight() == qdoc._bold
|
||||
assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD4]
|
||||
assert cFmt.foreground().color() == THEME.head
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToQTextDocument_SeparatorSkip(mockGUI):
|
||||
"""Test separator and skip in the ToQTextDocument class."""
|
||||
project = NWProject()
|
||||
qdoc = ToQTextDocument(project)
|
||||
qdoc.initDocument(CONFIG.textFont, THEME)
|
||||
|
||||
qdoc._isNovel = True
|
||||
qdoc._isFirst = True
|
||||
qdoc._text = (
|
||||
"#! Title\n"
|
||||
"## Chapter\n"
|
||||
"### Scene 1\n"
|
||||
"Text 1\n"
|
||||
"### Scene 2\n"
|
||||
"Text 2\n"
|
||||
"#### Section\n"
|
||||
"Text 3\n"
|
||||
)
|
||||
qdoc.setSceneFormat("* * *", False)
|
||||
qdoc.setSectionFormat("", False)
|
||||
qdoc.tokenizeText()
|
||||
qdoc.doConvert()
|
||||
assert qdoc.document.blockCount() == 7
|
||||
|
||||
# 0: Title
|
||||
block = qdoc.document.findBlockByNumber(0)
|
||||
assert block.text() == "Title"
|
||||
|
||||
# 1: Chapter
|
||||
block = qdoc.document.findBlockByNumber(1)
|
||||
assert block.text() == "Chapter"
|
||||
|
||||
# Hidden: Scene 1
|
||||
|
||||
# 2: Text 1
|
||||
block = qdoc.document.findBlockByNumber(2)
|
||||
assert block.text() == "Text 1"
|
||||
|
||||
# 3: Scene 2
|
||||
block = qdoc.document.findBlockByNumber(3)
|
||||
assert block.text() == "* * *"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mSep[0]
|
||||
assert bFmt.bottomMargin() == qdoc._mSep[1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.foreground().color() == THEME.text
|
||||
|
||||
# 4: Text 2
|
||||
block = qdoc.document.findBlockByNumber(4)
|
||||
assert block.text() == "Text 2"
|
||||
|
||||
# 5: Section
|
||||
block = qdoc.document.findBlockByNumber(5)
|
||||
assert block.text() == nwUnicode.U_NBSP
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mText[0]
|
||||
assert bFmt.bottomMargin() == qdoc._mText[1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.foreground().color() == THEME.text
|
||||
|
||||
# 6: Text 3
|
||||
block = qdoc.document.findBlockByNumber(6)
|
||||
assert block.text() == "Text 3"
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToQTextDocument_NovelMeta(mockGUI):
|
||||
"""Test novel meta formats in the ToQTextDocument class."""
|
||||
project = NWProject()
|
||||
qdoc = ToQTextDocument(project)
|
||||
qdoc.initDocument(CONFIG.textFont, THEME)
|
||||
|
||||
qdoc._isNovel = True
|
||||
qdoc._isFirst = True
|
||||
qdoc.setComments(True)
|
||||
qdoc.setSynopsis(True)
|
||||
qdoc.setKeywords(True)
|
||||
qdoc._text = (
|
||||
"### Scene\n\n"
|
||||
"@pov: Jane\n"
|
||||
"@char: John, Bob\n\n"
|
||||
"%Synopsis: Stuff that happened\n\n"
|
||||
"% A regular comment\n\n"
|
||||
"Text\n\n"
|
||||
)
|
||||
qdoc.tokenizeText()
|
||||
qdoc.doConvert()
|
||||
assert qdoc.document.blockCount() == 6
|
||||
|
||||
# 0: Scene
|
||||
block = qdoc.document.findBlockByNumber(0)
|
||||
assert block.text() == "Scene"
|
||||
|
||||
# 1: Jane
|
||||
block = qdoc.document.findBlockByNumber(1)
|
||||
assert block.text() == "Point of View: Jane"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mMeta[0]
|
||||
assert bFmt.bottomMargin() == 0.0
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.foreground().color() == THEME.keyword
|
||||
cFmt = charFmtInBlock(block, 16)
|
||||
assert cFmt.foreground().color() == THEME.tag
|
||||
|
||||
# 2: John, Bob
|
||||
block = qdoc.document.findBlockByNumber(2)
|
||||
assert block.text() == "Characters: John, Bob"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == 0.0
|
||||
assert bFmt.bottomMargin() == qdoc._mMeta[1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.foreground().color() == THEME.keyword
|
||||
cFmt = charFmtInBlock(block, 13)
|
||||
assert cFmt.foreground().color() == THEME.tag
|
||||
|
||||
# 3: Synopsis
|
||||
block = qdoc.document.findBlockByNumber(3)
|
||||
assert block.text() == "Synopsis: Stuff that happened"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mText[0]
|
||||
assert bFmt.bottomMargin() == qdoc._mText[1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.foreground().color() == THEME.modifier
|
||||
cFmt = charFmtInBlock(block, 11)
|
||||
assert cFmt.foreground().color() == THEME.note
|
||||
|
||||
# 4: Comment
|
||||
block = qdoc.document.findBlockByNumber(4)
|
||||
assert block.text() == "Comment: A regular comment"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mText[0]
|
||||
assert bFmt.bottomMargin() == qdoc._mText[1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.foreground().color() == THEME.comment
|
||||
cFmt = charFmtInBlock(block, 10)
|
||||
assert cFmt.foreground().color() == THEME.comment
|
||||
|
||||
# 5: Text
|
||||
block = qdoc.document.findBlockByNumber(5)
|
||||
assert block.text() == "Text"
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToQTextDocument_NoteMeta(mockGUI):
|
||||
"""Test note meta formats in the ToQTextDocument class."""
|
||||
project = NWProject()
|
||||
qdoc = ToQTextDocument(project)
|
||||
qdoc.initDocument(CONFIG.textFont, THEME)
|
||||
|
||||
qdoc._isNovel = False
|
||||
qdoc._isFirst = True
|
||||
qdoc.setComments(True)
|
||||
qdoc.setSynopsis(True)
|
||||
qdoc.setKeywords(True)
|
||||
qdoc._text = (
|
||||
"# Jane Smith\n\n"
|
||||
"@tag: Jane | Jane Smith\n"
|
||||
"%Short: All about Jane\n\n"
|
||||
"Text\n\n"
|
||||
)
|
||||
qdoc.tokenizeText()
|
||||
qdoc.doConvert()
|
||||
assert qdoc.document.blockCount() == 4
|
||||
|
||||
# 0: Title
|
||||
block = qdoc.document.findBlockByNumber(0)
|
||||
assert block.text() == "Jane Smith"
|
||||
|
||||
# 1: Tag
|
||||
block = qdoc.document.findBlockByNumber(1)
|
||||
assert block.text() == "Tag: Jane | Jane Smith"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mMeta[0]
|
||||
assert bFmt.bottomMargin() == qdoc._mMeta[1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.foreground().color() == THEME.keyword
|
||||
cFmt = charFmtInBlock(block, 6)
|
||||
assert cFmt.foreground().color() == THEME.tag
|
||||
cFmt = charFmtInBlock(block, 11)
|
||||
assert cFmt.foreground().color() == THEME.text
|
||||
cFmt = charFmtInBlock(block, 13)
|
||||
assert cFmt.foreground().color() == THEME.optional
|
||||
|
||||
# 2: Short
|
||||
block = qdoc.document.findBlockByNumber(2)
|
||||
assert block.text() == "Short Description: All about Jane"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.topMargin() == qdoc._mText[0]
|
||||
assert bFmt.bottomMargin() == qdoc._mText[1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.foreground().color() == THEME.modifier
|
||||
cFmt = charFmtInBlock(block, 20)
|
||||
assert cFmt.foreground().color() == THEME.note
|
||||
|
||||
# 3: Text
|
||||
block = qdoc.document.findBlockByNumber(3)
|
||||
assert block.text() == "Text"
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToQTextDocument_TextBlockFormats(mockGUI):
|
||||
"""Test text block formats in the ToQTextDocument class."""
|
||||
project = NWProject()
|
||||
qdoc = ToQTextDocument(project)
|
||||
qdoc.initDocument(CONFIG.textFont, THEME)
|
||||
|
||||
qdoc._isNovel = True
|
||||
qdoc._isFirst = True
|
||||
|
||||
# Alignment & Indent
|
||||
# ==================
|
||||
qdoc.document.clear()
|
||||
|
||||
qdoc._text = (
|
||||
"### Scene\n\n"
|
||||
"Left <<\n\n"
|
||||
">> Center <<\n\n"
|
||||
">> Right\n\n"
|
||||
"> Left Indent\n\n"
|
||||
"Right Indent <\n\n"
|
||||
"> Double Indent <\n\n"
|
||||
)
|
||||
qdoc.tokenizeText()
|
||||
qdoc.doConvert()
|
||||
assert qdoc.document.blockCount() == 7
|
||||
|
||||
# 0: Scene
|
||||
block = qdoc.document.findBlockByNumber(0)
|
||||
assert block.text() == "Scene"
|
||||
|
||||
# 1: Left
|
||||
block = qdoc.document.findBlockByNumber(1)
|
||||
assert block.text() == "Left"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.alignment() == QtAlignLeft
|
||||
|
||||
# 2: Center
|
||||
block = qdoc.document.findBlockByNumber(2)
|
||||
assert block.text() == "Center"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.alignment() == QtAlignCenter
|
||||
|
||||
# 3: Right
|
||||
block = qdoc.document.findBlockByNumber(3)
|
||||
assert block.text() == "Right"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.alignment() == QtAlignRight
|
||||
|
||||
# 4: Left Indent
|
||||
block = qdoc.document.findBlockByNumber(4)
|
||||
assert block.text() == "Left Indent"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.alignment() == QtAlignAbsolute
|
||||
assert bFmt.leftMargin() == qdoc._mIndent
|
||||
assert bFmt.rightMargin() == 0.0
|
||||
|
||||
# 5: Right Indent
|
||||
block = qdoc.document.findBlockByNumber(5)
|
||||
assert block.text() == "Right Indent"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.alignment() == QtAlignAbsolute
|
||||
assert bFmt.leftMargin() == 0.0
|
||||
assert bFmt.rightMargin() == qdoc._mIndent
|
||||
|
||||
# 6: Double Indent
|
||||
block = qdoc.document.findBlockByNumber(6)
|
||||
assert block.text() == "Double Indent"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.alignment() == QtAlignAbsolute
|
||||
assert bFmt.leftMargin() == qdoc._mIndent
|
||||
assert bFmt.rightMargin() == qdoc._mIndent
|
||||
|
||||
# Unreachable
|
||||
# ===========
|
||||
# Some formatting markers are currently not reachable
|
||||
qdoc.document.clear()
|
||||
|
||||
qdoc._tokens = [
|
||||
(qdoc.T_TEXT, 1, "This is justified", [], qdoc.A_JUSTIFY),
|
||||
(qdoc.T_TEXT, 1, "This has a page break", [], qdoc.A_PBA),
|
||||
]
|
||||
qdoc.doConvert()
|
||||
assert qdoc.document.blockCount() == 2
|
||||
|
||||
# 0: Justify
|
||||
block = qdoc.document.findBlockByNumber(0)
|
||||
assert block.text() == "This is justified"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.alignment() == QtAlignJustify
|
||||
|
||||
# 1: Page Break After
|
||||
block = qdoc.document.findBlockByNumber(1)
|
||||
assert block.text() == "This has a page break"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.pageBreakPolicy() == QtPageBreakAfter
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToQTextDocument_TextCharFormats(mockGUI):
|
||||
"""Test text char formats in the ToQTextDocument class."""
|
||||
project = NWProject()
|
||||
qdoc = ToQTextDocument(project)
|
||||
|
||||
# Convert before init
|
||||
qdoc._text = "Blabla"
|
||||
qdoc.doConvert()
|
||||
qdoc.tokenizeText()
|
||||
assert qdoc.document.toPlainText() == ""
|
||||
|
||||
# Init
|
||||
qdoc.initDocument(CONFIG.textFont, THEME)
|
||||
|
||||
qdoc._isNovel = True
|
||||
qdoc._isFirst = True
|
||||
|
||||
qdoc._text = (
|
||||
"### Scene\n\n"
|
||||
"With [b]bold[/b] text\n\n"
|
||||
"With [i]italic[/i] text\n\n"
|
||||
"With [s]deleted[/s] text\n\n"
|
||||
"With [u]underlined[/u] text\n\n"
|
||||
"With [m]highlighted[/m] text\n\n"
|
||||
"With super[sup]script[/sup] text\n\n"
|
||||
"With sub[sub]script[/sub] text\n\n"
|
||||
)
|
||||
qdoc.tokenizeText()
|
||||
qdoc.doConvert()
|
||||
assert qdoc.document.blockCount() == 8
|
||||
|
||||
# 0: Scene
|
||||
block = qdoc.document.findBlockByNumber(0)
|
||||
assert block.text() == "Scene"
|
||||
|
||||
# 1: Bold
|
||||
block = qdoc.document.findBlockByNumber(1)
|
||||
assert block.text() == "With bold text"
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.fontWeight() == qdoc._normal
|
||||
cFmt = charFmtInBlock(block, 6)
|
||||
assert cFmt.fontWeight() == qdoc._bold
|
||||
cFmt = charFmtInBlock(block, 10)
|
||||
assert cFmt.fontWeight() == qdoc._normal
|
||||
|
||||
# 2: Italic
|
||||
block = qdoc.document.findBlockByNumber(2)
|
||||
assert block.text() == "With italic text"
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.fontItalic() is False
|
||||
cFmt = charFmtInBlock(block, 6)
|
||||
assert cFmt.fontItalic() is True
|
||||
cFmt = charFmtInBlock(block, 12)
|
||||
assert cFmt.fontItalic() is False
|
||||
|
||||
# 3: Deleted
|
||||
block = qdoc.document.findBlockByNumber(3)
|
||||
assert block.text() == "With deleted text"
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.fontStrikeOut() is False
|
||||
cFmt = charFmtInBlock(block, 6)
|
||||
assert cFmt.fontStrikeOut() is True
|
||||
cFmt = charFmtInBlock(block, 13)
|
||||
assert cFmt.fontStrikeOut() is False
|
||||
|
||||
# 4: Underlined
|
||||
block = qdoc.document.findBlockByNumber(4)
|
||||
assert block.text() == "With underlined text"
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.fontUnderline() is False
|
||||
cFmt = charFmtInBlock(block, 6)
|
||||
assert cFmt.fontUnderline() is True
|
||||
cFmt = charFmtInBlock(block, 16)
|
||||
assert cFmt.fontUnderline() is False
|
||||
|
||||
# 5: Highlighted
|
||||
block = qdoc.document.findBlockByNumber(5)
|
||||
assert block.text() == "With highlighted text"
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.background() == QtTransparent
|
||||
cFmt = charFmtInBlock(block, 6)
|
||||
assert cFmt.background() == THEME.highlight
|
||||
cFmt = charFmtInBlock(block, 17)
|
||||
assert cFmt.background() == QtTransparent
|
||||
|
||||
# 6: Superscript
|
||||
block = qdoc.document.findBlockByNumber(6)
|
||||
assert block.text() == "With superscript text"
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.verticalAlignment() == QtVAlignNormal
|
||||
cFmt = charFmtInBlock(block, 11)
|
||||
assert cFmt.verticalAlignment() == QtVAlignSuper
|
||||
cFmt = charFmtInBlock(block, 17)
|
||||
assert cFmt.verticalAlignment() == QtVAlignNormal
|
||||
|
||||
# 7: Subscript
|
||||
block = qdoc.document.findBlockByNumber(7)
|
||||
assert block.text() == "With subscript text"
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.verticalAlignment() == QtVAlignNormal
|
||||
cFmt = charFmtInBlock(block, 9)
|
||||
assert cFmt.verticalAlignment() == QtVAlignSub
|
||||
cFmt = charFmtInBlock(block, 15)
|
||||
assert cFmt.verticalAlignment() == QtVAlignNormal
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToQTextDocument_Footnotes(mockGUI):
|
||||
"""Test footnotes in the ToQTextDocument class."""
|
||||
project = NWProject()
|
||||
qdoc = ToQTextDocument(project)
|
||||
qdoc.initDocument(CONFIG.textFont, THEME)
|
||||
|
||||
qdoc._isNovel = True
|
||||
qdoc._isFirst = True
|
||||
|
||||
qdoc._text = (
|
||||
"### Scene\n\n"
|
||||
"Text with valid[footnote:fn1] and invalid[footnote:fn2] footnotes.\n\n"
|
||||
"%Footnote.fn1: Here's the first note.\n\n"
|
||||
)
|
||||
qdoc.tokenizeText()
|
||||
qdoc.doConvert()
|
||||
qdoc.appendFootnotes()
|
||||
assert qdoc.document.blockCount() == 4
|
||||
|
||||
# 0: Scene
|
||||
block = qdoc.document.findBlockByNumber(0)
|
||||
assert block.text() == "Scene"
|
||||
|
||||
# 1: Text
|
||||
block = qdoc.document.findBlockByNumber(1)
|
||||
assert block.text() == "Text with valid[1] and invalid[ERR] footnotes."
|
||||
|
||||
# 2: Footnotes
|
||||
block = qdoc.document.findBlockByNumber(2)
|
||||
assert block.text() == "Footnotes"
|
||||
|
||||
# 3: Footnote 1
|
||||
block = qdoc.document.findBlockByNumber(3)
|
||||
assert block.text() == "1. Here's the first note."
|
||||
@@ -27,7 +27,7 @@ from PyQt5.QtGui import QMouseEvent, QTextCursor
|
||||
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
|
||||
@@ -157,7 +157,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
docViewer.setTextCursor(cursor)
|
||||
docViewer._makeSelection(QTextCursor.WordUnderCursor)
|
||||
rect = docViewer.cursorRect()
|
||||
docViewer._linkClicked(QUrl("#char=Bod"))
|
||||
docViewer._linkClicked(QUrl("#tag_bod"))
|
||||
assert docViewer.docHandle == "4c4f28287af27"
|
||||
|
||||
# Click mouse nav buttons
|
||||
@@ -196,19 +196,19 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
|
||||
# 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()) == 683
|
||||
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."
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ from tests.tools import buildTestProject
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testManuscriptBuild_Main(
|
||||
def testToolManuscriptBuild_Main(
|
||||
monkeypatch, qtbot: QtBot, nwGUI: GuiMain, fncPath: Path, projPath: Path, mockRnd
|
||||
):
|
||||
"""Test the GuiManuscriptBuild dialog."""
|
||||
|
||||
@@ -28,20 +28,19 @@ from PyQt5.QtCore import pyqtSlot
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
|
||||
def testToolManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
|
||||
"""Test the init/main functionality of the GuiManuscript dialog."""
|
||||
buildTestProject(nwGUI, projPath)
|
||||
nwGUI.openProject(projPath)
|
||||
@@ -55,38 +54,27 @@ def testManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
|
||||
manus.show()
|
||||
assert manus.docPreview.toPlainText().strip() == ""
|
||||
|
||||
# Run the default build
|
||||
# First load should have create a default build
|
||||
assert manus.buildList.count() == 1
|
||||
|
||||
# Loading again should not add a new build
|
||||
manus.loadContent()
|
||||
assert manus.buildList.count() == 1
|
||||
|
||||
# Build a preview
|
||||
manus.buildList.clearSelection()
|
||||
manus.buildList.setCurrentRow(0)
|
||||
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
|
||||
manus.btnPreview.click()
|
||||
assert manus.docPreview.toPlainText().strip() == allText
|
||||
|
||||
manus.close()
|
||||
|
||||
# A new dialog should load the old build
|
||||
manus = GuiManuscript(nwGUI)
|
||||
manus.show()
|
||||
manus.loadContent()
|
||||
assert manus.docPreview.toPlainText().strip() == allText
|
||||
manus.close()
|
||||
|
||||
# But blocking the reload should leave it empty
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", lambda *a, **k: causeOSError)
|
||||
manus = GuiManuscript(nwGUI)
|
||||
manus.show()
|
||||
manus.loadContent()
|
||||
assert manus.docPreview.toPlainText().strip() == ""
|
||||
|
||||
nwGUI.closeProject() # This should auto-close the manuscript tool
|
||||
assert manus.isHidden()
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testManuscript_Builds(qtbot, nwGUI, projPath):
|
||||
def testToolManuscript_Builds(qtbot, nwGUI, projPath):
|
||||
"""Test the handling of builds in the GuiManuscript dialog."""
|
||||
buildTestProject(nwGUI, projPath)
|
||||
nwGUI.openProject(projPath)
|
||||
@@ -157,7 +145,7 @@ def testManuscript_Builds(qtbot, nwGUI, projPath):
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
|
||||
def testToolManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
|
||||
"""Test other features of the GuiManuscript dialog."""
|
||||
buildTestProject(nwGUI, projPath)
|
||||
nwGUI.openProject(projPath)
|
||||
@@ -186,7 +174,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 +186,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 +197,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 +242,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()
|
||||
|
||||
@@ -299,7 +269,7 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
|
||||
|
||||
@pytest.mark.gui
|
||||
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
|
||||
def testManuscript_Print(monkeypatch, qtbot, nwGUI, projPath):
|
||||
def testToolManuscript_Print(monkeypatch, qtbot, nwGUI, projPath):
|
||||
"""Test the print feature of the GuiManuscript dialog."""
|
||||
buildTestProject(nwGUI, projPath)
|
||||
nwGUI.openProject(projPath)
|
||||
|
||||
@@ -40,7 +40,7 @@ from tests.tools import C, buildTestProject
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
|
||||
def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
|
||||
"""Test the initialisation of the GuiBuildSettings dialog."""
|
||||
buildTestProject(nwGUI, projPath)
|
||||
nwGUI.openProject(projPath)
|
||||
@@ -110,7 +110,7 @@ def testBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
|
||||
def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
|
||||
"""Test the Filter Tab of the GuiBuildSettings dialog."""
|
||||
buildTestProject(nwGUI, projPath)
|
||||
nwGUI.openProject(projPath)
|
||||
@@ -314,7 +314,7 @@ def testBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testBuildSettings_Headings(qtbot, nwGUI):
|
||||
def testToolBuildSettings_Headings(qtbot, nwGUI):
|
||||
"""Test the Headings Tab of the GuiBuildSettings dialog."""
|
||||
build = BuildSettings()
|
||||
|
||||
@@ -484,7 +484,7 @@ def testBuildSettings_Headings(qtbot, nwGUI):
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testBuildSettings_Content(qtbot, nwGUI):
|
||||
def testToolBuildSettings_Content(qtbot, nwGUI):
|
||||
"""Test the Content Tab of the GuiBuildSettings dialog."""
|
||||
build = BuildSettings()
|
||||
|
||||
@@ -544,7 +544,7 @@ def testBuildSettings_Content(qtbot, nwGUI):
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testBuildSettings_Format(monkeypatch, qtbot, nwGUI):
|
||||
def testToolBuildSettings_Format(monkeypatch, qtbot, nwGUI):
|
||||
"""Test the Format Tab of the GuiBuildSettings dialog."""
|
||||
build = BuildSettings()
|
||||
|
||||
@@ -650,7 +650,7 @@ def testBuildSettings_Format(monkeypatch, qtbot, nwGUI):
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testBuildSettings_Output(qtbot, nwGUI):
|
||||
def testToolBuildSettings_Output(qtbot, nwGUI):
|
||||
"""Test the Output Tab of the GuiBuildSettings dialog."""
|
||||
build = BuildSettings()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user