"""
novelWriter – GUI Build Manuscript
==================================
GUI classes for the Manuscript Build Tool
File History:
Created: 2023-05-13 [2.1b1] GuiManuscript
Created: 2023-05-13 [2.1b1] GuiManuscriptPreview
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 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,
QPushButton, QSplitter, QTextBrowser, QVBoxLayout, QWidget, qApp
)
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
if TYPE_CHECKING:
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__)
class GuiManuscript(QDialog):
def __init__(self, mainGui: GuiMain):
super().__init__(parent=mainGui)
logger.debug("Created: GuiManuscript")
self.setObjectName("GuiManuscript")
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
self._builds = BuildCollection(self.theProject)
self._buildMap = {}
self.setWindowTitle(self.tr("Build Manuscript"))
self.setMinimumWidth(CONFIG.pxInt(600))
self.setMinimumHeight(CONFIG.pxInt(500))
wWin = CONFIG.pxInt(900)
hWin = CONFIG.pxInt(600)
pOptions = self.theProject.options
self.resize(
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "winWidth", wWin)),
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "winHeight", hWin))
)
# Build Controls
# ==============
self.buildList = QListWidget()
self.btnNew = QPushButton(self.tr("New"))
self.btnNew.clicked.connect(self._createNewBuild)
self.btnEdit = QPushButton(self.tr("Edit"))
self.btnEdit.clicked.connect(self._editSelectedBuild)
self.btnDelete = QPushButton(self.tr("Delete"))
# Process Controls
# ================
self.buildProgress = QProgressBar()
self.btnPreview = QPushButton(self.tr("Build Preview"))
self.btnPreview.clicked.connect(self._generatePreview)
self.manPreview = GuiManuscriptPreview(self.mainGui)
self.menuPrint = QMenu(self)
self.aPrintSend = self.menuPrint.addAction(self.tr("Print Preview"))
self.aPrintFile = self.menuPrint.addAction(self.tr("Print to PDF"))
self.menuSave = QMenu(self)
self.aSaveODT = self.menuSave.addAction(self.tr("Open Document (.odt)"))
self.aSaveFODT = self.menuSave.addAction(self.tr("Flat Open Document (.fodt)"))
self.aSaveHTM = self.menuSave.addAction(self.tr("novelWriter HTML (.htm)"))
self.aSaveNWD = self.menuSave.addAction(self.tr("novelWriter Markdown (.nwd)"))
self.aSaveMD = self.menuSave.addAction(self.tr("Standard Markdown (.md)"))
self.aSaveGH = self.menuSave.addAction(self.tr("GitHub Markdown (.md)"))
self.aSaveJsonH = self.menuSave.addAction(self.tr("JSON + novelWriter HTML (.json)"))
self.aSaveJsonM = self.menuSave.addAction(self.tr("JSON + novelWriter Markdown (.json)"))
self.btnPrint = QPushButton(self.tr("Print"))
self.btnPrint.setMenu(self.menuPrint)
self.btnSave = QPushButton(self.tr("Save As"))
self.btnSave.setMenu(self.menuSave)
self.btnClose = QPushButton(self.tr("Close"))
self.btnClose.clicked.connect(self._doClose)
# Assemble GUI
# ============
self.buildBox = QHBoxLayout()
self.buildBox.addWidget(self.btnNew)
self.buildBox.addWidget(self.btnEdit)
self.buildBox.addWidget(self.btnDelete)
self.processBox = QHBoxLayout()
self.processBox.addWidget(self.btnSave)
self.processBox.addWidget(self.btnPrint)
self.processBox.addWidget(self.btnClose)
self.controlBox = QVBoxLayout()
self.controlBox.addWidget(self.buildList)
self.controlBox.addLayout(self.buildBox)
self.controlBox.addWidget(self.buildProgress)
self.controlBox.addWidget(self.btnPreview)
self.controlBox.addLayout(self.processBox)
self.controlBox.setContentsMargins(0, 0, 0, 0)
self.optsWidget = QWidget()
self.optsWidget.setLayout(self.controlBox)
self.mainSplit = QSplitter()
self.mainSplit.addWidget(self.optsWidget)
self.mainSplit.addWidget(self.manPreview)
self.mainSplit.setSizes([
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "optsWidth", wWin//3)),
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "viewWidth", 2*wWin//3)),
])
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.mainSplit)
self.setLayout(self.outerBox)
self.setSizeGripEnabled(True)
logger.debug("Ready: GuiManuscript")
return
def __del__(self):
logger.debug("Deleted: GuiManuscript")
return
def loadContent(self):
"""Load dialog content from project data.
"""
self._builds.loadCollection()
self._updateBuildsList()
return
##
# Events
##
def closeEvent(self, event):
"""Capture the user closing the window so we can save settings.
"""
self._saveSettings()
for obj in self.children():
# Make sure we don't have any settings windows open
if isinstance(obj, GuiBuildSettings) and obj.isVisible():
obj.close()
event.accept()
self.deleteLater()
return
##
# Private Slots
##
@pyqtSlot()
def _createNewBuild(self):
"""Open the build settings dialog for a new build.
"""
build = BuildSettings()
build.setName(self.tr("My Manuscript"))
self._openSettingsDialog(build)
return
@pyqtSlot()
def _editSelectedBuild(self):
"""Edit the currently selected build settings entry.
"""
build = self._getSelectedBuild()
if build is not None:
self._openSettingsDialog(build)
return
@pyqtSlot(BuildSettings)
def _processNewSettings(self, build: BuildSettings):
"""Process new build settings from the settings dialog.
"""
self._builds.setBuild(build)
self._builds.saveCollection()
self._updateBuildItem(build)
return
@pyqtSlot()
def _generatePreview(self):
"""Run the document builder on the current build settings for
the preview widget.
"""
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.
"""
self.close()
return
##
# 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.
"""
return
def _saveSettings(self):
"""Save the various user settings.
"""
logger.debug("Saving GuiManuscript settings")
winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height())
mainSplit = self.mainSplit.sizes()
optsWidth = CONFIG.rpxInt(mainSplit[0])
viewWidth = CONFIG.rpxInt(mainSplit[1])
pOptions = self.theProject.options
pOptions.setValue("GuiManuscript", "winWidth", winWidth)
pOptions.setValue("GuiManuscript", "winHeight", winHeight)
pOptions.setValue("GuiManuscript", "optsWidth", optsWidth)
pOptions.setValue("GuiManuscript", "viewWidth", viewWidth)
pOptions.saveSettings()
return
def _openSettingsDialog(self, build: BuildSettings):
"""Open the build settings dialog.
"""
dlgSettings = GuiBuildSettings(self, self.mainGui, build)
dlgSettings.setModal(False)
dlgSettings.show()
dlgSettings.raise_()
qApp.processEvents()
dlgSettings.loadContent()
dlgSettings.newSettingsReady.connect(self._processNewSettings)
return
def _updateBuildsList(self):
"""Update the list of available builds.
"""
self.buildList.clear()
for key, name in self._builds.builds():
bItem = QListWidgetItem()
bItem.setText(name)
bItem.setData(Qt.UserRole, key)
self.buildList.addItem(bItem)
self._buildMap[key] = bItem
return
def _updateBuildItem(self, build: BuildSettings):
"""Update the entry of a specific build item.
"""
bItem = self._buildMap.get(build.buildID, None)
if isinstance(bItem, QListWidgetItem):
bItem.setText(build.name)
else: # Propbably a new item
self._updateBuildsList()
return
# END Class GuiManuscript
class GuiManuscriptPreview(QTextBrowser):
def __init__(self, mainGui: GuiMain):
super().__init__(parent=mainGui)
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
return
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()
while self.find("!!tab!!"):
theCursor = self.textCursor()
theCursor.insertText("\t")
self.verticalScrollBar().setValue(sPos)
return
# END Class GuiManuscriptPreview