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("
") + + 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)) diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index cad13292..25399053 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -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) diff --git a/novelwriter/types.py b/novelwriter/types.py index e9db0876..e997f619 100644 --- a/novelwriter/types.py +++ b/novelwriter/types.py @@ -24,7 +24,10 @@ along with this program. If not, see . 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) diff --git a/tests/reference/coreTools_ProjectBuilderA_Project.md b/tests/reference/coreTools_ProjectBuilderA_Project.md new file mode 100644 index 00000000..f1fe2be4 --- /dev/null +++ b/tests/reference/coreTools_ProjectBuilderA_Project.md @@ -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 + +* * * + +* * * + diff --git a/tests/reference/coreTools_ProjectBuilderB_Project.md b/tests/reference/coreTools_ProjectBuilderB_Project.md new file mode 100644 index 00000000..386a773c --- /dev/null +++ b/tests/reference/coreTools_ProjectBuilderB_Project.md @@ -0,0 +1,30 @@ +Jane Doe +Address 1 +Address 2 + +  + +  + +  + +  + +  + +# Test Project B + +**By Jane Doe** + +Word Count: 11 + +* * * + +* * * + +* * * + +* * * + +* * * + diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index 3827db7a..785b6b58 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -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): diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py index 04ec4e75..cbb6c52f 100644 --- a/tests/test_core/test_core_docbuild.py +++ b/tests/test_core/test_core_docbuild.py @@ -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 diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py index df528751..6d94477a 100644 --- a/tests/test_tools/test_tools_manuscript.py +++ b/tests/test_tools/test_tools_manuscript.py @@ -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)