diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index 6129de05..c44d9223 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -390,7 +390,7 @@ class BuildSettings: def setValue(self, key: str, value: T_BuildValue) -> None: """Set a specific value for a build setting.""" if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]): - self._changed = value != self._settings[key] + self._changed |= (value != self._settings[key]) self._settings[key] = value return diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 521e08a0..e5ddc66b 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -80,7 +80,8 @@ class GuiBuildSettings(NToolDialog): logger.debug("Create: GuiBuildSettings") self.setObjectName("GuiBuildSettings") - self._build = build + # Make a copy of the build object + self._build = BuildSettings.fromDict(build.pack()) self.setWindowTitle(self.tr("Manuscript Build Settings")) self.setMinimumSize(700, 400) @@ -184,6 +185,7 @@ class GuiBuildSettings(NToolDialog): settings. """ logger.debug("Closing: GuiBuildSettings") + self._applyChanges() self._askToSaveBuild() self._saveSettings() event.accept() @@ -211,11 +213,14 @@ class GuiBuildSettings(NToolDialog): """Handle button clicks from the dialog button box.""" role = self.buttonBox.buttonRole(button) if role == QtRoleApply: + self._applyChanges() self._emitBuildData() elif role == QtRoleAccept: + self._applyChanges() self._emitBuildData() self.close() elif role == QtRoleReject: + self._build.resetChangedState() self.close() return @@ -228,10 +233,9 @@ class GuiBuildSettings(NToolDialog): whether the user wants to save them. """ if self._build.changed: - response = SHARED.question(self.tr( + if SHARED.question(self.tr( "Do you want to save your changes to '{0}'?" - ).format(self._build.name)) - if response: + ).format(self._build.name)): self._emitBuildData() self._build.resetChangedState() return @@ -246,14 +250,17 @@ class GuiBuildSettings(NToolDialog): pOptions.setValue("GuiBuildSettings", "treeWidth", treeWidth) pOptions.setValue("GuiBuildSettings", "filterWidth", filterWidth) pOptions.saveSettings() + return + def _applyChanges(self) -> None: + """Apply all settings changes to the build object.""" + self._build.setName(self.editBuildName.text()) + self.optTabHeadings.saveContent() + self.optTabFormatting.saveContent() return def _emitBuildData(self) -> None: """Assemble the build data and emit the signal.""" - self._build.setName(self.editBuildName.text()) - self.optTabHeadings.saveContent() - self.optTabFormatting.saveContent() self.newSettingsReady.emit(self._build) self._build.resetChangedState() return diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index b2d0479f..91a9f4f9 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -72,7 +72,7 @@ def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd): @pyqtSlot(BuildSettings) def _testNewSettingsReady(new: BuildSettings): nonlocal triggered - assert new is build + assert new.buildID == build.buildID triggered = True # Capture Apply button @@ -103,6 +103,7 @@ def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd): with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): bSettings.newSettingsReady.connect(_testNewSettingsReady) + bSettings._build._changed = True bSettings.close() assert triggered @@ -140,6 +141,9 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): bSettings.show() bSettings.loadContent() + sBuild = bSettings._build + assert sBuild.buildID == build.buildID + filterTab = bSettings.optTabSelect button = bSettings.sidebar._group.button(bSettings.OPT_FILTERS) assert button is not None @@ -153,15 +157,15 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): # Un-toggle note folders filterTab.filterOpt._widgets[switchMap["worldRoot"]].setChecked(False) # World Root assert filterTab.optTree.topLevelItemCount() == 3 - assert C.hWorldRoot in build._skipRoot + assert C.hWorldRoot in sBuild._skipRoot filterTab.filterOpt._widgets[switchMap["charRoot"]].setChecked(False) # Char Root assert filterTab.optTree.topLevelItemCount() == 2 - assert C.hCharRoot in build._skipRoot + assert C.hCharRoot in sBuild._skipRoot filterTab.filterOpt._widgets[switchMap["plotRoot"]].setChecked(False) # Plot Root assert filterTab.optTree.topLevelItemCount() == 1 - assert C.hPlotRoot in build._skipRoot + assert C.hPlotRoot in sBuild._skipRoot # Reset Plot and Char filterTab.filterOpt._widgets[switchMap["plotRoot"]].setChecked(True) @@ -170,7 +174,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): # Switch off novel docs filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(False) - assert build.buildItemFilter(SHARED.project) == { + assert sBuild.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -186,7 +190,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): # Switch on note docs filterTab.filterOpt._widgets[switchMap["incNotes"]].setChecked(True) - assert build.buildItemFilter(SHARED.project) == { + assert sBuild.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -202,7 +206,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): # Switch on inactive docs filterTab.filterOpt._widgets[switchMap["incInactive"]].setChecked(True) - assert build.buildItemFilter(SHARED.project) == { + assert sBuild.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -220,7 +224,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): filterTab._treeMap[C.hChapterDoc].setSelected(True) filterTab._treeMap[C.hSceneDoc].setSelected(True) filterTab.includedButton.click() - assert build.buildItemFilter(SHARED.project) == { + assert sBuild.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -239,7 +243,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): filterTab._treeMap[hPlotDoc].setSelected(True) # type: ignore filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore filterTab.excludedButton.click() - assert build.buildItemFilter(SHARED.project) == { + assert sBuild.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -255,7 +259,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): # Switch on novel docs filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(True) - assert build.buildItemFilter(SHARED.project) == { + assert sBuild.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (True, FilterMode.FILTERED), # Now enabled C.hChapterDir: (False, FilterMode.SKIPPED), @@ -273,7 +277,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): filterTab.optTree.clearSelection() filterTab._treeMap[C.hNovelRoot].setSelected(True) filterTab.resetButton.click() - assert build.buildItemFilter(SHARED.project) == { + assert sBuild.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (True, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -294,7 +298,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): filterTab._treeMap[hPlotDoc].setSelected(True) # type: ignore filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore filterTab.resetButton.click() - assert build.buildItemFilter(SHARED.project) == { + assert sBuild.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (True, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -429,6 +433,8 @@ def testToolBuildSettings_Headings(qtbot, nwGUI): # Edit a Heading # ============== + sBuild = bSettings._build + assert sBuild.buildID == build.buildID # Create new format of all bits headTab.btnChapter.click() @@ -447,13 +453,13 @@ def testToolBuildSettings_Headings(qtbot, nwGUI): headTab.aInsScAbs.trigger() assert headTab.editTextBox.toPlainText() == allFmt headTab.btnApply.click() - assert build.getStr("headings.fmtChapter") == allFmt + assert sBuild.getStr("headings.fmtChapter") == allFmt # Check complex format headTab.btnChapter.click() headTab.editTextBox.setPlainText(f"Chapter {nwHeadFmt.CH_NUM}\n{nwHeadFmt.TITLE}\n") headTab.btnApply.click() - assert build.getStr("headings.fmtChapter") == ( + assert sBuild.getStr("headings.fmtChapter") == ( f"Chapter {nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}" ) @@ -461,39 +467,42 @@ def testToolBuildSettings_Headings(qtbot, nwGUI): headTab.btnPart.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() - assert build.getStr("headings.fmtPart") == nwHeadFmt.TITLE + assert sBuild.getStr("headings.fmtPart") == nwHeadFmt.TITLE headTab.btnChapter.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() - assert build.getStr("headings.fmtChapter") == nwHeadFmt.TITLE + assert sBuild.getStr("headings.fmtChapter") == nwHeadFmt.TITLE headTab.btnUnnumbered.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() - assert build.getStr("headings.fmtUnnumbered") == nwHeadFmt.TITLE + assert sBuild.getStr("headings.fmtUnnumbered") == nwHeadFmt.TITLE headTab.btnScene.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() - assert build.getStr("headings.fmtScene") == nwHeadFmt.TITLE + assert sBuild.getStr("headings.fmtScene") == nwHeadFmt.TITLE headTab.btnAScene.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() - assert build.getStr("headings.fmtAltScene") == nwHeadFmt.TITLE + assert sBuild.getStr("headings.fmtAltScene") == nwHeadFmt.TITLE headTab.btnSection.click() headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) headTab.btnApply.click() - assert build.getStr("headings.fmtSection") == nwHeadFmt.TITLE + assert sBuild.getStr("headings.fmtSection") == nwHeadFmt.TITLE # Check hide switches headTab.swtScene.setChecked(True) headTab.swtSection.setChecked(True) headTab.saveContent() - assert build.getBool("headings.hideScene") is True - assert build.getBool("headings.hideSection") is True + sBuild = bSettings._build + assert sBuild.buildID == build.buildID + + assert sBuild.getBool("headings.hideScene") is True + assert sBuild.getBool("headings.hideSection") is True # Finish button = bSettings.buttonBox.button(QtDialogClose) @@ -556,16 +565,18 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI): # Save values fmtTab.saveContent() + sBuild = bSettings._build + assert sBuild.buildID == build.buildID - assert build.getBool("text.includeBodyText") is True - assert build.getBool("text.includeSynopsis") is True - assert build.getBool("text.includeComments") is True - assert build.getBool("text.includeStory") is True - assert build.getBool("text.includeNotes") is True - assert build.getBool("text.includeKeywords") is True - assert build.getStr("text.ignoredKeywords") in ("@custom, @object", "@object, @custom") + assert sBuild.getBool("text.includeBodyText") is True + assert sBuild.getBool("text.includeSynopsis") is True + assert sBuild.getBool("text.includeComments") is True + assert sBuild.getBool("text.includeStory") is True + assert sBuild.getBool("text.includeNotes") is True + assert sBuild.getBool("text.includeKeywords") is True + assert sBuild.getStr("text.ignoredKeywords") in ("@custom, @object", "@object, @custom") - assert build.getBool("text.addNoteHeadings") is True + assert sBuild.getBool("text.addNoteHeadings") is True # Finish button = bSettings.buttonBox.button(QtDialogClose) @@ -623,15 +634,17 @@ def testToolBuildSettings_FormatTextFormat(monkeypatch, qtbot, nwGUI): # Save values fmtTab.saveContent() + sBuild = bSettings._build + assert sBuild.buildID == build.buildID - assert build.getStr("format.textFont") == testFont.toString() - assert build.getFloat("format.lineHeight") == 1.15 + assert sBuild.getStr("format.textFont") == testFont.toString() + assert sBuild.getFloat("format.lineHeight") == 1.15 - assert build.getBool("format.justifyText") is True - assert build.getBool("format.stripUnicode") is True - assert build.getBool("format.replaceTabs") is True - assert build.getBool("format.keepBreaks") is False - assert build.getBool("format.showDialogue") is True + assert sBuild.getBool("format.justifyText") is True + assert sBuild.getBool("format.stripUnicode") is True + assert sBuild.getBool("format.replaceTabs") is True + assert sBuild.getBool("format.keepBreaks") is False + assert sBuild.getBool("format.showDialogue") is True # Check that the font dialog doesn't fail with monkeypatch.context() as mp: @@ -682,10 +695,12 @@ def testToolBuildSettings_FormatFirstLineIndent(monkeypatch, qtbot, nwGUI): # Save values fmtTab.saveContent() + sBuild = bSettings._build + assert sBuild.buildID == build.buildID - assert build.getBool("format.firstLineIndent") is True - assert build.getFloat("format.firstIndentWidth") == 2.0 - assert build.getBool("format.indentFirstPar") is True + assert sBuild.getBool("format.firstLineIndent") is True + assert sBuild.getFloat("format.firstIndentWidth") == 2.0 + assert sBuild.getBool("format.indentFirstPar") is True # Finish button = bSettings.buttonBox.button(QtDialogClose) @@ -800,15 +815,17 @@ def testToolBuildSettings_FormatOutput(qtbot, nwGUI): # Save values fmtTab.saveContent() + sBuild = bSettings._build + assert sBuild.buildID == build.buildID - assert build.getStr("doc.pageHeader") == "Stuff" - assert build.getInt("doc.pageCountOffset") == 1 - assert build.getBool("doc.colorHeadings") is False - assert build.getBool("doc.scaleHeadings") is False - assert build.getBool("doc.boldHeadings") is False + assert sBuild.getStr("doc.pageHeader") == "Stuff" + assert sBuild.getInt("doc.pageCountOffset") == 1 + assert sBuild.getBool("doc.colorHeadings") is False + assert sBuild.getBool("doc.scaleHeadings") is False + assert sBuild.getBool("doc.boldHeadings") is False - assert build.getBool("html.addStyles") is True - assert build.getBool("html.preserveTabs") is True + assert sBuild.getBool("html.addStyles") is True + assert sBuild.getBool("html.preserveTabs") is True # Reset header format fmtTab.btnPageHeader.click()