Add the build preview functionality from the old build tool

This commit is contained in:
Veronica Berglyd Olsen
2023-06-03 16:51:45 +02:00
parent 3a2733eee5
commit 9514578554
2 changed files with 189 additions and 25 deletions
+11 -16
View File
@@ -22,7 +22,6 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import json import json
@@ -31,6 +30,7 @@ import hashlib
import logging import logging
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import Any
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from configparser import ConfigParser from configparser import ConfigParser
@@ -49,9 +49,8 @@ logger = logging.getLogger(__name__)
# Checker Functions # Checker Functions
# =============================================================================================== # # =============================================================================================== #
def checkStringNone(value, default): def checkStringNone(value: Any, default: str | None) -> str | None:
"""Check if a variable is a string or a None. """Check if a variable is a string or a None."""
"""
if value is None or value == "None": if value is None or value == "None":
return None return None
if isinstance(value, str): if isinstance(value, str):
@@ -59,35 +58,31 @@ def checkStringNone(value, default):
return default return default
def checkString(value, default): def checkString(value: Any, default: str) -> str:
"""Check if a variable is a string. """Check if a variable is a string."""
"""
if isinstance(value, str): if isinstance(value, str):
return str(value) return str(value)
return default return default
def checkInt(value, default): def checkInt(value: Any, default: int) -> int:
"""Check if a variable is an integer. """Check if a variable is an integer."""
"""
try: try:
return int(value) return int(value)
except Exception: except Exception:
return default return default
def checkFloat(value, default): def checkFloat(value: Any, default: float) -> float:
"""Check if a variable is a float. """Check if a variable is a float."""
"""
try: try:
return float(value) return float(value)
except Exception: except Exception:
return default return default
def checkBool(value, default): def checkBool(value: Any, default: bool) -> bool:
"""Check if a variable is a boolean. """Check if a variable is a boolean."""
"""
if isinstance(value, bool): if isinstance(value, bool):
return value return value
elif isinstance(value, str): elif isinstance(value, str):
+178 -9
View File
@@ -28,16 +28,19 @@ import logging
from time import time from time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from datetime import datetime
from PyQt5.QtGui import QCursor from PyQt5.QtGui import QColor, QCursor, QFont, QPalette, QResizeEvent
from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtCore import QTimer, Qt, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QListWidget, QListWidgetItem, QMenu, QProgressBar, QDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QMenu,
QPushButton, QSplitter, QTextBrowser, QVBoxLayout, QWidget, qApp QProgressBar, QPushButton, QSplitter, QTextBrowser, QVBoxLayout, QWidget,
qApp
) )
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkInt, fuzzyTime
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.buildsettings import BuildCollection, BuildSettings from novelwriter.core.buildsettings import BuildCollection, BuildSettings
@@ -178,6 +181,21 @@ class GuiManuscript(QDialog):
"""Load dialog content from project data.""" """Load dialog content from project data."""
self._builds.loadCollection() self._builds.loadCollection()
self._updateBuildsList() self._updateBuildsList()
logger.debug("Loading build cache")
cache = CONFIG.dataPath("cache") / f"build_{self.theProject.data.uuid}.json"
if cache.is_file():
try:
with open(cache, mode="r", encoding="utf-8") as fObj:
data = json.load(fObj)
build = self._builds.getBuild(data.get("uuid", ""))
if isinstance(build, BuildSettings):
self._updatePreview(data, build)
except Exception:
logger.error("Failed to save build cache")
logException()
return
return return
## ##
@@ -245,13 +263,16 @@ class GuiManuscript(QDialog):
buildObj = docBuild.lastBuild buildObj = docBuild.lastBuild
assert isinstance(buildObj, ToHtml) assert isinstance(buildObj, ToHtml)
result = { result = {
"uuid": build.buildID,
"time": int(time()), "time": int(time()),
"style": buildObj.getStyleSheet(), "styles": buildObj.getStyleSheet(),
"html": buildObj.fullHTML, "html": buildObj.fullHTML,
} }
self._updatePreview(result, build)
logger.debug("Saving build cache") logger.debug("Saving build cache")
cache = CONFIG.dataPath("cache") / f"build_{build.buildID}.json" cache = CONFIG.dataPath("cache") / f"build_{self.theProject.data.uuid}.json"
try: try:
with open(cache, mode="w+", encoding="utf-8") as outFile: with open(cache, mode="w+", encoding="utf-8") as outFile:
outFile.write(json.dumps(result, indent=2)) outFile.write(json.dumps(result, indent=2))
@@ -260,8 +281,6 @@ class GuiManuscript(QDialog):
logException() logException()
return return
self.manPreview.setContent(result)
return return
@pyqtSlot() @pyqtSlot()
@@ -274,6 +293,19 @@ class GuiManuscript(QDialog):
# Internal Functions # Internal Functions
## ##
def _updatePreview(self, data: dict, build: BuildSettings):
"""Update the preview widget and set relevant values."""
self.manPreview.setContent(data)
self.manPreview.setBuildName(build.name)
self.manPreview.setTextFont(
build.getStr("format.textFont"),
build.getInt("format.textSize")
)
self.manPreview.setJustify(
build.getBool("format.justifyText")
)
return
def _getSelectedBuild(self) -> BuildSettings | None: def _getSelectedBuild(self) -> BuildSettings | None:
"""Get the currently selected build.""" """Get the currently selected build."""
bItems = self.buildList.selectedItems() bItems = self.buildList.selectedItems()
@@ -350,26 +382,163 @@ class _PreviewWidget(QTextBrowser):
self.mainTheme = mainGui.mainTheme self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject self.theProject = mainGui.theProject
self._docTime = 0
self._buildName = ""
# Document Setup
dPalette = self.palette()
dPalette.setColor(QPalette.Base, QColor(255, 255, 255))
dPalette.setColor(QPalette.Text, QColor(0, 0, 0))
self.setPalette(dPalette)
self.setMinimumWidth(40*self.mainGui.mainTheme.textNWidth)
self.setTextFont(CONFIG.textFont, CONFIG.textSize)
self.setTabStopDistance(CONFIG.getTabWidth())
self.setOpenExternalLinks(False)
self.document().setDocumentMargin(CONFIG.getTextMargin())
self.setPlaceholderText(self.tr(
"Press the \"Build Preview\" button to generate ..."
))
# Document Age
aPalette = self.palette()
aPalette.setColor(QPalette.Background, aPalette.toolTipBase().color())
aPalette.setColor(QPalette.Foreground, aPalette.toolTipText().color())
aFont = self.font()
aFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
self.ageLabel = QLabel("", self)
self.ageLabel.setIndent(0)
self.ageLabel.setFont(aFont)
self.ageLabel.setPalette(aPalette)
self.ageLabel.setAutoFillBackground(True)
self.ageLabel.setAlignment(Qt.AlignCenter)
self.ageLabel.setFixedHeight(int(2.1*self.mainTheme.fontPixelSize))
self._updateDocMargins()
self._updateBuildAge()
# Age Timer
self.ageTimer = QTimer()
self.ageTimer.setInterval(10)
self.ageTimer.timeout.connect(self._updateBuildAge)
self.ageTimer.start()
return return
##
# Setters
##
def setBuildName(self, name: str):
"""Set the build name for the document label."""
self._buildName = name
self._updateBuildAge()
return
def setJustify(self, state: bool):
"""Enable/disable the justify text option."""
options = self.document().defaultTextOption()
if state:
options.setAlignment(Qt.AlignJustify)
else:
options.setAlignment(Qt.AlignAbsolute)
self.document().setDefaultTextOption(options)
return
def setTextFont(self, family: str, size: int):
"""Set the text font properties."""
font = QFont()
font.setFamily(family)
font.setPointSize(size)
self.setFont(font)
return
##
# Methods
##
def setContent(self, data: dict): def setContent(self, data: dict):
"""Set the content of the preview widget.""" """Set the content of the preview widget."""
sPos = self.verticalScrollBar().value() sPos = self.verticalScrollBar().value()
qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
styles = "\n".join(data.get("styles", [
"h1, h2 {color: rgb(66, 113, 174);}",
"h3, h4 {color: rgb(50, 50, 50);}",
"a {color: rgb(66, 113, 174);}",
".tags {color: rgb(245, 135, 31); font-weight: bold;}",
]))
self.document().setDefaultStyleSheet(styles)
html = "".join(data.get("html", [])) html = "".join(data.get("html", []))
html = html.replace("\t", "!!tab!!") html = html.replace("\t", "!!tab!!")
html = html.replace("<del>", "<span style='text-decoration: line-through;'>") html = html.replace("<del>", "<span style='text-decoration: line-through;'>")
html = html.replace("</del>", "</span>") html = html.replace("</del>", "</span>")
self.setHtml(html) self.setHtml(html)
qApp.processEvents() qApp.processEvents()
while self.find("!!tab!!"): while self.find("!!tab!!"):
theCursor = self.textCursor() theCursor = self.textCursor()
theCursor.insertText("\t") theCursor.insertText("\t")
self.verticalScrollBar().setValue(sPos) self.verticalScrollBar().setValue(sPos)
self._docTime = checkInt(data.get("time"), 0)
self._updateBuildAge()
# Since we change the content while it may still be rendering, we mark
# the document dirty again to make sure it's re-rendered properly.
self.document().markContentsDirty(0, self.document().characterCount())
qApp.restoreOverrideCursor()
return return
##
# Events
##
def resizeEvent(self, event: QResizeEvent):
"""Capture resize and update the document margins."""
super().resizeEvent(event)
self._updateDocMargins()
return
##
# Private Slots
##
@pyqtSlot()
def _updateBuildAge(self):
"""Update the build time and the fuzzy age."""
if self._docTime > 0:
strBuildTime = "%s (%s)" % (
datetime.fromtimestamp(self._docTime).strftime("%x %X"),
fuzzyTime(time() - self._docTime)
)
else:
strBuildTime = self.tr("Unknown")
text = "{0} {1}".format(self.tr("Built"), strBuildTime)
if self._buildName:
text = "<b>{0}</b><br>{1}".format(self._buildName, text)
self.ageLabel.setText(text)
return
##
# Internal Functions
##
def _updateDocMargins(self):
"""Automatically adjust the header to fill the top of the
document within the viewport.
"""
vBar = self.verticalScrollBar()
sW = vBar.width() if vBar.isVisible() else 0
tB = self.frameWidth()
tW = self.width() - 2*tB - sW
tH = self.ageLabel.height()
self.ageLabel.setGeometry(tB, tB, tW, tH)
self.setViewportMargins(0, tH, 0, 0)
return
# END Class _PreviewWidget # END Class _PreviewWidget