From 510b6ff6d2bdbc13c456ebaabafd836197d4ce0e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 31 May 2023 18:10:41 +0200 Subject: [PATCH] Add simple build preview --- novelwriter/core/docbuild.py | 47 ++++++++----- novelwriter/tools/manusbuild.py | 42 ++++++++++++ novelwriter/tools/manuscript.py | 95 ++++++++++++++++++++++----- novelwriter/tools/manussettings.py | 2 +- sample/nwProject.nwx | 6 +- tests/test_core/test_core_docbuild.py | 8 +-- 6 files changed, 159 insertions(+), 41 deletions(-) create mode 100644 novelwriter/tools/manusbuild.py diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 1ab1a09e..b946d20f 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -33,7 +33,6 @@ from PyQt5.QtGui import QFont, QFontInfo from novelwriter import CONFIG from novelwriter.error import formatException -from novelwriter.constants import nwConst from novelwriter.core.tomd import ToMarkdown from novelwriter.core.toodt import ToOdt from novelwriter.core.tohtml import ToHtml @@ -46,13 +45,14 @@ logger = logging.getLogger(__name__) class NWBuildDocument: - def __init__(self, project: NWProject, build: BuildSettings): + __slots__ = ("_project", "_build", "_queue", "_error", "_cache") + def __init__(self, project: NWProject, build: BuildSettings): self._project = project self._build = build self._queue = [] self._error = None - + self._cache = None return ## @@ -64,7 +64,14 @@ class NWBuildDocument: return self._error @property - def buildLength(self) -> int: + def lastBuild(self) -> Tokenizer | None: + return self._cache + + ## + # Special Methods + ## + + def __len__(self) -> int: return len(self._queue) ## @@ -89,7 +96,7 @@ class NWBuildDocument: self._queue.append(item.itemHandle) return - def iterBuildOpenDocument(self, savePath: Path, isFlat: bool) -> Iterable[tuple[int, bool]]: + def iterBuildOpenDocument(self, path: Path, isFlat: bool) -> Iterable[tuple[int, bool]]: """Build an Open Document file. """ makeOdt = ToOdt(self._project, isFlat=isFlat) @@ -102,18 +109,21 @@ class NWBuildDocument: makeOdt.closeDocument() self._error = None + self._cache = makeOdt + try: if isFlat: - makeOdt.saveFlatXML(savePath) + makeOdt.saveFlatXML(path) else: - makeOdt.saveOpenDocText(savePath) + makeOdt.saveOpenDocText(path) except Exception as exc: self._error = formatException(exc) return - def iterBuildHTML(self, savePath: Path) -> Iterable[tuple[int, bool]]: - """Build an HTML file. + def iterBuildHTML(self, path: Path | None) -> Iterable[tuple[int, bool]]: + """Build an HTML file. If path is None, no file is saved. This + is used for generating build previews. """ makeHtml = ToHtml(self._project) self._setupBuild(makeHtml) @@ -125,14 +135,17 @@ class NWBuildDocument: yield i, self._doBuild(makeHtml, tHandle) self._error = None - try: - makeHtml.saveHTML5(savePath) - except Exception as exc: - self._error = formatException(exc) + self._cache = makeHtml + + if isinstance(path, Path): + try: + makeHtml.saveHTML5(path) + except Exception as exc: + self._error = formatException(exc) return - def iterBuildMarkdown(self, savePath: Path, extendedMd: bool) -> Iterable[tuple[int, bool]]: + def iterBuildMarkdown(self, path: Path, extendedMd: bool) -> Iterable[tuple[int, bool]]: """Build a Markdown file. """ makeMd = ToMarkdown(self._project) @@ -150,8 +163,10 @@ class NWBuildDocument: yield i, self._doBuild(makeMd, tHandle) self._error = None + self._cache = makeMd + try: - makeMd.saveMarkdown(savePath) + makeMd.saveMarkdown(path) except Exception as exc: self._error = formatException(exc) @@ -194,8 +209,6 @@ class NWBuildDocument: # Get font information if not textFont: textFont = str(CONFIG.textFont) - if not textFont: - textFont = nwConst.SYSTEM_FONT bldFont = QFont(textFont, textSize) fontInfo = QFontInfo(bldFont) diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py new file mode 100644 index 00000000..4dd8343c --- /dev/null +++ b/novelwriter/tools/manusbuild.py @@ -0,0 +1,42 @@ +""" +novelWriter – GUI Build Manuscript +================================== +GUI classes for the Manuscript Build Tool + +File History: +Created: 2023-05-24 [2.1b1] + +This file is a part of novelWriter +Copyright 2018–2023, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from __future__ import annotations + +import logging + +from PyQt5.QtWidgets import QDialog, QWidget + +logger = logging.getLogger(__name__) + + +class GuiManuscriptBuild(QDialog): + + def __init__(self, parent: QWidget): + super().__init__(parent=parent) + self._parent = parent + + return + +# END Class GuiManuscriptBuild diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index e638776a..59abadbd 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -6,7 +6,6 @@ GUI classes for the Manuscript Build Tool File History: Created: 2023-05-13 [2.1b1] GuiManuscript Created: 2023-05-13 [2.1b1] GuiManuscriptPreview -Created: 2023-05-24 [2.1b1] GuiManuscriptBuild This file is a part of novelWriter Copyright 2018–2023, Veronica Berglyd Olsen @@ -26,10 +25,13 @@ along with this program. If not, see . """ from __future__ import annotations +import json import logging +from time import time from typing import TYPE_CHECKING +from PyQt5.QtGui import QCursor from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( QDialog, QHBoxLayout, QListWidget, QListWidgetItem, QMenu, QProgressBar, @@ -37,6 +39,9 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG +from novelwriter.error import logException +from novelwriter.core.tohtml import ToHtml +from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.buildsettings import BuildCollection, BuildSettings from novelwriter.tools.manussettings import GuiBuildSettings @@ -92,7 +97,8 @@ class GuiManuscript(QDialog): self.buildProgress = QProgressBar() self.btnPreview = QPushButton(self.tr("Build Preview")) - self.manPreview = GuiManuscriptPreview(self) + self.btnPreview.clicked.connect(self._generatePreview) + self.manPreview = GuiManuscriptPreview(self.mainGui) self.menuPrint = QMenu(self) self.aPrintSend = self.menuPrint.addAction(self.tr("Print Preview")) @@ -153,6 +159,7 @@ class GuiManuscript(QDialog): self.outerBox.addWidget(self.mainSplit) self.setLayout(self.outerBox) + self.setSizeGripEnabled(True) logger.debug("GuiManuscript initialisation complete") @@ -193,11 +200,9 @@ class GuiManuscript(QDialog): def _editSelectedBuild(self): """Edit the currently selected build settings entry. """ - bItems = self.buildList.selectedItems() - if bItems: - build = self._builds.getBuild(bItems[0].data(Qt.UserRole)) - if isinstance(build, BuildSettings): - self._openSettingsDialog(build) + build = self._getSelectedBuild() + if build is not None: + self._openSettingsDialog(build) return @pyqtSlot(BuildSettings) @@ -209,6 +214,43 @@ class GuiManuscript(QDialog): self._updateBuildItem(build) return + @pyqtSlot() + def _generatePreview(self): + """ + """ + build = self._getSelectedBuild() + if build is None: + return + + docBuild = NWBuildDocument(self.theProject, build) + docBuild.queueAll() + + self.buildProgress.setMaximum(len(docBuild)) + for step, status in docBuild.iterBuildHTML(None): + self.buildProgress.setValue(step + 1) + + buildObj = docBuild.lastBuild + assert isinstance(buildObj, ToHtml) + result = { + "time": int(time()), + "style": buildObj.getStyleSheet(), + "html": buildObj.fullHTML, + } + + logger.debug("Saving build cache") + cache = CONFIG.dataPath("cache") / f"build_{build.buildID}.json" + try: + with open(cache, mode="w+", encoding="utf-8") as outFile: + outFile.write(json.dumps(result, indent=2)) + except Exception: + logger.error("Failed to save build cache") + logException() + return + + self.manPreview.setContent(result) + + return + @pyqtSlot() def _doClose(self): """The close button has been clicked. @@ -221,6 +263,16 @@ class GuiManuscript(QDialog): # Internal Functions ## + def _getSelectedBuild(self) -> BuildSettings | None: + """Get the currently selected build. + """ + bItems = self.buildList.selectedItems() + if bItems: + build = self._builds.getBuild(bItems[0].data(Qt.UserRole)) + if isinstance(build, BuildSettings): + return build + return None + def _saveManuscript(self, outFormat: int): """Save the manuscript file or files. """ @@ -244,8 +296,8 @@ class GuiManuscript(QDialog): winHeight = CONFIG.rpxInt(self.height()) mainSplit = self.mainSplit.sizes() - optsWidth = mainSplit[0] - viewWidth = mainSplit[1] + optsWidth = CONFIG.rpxInt(mainSplit[0]) + viewWidth = CONFIG.rpxInt(mainSplit[1]) pOptions = self.theProject.options pOptions.setValue("GuiManuscript", "winWidth", winWidth) @@ -266,6 +318,7 @@ class GuiManuscript(QDialog): qApp.processEvents() dlgSettings.loadContent() dlgSettings.newSettingsReady.connect(self._processNewSettings) + return def _updateBuildsList(self): @@ -295,7 +348,7 @@ class GuiManuscript(QDialog): class GuiManuscriptPreview(QTextBrowser): - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain): super().__init__(parent=mainGui) self.mainGui = mainGui @@ -304,15 +357,25 @@ class GuiManuscriptPreview(QTextBrowser): return -# END Class GuiManuscriptPreview + def setContent(self, data: dict): + """ + """ + sPos = self.verticalScrollBar().value() + qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) + html = "".join(data.get("html", [])) + html = html.replace("\t", "!!tab!!") + html = html.replace("", "") + html = html.replace("", "") + self.setHtml(html) + qApp.processEvents() -class GuiManuscriptBuild(QDialog): + while self.find("!!tab!!"): + theCursor = self.textCursor() + theCursor.insertText("\t") - def __init__(self, parent): - super().__init__(parent=parent) - self._manusGui = parent + self.verticalScrollBar().setValue(sPos) return -# END Class GuiManuscriptBuild +# END Class GuiManuscriptPreview diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 5f2a904e..c638edc9 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -398,7 +398,7 @@ class GuiBuildFilterTab(QWidget): sizes = self.mainSplit.sizes() if len(sizes) < 2: return 0, 0 - return sizes[0], sizes[1] + return CONFIG.rpxInt(sizes[0]), CONFIG.rpxInt(sizes[1]) ## # Slots diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index afb0157a..6d04fd18 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Sample Project Jane Smith @@ -65,7 +65,7 @@ Chapter One - + Making a Scene diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py index dde00264..0dabad71 100644 --- a/tests/test_core/test_core_docbuild.py +++ b/tests/test_core/test_core_docbuild.py @@ -80,7 +80,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP docBuild = NWBuildDocument(project, build) docBuild.queueAll() - assert docBuild.buildLength == 21 + assert len(docBuild) == 21 # Check FODT Build # ================ @@ -139,7 +139,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP mp.setattr("novelwriter.core.toodt.ToOdt.doConvert", causeException) docBuild.addDocument("0000000000000") - assert docBuild.buildLength == 22 + assert len(docBuild) == 22 count = 0 error = [] @@ -188,7 +188,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): docBuild = NWBuildDocument(project, build) docBuild.queueAll() - assert docBuild.buildLength == 21 + assert len(docBuild) == 21 # Check HTML5 Build # ================= @@ -239,7 +239,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths docBuild = NWBuildDocument(project, build) docBuild.queueAll() - assert docBuild.buildLength == 21 + assert len(docBuild) == 21 # Check Standard Markdown Build # =============================