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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import json
@@ -31,6 +30,7 @@ import hashlib
import logging
import xml.etree.ElementTree as ET
from typing import Any
from pathlib import Path
from datetime import datetime
from configparser import ConfigParser
@@ -49,9 +49,8 @@ logger = logging.getLogger(__name__)
# Checker Functions
# =============================================================================================== #
def checkStringNone(value, default):
"""Check if a variable is a string or a None.
"""
def checkStringNone(value: Any, default: str | None) -> str | None:
"""Check if a variable is a string or a None."""
if value is None or value == "None":
return None
if isinstance(value, str):
@@ -59,35 +58,31 @@ def checkStringNone(value, default):
return default
def checkString(value, default):
"""Check if a variable is a string.
"""
def checkString(value: Any, default: str) -> str:
"""Check if a variable is a string."""
if isinstance(value, str):
return str(value)
return default
def checkInt(value, default):
"""Check if a variable is an integer.
"""
def checkInt(value: Any, default: int) -> int:
"""Check if a variable is an integer."""
try:
return int(value)
except Exception:
return default
def checkFloat(value, default):
"""Check if a variable is a float.
"""
def checkFloat(value: Any, default: float) -> float:
"""Check if a variable is a float."""
try:
return float(value)
except Exception:
return default
def checkBool(value, default):
"""Check if a variable is a boolean.
"""
def checkBool(value: Any, default: bool) -> bool:
"""Check if a variable is a boolean."""
if isinstance(value, bool):
return value
elif isinstance(value, str):
+178 -9
View File
@@ -28,16 +28,19 @@ import logging
from time import time
from typing import TYPE_CHECKING
from datetime import datetime
from PyQt5.QtGui import QCursor
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QColor, QCursor, QFont, QPalette, QResizeEvent
from PyQt5.QtCore import QTimer, Qt, pyqtSlot
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QListWidget, QListWidgetItem, QMenu, QProgressBar,
QPushButton, QSplitter, QTextBrowser, QVBoxLayout, QWidget, qApp
QDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QMenu,
QProgressBar, QPushButton, QSplitter, QTextBrowser, QVBoxLayout, QWidget,
qApp
)
from novelwriter import CONFIG
from novelwriter.error import logException
from novelwriter.common import checkInt, fuzzyTime
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
@@ -178,6 +181,21 @@ class GuiManuscript(QDialog):
"""Load dialog content from project data."""
self._builds.loadCollection()
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
##
@@ -245,13 +263,16 @@ class GuiManuscript(QDialog):
buildObj = docBuild.lastBuild
assert isinstance(buildObj, ToHtml)
result = {
"uuid": build.buildID,
"time": int(time()),
"style": buildObj.getStyleSheet(),
"styles": buildObj.getStyleSheet(),
"html": buildObj.fullHTML,
}
self._updatePreview(result, build)
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:
with open(cache, mode="w+", encoding="utf-8") as outFile:
outFile.write(json.dumps(result, indent=2))
@@ -260,8 +281,6 @@ class GuiManuscript(QDialog):
logException()
return
self.manPreview.setContent(result)
return
@pyqtSlot()
@@ -274,6 +293,19 @@ class GuiManuscript(QDialog):
# 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:
"""Get the currently selected build."""
bItems = self.buildList.selectedItems()
@@ -350,26 +382,163 @@ class _PreviewWidget(QTextBrowser):
self.mainTheme = mainGui.mainTheme
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
##
# 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):
"""Set the content of the preview widget."""
sPos = self.verticalScrollBar().value()
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 = html.replace("\t", "!!tab!!")
html = html.replace("<del>", "<span style='text-decoration: line-through;'>")
html = html.replace("</del>", "</span>")
self.setHtml(html)
qApp.processEvents()
while self.find("!!tab!!"):
theCursor = self.textCursor()
theCursor.insertText("\t")
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
##
# 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