Make page breaks visible in manuscript preview (#2086)

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