diff --git a/novelwriter/constants.py b/novelwriter/constants.py index ce8223ea..d4a944cc 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -309,7 +309,13 @@ class nwHeadFmt: SC_NUM = "{Scene}" SC_ABS = "{Scene:Abs}" - ALL = [TITLE, CH_NUM, CH_WORD, CH_ROMU, CH_ROML, SC_NUM, SC_ABS] + PAGE_HEADERS = [TITLE, CH_NUM, CH_WORD, CH_ROMU, CH_ROML, SC_NUM, SC_ABS] + + # ODT Document Page Header + ODT_PROJECT = "{Project}" + ODT_AUTHOR = "{Author}" + ODT_PAGE = "{Page}" + ODT_AUTO = "{Project} / {Author} / {Page}" # END Class nwHeadFmt diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index a796940c..4081b6dc 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -79,6 +79,8 @@ SETTINGS_TEMPLATE = { "format.leftMargin": (float, 2.0), "format.rightMargin": (float, 2.0), "odt.addColours": (bool, True), + "odt.pageHeader": (str, nwHeadFmt.ODT_AUTO), + "odt.pageCountOffset": (int, 0), "html.addStyles": (bool, True), } @@ -125,6 +127,8 @@ SETTINGS_LABELS = { "odt": QT_TRANSLATE_NOOP("Builds", "Open Document (.odt)"), "odt.addColours": QT_TRANSLATE_NOOP("Builds", "Add Highlight Colours"), + "odt.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"), + "odt.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"), "html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"), "html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"), diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 2caa9abe..35bfa79d 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -284,6 +284,9 @@ class NWBuildDocument: if isinstance(bldObj, ToOdt): bldObj.setColourHeaders(self._build.getBool("odt.addColours")) bldObj.setLanguage(self._project.data.language) + bldObj.setHeaderFormat( + self._build.getStr("odt.pageHeader"), self._build.getInt("odt.pageCountOffset") + ) scale = nwLabels.UNIT_SCALE.get(self._build.getStr("format.pageUnit"), 1.0) pW, pH = nwLabels.PAPER_SIZE.get(self._build.getStr("format.pageSize"), (-1.0, -1.0)) diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index 7c9ba6d9..ca21250d 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -34,11 +34,11 @@ from pathlib import Path from functools import partial from PyQt5.QtCore import QCoreApplication, QRegularExpression -from novelwriter.core.index import processComment from novelwriter.enum import nwComment, nwItemLayout from novelwriter.common import formatTimeStamp, numberToRoman, checkInt from novelwriter.constants import nwHeadFmt, nwRegEx, nwShortcode, nwUnicode +from novelwriter.core.index import processComment from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py index 0c2bd5e1..97201f70 100644 --- a/novelwriter/core/toodt.py +++ b/novelwriter/core/toodt.py @@ -132,11 +132,12 @@ class ToOdt(Tokenizer): self._errData = [] # List of errors encountered # Properties - self._textFont = "Liberation Serif" - self._textSize = 12 - self._textFixed = False - self._colourHead = False - self._headerText = "" + self._textFont = "Liberation Serif" + self._textSize = 12 + self._textFixed = False + self._colourHead = False + self._headerFormat = "" + self._pageOffset = 0 # Internal self._fontFamily = "'Liberation Serif'" @@ -222,6 +223,12 @@ class ToOdt(Tokenizer): self._mDocRight = f"{right/10.0:.3f}cm" return + def setHeaderFormat(self, format: str, offset: int) -> None: + """Set the document header format.""" + self._headerFormat = format.strip() + self._pageOffset = offset + return + ## # Class Methods ## @@ -279,12 +286,6 @@ class ToOdt(Tokenizer): # Clear Errors self._errData = [] - # Document Header - # =============== - - if self._headerText == "": - self._headerText = f"{self._project.data.name} / {self._project.data.author} /" - # Create Roots # ============ @@ -1010,17 +1011,28 @@ class ToOdt(Tokenizer): xPage = ET.SubElement(self._xMast, _mkTag("style", "master-page"), attrib=tAttr) # Standard Page Header - xHead = ET.SubElement(xPage, _mkTag("style", "header")) - xPar = ET.SubElement(xHead, _mkTag("text", "p"), attrib={ - _mkTag("text", "style-name"): "Header" - }) - xPar.text = self._headerText.strip() + " " + if self._headerFormat: + pre, page, post = self._headerFormat.partition(nwHeadFmt.ODT_PAGE) - xTail = ET.SubElement(xPar, _mkTag("text", "page-number"), attrib={ - _mkTag("text", "select-page"): "current" - }) - xTail.text = "2" - xTail.tail = "" # Prevent line break in indented XML + pre = pre.replace(nwHeadFmt.ODT_PROJECT, self._project.data.name) + pre = pre.replace(nwHeadFmt.ODT_AUTHOR, self._project.data.author) + post = post.replace(nwHeadFmt.ODT_PROJECT, self._project.data.name) + post = post.replace(nwHeadFmt.ODT_AUTHOR, self._project.data.author) + + xHead = ET.SubElement(xPage, _mkTag("style", "header")) + xPar = ET.SubElement(xHead, _mkTag("text", "p"), attrib={ + _mkTag("text", "style-name"): "Header" + }) + xPar.text = pre + if page: + attrib = {_mkTag("text", "select-page"): "current"} + if self._pageOffset > 0: + attrib = {_mkTag("text", "page-adjust"): str(0 - self._pageOffset)} + xTail = ET.SubElement(xPar, _mkTag("text", "page-number"), attrib=attrib) + xTail.text = "2" + xTail.tail = post + else: + xPar.text += post # First Page Header xHead = ET.SubElement(xPage, _mkTag("style", "header-first")) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 1ba232fc..b62d0491 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -79,17 +79,16 @@ class GuiBuildSettings(QDialog): self._build = build self.setWindowTitle(self.tr("Manuscript Build Settings")) - self.setMinimumWidth(CONFIG.pxInt(700)) - self.setMinimumHeight(CONFIG.pxInt(400)) + self.setMinimumSize(CONFIG.pxInt(700), CONFIG.pxInt(400)) mPx = CONFIG.pxInt(150) wWin = CONFIG.pxInt(750) hWin = CONFIG.pxInt(550) - pOptions = SHARED.project.options + options = SHARED.project.options self.resize( - CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "winWidth", wWin)), - CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "winHeight", hWin)) + CONFIG.pxInt(options.getInt("GuiBuildSettings", "winWidth", wWin)), + CONFIG.pxInt(options.getInt("GuiBuildSettings", "winHeight", hWin)) ) # Title @@ -604,9 +603,9 @@ class _HeadingsTab(NScrollablePage): # Title Heading self.lblTitle = QLabel(self._build.getLabel("headings.fmtTitle")) - self.fmtTitle = QLineEdit("") + self.fmtTitle = QLineEdit("", self) self.fmtTitle.setReadOnly(True) - self.btnTitle = QToolButton() + self.btnTitle = QToolButton(self) self.btnTitle.setIcon(SHARED.theme.getIcon("edit")) self.btnTitle.clicked.connect(lambda: self._editHeading(self.EDIT_TITLE)) @@ -620,9 +619,9 @@ class _HeadingsTab(NScrollablePage): # Chapter Heading self.lblChapter = QLabel(self._build.getLabel("headings.fmtChapter")) - self.fmtChapter = QLineEdit("") + self.fmtChapter = QLineEdit("", self) self.fmtChapter.setReadOnly(True) - self.btnChapter = QToolButton() + self.btnChapter = QToolButton(self) self.btnChapter.setIcon(SHARED.theme.getIcon("edit")) self.btnChapter.clicked.connect(lambda: self._editHeading(self.EDIT_CHAPTER)) @@ -636,9 +635,9 @@ class _HeadingsTab(NScrollablePage): # Unnumbered Chapter Heading self.lblUnnumbered = QLabel(self._build.getLabel("headings.fmtUnnumbered")) - self.fmtUnnumbered = QLineEdit("") + self.fmtUnnumbered = QLineEdit("", self) self.fmtUnnumbered.setReadOnly(True) - self.btnUnnumbered = QToolButton() + self.btnUnnumbered = QToolButton(self) self.btnUnnumbered.setIcon(SHARED.theme.getIcon("edit")) self.btnUnnumbered.clicked.connect(lambda: self._editHeading(self.EDIT_UNNUM)) @@ -653,9 +652,9 @@ class _HeadingsTab(NScrollablePage): # Scene Heading sceneHideTip = self._build.getLabel("headings.hideScene") self.lblScene = QLabel(self._build.getLabel("headings.fmtScene")) - self.fmtScene = QLineEdit("") + self.fmtScene = QLineEdit("", self) self.fmtScene.setReadOnly(True) - self.btnScene = QToolButton() + self.btnScene = QToolButton(self) self.btnScene.setIcon(SHARED.theme.getIcon("edit")) self.btnScene.clicked.connect(lambda: self._editHeading(self.EDIT_SCENE)) self.hdeScene = QLabel(self.tr("Hide")) @@ -680,9 +679,9 @@ class _HeadingsTab(NScrollablePage): # Section Heading sectionHideTip = self._build.getLabel("headings.hideSection") self.lblSection = QLabel(self._build.getLabel("headings.fmtSection")) - self.fmtSection = QLineEdit("") + self.fmtSection = QLineEdit("", self) self.fmtSection.setReadOnly(True) - self.btnSection = QToolButton() + self.btnSection = QToolButton(self) self.btnSection.setIcon(SHARED.theme.getIcon("edit")) self.btnSection.clicked.connect(lambda: self._editHeading(self.EDIT_SECTION)) self.hdeSection = QLabel(self.tr("Hide")) @@ -867,7 +866,7 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter): def highlightBlock(self, text: str) -> None: """Add syntax highlighting to the text block.""" - for heading in nwHeadFmt.ALL: + for heading in nwHeadFmt.PAGE_HEADERS: pos = text.find(heading) if pos >= 0: chars = len(heading) @@ -1211,14 +1210,34 @@ class _OutputTab(NScrollableForm): self._build = build iPx = SHARED.theme.baseIconSize + spW = 6*SHARED.theme.textNWidth # Open Document self.addGroupLabel(self._build.getLabel("odt")) + self.odtAddColours = NSwitch(self, width=2*iPx, height=iPx) self.addRow(self._build.getLabel("odt.addColours"), self.odtAddColours) + self.odtPageHeader = QLineEdit(self) + self.odtPageHeader.setMinimumWidth(CONFIG.pxInt(200)) + self.btnPageHeader = QToolButton(self) + self.btnPageHeader.setIcon(SHARED.theme.getIcon("revert")) + self.btnPageHeader.clicked.connect(self._resetPageHeader) + self.addRow( + self._build.getLabel("odt.pageHeader"), self.odtPageHeader, + button=self.btnPageHeader, stretch=(1, 1) + ) + + self.odtPageCountOffset = QSpinBox(self) + self.odtPageCountOffset.setMinimum(0) + self.odtPageCountOffset.setMaximum(999) + self.odtPageCountOffset.setSingleStep(1) + self.odtPageCountOffset.setMinimumWidth(spW) + self.addRow(self._build.getLabel("odt.pageCountOffset"), self.odtPageCountOffset) + # HTML Document self.addGroupLabel(self._build.getLabel("html")) + self.htmlAddStyles = NSwitch(self, width=2*iPx, height=iPx) self.addRow(self._build.getLabel("html.addStyles"), self.htmlAddStyles) @@ -1230,13 +1249,26 @@ class _OutputTab(NScrollableForm): def loadContent(self) -> None: """Populate the widgets.""" self.odtAddColours.setChecked(self._build.getBool("odt.addColours")) + self.odtPageHeader.setText(self._build.getStr("odt.pageHeader")) + self.odtPageCountOffset.setValue(self._build.getInt("odt.pageCountOffset")) self.htmlAddStyles.setChecked(self._build.getBool("html.addStyles")) return def saveContent(self) -> None: """Save choices back into build object.""" self._build.setValue("odt.addColours", self.odtAddColours.isChecked()) + self._build.setValue("odt.pageHeader", self.odtPageHeader.text()) + self._build.setValue("odt.pageCountOffset", self.odtPageCountOffset.value()) self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked()) return + ## + # Private Slots + ## + + def _resetPageHeader(self) -> None: + """Reset the ODT header format to default.""" + self.odtPageHeader.setText(nwHeadFmt.ODT_AUTO) + return + # END Class _OutputTab diff --git a/tests/reference/coreToOdt_SaveFlat_document.fodt b/tests/reference/coreToOdt_SaveFlat_document.fodt index e684d070..29f75d9e 100644 --- a/tests/reference/coreToOdt_SaveFlat_document.fodt +++ b/tests/reference/coreToOdt_SaveFlat_document.fodt @@ -1,13 +1,13 @@ - 2023-10-20T22:51:30 - novelWriter/2.2-alpha1 + 2024-01-28T15:48:36 + novelWriter/2.3a3 Jane Smith 1234 P42DT12H34M56S Test Project - 2023-10-20T22:51:30 + 2024-01-28T15:48:36 Jane Smith @@ -76,7 +76,7 @@ - Test Project / Jane Smith / 2 + Test Project / Jane Smith / 2 diff --git a/tests/reference/coreToOdt_SaveFull_styles.xml b/tests/reference/coreToOdt_SaveFull_styles.xml index c52c6938..b276efed 100644 --- a/tests/reference/coreToOdt_SaveFull_styles.xml +++ b/tests/reference/coreToOdt_SaveFull_styles.xml @@ -63,8 +63,7 @@ - Test Project / Jane Smith / 2 - + Test Project - Jane Smith diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index 23f98d20..c9039236 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -29,6 +29,7 @@ from shutil import copyfile from tools import ODT_IGNORE, cmpFiles from novelwriter.common import xmlIndent +from novelwriter.constants import nwHeadFmt from novelwriter.core.toodt import ToOdt, ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag from novelwriter.core.project import NWProject @@ -691,6 +692,9 @@ def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths): odt.setLanguage("nb_NO") assert odt._dLanguage == "nb" odt.setColourHeaders(True) + assert odt._colourHead is True + odt.setHeaderFormat(nwHeadFmt.ODT_AUTO, 1) + assert odt._headerFormat == nwHeadFmt.ODT_AUTO odt.setPageLayout(148, 210, 20, 18, 17, 15) assert odt._mDocWidth == "14.800cm" @@ -736,6 +740,9 @@ def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths): odt = ToOdt(project, isFlat=False) odt._isNovel = True + # Set a format without page number + odt.setHeaderFormat(f"{nwHeadFmt.ODT_PROJECT} - {nwHeadFmt.ODT_AUTHOR}", 0) + odt._text = ( "## Chapter One\n\n" "Text\n\n" diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index d347d817..13fc49d6 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -649,18 +649,27 @@ def testBuildSettings_Output(qtbot: QtBot, nwGUI: GuiMain): # Check initial values assert outTab.odtAddColours.isChecked() is False + assert outTab.odtPageHeader.text() == nwHeadFmt.ODT_AUTO assert outTab.htmlAddStyles.isChecked() is False # Toggle all outTab.odtAddColours.setChecked(True) outTab.htmlAddStyles.setChecked(True) + # Change header format + outTab.odtPageHeader.setText("Stuff") + # Save values outTab.saveContent() assert build.getBool("odt.addColours") is True + assert build.getStr("odt.pageHeader") == "Stuff" assert build.getBool("html.addStyles") is True + # Reset header format + outTab.btnPageHeader.click() + assert outTab.odtPageHeader.text() == nwHeadFmt.ODT_AUTO + # Finish bSettings._dialogButtonClicked(bSettings.buttonBox.button(QDialogButtonBox.Close)) # qtbot.stop()