From a212900554dd6f1d641d77e340bc382947f19e5c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 May 2023 19:30:42 +0200 Subject: [PATCH] Add a Headings format edit form --- novelwriter/constants.py | 13 ++ novelwriter/core/buildsettings.py | 15 +- novelwriter/tools/manussettings.py | 241 ++++++++++++++++++++++++++--- 3 files changed, 244 insertions(+), 25 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index a073307e..bb03cdfb 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -203,6 +203,19 @@ class nwLabels: # END Class nwLabels +class nwHeadingFormats: + + TITLE = "{Title}" + CH_NUM = "{Chapter}" + CH_WORD = "{Chapter:Word}" + CH_ROMU = "{Chapter:RomanU}" + CH_ROML = "{Chapter:RomanL}" + SC_NUM = "{Scene}" + SC_ABS = "{Scene:Abs}" + +# END Class nwHeadingFormats + + class nwQuotes: """Allowed quotation marks. Source: https://en.wikipedia.org/wiki/Quotation_mark diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index f914ce46..583c9cd1 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -31,6 +31,7 @@ from enum import Enum from PyQt5.QtCore import QT_TRANSLATE_NOOP from novelwriter.common import checkUuid, isHandle +from novelwriter.constants import nwHeadingFormats from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject @@ -45,13 +46,13 @@ SETTINGS_TEMPLATE = { "filter.includeNovel": (bool, True), "filter.includeNotes": (bool, False), "filter.includeInactive": (bool, False), - "headings.fmtTitle": (str, "%title%"), - "headings.fmtChapter": (str, "%title%"), - "headings.fmtUnnumbered": (str, "%title%"), - "headings.fmtScene": (str, "%title%"), - "headings.fmtSection": (str, "%title%"), + "headings.fmtTitle": (str, nwHeadingFormats.TITLE), + "headings.fmtChapter": (str, nwHeadingFormats.TITLE), + "headings.fmtUnnumbered": (str, nwHeadingFormats.TITLE), + "headings.fmtScene": (str, "* * *"), + "headings.fmtSection": (str, ""), "headings.hideScene": (bool, False), - "headings.hideSection": (bool, False), + "headings.hideSection": (bool, True), "text.includeSynopsis": (bool, False), "text.includeComments": (bool, False), "text.includeKeywords": (bool, False), @@ -222,8 +223,8 @@ class BuildSettings: return False if len(definition) == 4: value = min(max(value, definition[2]), definition[3]) + self._changed = value != self._settings[key] self._settings[key] = value - self._changed = True logger.debug(f"Build Setting '{key}' set to: {value}") return True diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index dcca1851..c7cfe5b3 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -28,16 +28,17 @@ import logging from typing import TYPE_CHECKING -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QColor, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument from PyQt5.QtCore import QEvent, QSize, Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QAbstractButton, QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, - QHBoxLayout, QHeaderView, QLabel, QLineEdit, QPushButton, QSplitter, + QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMenu, QPlainTextEdit, QPushButton, QSplitter, QStackedWidget, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG +from novelwriter.constants import nwHeadingFormats from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switchbox import NSwitchBox @@ -45,6 +46,7 @@ from novelwriter.extensions.pagedsidebar import NPagedSideBar if TYPE_CHECKING: from novelwriter.guimain import GuiMain + from novelwriter.gui.theme import GuiTheme logger = logging.getLogger(__name__) @@ -96,14 +98,14 @@ class GuiBuildSettings(QDialog): self.optSideBar.setMaximumWidth(mPx) self.optSideBar.setLabelColor(self.mainTheme.helpText) - self.optSideBar.addLabel(self.tr("Options")) + self.optSideBar.addLabel(self.tr("Content")) self.optSideBar.addButton(self.tr("Filters"), self.OPT_FILTERS) self.optSideBar.addButton(self.tr("Headings"), self.OPT_HEADINGS) self.optSideBar.addButton(self.tr("Format"), self.OPT_FORMAT) self.optSideBar.addButton(self.tr("Content"), self.OPT_CONTENT) self.optSideBar.addSeparator() - self.optSideBar.addLabel(self.tr("Build")) + self.optSideBar.addLabel(self.tr("Output")) self.optSideBar.addButton(self.tr("HTML"), self.BLD_HTML) self.optSideBar.addButton(self.tr("Markdown"), self.BLD_MARKDOWN) self.optSideBar.addButton(self.tr("Open Document"), self.BLD_ODT) @@ -173,7 +175,8 @@ class GuiBuildSettings(QDialog): """Populate the child widgets. """ self.editBuildName.setText(self._build.name) - self.optTabSelect.loadContent(self._build) + self.optTabSelect.loadContent() + self.optTabHeadings.loadContent() return ## @@ -368,10 +371,9 @@ class GuiBuildFilterTab(QWidget): return - def loadContent(self, build: BuildSettings): + def loadContent(self): """Populate the widgets. """ - self._build = build self._populateTree() self._populateFilters() return @@ -540,6 +542,12 @@ class GuiBuildFilterTab(QWidget): class GuiBuildHeadingsTab(QWidget): + EDIT_TITLE = 1 + EDIT_CHAPTER = 2 + EDIT_UNNUM = 3 + EDIT_SCENE = 4 + EDIT_SECTION = 5 + def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings): super().__init__(parent=buildMain) @@ -548,6 +556,7 @@ class GuiBuildHeadingsTab(QWidget): self.theProject = buildMain.mainGui.theProject self._build = build + self._editing = 0 iPx = self.mainTheme.baseIconSize vSp = CONFIG.pxInt(12) @@ -564,6 +573,7 @@ class GuiBuildHeadingsTab(QWidget): self.fmtTitle.setEnabled(False) self.btnTitle = QToolButton() self.btnTitle.setIcon(self.mainTheme.getIcon("edit")) + self.btnTitle.clicked.connect(lambda: self._editHeading(self.EDIT_TITLE)) wrapTitle = QHBoxLayout() wrapTitle.addWidget(self.fmtTitle) @@ -579,6 +589,7 @@ class GuiBuildHeadingsTab(QWidget): self.fmtChapter.setEnabled(False) self.btnChapter = QToolButton() self.btnChapter.setIcon(self.mainTheme.getIcon("edit")) + self.btnChapter.clicked.connect(lambda: self._editHeading(self.EDIT_CHAPTER)) wrapChapter = QHBoxLayout() wrapChapter.addWidget(self.fmtChapter) @@ -589,19 +600,20 @@ class GuiBuildHeadingsTab(QWidget): self.formatBox.addLayout(wrapChapter, 1, 1, Qt.AlignLeft) # Unnumbered Chapter Heading - self.lblUnChapter = QLabel(BuildSettings.getLabel("headings.fmtUnnumbered")) - self.fmtUnChapter = QLineEdit("") - self.fmtUnChapter.setEnabled(False) - self.btnUnChapter = QToolButton() - self.btnUnChapter.setIcon(self.mainTheme.getIcon("edit")) + self.lblUnnumbered = QLabel(BuildSettings.getLabel("headings.fmtUnnumbered")) + self.fmtUnnumbered = QLineEdit("") + self.fmtUnnumbered.setEnabled(False) + self.btnUnnumbered = QToolButton() + self.btnUnnumbered.setIcon(self.mainTheme.getIcon("edit")) + self.btnUnnumbered.clicked.connect(lambda: self._editHeading(self.EDIT_UNNUM)) - wrapUnChapter = QHBoxLayout() - wrapUnChapter.addWidget(self.fmtUnChapter) - wrapUnChapter.addWidget(self.btnUnChapter) - wrapUnChapter.setSpacing(bSp) + wrapUnnumbered = QHBoxLayout() + wrapUnnumbered.addWidget(self.fmtUnnumbered) + wrapUnnumbered.addWidget(self.btnUnnumbered) + wrapUnnumbered.setSpacing(bSp) - self.formatBox.addWidget(self.lblUnChapter, 2, 0, Qt.AlignLeft) - self.formatBox.addLayout(wrapUnChapter, 2, 1, Qt.AlignLeft) + self.formatBox.addWidget(self.lblUnnumbered, 2, 0, Qt.AlignLeft) + self.formatBox.addLayout(wrapUnnumbered, 2, 1, Qt.AlignLeft) # Scene Heading self.lblScene = QLabel(BuildSettings.getLabel("headings.fmtScene")) @@ -609,7 +621,9 @@ class GuiBuildHeadingsTab(QWidget): self.fmtScene.setEnabled(False) self.btnScene = QToolButton() self.btnScene.setIcon(self.mainTheme.getIcon("edit")) + self.btnScene.clicked.connect(lambda: self._editHeading(self.EDIT_SCENE)) self.swtScene = NSwitch(width=2*iPx, height=iPx) + self.swtScene.toggled.connect(self._hideScene) wrapScene = QHBoxLayout() wrapScene.addWidget(self.fmtScene) @@ -631,7 +645,9 @@ class GuiBuildHeadingsTab(QWidget): self.fmtSection.setEnabled(False) self.btnSection = QToolButton() self.btnSection.setIcon(self.mainTheme.getIcon("edit")) + self.btnSection.clicked.connect(lambda: self._editHeading(self.EDIT_SECTION)) self.swtSection = NSwitch(width=2*iPx, height=iPx) + self.swtSection.toggled.connect(self._hideSection) wrapSection = QHBoxLayout() wrapSection.addWidget(self.fmtSection) @@ -647,17 +663,165 @@ class GuiBuildHeadingsTab(QWidget): self.formatBox.addLayout(wrapSection, 4, 1, Qt.AlignLeft) self.formatBox.addLayout(wrapSectionHide, 4, 2, Qt.AlignLeft) + # Edit Form + # ========= + + self.lblEditForm = QLabel(self.tr("Editing: {0}").format(self.tr("None"))) + + self.editTextBox = QPlainTextEdit() + self.editTextBox.setFixedHeight(5*iPx) + self.editTextBox.setEnabled(False) + + self.formSyntax = GuiHeadingSyntax(self.editTextBox.document(), self.mainTheme) + + self.menuInsert = QMenu() + self.aInsTitle = self.menuInsert.addAction(self.tr("Title")) + self.aInsChNum = self.menuInsert.addAction(self.tr("Chapter Number")) + self.aInsChWord = self.menuInsert.addAction(self.tr("Chapter Number (Word)")) + self.aInsChRomU = self.menuInsert.addAction(self.tr("Chapter Number (Upper Case Roman)")) + self.aInsChRomL = self.menuInsert.addAction(self.tr("Chapter Number (Lower Case Roman)")) + self.aInsScNum = self.menuInsert.addAction(self.tr("Scene Number (In Chapter)")) + self.aInsScAbs = self.menuInsert.addAction(self.tr("Scene Number (Absolute)")) + + self.aInsTitle.triggered.connect(lambda: self._insertIntoForm(nwHeadingFormats.TITLE)) + self.aInsChNum.triggered.connect(lambda: self._insertIntoForm(nwHeadingFormats.CH_NUM)) + self.aInsChWord.triggered.connect(lambda: self._insertIntoForm(nwHeadingFormats.CH_WORD)) + self.aInsChRomU.triggered.connect(lambda: self._insertIntoForm(nwHeadingFormats.CH_ROMU)) + self.aInsChRomL.triggered.connect(lambda: self._insertIntoForm(nwHeadingFormats.CH_ROML)) + self.aInsScNum.triggered.connect(lambda: self._insertIntoForm(nwHeadingFormats.SC_NUM)) + self.aInsScAbs.triggered.connect(lambda: self._insertIntoForm(nwHeadingFormats.SC_ABS)) + + self.btnInsert = QPushButton(self.tr("Insert")) + self.btnInsert.setMenu(self.menuInsert) + + self.btnApply = QPushButton(self.tr("Apply")) + self.btnApply.clicked.connect(self._saveFormat) + + self.formButtonBox = QHBoxLayout() + self.formButtonBox.addStretch(1) + self.formButtonBox.addWidget(self.btnInsert) + self.formButtonBox.addWidget(self.btnApply) + + self.editFormBox = QVBoxLayout() + self.editFormBox.addWidget(self.lblEditForm) + self.editFormBox.addWidget(self.editTextBox) + self.editFormBox.addLayout(self.formButtonBox) + # Assemble # ======== self.outerBox = QVBoxLayout() self.outerBox.addLayout(self.formatBox) + self.outerBox.addSpacing(CONFIG.pxInt(16)) + self.outerBox.addLayout(self.editFormBox) self.outerBox.addStretch(1) self.setLayout(self.outerBox) return + 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"))) + return + + ## + # Internal Functions + ## + + def _insertIntoForm(self, text: str): + """Insert formatting text from the dropdown menu. + """ + if self._editing > 0: + cursor = self.editTextBox.textCursor() + cursor.insertText(text) + self.editTextBox.setFocus() + return + + def _editHeading(self, heading: int): + """Populate the form with a specific heading format. + """ + self._editing = heading + self.editTextBox.setEnabled(True) + if heading == self.EDIT_TITLE: + text = self.fmtTitle.text() + label = self._build.getLabel("headings.fmtTitle") + elif heading == self.EDIT_CHAPTER: + text = self.fmtChapter.text() + label = self._build.getLabel("headings.fmtChapter") + elif heading == self.EDIT_UNNUM: + text = self.fmtUnnumbered.text() + label = self._build.getLabel("headings.fmtUnnumbered") + elif heading == self.EDIT_SCENE: + text = self.fmtScene.text() + label = self._build.getLabel("headings.fmtScene") + elif heading == self.EDIT_SECTION: + text = self.fmtSection.text() + label = self._build.getLabel("headings.fmtSection") + else: + self._editing = 0 + self.editTextBox.setEnabled(False) + text = "" + label = self.tr("None") + + self.editTextBox.setPlainText(text.replace("//", "\n")) + self.lblEditForm.setText(self.tr("Editing: {0}").format(label)) + + return + + ## + # Private Slots + ## + + def _saveFormat(self): + """Save the format from the edit text box. + """ + heading = self._editing + text = self.editTextBox.toPlainText().replace("\n", "//") + if heading == self.EDIT_TITLE: + self.fmtTitle.setText(text) + self._build.setValue("headings.fmtTitle", text) + elif heading == self.EDIT_CHAPTER: + self.fmtChapter.setText(text) + self._build.setValue("headings.fmtChapter", text) + elif heading == self.EDIT_UNNUM: + self.fmtUnnumbered.setText(text) + self._build.setValue("headings.fmtUnnumbered", text) + elif heading == self.EDIT_SCENE: + self.fmtScene.setText(text) + self._build.setValue("headings.fmtScene", text) + elif heading == self.EDIT_SECTION: + self.fmtSection.setText(text) + self._build.setValue("headings.fmtSection", text) + else: + return + + self._editHeading(0) + self.editTextBox.clear() + + return + + @pyqtSlot(bool) + def _hideScene(self, status): + """Hide scene heading + """ + self._build.setValue("headings.hideScene", status) + return + + @pyqtSlot(bool) + def _hideSection(self, status): + """Hide section heading + """ + self._build.setValue("headings.hideSection", status) + return + # END Class GuiBuildHeadingsTab @@ -719,3 +883,44 @@ class GuiBuildODTTab(QWidget): return # END Class GuiBuildODTTab + + +class GuiHeadingSyntax(QSyntaxHighlighter): + + def __init__(self, document: QTextDocument, mainTheme: GuiTheme): + super().__init__(document) + + self._valid = [ + nwHeadingFormats.TITLE.lower(), + nwHeadingFormats.CH_NUM.lower(), + nwHeadingFormats.CH_WORD.lower(), + nwHeadingFormats.CH_ROMU.lower(), + nwHeadingFormats.CH_ROML.lower(), + nwHeadingFormats.SC_NUM.lower(), + nwHeadingFormats.SC_ABS.lower(), + ] + + self._fmtSymbol = QTextCharFormat() + self._fmtSymbol.setForeground(QColor(*mainTheme.colHead)) + + self._fmtFormat = QTextCharFormat() + self._fmtFormat.setForeground(QColor(*mainTheme.colEmph)) + + return + + def highlightBlock(self, text: str): + """Add syntax highlighting to the text block. + """ + check = text.lower() + for heading in self._valid: + pos = check.find(heading) + if pos >= 0: + chars = len(heading) + self.setFormat(pos, chars, self._fmtSymbol) + self.setFormat(pos + 1, chars - 2, self._fmtFormat) + ddots = heading.find(":") + if ddots > 0: + self.setFormat(pos + ddots, 1, self._fmtSymbol) + return + +# END Class GuiHeadingSyntax