diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index 5ecc6f32..91f97ebf 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -63,12 +63,14 @@ SETTINGS_TEMPLATE = { "text.includeComments": (bool, False), "text.includeKeywords": (bool, False), "text.includeBody": (bool, True), + "text.addNoteHeadings": (bool, True), "format.buildLang": (str, "en_GB"), "format.textFont": (str, ""), "format.textSize": (int, 12), "format.lineHeight": (float, 1.15, 0.75, 3.0), "format.justifyText": (bool, False), "format.stripUnicode": (bool, False), + "format.replaceTabs": (bool, False), "odt.addColours": (bool, True), "html.addStyles": (bool, False), } @@ -93,6 +95,7 @@ SETTINGS_LABELS = { "text.includeComments": QT_TRANSLATE_NOOP("Builds", "Comments"), "text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Keywords"), "text.includeBody": QT_TRANSLATE_NOOP("Builds", "Body Text"), + "text.addNoteHeadings": QT_TRANSLATE_NOOP("Builds", "Add Titles for Notes"), "format": QT_TRANSLATE_NOOP("Builds", "Text Format"), "format.buildLang": QT_TRANSLATE_NOOP("Builds", "Build Language"), @@ -101,6 +104,7 @@ SETTINGS_LABELS = { "format.lineHeight": QT_TRANSLATE_NOOP("Builds", "Line Height"), "format.justifyText": QT_TRANSLATE_NOOP("Builds", "Justify Text Margins"), "format.stripUnicode": QT_TRANSLATE_NOOP("Builds", "Replace Unicode Characters"), + "format.replaceTabs": QT_TRANSLATE_NOOP("Builds", "Replace Tabs with Spaces"), "odt": QT_TRANSLATE_NOOP("Builds", "Open Document"), "odt.addColours": QT_TRANSLATE_NOOP("Builds", "Add Highlight Colours"), @@ -159,10 +163,33 @@ class BuildSettings: """ return SETTINGS_LABELS.get(key, "ERROR") - def getValue(self, key: str) -> str | int | bool | float: - """Get the value for a specific item, or return the default. + def getStr(self, key: str) -> str: + """Type safe value access for strings. """ - return self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1])) + value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1])) + return str(value) + + def getBool(self, key: str) -> bool: + """Type safe value access for bools. + """ + value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1])) + return bool(value) + + def getInt(self, key: str) -> int: + """Type safe value access for integers. + """ + value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1])) + if isinstance(value, int): + return value + return 0 + + def getFloat(self, key: str) -> float: + """Type safe value access for float. + """ + value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1])) + if isinstance(value, float): + return value + return 0.0 ## # Setters @@ -258,9 +285,9 @@ class BuildSettings: if not isinstance(project, NWProject): return result - incNovel = bool(self.getValue("filter.includeNovel")) - incNotes = bool(self.getValue("filter.includeNotes")) - incInactive = bool(self.getValue("filter.includeInactive")) + incNovel = bool(self.getBool("filter.includeNovel")) + incNotes = bool(self.getBool("filter.includeNotes")) + incInactive = bool(self.getBool("filter.includeInactive")) for item in project.tree: tHandle = item.itemHandle diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 64f4326a..63020838 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -22,27 +22,33 @@ 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 logging +from pathlib import Path +from typing import Iterable -from PyQt5.QtGui import QFont, QFontInfo +from PyQt5.QtGui import QFont, QFontDatabase, QFontInfo from novelwriter import CONFIG +from novelwriter.core.tokenizer import Tokenizer from novelwriter.error import formatException from novelwriter.core.tomd import ToMarkdown from novelwriter.core.toodt import ToOdt from novelwriter.core.tohtml import ToHtml +from novelwriter.core.project import NWProject +from novelwriter.core.buildsettings import BuildSettings logger = logging.getLogger(__name__) class NWBuildDocument: - def __init__(self, project): + def __init__(self, project: NWProject, build: BuildSettings): self._project = project - self._build = {} - self._documents = [] + self._build = build + self._queue = [] self._error = None return @@ -52,41 +58,43 @@ class NWBuildDocument: ## @property - def error(self): + def error(self) -> str | None: return self._error @property - def buildLength(self): - return len(self._documents) - - ## - # Setters - ## - - def setBuildConfig(self, config): - """Set the build config dictionary. - """ - self._build = config - return - - def addDocument(self, tHandle): - """Add a document to the build queue. - """ - self._documents.append(tHandle) - return + def buildLength(self) -> int: + return len(self._queue) ## # Methods ## - def iterBuildOpenDocument(self, savePath, isFlat): + def addDocument(self, tHandle: str): + """Add a document to the build queue manually. + """ + self._queue.append(tHandle) + return + + def queueAll(self): + """Queue all document as defined by the build setup. + """ + filtered = self._build.buildItemFilter(self._project) + noteTitles = self._build.getValue("text.addNoteHeadings") + for item in self._project.tree: + if filtered.get(item.itemHandle, False): + self._queue.append(item.itemHandle) + elif item.isRootType() and noteTitles: + self._queue.append(item.itemHandle) + return + + def iterBuildOpenDocument(self, savePath: Path, isFlat: bool) -> Iterable[tuple[int, bool]]: """Build an Open Document file. """ makeOdt = ToOdt(self._project, isFlat=isFlat) self._setupBuild(makeOdt) makeOdt.initDocument() - for i, tHandle in enumerate(self._documents): + for i, tHandle in enumerate(self._queue): yield i, self._doBuild(makeOdt, tHandle) makeOdt.closeDocument() @@ -102,16 +110,16 @@ class NWBuildDocument: return - def iterBuildHTML(self, savePath): + def iterBuildHTML(self, savePath: Path) -> Iterable[tuple[int, bool]]: """Build an HTML file. """ makeHtml = ToHtml(self._project) self._setupBuild(makeHtml) - if self._build.get("process.replaceTabs", False): + if self._build.getValue("format.replaceTabs"): makeHtml.replaceTabs() - for i, tHandle in enumerate(self._documents): + for i, tHandle in enumerate(self._queue): yield i, self._doBuild(makeHtml, tHandle) self._error = None @@ -122,7 +130,7 @@ class NWBuildDocument: return - def iterBuildMarkdown(self, savePath, extendedMd): + def iterBuildMarkdown(self, savePath: Path, extendedMd: bool) -> Iterable[tuple[int, bool]]: """Build a Markdown file. """ makeMd = ToMarkdown(self._project) @@ -133,10 +141,10 @@ class NWBuildDocument: else: makeMd.setStandardMarkdown() - if self._build.get("process.replaceTabs", False): + if self._build.getValue("format.replaceTabs"): makeMd.replaceTabs(nSpaces=4, spaceChar=" ") - for i, tHandle in enumerate(self._documents): + for i, tHandle in enumerate(self._queue): yield i, self._doBuild(makeMd, tHandle) self._error = None @@ -151,34 +159,44 @@ class NWBuildDocument: # Internal Functions ## - def _setupBuild(self, bldObj): + def _setupBuild(self, bldObj: Tokenizer): """Configure the build object. """ # Get Settings - fmtTitle = self._build.get("format.fmtTitle", "%title%") - fmtChapter = self._build.get("format.fmtChapter", "%title%") - fmtUnnumbered = self._build.get("format.fmtUnnumbered", "%title%") - fmtScene = self._build.get("format.fmtScene", "%title%") - fmtSection = self._build.get("format.fmtSection", "%title%") - buildLang = self._build.get("format.buildLang", "en_GB") - hideScene = self._build.get("format.hideScene", False) - hideSection = self._build.get("format.hideSection", False) - textFont = self._build.get("format.textFont", CONFIG.textFont) - textSize = self._build.get("format.textSize", CONFIG.textSize) - lineHeight = self._build.get("format.lineHeight", 1.15) - justifyText = self._build.get("format.justifyText", False) - noStyling = self._build.get("format.noStyling", False) - replaceUCode = self._build.get("format.replaceUCode", False) - incSynopsis = self._build.get("filter.includeSynopsis", False) - incComments = self._build.get("filter.includeComments", False) - incKeywords = self._build.get("filter.includeKeywords", False) - includeBody = self._build.get("filter.includeBody", True) + fmtTitle = self._build.getStr("headings.fmtTitle") + fmtChapter = self._build.getStr("headings.fmtChapter") + fmtUnnumbered = self._build.getStr("headings.fmtUnnumbered") + fmtScene = self._build.getStr("headings.fmtScene") + fmtSection = self._build.getStr("headings.fmtSection") + hideScene = self._build.getBool("headings.hideScene") + hideSection = self._build.getBool("headings.hideSection") + + incSynopsis = self._build.getBool("text.includeSynopsis") + incComments = self._build.getBool("text.includeComments") + incKeywords = self._build.getBool("text.includeKeywords") + includeBody = self._build.getBool("text.includeBody") + + buildLang = self._build.getStr("format.buildLang") + textFont = self._build.getStr("format.textFont") + textSize = self._build.getInt("format.textSize") + lineHeight = self._build.getFloat("format.lineHeight") + justifyText = self._build.getBool("format.justifyText") + replaceUCode = self._build.getBool("format.stripUnicode") + + odtAddColours = self._build.getBool("odt.addColours") + htmlAddStyles = self._build.getBool("html.addStyles") # The language lookup dict is reloaded if needed self._project.setProjectLang(buildLang) # Get font information - fontInfo = QFontInfo(QFont(textFont, textSize)) + if not textFont: + textFont = str(CONFIG.textFont) + if not textFont: + textFont = QFontDatabase.systemFont(QFontDatabase.GeneralFont).family() + + bldFont = QFont(family=textFont, pointSize=textSize) + fontInfo = QFontInfo(bldFont) textFixed = fontInfo.fixedPitch() bldObj.setTitleFormat(fmtTitle) @@ -197,16 +215,16 @@ class NWBuildDocument: bldObj.setBodyText(includeBody) if isinstance(bldObj, ToHtml): - bldObj.setStyles(not noStyling) + bldObj.setStyles(htmlAddStyles) bldObj.setReplaceUnicode(replaceUCode) if isinstance(bldObj, ToOdt): - bldObj.setColourHeaders(not noStyling) + bldObj.setColourHeaders(odtAddColours) bldObj.setLanguage(buildLang) return - def _doBuild(self, bldObj, tHandle): + def _doBuild(self, bldObj: Tokenizer, tHandle: str) -> bool: """Build a single document and add it to the build object. """ self._error = None @@ -226,7 +244,6 @@ class NWBuildDocument: bldObj.tokenizeText() bldObj.doHeaders() bldObj.doConvert() - bldObj.doPostProcessing() else: logger.info(f"Build: Skipping '{tHandle}'") diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index cdebd0ea..986f1d26 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -30,8 +30,8 @@ from typing import TYPE_CHECKING from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( - QDialog, QHBoxLayout, QListWidget, QListWidgetItem, QPushButton, QSplitter, - QTextBrowser, QVBoxLayout, QWidget, qApp + QDialog, QHBoxLayout, QListWidget, QListWidgetItem, QMenu, QProgressBar, + QPushButton, QSplitter, QTextBrowser, QVBoxLayout, QWidget, qApp ) from novelwriter import CONFIG @@ -69,8 +69,8 @@ class GuiBuildManuscript(QDialog): CONFIG.pxInt(pOptions.getInt("GuiBuildManuscript", "winHeight", hWin)) ) - # Controls - # ======== + # Build Controls + # ============== self.buildList = QListWidget() @@ -80,21 +80,57 @@ class GuiBuildManuscript(QDialog): self.btnEdit = QPushButton(self.tr("Edit")) self.btnEdit.clicked.connect(self._editSelectedBuild) - self.btnDel = QPushButton(self.tr("Delete")) + self.btnDelete = QPushButton(self.tr("Delete")) - self.buttonBox = QHBoxLayout() - self.buttonBox.addWidget(self.btnNew) - self.buttonBox.addWidget(self.btnEdit) - self.buttonBox.addWidget(self.btnDel) + # Process Controls + # ================ + self.buildProgress = QProgressBar() + self.btnPreview = QPushButton(self.tr("Build Preview")) self.manPreview = GuiManuscriptPreview(self) + 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")) + # 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.buttonBox) + 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) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index f7d69287..a5200d33 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -457,19 +457,19 @@ class GuiBuildFilterTab(QWidget): self.mainTheme.getIcon("proj_scene"), self._build.getLabel("filter.includeNovel"), "doc:filter.includeNovel", - default=self._build.getValue("filter.includeNovel") or False + default=self._build.getBool("filter.includeNovel") ) self.filterOpt.addItem( self.mainTheme.getIcon("proj_note"), self._build.getLabel("filter.includeNotes"), "doc:filter.includeNotes", - default=self._build.getValue("filter.includeNotes") or False + default=self._build.getBool("filter.includeNotes") ) self.filterOpt.addItem( self.mainTheme.getIcon("unchecked"), self._build.getLabel("filter.includeInactive"), "doc:filter.includeInactive", - default=self._build.getValue("filter.includeInactive") or False + default=self._build.getBool("filter.includeInactive") ) self.filterOpt.addSeparator() @@ -708,13 +708,13 @@ class GuiBuildHeadingsTab(QWidget): def loadContent(self): """Populate the widgets. """ - self.fmtTitle.setText(str(self._build.getValue("headings.fmtTitle"))) - self.fmtChapter.setText(str(self._build.getValue("headings.fmtChapter"))) - self.fmtUnnumbered.setText(str(self._build.getValue("headings.fmtUnnumbered"))) - self.fmtScene.setText(str(self._build.getValue("headings.fmtScene"))) - self.fmtSection.setText(str(self._build.getValue("headings.fmtSection"))) - self.swtScene.setChecked(bool(self._build.getValue("headings.hideScene"))) - self.swtSection.setChecked(bool(self._build.getValue("headings.hideSection"))) + self.fmtTitle.setText(self._build.getStr("headings.fmtTitle")) + self.fmtChapter.setText(self._build.getStr("headings.fmtChapter")) + self.fmtUnnumbered.setText(self._build.getStr("headings.fmtUnnumbered")) + self.fmtScene.setText(self._build.getStr("headings.fmtScene")) + self.fmtSection.setText(self._build.getStr("headings.fmtSection")) + self.swtScene.setChecked(self._build.getBool("headings.hideScene")) + self.swtSection.setChecked(self._build.getBool("headings.hideSection")) return ##