diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 445f8b71..da807c6d 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -85,6 +85,7 @@ class nwFiles: OPTS_FILE = "guiOptions.json" RECENT_FILE = "recentProjects.json" BUILD_CACHE = "prevBuild.json" + BUILDS_FILE = "builds.json" # END Class nwFiles diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index a8d95692..5ecc6f32 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -4,7 +4,8 @@ novelWriter – Build Settings Class A class to hold build settings for the build tool File History: -Created: 2023-02-14 [2.1b1] +Created: 2023-02-14 [2.1b1] BuildSettings +Created: 2023-05-22 [2.1b1] BuildCollection This file is a part of novelWriter Copyright 2018–2023, Veronica Berglyd Olsen @@ -24,17 +25,21 @@ along with this program. If not, see . """ from __future__ import annotations +import json import uuid import logging from enum import Enum +from typing import Iterable +from pathlib import Path from PyQt5.QtCore import QT_TRANSLATE_NOOP -from novelwriter.common import checkUuid, isHandle -from novelwriter.constants import nwHeadingFormats +from novelwriter.common import checkUuid, isHandle, jsonEncode +from novelwriter.constants import nwFiles, nwHeadingFormats from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject +from novelwriter.error import logException logger = logging.getLogger(__name__) @@ -305,18 +310,21 @@ class BuildSettings: "name": self._name, "uuid": self._uuid, "settings": self._settings.copy(), - "included": list(self._included), - "excluded": list(self._excluded), - "skipRoot": list(self._skipRoot), + "content": { + "included": list(self._included), + "excluded": list(self._excluded), + "skipRoot": list(self._skipRoot), + } } def unpack(self, data: dict): """Unpack a dictionary and populate the class. """ - included = data.get("included", []) - excluded = data.get("excluded", []) - skipRoot = data.get("skipRoot", []) settings = data.get("settings", {}) + content = data.get("content", {}) + included = content.get("included", []) + excluded = content.get("excluded", []) + skipRoot = content.get("skipRoot", []) self.setName(data.get("name", "")) self.setBuildID(data.get("uuid", "")) @@ -337,3 +345,90 @@ class BuildSettings: return # END Class BuildSettings + + +class BuildCollection: + + def __init__(self, project: NWProject): + self._project = project + self._builds = {} + return + + def getBuild(self, buildID: str) -> BuildSettings | None: + """ + """ + if buildID not in self._builds: + return None + build = BuildSettings() + build.unpack(self._builds[buildID]) + return build + + def setBuild(self, build: BuildSettings) -> bool: + """ + """ + if not isinstance(build, BuildSettings): + return False + buildID = build.buildID + self._builds[buildID] = build.pack() + return True + + def builds(self) -> Iterable[tuple[str, str]]: + """ + """ + for buildID in self._builds: + yield buildID, self._builds[buildID].get("name", "") + return + + def loadCollection(self) -> bool: + """ + """ + buildsFile = self._project.storage.getMetaFile(nwFiles.BUILDS_FILE) + if not isinstance(buildsFile, Path): + return False + + data = {} + if buildsFile.exists(): + logger.debug("Loading builds file") + try: + with open(buildsFile, mode="r", encoding="utf-8") as inFile: + data = json.load(inFile) + except Exception: + logger.error("Failed to load builds file") + logException() + return False + + if not isinstance(data, dict): + logger.error("Builds file is not a JSON object") + return False + + builds = data.get("novelWriter.builds", None) + if not isinstance(builds, dict): + logger.error("No novelWriter.builds in the builds file") + return False + + for key, entry in builds.items(): + if isinstance(entry, dict): + self._builds[key] = entry + + return True + + def saveCollection(self) -> bool: + """ + """ + buildsFile = self._project.storage.getMetaFile(nwFiles.BUILDS_FILE) + if not isinstance(buildsFile, Path): + return False + + logger.debug("Saving builds file") + try: + data = {"novelWriter.builds": self._builds} + with open(buildsFile, mode="w+", encoding="utf-8") as outFile: + outFile.write(jsonEncode(data, nmax=4)) + except Exception: + logger.error("Failed to save builds file") + logException() + return False + + return True + +# END Class BuildCollection diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index 912532b7..cdebd0ea 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -28,14 +28,14 @@ import logging from typing import TYPE_CHECKING -from PyQt5.QtCore import pyqtSlot +from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( - QDialog, QGridLayout, QPushButton, QSplitter, QTextBrowser, QVBoxLayout, - QWidget, qApp + QDialog, QHBoxLayout, QListWidget, QListWidgetItem, QPushButton, QSplitter, + QTextBrowser, QVBoxLayout, QWidget, qApp ) from novelwriter import CONFIG -from novelwriter.core.buildsettings import BuildSettings +from novelwriter.core.buildsettings import BuildCollection, BuildSettings from novelwriter.tools.manussettings import GuiBuildSettings if TYPE_CHECKING: @@ -53,6 +53,9 @@ class GuiBuildManuscript(QDialog): 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)) @@ -69,19 +72,32 @@ class GuiBuildManuscript(QDialog): # Controls # ======== - self.btnNew = QPushButton(self.tr("New Build")) + self.buildList = QListWidget() + + self.btnNew = QPushButton(self.tr("New")) self.btnNew.clicked.connect(self._createNewBuild) - self.optsGrid = QGridLayout() - self.optsGrid.addWidget(self.btnNew, 0, 0) + self.btnEdit = QPushButton(self.tr("Edit")) + self.btnEdit.clicked.connect(self._editSelectedBuild) + + self.btnDel = QPushButton(self.tr("Delete")) + + self.buttonBox = QHBoxLayout() + self.buttonBox.addWidget(self.btnNew) + self.buttonBox.addWidget(self.btnEdit) + self.buttonBox.addWidget(self.btnDel) self.manPreview = GuiManuscriptPreview(self) # Assemble GUI # ============ + self.controlBox = QVBoxLayout() + self.controlBox.addWidget(self.buildList) + self.controlBox.addLayout(self.buttonBox) + self.optsWidget = QWidget() - self.optsWidget.setLayout(self.optsGrid) + self.optsWidget.setLayout(self.controlBox) self.mainSplit = QSplitter() self.mainSplit.addWidget(self.optsWidget) @@ -101,6 +117,8 @@ class GuiBuildManuscript(QDialog): def loadContent(self): """ """ + self._builds.loadCollection() + self._updateBuildsList() return ## @@ -124,22 +142,27 @@ class GuiBuildManuscript(QDialog): """ build = BuildSettings() build.setName(self.tr("My Manuscript")) - - dlgSettings = GuiBuildSettings(self.mainGui, build) - dlgSettings.setModal(False) - dlgSettings.show() - dlgSettings.raise_() - qApp.processEvents() - dlgSettings.loadContent() - dlgSettings.newSettingsReady.connect(self._processNewSettings) - + self._openSettingsDialog(build) return - @pyqtSlot(dict) - def _processNewSettings(self, data: dict): + @pyqtSlot() + def _editSelectedBuild(self): + """Edit the currently selected build settings entry. """ + bItems = self.buildList.selectedItems() + if bItems: + build = self._builds.getBuild(bItems[0].data(Qt.UserRole)) + if isinstance(build, BuildSettings): + self._openSettingsDialog(build) + return + + @pyqtSlot(BuildSettings) + def _processNewSettings(self, build: BuildSettings): + """Process new build settings from the settings dialog. """ - print(data) + self._builds.setBuild(build) + self._builds.saveCollection() + self._updateBuildItem(build) return ## @@ -167,6 +190,40 @@ class GuiBuildManuscript(QDialog): return + def _openSettingsDialog(self, build: BuildSettings): + """Open a new build settings dialog. + """ + dlgSettings = GuiBuildSettings(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 GuiBuildManuscript diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 27cfdb0f..f7d69287 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -59,7 +59,7 @@ class GuiBuildSettings(QDialog): OPT_CONTENT = 4 OPT_OUTPUT = 5 - newSettingsReady = pyqtSignal(dict) + newSettingsReady = pyqtSignal(BuildSettings) def __init__(self, mainGui: GuiMain, build: BuildSettings): super().__init__(parent=mainGui) @@ -194,7 +194,7 @@ class GuiBuildSettings(QDialog): role = self.dlgButtons.buttonRole(button) if role in (QDialogButtonBox.ApplyRole, QDialogButtonBox.AcceptRole): self._build.setName(self.editBuildName.text()) - self.newSettingsReady.emit(self._build.pack()) + self.newSettingsReady.emit(self._build) self._saveSettings() if role == QDialogButtonBox.AcceptRole: