Make page breaks visible in manuscript preview (#2086)
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"Objects": "Objects",
|
||||
"Entities": "Entities",
|
||||
"Custom": "Custom",
|
||||
"New Page": "New Page",
|
||||
"0": "Zero",
|
||||
"1": "One",
|
||||
"2": "Two",
|
||||
|
||||
@@ -39,7 +39,7 @@ from PyQt5.QtCore import QCoreApplication
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import isHandle, minmax, simplified
|
||||
from novelwriter.constants import nwConst, nwFiles, nwItemClass
|
||||
from novelwriter.constants import nwConst, nwFiles, nwItemClass, nwStats
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.storage import NWStorageCreate
|
||||
@@ -428,7 +428,6 @@ class ProjectBuilder:
|
||||
|
||||
lblNewProject = self.tr("New Project")
|
||||
lblTitlePage = self.tr("Title Page")
|
||||
lblByAuthors = self.tr("By")
|
||||
|
||||
# Settings
|
||||
project.data.setUuid(None)
|
||||
@@ -441,14 +440,29 @@ class ProjectBuilder:
|
||||
# Add Root Folders
|
||||
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
|
||||
hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
|
||||
novelTitle = project.data.name
|
||||
|
||||
titlePage = f"#! {novelTitle}\n\n"
|
||||
if project.data.author:
|
||||
titlePage += f">> {lblByAuthors} {project.data.author} <<\n\n"
|
||||
|
||||
# Generate Title Page
|
||||
aDoc = project.storage.getDocument(hTitlePage)
|
||||
aDoc.writeDocument(titlePage)
|
||||
aDoc.writeDocument((
|
||||
"{author}[br]\n"
|
||||
"{address} 1[br]\n"
|
||||
"{address} 2 <<\n"
|
||||
"\n"
|
||||
"[vspace:5]\n"
|
||||
"\n"
|
||||
"#! {title}\n"
|
||||
"\n"
|
||||
">> **{by} {author}** <<\n"
|
||||
"\n"
|
||||
">> {count}: [field:{field}] <<\n"
|
||||
).format(
|
||||
author=project.data.author or "None",
|
||||
address=self.tr("Address"),
|
||||
title=project.data.name or "None",
|
||||
by=self.tr("By"),
|
||||
count=self.tr("Word Count"),
|
||||
field=nwStats.WORDS_TEXT,
|
||||
))
|
||||
|
||||
# Create a project structure based on selected root folders
|
||||
# and a number of chapters and scenes selected in the
|
||||
|
||||
@@ -87,15 +87,6 @@ class NWBuildDocument:
|
||||
"""
|
||||
return self._cache
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setBuildOutline(self, state: bool) -> None:
|
||||
"""Turn on/off outline for builds."""
|
||||
self._outline = state
|
||||
return
|
||||
|
||||
##
|
||||
# Special Methods
|
||||
##
|
||||
@@ -122,11 +113,12 @@ class NWBuildDocument:
|
||||
self._queue.append(item.itemHandle)
|
||||
return
|
||||
|
||||
def iterBuildPreview(self) -> Iterable[tuple[int, bool]]:
|
||||
def iterBuildPreview(self, newPage: bool) -> Iterable[tuple[int, bool]]:
|
||||
"""Build a preview QTextDocument."""
|
||||
makeObj = ToQTextDocument(self._project)
|
||||
filtered = self._setupBuild(makeObj)
|
||||
makeObj.initDocument()
|
||||
makeObj.setShowNewPage(newPage)
|
||||
self._outline = True
|
||||
yield from self._iterBuild(makeObj, filtered)
|
||||
makeObj.closeDocument()
|
||||
@@ -352,14 +344,17 @@ class NWBuildDocument:
|
||||
tItem = self._project.tree[tHandle]
|
||||
if isinstance(tItem, NWItem):
|
||||
try:
|
||||
if tItem.isRootType() and not tItem.isNovelLike():
|
||||
bldObj.addRootHeading(tHandle)
|
||||
if convert:
|
||||
bldObj.doConvert()
|
||||
if self._count:
|
||||
bldObj.countStats()
|
||||
if self._outline:
|
||||
bldObj.buildOutline()
|
||||
if tItem.isRootType():
|
||||
if tItem.isNovelLike():
|
||||
bldObj.setBreakNext()
|
||||
else:
|
||||
bldObj.addRootHeading(tHandle)
|
||||
if convert:
|
||||
bldObj.doConvert()
|
||||
if self._count:
|
||||
bldObj.countStats()
|
||||
if self._outline:
|
||||
bldObj.buildOutline()
|
||||
elif tItem.isFileType():
|
||||
bldObj.setText(tHandle)
|
||||
bldObj.doPreProcessing()
|
||||
@@ -370,8 +365,6 @@ class NWBuildDocument:
|
||||
bldObj.buildOutline()
|
||||
if convert:
|
||||
bldObj.doConvert()
|
||||
else:
|
||||
logger.info(f"Build: Skipping '{tHandle}'")
|
||||
|
||||
except Exception:
|
||||
self._error = f"Build: Failed to build '{tHandle}'"
|
||||
|
||||
@@ -60,7 +60,7 @@ VALID_MAP: dict[str, set[str]] = {
|
||||
},
|
||||
"GuiManuscript": {
|
||||
"winWidth", "winHeight", "optsWidth", "viewWidth", "listHeight",
|
||||
"detailsHeight", "detailsWidth", "detailsExpanded",
|
||||
"detailsHeight", "detailsWidth", "detailsExpanded", "showNewPage",
|
||||
},
|
||||
"GuiManuscriptBuild": {
|
||||
"winWidth", "winHeight", "fmtWidth", "sumWidth",
|
||||
|
||||
@@ -236,32 +236,31 @@ class ToDocX(Tokenizer):
|
||||
self._pars.append(par)
|
||||
|
||||
# Styles
|
||||
if tStyle is not None:
|
||||
if tStyle & BlockFmt.LEFT:
|
||||
par.setAlignment("left")
|
||||
elif tStyle & BlockFmt.RIGHT:
|
||||
par.setAlignment("right")
|
||||
elif tStyle & BlockFmt.CENTRE:
|
||||
par.setAlignment("center")
|
||||
elif tStyle & BlockFmt.JUSTIFY:
|
||||
par.setAlignment("both")
|
||||
if tStyle & BlockFmt.LEFT:
|
||||
par.setAlignment("left")
|
||||
elif tStyle & BlockFmt.RIGHT:
|
||||
par.setAlignment("right")
|
||||
elif tStyle & BlockFmt.CENTRE:
|
||||
par.setAlignment("center")
|
||||
elif tStyle & BlockFmt.JUSTIFY:
|
||||
par.setAlignment("both")
|
||||
|
||||
if tStyle & BlockFmt.PBB:
|
||||
par.setPageBreakBefore(True)
|
||||
if tStyle & BlockFmt.PBA:
|
||||
par.setPageBreakAfter(True)
|
||||
if tStyle & BlockFmt.PBB:
|
||||
par.setPageBreakBefore(True)
|
||||
if tStyle & BlockFmt.PBA:
|
||||
par.setPageBreakAfter(True)
|
||||
|
||||
if tStyle & BlockFmt.Z_BTM:
|
||||
par.setMarginBottom(0.0)
|
||||
if tStyle & BlockFmt.Z_TOP:
|
||||
par.setMarginTop(0.0)
|
||||
if tStyle & BlockFmt.Z_BTM:
|
||||
par.setMarginBottom(0.0)
|
||||
if tStyle & BlockFmt.Z_TOP:
|
||||
par.setMarginTop(0.0)
|
||||
|
||||
if tStyle & BlockFmt.IND_T:
|
||||
par.setIndentFirst(True)
|
||||
if tStyle & BlockFmt.IND_L:
|
||||
par.setMarginLeft(bIndent)
|
||||
if tStyle & BlockFmt.IND_R:
|
||||
par.setMarginRight(bIndent)
|
||||
if tStyle & BlockFmt.IND_T:
|
||||
par.setIndentFirst(True)
|
||||
if tStyle & BlockFmt.IND_L:
|
||||
par.setMarginLeft(bIndent)
|
||||
if tStyle & BlockFmt.IND_R:
|
||||
par.setMarginRight(bIndent)
|
||||
|
||||
# Process Text Types
|
||||
if tType == BlockTyp.TEXT:
|
||||
|
||||
@@ -171,7 +171,7 @@ class ToHtml(Tokenizer):
|
||||
|
||||
# Styles
|
||||
aStyle = []
|
||||
if tStyle is not None and self._cssStyles:
|
||||
if self._cssStyles:
|
||||
if tStyle & BlockFmt.LEFT:
|
||||
aStyle.append("text-align: left;")
|
||||
elif tStyle & BlockFmt.RIGHT:
|
||||
|
||||
@@ -166,6 +166,7 @@ class Tokenizer(ABC):
|
||||
self._hFormatter = HeadingFormatter(self._project)
|
||||
self._noSep = True # Flag to indicate that we don't want a scene separator
|
||||
self._noIndent = False # Flag to disable text indent on next paragraph
|
||||
self._breakNext = False # Add a page break on next token
|
||||
|
||||
# This File
|
||||
self._isNovel = False # Document is a novel document
|
||||
@@ -444,6 +445,11 @@ class Tokenizer(ABC):
|
||||
self._classes["optional"] = self._theme.optional
|
||||
return
|
||||
|
||||
def setBreakNext(self) -> None:
|
||||
"""Set a page break for next block."""
|
||||
self._breakNext = True
|
||||
return
|
||||
|
||||
def addRootHeading(self, tHandle: str) -> None:
|
||||
"""Add a heading at the start of a new root folder."""
|
||||
self._text = ""
|
||||
@@ -531,7 +537,6 @@ class Tokenizer(ABC):
|
||||
text = REGEX_PATTERNS.lineBreak.sub("\uffff", self._text)
|
||||
|
||||
nHead = 0
|
||||
breakNext = False
|
||||
rawText = []
|
||||
tHandle = self._handle or ""
|
||||
tBlocks: list[T_Block] = [B_EMPTY]
|
||||
@@ -546,11 +551,11 @@ class Tokenizer(ABC):
|
||||
rawText.append("\n")
|
||||
continue
|
||||
|
||||
if breakNext:
|
||||
sAlign = BlockFmt.PBB
|
||||
breakNext = False
|
||||
if self._breakNext:
|
||||
tStyle = BlockFmt.PBB
|
||||
self._breakNext = False
|
||||
else:
|
||||
sAlign = BlockFmt.NONE
|
||||
tStyle = BlockFmt.NONE
|
||||
|
||||
# Check Line Format
|
||||
# =================
|
||||
@@ -563,12 +568,12 @@ class Tokenizer(ABC):
|
||||
# therefore proceed to check other formats.
|
||||
|
||||
if sLine in ("[newpage]", "[new page]"):
|
||||
breakNext = True
|
||||
self._breakNext = True
|
||||
continue
|
||||
|
||||
elif sLine == "[vspace]":
|
||||
tBlocks.append(
|
||||
(BlockTyp.SKIP, "", "", [], sAlign)
|
||||
(BlockTyp.SKIP, "", "", [], tStyle)
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -576,7 +581,7 @@ class Tokenizer(ABC):
|
||||
nSkip = checkInt(sLine[8:-1], 0)
|
||||
if nSkip >= 1:
|
||||
tBlocks.append(
|
||||
(BlockTyp.SKIP, "", "", [], sAlign)
|
||||
(BlockTyp.SKIP, "", "", [], tStyle)
|
||||
)
|
||||
if nSkip > 1:
|
||||
tBlocks += (nSkip - 1) * [
|
||||
@@ -599,14 +604,14 @@ class Tokenizer(ABC):
|
||||
if cStyle == nwComment.PLAIN and not self._doComments:
|
||||
continue
|
||||
|
||||
if doJustify and not sAlign & BlockFmt.ALIGNED:
|
||||
sAlign |= BlockFmt.JUSTIFY
|
||||
if doJustify and not tStyle & BlockFmt.ALIGNED:
|
||||
tStyle |= BlockFmt.JUSTIFY
|
||||
|
||||
if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT, nwComment.PLAIN):
|
||||
bStyle = COMMENT_STYLE[cStyle]
|
||||
tLine, tFmt = self._formatComment(bStyle, cKey, cText)
|
||||
tBlocks.append((
|
||||
BlockTyp.COMMENT, "", tLine, tFmt, sAlign
|
||||
BlockTyp.COMMENT, "", tLine, tFmt, tStyle
|
||||
))
|
||||
if keepRaw:
|
||||
rawText.append(f"{aLine}\n")
|
||||
@@ -627,7 +632,7 @@ class Tokenizer(ABC):
|
||||
tTag, tLine, tFmt = self._formatMeta(aLine)
|
||||
if tLine:
|
||||
tBlocks.append((
|
||||
BlockTyp.KEYWORD, tTag[1:], tLine, tFmt, sAlign
|
||||
BlockTyp.KEYWORD, tTag[1:], tLine, tFmt, tStyle
|
||||
))
|
||||
if keepRaw:
|
||||
rawText.append(f"{aLine}\n")
|
||||
@@ -646,16 +651,16 @@ class Tokenizer(ABC):
|
||||
nHead += 1
|
||||
tText = aLine[2:].strip()
|
||||
tType = BlockTyp.HEAD1 if isPlain else BlockTyp.TITLE
|
||||
tStyle = BlockFmt.NONE if isPlain else self._titleStyle
|
||||
sHide = self._hidePart if isPlain else False
|
||||
if not (isPlain or isNovel and sHide):
|
||||
tStyle |= self._titleStyle
|
||||
if isNovel:
|
||||
if sHide:
|
||||
tText = ""
|
||||
tType = BlockTyp.EMPTY
|
||||
tStyle = BlockFmt.NONE
|
||||
elif isPlain:
|
||||
tText = self._hFormatter.apply(self._fmtPart, tText, nHead)
|
||||
tStyle = self._partStyle
|
||||
tStyle |= self._partStyle
|
||||
if isPlain:
|
||||
self._hFormatter.resetScene()
|
||||
else:
|
||||
@@ -682,7 +687,6 @@ class Tokenizer(ABC):
|
||||
nHead += 1
|
||||
tText = aLine[3:].strip()
|
||||
tType = BlockTyp.HEAD2
|
||||
tStyle = BlockFmt.NONE
|
||||
sHide = self._hideChapter if isPlain else self._hideUnNum
|
||||
tFormat = self._fmtChapter if isPlain else self._fmtUnNum
|
||||
if isNovel:
|
||||
@@ -693,7 +697,7 @@ class Tokenizer(ABC):
|
||||
tType = BlockTyp.EMPTY
|
||||
else:
|
||||
tText = self._hFormatter.apply(tFormat, tText, nHead)
|
||||
tStyle = self._chapterStyle
|
||||
tStyle |= self._chapterStyle
|
||||
self._hFormatter.resetScene()
|
||||
self._noSep = True
|
||||
|
||||
@@ -719,7 +723,6 @@ class Tokenizer(ABC):
|
||||
nHead += 1
|
||||
tText = aLine[4:].strip()
|
||||
tType = BlockTyp.HEAD3
|
||||
tStyle = BlockFmt.NONE
|
||||
sHide = self._hideScene if isPlain else self._hideHScene
|
||||
tFormat = self._fmtScene if isPlain else self._fmtHScene
|
||||
if isNovel:
|
||||
@@ -729,13 +732,13 @@ class Tokenizer(ABC):
|
||||
tType = BlockTyp.EMPTY
|
||||
else:
|
||||
tText = self._hFormatter.apply(tFormat, tText, nHead)
|
||||
tStyle = self._sceneStyle
|
||||
tStyle |= self._sceneStyle
|
||||
if tText == "": # Empty Format
|
||||
tType = BlockTyp.EMPTY if self._noSep else BlockTyp.SKIP
|
||||
elif tText == tFormat: # Static Format
|
||||
tText = "" if self._noSep else tText
|
||||
tType = BlockTyp.EMPTY if self._noSep else BlockTyp.SEP
|
||||
tStyle = BlockFmt.NONE if self._noSep else BlockFmt.CENTRE
|
||||
tStyle |= BlockFmt.NONE if self._noSep else BlockFmt.CENTRE
|
||||
self._noSep = False
|
||||
|
||||
tBlocks.append((
|
||||
@@ -755,7 +758,6 @@ class Tokenizer(ABC):
|
||||
nHead += 1
|
||||
tText = aLine[5:].strip()
|
||||
tType = BlockTyp.HEAD4
|
||||
tStyle = BlockFmt.NONE
|
||||
if isNovel:
|
||||
if self._hideSection:
|
||||
tText = ""
|
||||
@@ -766,7 +768,7 @@ class Tokenizer(ABC):
|
||||
tType = BlockTyp.SKIP
|
||||
elif tText == self._fmtSection: # Static Format
|
||||
tType = BlockTyp.SEP
|
||||
tStyle = BlockFmt.CENTRE
|
||||
tStyle |= BlockFmt.CENTRE
|
||||
|
||||
tBlocks.append((
|
||||
tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle
|
||||
@@ -803,35 +805,38 @@ class Tokenizer(ABC):
|
||||
bLine = bLine[:-1].rstrip(" ")
|
||||
|
||||
if alnLeft and alnRight:
|
||||
sAlign |= BlockFmt.CENTRE
|
||||
tStyle |= BlockFmt.CENTRE
|
||||
elif alnLeft:
|
||||
sAlign |= BlockFmt.LEFT
|
||||
tStyle |= BlockFmt.LEFT
|
||||
elif alnRight:
|
||||
sAlign |= BlockFmt.RIGHT
|
||||
tStyle |= BlockFmt.RIGHT
|
||||
|
||||
if indLeft:
|
||||
sAlign |= BlockFmt.IND_L
|
||||
tStyle |= BlockFmt.IND_L
|
||||
if indRight:
|
||||
sAlign |= BlockFmt.IND_R
|
||||
tStyle |= BlockFmt.IND_R
|
||||
|
||||
# Process formats
|
||||
tLine, tFmt = self._extractFormats(bLine, hDialog=isNovel)
|
||||
tBlocks.append((
|
||||
BlockTyp.TEXT, "", tLine, tFmt, sAlign
|
||||
BlockTyp.TEXT, "", tLine, tFmt, tStyle
|
||||
))
|
||||
if keepRaw:
|
||||
rawText.append(f"{aLine}\n")
|
||||
|
||||
# If we have content, turn off the first page flag
|
||||
if self._isFirst and len(tBlocks) > 1:
|
||||
if self._isFirst and tBlocks:
|
||||
self._isFirst = False # First document has been processed
|
||||
|
||||
# Make sure the blocks array doesn't start with a page break
|
||||
# on the very first page, adding a blank first page.
|
||||
if (cBlock := tBlocks[1])[4] & BlockFmt.PBB:
|
||||
tBlocks[1] = (
|
||||
cBlock[0], cBlock[1], cBlock[2], cBlock[3], cBlock[4] & ~BlockFmt.PBB
|
||||
)
|
||||
# on the very first block, adding a blank first page.
|
||||
for n, cBlock in enumerate(tBlocks):
|
||||
if cBlock[0] != BlockTyp.EMPTY:
|
||||
if cBlock[4] & BlockFmt.PBB:
|
||||
tBlocks[n] = (
|
||||
cBlock[0], cBlock[1], cBlock[2], cBlock[3], cBlock[4] & ~BlockFmt.PBB
|
||||
)
|
||||
break
|
||||
|
||||
# Always add an empty line at the end of the file
|
||||
tBlocks.append(B_EMPTY)
|
||||
|
||||
@@ -408,30 +408,29 @@ class ToOdt(Tokenizer):
|
||||
|
||||
# Styles
|
||||
oStyle = ODTParagraphStyle("New")
|
||||
if tStyle is not None:
|
||||
if tStyle & BlockFmt.LEFT:
|
||||
oStyle.setTextAlign("left")
|
||||
elif tStyle & BlockFmt.RIGHT:
|
||||
oStyle.setTextAlign("right")
|
||||
elif tStyle & BlockFmt.CENTRE:
|
||||
oStyle.setTextAlign("center")
|
||||
elif tStyle & BlockFmt.JUSTIFY:
|
||||
oStyle.setTextAlign("justify")
|
||||
if tStyle & BlockFmt.LEFT:
|
||||
oStyle.setTextAlign("left")
|
||||
elif tStyle & BlockFmt.RIGHT:
|
||||
oStyle.setTextAlign("right")
|
||||
elif tStyle & BlockFmt.CENTRE:
|
||||
oStyle.setTextAlign("center")
|
||||
elif tStyle & BlockFmt.JUSTIFY:
|
||||
oStyle.setTextAlign("justify")
|
||||
|
||||
if tStyle & BlockFmt.PBB:
|
||||
oStyle.setBreakBefore("page")
|
||||
if tStyle & BlockFmt.PBA:
|
||||
oStyle.setBreakAfter("page")
|
||||
if tStyle & BlockFmt.PBB:
|
||||
oStyle.setBreakBefore("page")
|
||||
if tStyle & BlockFmt.PBA:
|
||||
oStyle.setBreakAfter("page")
|
||||
|
||||
if tStyle & BlockFmt.Z_BTM:
|
||||
oStyle.setMarginBottom("0.000cm")
|
||||
if tStyle & BlockFmt.Z_TOP:
|
||||
oStyle.setMarginTop("0.000cm")
|
||||
if tStyle & BlockFmt.Z_BTM:
|
||||
oStyle.setMarginBottom("0.000cm")
|
||||
if tStyle & BlockFmt.Z_TOP:
|
||||
oStyle.setMarginTop("0.000cm")
|
||||
|
||||
if tStyle & BlockFmt.IND_L:
|
||||
oStyle.setMarginLeft(self._fBlockIndent)
|
||||
if tStyle & BlockFmt.IND_R:
|
||||
oStyle.setMarginRight(self._fBlockIndent)
|
||||
if tStyle & BlockFmt.IND_L:
|
||||
oStyle.setMarginLeft(self._fBlockIndent)
|
||||
if tStyle & BlockFmt.IND_R:
|
||||
oStyle.setMarginRight(self._fBlockIndent)
|
||||
|
||||
# Process Text Types
|
||||
if tType == BlockTyp.TEXT:
|
||||
|
||||
@@ -41,7 +41,7 @@ from novelwriter.formats.tokenizer import HEADINGS, Tokenizer
|
||||
from novelwriter.types import (
|
||||
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
|
||||
QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakBefore,
|
||||
QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
|
||||
QtPropLineHeight, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -75,6 +75,7 @@ class ToQTextDocument(Tokenizer):
|
||||
self._init = False
|
||||
self._bold = QFont.Weight.Bold
|
||||
self._normal = QFont.Weight.Normal
|
||||
self._newPage = False
|
||||
|
||||
self._pageSize = QPageSize(QPageSize.PageSizeId.A4)
|
||||
self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
|
||||
@@ -102,6 +103,11 @@ class ToQTextDocument(Tokenizer):
|
||||
self._pageMargins = QMarginsF(left, top, right, bottom)
|
||||
return
|
||||
|
||||
def setShowNewPage(self, state: bool) -> None:
|
||||
"""Add markers for page breaks."""
|
||||
self._newPage = state
|
||||
return
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
@@ -149,8 +155,6 @@ class ToQTextDocument(Tokenizer):
|
||||
# Text Formats
|
||||
# ============
|
||||
|
||||
QtPropLineHeight = QTextBlockFormat.LineHeightTypes.ProportionalHeight
|
||||
|
||||
self._blockFmt = QTextBlockFormat()
|
||||
self._blockFmt.setTopMargin(self._mText[0])
|
||||
self._blockFmt.setBottomMargin(self._mText[1])
|
||||
@@ -184,32 +188,32 @@ class ToQTextDocument(Tokenizer):
|
||||
bFmt.setTopMargin(self._mSep[0])
|
||||
bFmt.setBottomMargin(self._mSep[1])
|
||||
|
||||
if tStyle is not None:
|
||||
if tStyle & BlockFmt.LEFT:
|
||||
bFmt.setAlignment(QtAlignLeft)
|
||||
elif tStyle & BlockFmt.RIGHT:
|
||||
bFmt.setAlignment(QtAlignRight)
|
||||
elif tStyle & BlockFmt.CENTRE:
|
||||
bFmt.setAlignment(QtAlignCenter)
|
||||
elif tStyle & BlockFmt.JUSTIFY:
|
||||
bFmt.setAlignment(QtAlignJustify)
|
||||
if tStyle & BlockFmt.LEFT:
|
||||
bFmt.setAlignment(QtAlignLeft)
|
||||
elif tStyle & BlockFmt.RIGHT:
|
||||
bFmt.setAlignment(QtAlignRight)
|
||||
elif tStyle & BlockFmt.CENTRE:
|
||||
bFmt.setAlignment(QtAlignCenter)
|
||||
elif tStyle & BlockFmt.JUSTIFY:
|
||||
bFmt.setAlignment(QtAlignJustify)
|
||||
|
||||
if tStyle & BlockFmt.PBB:
|
||||
bFmt.setPageBreakPolicy(QtPageBreakBefore)
|
||||
if tStyle & BlockFmt.PBA:
|
||||
bFmt.setPageBreakPolicy(QtPageBreakAfter)
|
||||
if tStyle & BlockFmt.PBB:
|
||||
self._insertNewPageMarker(cursor)
|
||||
bFmt.setPageBreakPolicy(QtPageBreakBefore)
|
||||
if tStyle & BlockFmt.PBA:
|
||||
bFmt.setPageBreakPolicy(QtPageBreakAfter)
|
||||
|
||||
if tStyle & BlockFmt.Z_BTM:
|
||||
bFmt.setBottomMargin(0.0)
|
||||
if tStyle & BlockFmt.Z_TOP:
|
||||
bFmt.setTopMargin(0.0)
|
||||
if tStyle & BlockFmt.Z_BTM:
|
||||
bFmt.setBottomMargin(0.0)
|
||||
if tStyle & BlockFmt.Z_TOP:
|
||||
bFmt.setTopMargin(0.0)
|
||||
|
||||
if tStyle & BlockFmt.IND_L:
|
||||
bFmt.setLeftMargin(self._mIndent)
|
||||
if tStyle & BlockFmt.IND_R:
|
||||
bFmt.setRightMargin(self._mIndent)
|
||||
if tStyle & BlockFmt.IND_T:
|
||||
bFmt.setTextIndent(self._tIndent)
|
||||
if tStyle & BlockFmt.IND_L:
|
||||
bFmt.setLeftMargin(self._mIndent)
|
||||
if tStyle & BlockFmt.IND_R:
|
||||
bFmt.setRightMargin(self._mIndent)
|
||||
if tStyle & BlockFmt.IND_T:
|
||||
bFmt.setTextIndent(self._tIndent)
|
||||
|
||||
if tType in (BlockTyp.TEXT, BlockTyp.COMMENT, BlockTyp.KEYWORD):
|
||||
newBlock(cursor, bFmt)
|
||||
@@ -228,6 +232,9 @@ class ToQTextDocument(Tokenizer):
|
||||
newBlock(cursor, bFmt)
|
||||
cursor.insertText(nwUnicode.U_NBSP, self._charFmt)
|
||||
|
||||
if tStyle & BlockFmt.PBA:
|
||||
self._insertNewPageMarker(cursor)
|
||||
|
||||
self._document.blockSignals(False)
|
||||
|
||||
return
|
||||
@@ -386,6 +393,29 @@ class ToQTextDocument(Tokenizer):
|
||||
|
||||
return
|
||||
|
||||
def _insertNewPageMarker(self, cursor: QTextCursor) -> None:
|
||||
"""Insert a new page marker."""
|
||||
if self._newPage:
|
||||
cursor.insertHtml("<hr width='100%'>")
|
||||
|
||||
hFmt = cursor.blockFormat()
|
||||
hFmt.setBottomMargin(0.0)
|
||||
hFmt.setLineHeight(75.0, QtPropLineHeight)
|
||||
cursor.setBlockFormat(hFmt)
|
||||
|
||||
bFmt = QTextBlockFormat(self._blockFmt)
|
||||
bFmt.setAlignment(QtAlignCenter)
|
||||
bFmt.setTopMargin(0.0)
|
||||
bFmt.setLineHeight(75.0, QtPropLineHeight)
|
||||
|
||||
cFmt = QTextCharFormat(self._charFmt)
|
||||
cFmt.setFontPointSize(0.75*self._textFont.pointSizeF())
|
||||
cFmt.setForeground(self._theme.comment)
|
||||
|
||||
newBlock(cursor, bFmt)
|
||||
cursor.insertText(self._project.localLookup("New Page"), cFmt)
|
||||
return
|
||||
|
||||
def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle:
|
||||
"""Generate a heading style set."""
|
||||
mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
|
||||
|
||||
@@ -48,6 +48,7 @@ from novelwriter.core.buildsettings import BuildCollection, BuildSettings
|
||||
from novelwriter.core.docbuild import NWBuildDocument
|
||||
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
|
||||
from novelwriter.extensions.progressbars import NProgressCircle
|
||||
from novelwriter.extensions.switch import NSwitch
|
||||
from novelwriter.formats.tokenizer import HeadingFormatter
|
||||
from novelwriter.formats.toqdoc import ToQTextDocument
|
||||
from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
|
||||
@@ -87,6 +88,7 @@ class GuiManuscript(NToolDialog):
|
||||
self.setMinimumWidth(CONFIG.pxInt(600))
|
||||
self.setMinimumHeight(CONFIG.pxInt(500))
|
||||
|
||||
iPx = SHARED.theme.baseIconHeight
|
||||
iSz = SHARED.theme.baseIconSize
|
||||
wWin = CONFIG.pxInt(900)
|
||||
hWin = CONFIG.pxInt(600)
|
||||
@@ -191,15 +193,31 @@ class GuiManuscript(NToolDialog):
|
||||
self.processBox.addWidget(self.btnBuild, 1, 0)
|
||||
self.processBox.addWidget(self.btnClose, 1, 1)
|
||||
|
||||
# Preview Options
|
||||
# ===============
|
||||
|
||||
self.swtNewPage = NSwitch(self, height=iPx)
|
||||
self.swtNewPage.setChecked(pOptions.getBool("GuiManuscript", "showNewPage", True))
|
||||
self.swtNewPage.clicked.connect(self._generatePreview)
|
||||
|
||||
self.lblNewPage = QLabel(self.tr("Show Page Breaks"), self)
|
||||
self.lblNewPage.setBuddy(self.swtNewPage)
|
||||
|
||||
# Assemble GUI
|
||||
# ============
|
||||
|
||||
self.docPreview = _PreviewWidget(self)
|
||||
self.docStats = _StatsWidget(self)
|
||||
|
||||
self.docBar = QHBoxLayout()
|
||||
self.docBar.addWidget(self.docStats, 1, QtAlignTop)
|
||||
self.docBar.addWidget(self.lblNewPage, 0, QtAlignTop)
|
||||
self.docBar.addWidget(self.swtNewPage, 0, QtAlignTop)
|
||||
self.docBar.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.docBox = QVBoxLayout()
|
||||
self.docBox.addWidget(self.docPreview, 1)
|
||||
self.docBox.addWidget(self.docStats, 0)
|
||||
self.docBox.addLayout(self.docBar, 0)
|
||||
self.docBox.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.docWdiget = QWidget(self)
|
||||
@@ -344,6 +362,7 @@ class GuiManuscript(NToolDialog):
|
||||
return
|
||||
|
||||
start = time()
|
||||
showNewPage = self.swtNewPage.isChecked()
|
||||
|
||||
# Make sure editor content is saved before we start
|
||||
SHARED.saveEditor()
|
||||
@@ -352,7 +371,7 @@ class GuiManuscript(NToolDialog):
|
||||
docBuild.queueAll()
|
||||
|
||||
self.docPreview.beginNewBuild(len(docBuild))
|
||||
for step, _ in docBuild.iterBuildPreview():
|
||||
for step, _ in docBuild.iterBuildPreview(showNewPage):
|
||||
self.docPreview.buildStep(step + 1)
|
||||
QApplication.processEvents()
|
||||
|
||||
@@ -434,6 +453,7 @@ class GuiManuscript(NToolDialog):
|
||||
detailsHeight = CONFIG.rpxInt(buildSplit[1])
|
||||
detailsWidth = CONFIG.rpxInt(self.buildDetails.getColumnWidth())
|
||||
detailsExpanded = self.buildDetails.getExpandedState()
|
||||
showNewPage = self.swtNewPage.isChecked()
|
||||
|
||||
logger.debug("Saving State: GuiManuscript")
|
||||
pOptions = SHARED.project.options
|
||||
@@ -445,6 +465,7 @@ class GuiManuscript(NToolDialog):
|
||||
pOptions.setValue("GuiManuscript", "detailsHeight", detailsHeight)
|
||||
pOptions.setValue("GuiManuscript", "detailsWidth", detailsWidth)
|
||||
pOptions.setValue("GuiManuscript", "detailsExpanded", detailsExpanded)
|
||||
pOptions.setValue("GuiManuscript", "showNewPage", showNewPage)
|
||||
pOptions.saveSettings()
|
||||
|
||||
return
|
||||
@@ -945,7 +966,7 @@ class _StatsWidget(QWidget):
|
||||
self.toggleButton = NIconToggleButton(self, SHARED.theme.baseIconSize, "unfold")
|
||||
self.toggleButton.toggled.connect(self._toggleView)
|
||||
|
||||
self._buildStatsPanel()
|
||||
self._buildBottomPanel()
|
||||
|
||||
self.mainStack = QStackedWidget(self)
|
||||
self.mainStack.addWidget(self.minWidget)
|
||||
@@ -1010,8 +1031,8 @@ class _StatsWidget(QWidget):
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _buildStatsPanel(self) -> None:
|
||||
"""Build the minimal stats page."""
|
||||
def _buildBottomPanel(self) -> None:
|
||||
"""Build the bottom page."""
|
||||
mPx = CONFIG.pxInt(8)
|
||||
hPx = CONFIG.pxInt(12)
|
||||
vPx = CONFIG.pxInt(4)
|
||||
|
||||
@@ -24,7 +24,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QColor, QFont, QPainter, QTextCharFormat, QTextCursor, QTextFormat
|
||||
from PyQt5.QtGui import (
|
||||
QColor, QFont, QPainter, QTextBlockFormat, QTextCharFormat, QTextCursor,
|
||||
QTextFormat
|
||||
)
|
||||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QStyle
|
||||
|
||||
# Qt Alignment Flags
|
||||
@@ -48,11 +51,13 @@ QtVAlignNormal = QTextCharFormat.VerticalAlignment.AlignNormal
|
||||
QtVAlignSub = QTextCharFormat.VerticalAlignment.AlignSubScript
|
||||
QtVAlignSuper = QTextCharFormat.VerticalAlignment.AlignSuperScript
|
||||
|
||||
# Qt Page Break
|
||||
# Qt Text Formats
|
||||
|
||||
QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore
|
||||
QtPageBreakAfter = QTextFormat.PageBreakFlag.PageBreak_AlwaysAfter
|
||||
|
||||
QtPropLineHeight = QTextBlockFormat.LineHeightTypes.ProportionalHeight
|
||||
|
||||
# Qt Painter Types
|
||||
|
||||
QtTransparent = QColor(0, 0, 0, 0)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
Jane Doe
|
||||
Address 1
|
||||
Address 2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Test Project A
|
||||
|
||||
**By Jane Doe**
|
||||
|
||||
Word Count: 11
|
||||
|
||||
## Chapter 1
|
||||
|
||||
* * *
|
||||
|
||||
* * *
|
||||
|
||||
## Chapter 2
|
||||
|
||||
* * *
|
||||
|
||||
* * *
|
||||
|
||||
## Chapter 3
|
||||
|
||||
* * *
|
||||
|
||||
* * *
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
Jane Doe
|
||||
Address 1
|
||||
Address 2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Test Project B
|
||||
|
||||
**By Jane Doe**
|
||||
|
||||
Word Count: 11
|
||||
|
||||
* * *
|
||||
|
||||
* * *
|
||||
|
||||
* * *
|
||||
|
||||
* * *
|
||||
|
||||
* * *
|
||||
|
||||
@@ -31,10 +31,13 @@ import pytest
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwConst, nwFiles, nwItemClass
|
||||
from novelwriter.core.buildsettings import BuildSettings
|
||||
from novelwriter.core.coretools import (
|
||||
DocDuplicator, DocMerger, DocSearch, DocSplitter, ProjectBuilder
|
||||
)
|
||||
from novelwriter.core.docbuild import NWBuildDocument
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.enum import nwBuildFmt
|
||||
|
||||
from tests.mocked import causeOSError
|
||||
from tests.tools import NWD_IGNORE, XML_IGNORE, C, buildTestProject, cmpFiles
|
||||
@@ -507,10 +510,6 @@ def testCoreTools_ProjectBuilderA(monkeypatch, fncPath, tstPaths, mockGUI, mockR
|
||||
"""Create a new project from a project dictionary, with chapters."""
|
||||
monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed"))
|
||||
|
||||
projFile = fncPath / "nwProject.nwx"
|
||||
testFile = tstPaths.outDir / "coreTools_ProjectBuilderA_nwProject.nwx"
|
||||
compFile = tstPaths.refDir / "coreTools_ProjectBuilderA_nwProject.nwx"
|
||||
|
||||
data = {
|
||||
"name": "Test Project A",
|
||||
"author": "Jane Doe",
|
||||
@@ -531,19 +530,34 @@ def testCoreTools_ProjectBuilderA(monkeypatch, fncPath, tstPaths, mockGUI, mockR
|
||||
builder = ProjectBuilder()
|
||||
assert builder.buildProject(data) is True
|
||||
|
||||
projFile = fncPath / "nwProject.nwx"
|
||||
testFile = tstPaths.outDir / "coreTools_ProjectBuilderA_nwProject.nwx"
|
||||
compFile = tstPaths.refDir / "coreTools_ProjectBuilderA_nwProject.nwx"
|
||||
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
|
||||
# Check Content
|
||||
project = NWProject()
|
||||
project.openProject(fncPath)
|
||||
|
||||
build = BuildSettings()
|
||||
|
||||
docBuild = NWBuildDocument(project, build)
|
||||
docBuild.queueAll()
|
||||
|
||||
testFile = tstPaths.outDir / "coreTools_ProjectBuilderA_Project.md"
|
||||
compFile = tstPaths.refDir / "coreTools_ProjectBuilderA_Project.md"
|
||||
|
||||
assert list(docBuild.iterBuildDocument(testFile, nwBuildFmt.EXT_MD))
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTools_ProjectBuilderB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
|
||||
"""Create a new project from a project dictionary, without chapters."""
|
||||
monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed"))
|
||||
|
||||
projFile = fncPath / "nwProject.nwx"
|
||||
testFile = tstPaths.outDir / "coreTools_ProjectBuilderB_nwProject.nwx"
|
||||
compFile = tstPaths.refDir / "coreTools_ProjectBuilderB_nwProject.nwx"
|
||||
|
||||
data = {
|
||||
"name": "Test Project B",
|
||||
"author": "Jane Doe",
|
||||
@@ -564,9 +578,28 @@ def testCoreTools_ProjectBuilderB(monkeypatch, fncPath, tstPaths, mockGUI, mockR
|
||||
builder = ProjectBuilder()
|
||||
assert builder.buildProject(data) is True
|
||||
|
||||
projFile = fncPath / "nwProject.nwx"
|
||||
testFile = tstPaths.outDir / "coreTools_ProjectBuilderB_nwProject.nwx"
|
||||
compFile = tstPaths.refDir / "coreTools_ProjectBuilderB_nwProject.nwx"
|
||||
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
|
||||
# Check Content
|
||||
project = NWProject()
|
||||
project.openProject(fncPath)
|
||||
|
||||
build = BuildSettings()
|
||||
|
||||
docBuild = NWBuildDocument(project, build)
|
||||
docBuild.queueAll()
|
||||
|
||||
testFile = tstPaths.outDir / "coreTools_ProjectBuilderB_Project.md"
|
||||
compFile = tstPaths.refDir / "coreTools_ProjectBuilderB_Project.md"
|
||||
|
||||
assert list(docBuild.iterBuildDocument(testFile, nwBuildFmt.EXT_MD))
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTools_ProjectBuilderCopyPlain(monkeypatch, caplog, mockGUI, prjLipsum, fncPath):
|
||||
|
||||
@@ -88,7 +88,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
|
||||
build.unpack(BUILD_CONF)
|
||||
|
||||
docBuild = NWBuildDocument(project, build)
|
||||
docBuild.setBuildOutline(True)
|
||||
docBuild._outline = True
|
||||
docBuild.queueAll()
|
||||
|
||||
assert docBuild._outline is True
|
||||
|
||||
@@ -48,7 +48,7 @@ def testToolManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
|
||||
buildTestProject(nwGUI, projPath)
|
||||
nwGUI.openProject(projPath)
|
||||
SHARED.project.storage.getDocument(C.hChapterDoc).writeDocument("## A Chapter\n\n\t\tHi")
|
||||
allText = "New Novel\nBy Jane Doe\nA Chapter\n\t\tHi"
|
||||
allText = "New Novel\nBy Jane Doe\n\nNew Page\nA Chapter\n\t\tHi"
|
||||
|
||||
nwGUI.mainMenu.aBuildManuscript.activate(QAction.ActionEvent.Trigger)
|
||||
qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(GuiManuscript) is not None, timeout=1000)
|
||||
|
||||
Reference in New Issue
Block a user