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
# =============================