diff --git a/novelwriter/assets/i18n/project_en_GB.json b/novelwriter/assets/i18n/project_en_GB.json index 39d52e6e..19cdda1e 100644 --- a/novelwriter/assets/i18n/project_en_GB.json +++ b/novelwriter/assets/i18n/project_en_GB.json @@ -14,6 +14,7 @@ "Objects": "Objects", "Entities": "Entities", "Custom": "Custom", + "New Page": "New Page", "0": "Zero", "1": "One", "2": "Two", diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index 131a0104..e2bdd342 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -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 diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 7161a794..96d88145 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -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}'" diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 419e4ec2..35bd72d4 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -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", diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index fe634b5e..5b499b45 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -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: diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py index f3ef49d8..69a48ac2 100644 --- a/novelwriter/formats/tohtml.py +++ b/novelwriter/formats/tohtml.py @@ -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: diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 51bfaef1..b7eba033 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -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) diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index f72aa6a4..af78f892 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -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: diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index de0a344c..bd9b3cb1 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -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("