Add the build preview functionality from the old build tool
This commit is contained in:
+11
-16
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user