Add simple build preview

This commit is contained in:
Veronica Berglyd Olsen
2023-05-31 18:10:41 +02:00
parent 704fffada3
commit 510b6ff6d2
6 changed files with 159 additions and 41 deletions
+30 -17
View File
@@ -33,7 +33,6 @@ from PyQt5.QtGui import QFont, QFontInfo
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.error import formatException from novelwriter.error import formatException
from novelwriter.constants import nwConst
from novelwriter.core.tomd import ToMarkdown from novelwriter.core.tomd import ToMarkdown
from novelwriter.core.toodt import ToOdt from novelwriter.core.toodt import ToOdt
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
@@ -46,13 +45,14 @@ logger = logging.getLogger(__name__)
class NWBuildDocument: 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._project = project
self._build = build self._build = build
self._queue = [] self._queue = []
self._error = None self._error = None
self._cache = None
return return
## ##
@@ -64,7 +64,14 @@ class NWBuildDocument:
return self._error return self._error
@property @property
def buildLength(self) -> int: def lastBuild(self) -> Tokenizer | None:
return self._cache
##
# Special Methods
##
def __len__(self) -> int:
return len(self._queue) return len(self._queue)
## ##
@@ -89,7 +96,7 @@ class NWBuildDocument:
self._queue.append(item.itemHandle) self._queue.append(item.itemHandle)
return 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. """Build an Open Document file.
""" """
makeOdt = ToOdt(self._project, isFlat=isFlat) makeOdt = ToOdt(self._project, isFlat=isFlat)
@@ -102,18 +109,21 @@ class NWBuildDocument:
makeOdt.closeDocument() makeOdt.closeDocument()
self._error = None self._error = None
self._cache = makeOdt
try: try:
if isFlat: if isFlat:
makeOdt.saveFlatXML(savePath) makeOdt.saveFlatXML(path)
else: else:
makeOdt.saveOpenDocText(savePath) makeOdt.saveOpenDocText(path)
except Exception as exc: except Exception as exc:
self._error = formatException(exc) self._error = formatException(exc)
return return
def iterBuildHTML(self, savePath: Path) -> Iterable[tuple[int, bool]]: def iterBuildHTML(self, path: Path | None) -> Iterable[tuple[int, bool]]:
"""Build an HTML file. """Build an HTML file. If path is None, no file is saved. This
is used for generating build previews.
""" """
makeHtml = ToHtml(self._project) makeHtml = ToHtml(self._project)
self._setupBuild(makeHtml) self._setupBuild(makeHtml)
@@ -125,14 +135,17 @@ class NWBuildDocument:
yield i, self._doBuild(makeHtml, tHandle) yield i, self._doBuild(makeHtml, tHandle)
self._error = None self._error = None
try: self._cache = makeHtml
makeHtml.saveHTML5(savePath)
except Exception as exc: if isinstance(path, Path):
self._error = formatException(exc) try:
makeHtml.saveHTML5(path)
except Exception as exc:
self._error = formatException(exc)
return 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. """Build a Markdown file.
""" """
makeMd = ToMarkdown(self._project) makeMd = ToMarkdown(self._project)
@@ -150,8 +163,10 @@ class NWBuildDocument:
yield i, self._doBuild(makeMd, tHandle) yield i, self._doBuild(makeMd, tHandle)
self._error = None self._error = None
self._cache = makeMd
try: try:
makeMd.saveMarkdown(savePath) makeMd.saveMarkdown(path)
except Exception as exc: except Exception as exc:
self._error = formatException(exc) self._error = formatException(exc)
@@ -194,8 +209,6 @@ class NWBuildDocument:
# Get font information # Get font information
if not textFont: if not textFont:
textFont = str(CONFIG.textFont) textFont = str(CONFIG.textFont)
if not textFont:
textFont = nwConst.SYSTEM_FONT
bldFont = QFont(textFont, textSize) bldFont = QFont(textFont, textSize)
fontInfo = QFontInfo(bldFont) fontInfo = QFontInfo(bldFont)
+42
View File
@@ -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 20182023, 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 <https://www.gnu.org/licenses/>.
"""
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
+79 -16
View File
@@ -6,7 +6,6 @@ GUI classes for the Manuscript Build Tool
File History: File History:
Created: 2023-05-13 [2.1b1] GuiManuscript Created: 2023-05-13 [2.1b1] GuiManuscript
Created: 2023-05-13 [2.1b1] GuiManuscriptPreview Created: 2023-05-13 [2.1b1] GuiManuscriptPreview
Created: 2023-05-24 [2.1b1] GuiManuscriptBuild
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen Copyright 20182023, Veronica Berglyd Olsen
@@ -26,10 +25,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import json
import logging import logging
from time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtGui import QCursor
from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QListWidget, QListWidgetItem, QMenu, QProgressBar, QDialog, QHBoxLayout, QListWidget, QListWidgetItem, QMenu, QProgressBar,
@@ -37,6 +39,9 @@ from PyQt5.QtWidgets import (
) )
from novelwriter import CONFIG 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.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.tools.manussettings import GuiBuildSettings from novelwriter.tools.manussettings import GuiBuildSettings
@@ -92,7 +97,8 @@ class GuiManuscript(QDialog):
self.buildProgress = QProgressBar() self.buildProgress = QProgressBar()
self.btnPreview = QPushButton(self.tr("Build Preview")) 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.menuPrint = QMenu(self)
self.aPrintSend = self.menuPrint.addAction(self.tr("Print Preview")) self.aPrintSend = self.menuPrint.addAction(self.tr("Print Preview"))
@@ -153,6 +159,7 @@ class GuiManuscript(QDialog):
self.outerBox.addWidget(self.mainSplit) self.outerBox.addWidget(self.mainSplit)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
self.setSizeGripEnabled(True)
logger.debug("GuiManuscript initialisation complete") logger.debug("GuiManuscript initialisation complete")
@@ -193,11 +200,9 @@ class GuiManuscript(QDialog):
def _editSelectedBuild(self): def _editSelectedBuild(self):
"""Edit the currently selected build settings entry. """Edit the currently selected build settings entry.
""" """
bItems = self.buildList.selectedItems() build = self._getSelectedBuild()
if bItems: if build is not None:
build = self._builds.getBuild(bItems[0].data(Qt.UserRole)) self._openSettingsDialog(build)
if isinstance(build, BuildSettings):
self._openSettingsDialog(build)
return return
@pyqtSlot(BuildSettings) @pyqtSlot(BuildSettings)
@@ -209,6 +214,43 @@ class GuiManuscript(QDialog):
self._updateBuildItem(build) self._updateBuildItem(build)
return 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() @pyqtSlot()
def _doClose(self): def _doClose(self):
"""The close button has been clicked. """The close button has been clicked.
@@ -221,6 +263,16 @@ class GuiManuscript(QDialog):
# Internal Functions # 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): def _saveManuscript(self, outFormat: int):
"""Save the manuscript file or files. """Save the manuscript file or files.
""" """
@@ -244,8 +296,8 @@ class GuiManuscript(QDialog):
winHeight = CONFIG.rpxInt(self.height()) winHeight = CONFIG.rpxInt(self.height())
mainSplit = self.mainSplit.sizes() mainSplit = self.mainSplit.sizes()
optsWidth = mainSplit[0] optsWidth = CONFIG.rpxInt(mainSplit[0])
viewWidth = mainSplit[1] viewWidth = CONFIG.rpxInt(mainSplit[1])
pOptions = self.theProject.options pOptions = self.theProject.options
pOptions.setValue("GuiManuscript", "winWidth", winWidth) pOptions.setValue("GuiManuscript", "winWidth", winWidth)
@@ -266,6 +318,7 @@ class GuiManuscript(QDialog):
qApp.processEvents() qApp.processEvents()
dlgSettings.loadContent() dlgSettings.loadContent()
dlgSettings.newSettingsReady.connect(self._processNewSettings) dlgSettings.newSettingsReady.connect(self._processNewSettings)
return return
def _updateBuildsList(self): def _updateBuildsList(self):
@@ -295,7 +348,7 @@ class GuiManuscript(QDialog):
class GuiManuscriptPreview(QTextBrowser): class GuiManuscriptPreview(QTextBrowser):
def __init__(self, mainGui): def __init__(self, mainGui: GuiMain):
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
self.mainGui = mainGui self.mainGui = mainGui
@@ -304,15 +357,25 @@ class GuiManuscriptPreview(QTextBrowser):
return 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("<del>", "<span style='text-decoration: line-through;'>")
html = html.replace("</del>", "</span>")
self.setHtml(html)
qApp.processEvents()
class GuiManuscriptBuild(QDialog): while self.find("!!tab!!"):
theCursor = self.textCursor()
theCursor.insertText("\t")
def __init__(self, parent): self.verticalScrollBar().setValue(sPos)
super().__init__(parent=parent)
self._manusGui = parent
return return
# END Class GuiManuscriptBuild # END Class GuiManuscriptPreview
+1 -1
View File
@@ -398,7 +398,7 @@ class GuiBuildFilterTab(QWidget):
sizes = self.mainSplit.sizes() sizes = self.mainSplit.sizes()
if len(sizes) < 2: if len(sizes) < 2:
return 0, 0 return 0, 0
return sizes[0], sizes[1] return CONFIG.rpxInt(sizes[0]), CONFIG.rpxInt(sizes[1])
## ##
# Slots # Slots
+3 -3
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" timeStamp="2023-05-29 21:29:35"> <novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" timeStamp="2023-05-31 17:53:27">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1505" autoCount="237" editTime="74902"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1506" autoCount="237" editTime="75085">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
@@ -65,7 +65,7 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name> <name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item> </item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="1369" /> <meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="18" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name> <name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item> </item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
+4 -4
View File
@@ -80,7 +80,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
docBuild = NWBuildDocument(project, build) docBuild = NWBuildDocument(project, build)
docBuild.queueAll() docBuild.queueAll()
assert docBuild.buildLength == 21 assert len(docBuild) == 21
# Check FODT Build # Check FODT Build
# ================ # ================
@@ -139,7 +139,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
mp.setattr("novelwriter.core.toodt.ToOdt.doConvert", causeException) mp.setattr("novelwriter.core.toodt.ToOdt.doConvert", causeException)
docBuild.addDocument("0000000000000") docBuild.addDocument("0000000000000")
assert docBuild.buildLength == 22 assert len(docBuild) == 22
count = 0 count = 0
error = [] error = []
@@ -188,7 +188,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
docBuild = NWBuildDocument(project, build) docBuild = NWBuildDocument(project, build)
docBuild.queueAll() docBuild.queueAll()
assert docBuild.buildLength == 21 assert len(docBuild) == 21
# Check HTML5 Build # Check HTML5 Build
# ================= # =================
@@ -239,7 +239,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths
docBuild = NWBuildDocument(project, build) docBuild = NWBuildDocument(project, build)
docBuild.queueAll() docBuild.queueAll()
assert docBuild.buildLength == 21 assert len(docBuild) == 21
# Check Standard Markdown Build # Check Standard Markdown Build
# ============================= # =============================