diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index 26772ee8..39383c9a 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -59,8 +59,7 @@ class NPagedSideBar(QToolBar): return def setLabelColor(self, color): - """Set the text color for the labels. - """ + """Set the text color for the labels.""" if isinstance(color, list): self._labelCol = QColor(*color) elif isinstance(color, QColor): @@ -68,8 +67,7 @@ class NPagedSideBar(QToolBar): return def addSeparator(self): - """Add a spacer widget. - """ + """Add a spacer widget.""" spacer = QWidget(self) spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) spacer.setFixedHeight(self._spacerHeight) @@ -77,16 +75,14 @@ class NPagedSideBar(QToolBar): return def addLabel(self, text): - """Add a new label to the toolbar. - """ + """Add a new label to the toolbar.""" label = NPagedToolLabel(self, self._labelCol) label.setText(text) self.insertWidget(self._stretchAction, label) return def addButton(self, text, buttonId=-1): - """Add a new button to the toolbar. - """ + """Add a new button to the toolbar.""" button = NPagedToolButton(self) button.setText(text) @@ -99,8 +95,7 @@ class NPagedSideBar(QToolBar): return action def setSelected(self, buttonId): - """Set the selected button. - """ + """Set the selected button.""" self._group.button(buttonId).setChecked(True) return @@ -110,8 +105,7 @@ class NPagedSideBar(QToolBar): @pyqtSlot("QAbstractButton*") def _buttonClicked(self, button): - """A button was clicked in the group, emit its id. - """ + """A button was clicked in the group, emit its id.""" buttonId = self._group.id(button) if buttonId != -1: self.buttonClicked.emit(buttonId) @@ -155,7 +149,7 @@ class NPagedToolButton(QToolButton): height = self.height() palette = self.palette() - if opt.state & QStyle.State_MouseOver: + if opt.state & QStyle.State_MouseOver == QStyle.State_MouseOver: backCol = palette.alternateBase() paint.setBrush(backCol) paint.setOpacity(0.5) diff --git a/novelwriter/extensions/switchbox.py b/novelwriter/extensions/switchbox.py index 03d8befc..7eb30957 100644 --- a/novelwriter/extensions/switchbox.py +++ b/novelwriter/extensions/switchbox.py @@ -45,11 +45,15 @@ class NSwitchBox(QScrollArea): self._hSwitch = baseSize self._wSwitch = 2*self._hSwitch self._sIcon = baseSize + self._widgets = [] self.clear() return def clear(self): """Rebuild the content of the core widget.""" + self._index = 0 + self._widgets = [] + self._content = QGridLayout() self._content.setColumnStretch(1, 1) @@ -69,6 +73,7 @@ class NSwitchBox(QScrollArea): font.setBold(True) label.setFont(font) self._content.addWidget(label, self._index, 0, 1, 3, Qt.AlignLeft) + self._widgets.append(label) self._bumpIndex() return @@ -87,16 +92,17 @@ class NSwitchBox(QScrollArea): switch.toggled.connect(lambda state: self._emitSwitchSignal(identifier, state)) self._content.addWidget(switch, self._index, 2, Qt.AlignRight) + self._widgets.append(switch) self._bumpIndex() return def addSeparator(self): - """Add a blank entry in the content box. - """ + """Add a blank entry in the content box.""" spacer = QWidget() spacer.setFixedHeight(int(0.5*self._sIcon)) self._content.addWidget(spacer, self._index, 0, 1, 3, Qt.AlignLeft) + self._widgets.append(spacer) self._bumpIndex() return diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 02a32e20..3e390c2c 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -118,8 +118,8 @@ class GuiBuildSettings(QDialog): # Create Tabs self.optTabSelect = _FilterTab(self, self._build) self.optTabHeadings = _HeadingsTab(self, self._build) - self.optTabFormat = _FormatTab(self, self._build) self.optTabContent = _ContentTab(self, self._build) + self.optTabFormat = _FormatTab(self, self._build) self.optTabOutput = _OutputTab(self, self._build) # Add Tabs @@ -408,9 +408,8 @@ class _FilterTab(QWidget): def mainSplitSizes(self) -> tuple[int, int]: """Extract the sizes of the main splitter.""" sizes = self.mainSplit.sizes() - if len(sizes) < 2: - return 0, 0 - return CONFIG.rpxInt(sizes[0]), CONFIG.rpxInt(sizes[1]) + m, n = (sizes[0], sizes[1]) if len(sizes) >= 2 else (0, 0) + return CONFIG.rpxInt(m), CONFIG.rpxInt(n) ## # Slots @@ -436,7 +435,7 @@ class _FilterTab(QWidget): logger.debug("Building project tree") self._treeMap = {} self.optTree.clear() - for nwItem in self.theProject.tree: + for nwItem in self.theProject.getProjectItems(): tHandle = nwItem.itemHandle pHandle = nwItem.itemParent @@ -512,14 +511,14 @@ class _FilterTab(QWidget): # Root Classes self.filterOpt.addLabel(self.tr("Root Folders")) - for nwItem in self.theProject.getProjectItems(): + for tHandle, nwItem in self.theProject.tree.iterRoots(None): if not nwItem.isInactiveClass(): itemIcon = self.mainTheme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout ) self.filterOpt.addItem( - itemIcon, nwItem.itemName, f"root:{nwItem.itemHandle}", - default=self._build.isRootAllowed(nwItem.itemHandle) + itemIcon, nwItem.itemName, f"root:{tHandle}", + default=self._build.isRootAllowed(tHandle) ) return @@ -527,18 +526,16 @@ class _FilterTab(QWidget): def _setSelectedMode(self, mode: int): """Set the mode for the selected items.""" for item in self.optTree.selectedItems(): - if not isinstance(item, QTreeWidgetItem): - continue - - tHandle = item.data(self.C_DATA, self.D_HANDLE) - isFile = item.data(self.C_DATA, self.D_FILE) - if isFile: - if mode == self.F_FILTERED: - self._build.setFiltered(tHandle) - elif mode == self.F_INCLUDED: - self._build.setIncluded(tHandle) - elif mode == self.F_EXCLUDED: - self._build.setExcluded(tHandle) + if isinstance(item, QTreeWidgetItem): + tHandle = item.data(self.C_DATA, self.D_HANDLE) + isFile = item.data(self.C_DATA, self.D_FILE) + if isFile: + if mode == self.F_FILTERED: + self._build.setFiltered(tHandle) + elif mode == self.F_INCLUDED: + self._build.setIncluded(tHandle) + elif mode == self.F_EXCLUDED: + self._build.setExcluded(tHandle) self._setTreeItemMode() @@ -592,7 +589,7 @@ class _HeadingsTab(QWidget): # Title Heading self.lblTitle = QLabel(self._build.getLabel("headings.fmtTitle")) self.fmtTitle = QLineEdit("") - self.fmtTitle.setEnabled(False) + self.fmtTitle.setReadOnly(True) self.btnTitle = QToolButton() self.btnTitle.setIcon(self.mainTheme.getIcon("edit")) self.btnTitle.clicked.connect(lambda: self._editHeading(self.EDIT_TITLE)) @@ -608,7 +605,7 @@ class _HeadingsTab(QWidget): # Chapter Heading self.lblChapter = QLabel(self._build.getLabel("headings.fmtChapter")) self.fmtChapter = QLineEdit("") - self.fmtChapter.setEnabled(False) + self.fmtChapter.setReadOnly(True) self.btnChapter = QToolButton() self.btnChapter.setIcon(self.mainTheme.getIcon("edit")) self.btnChapter.clicked.connect(lambda: self._editHeading(self.EDIT_CHAPTER)) @@ -624,7 +621,7 @@ class _HeadingsTab(QWidget): # Unnumbered Chapter Heading self.lblUnnumbered = QLabel(self._build.getLabel("headings.fmtUnnumbered")) self.fmtUnnumbered = QLineEdit("") - self.fmtUnnumbered.setEnabled(False) + self.fmtUnnumbered.setReadOnly(True) self.btnUnnumbered = QToolButton() self.btnUnnumbered.setIcon(self.mainTheme.getIcon("edit")) self.btnUnnumbered.clicked.connect(lambda: self._editHeading(self.EDIT_UNNUM)) @@ -641,7 +638,7 @@ class _HeadingsTab(QWidget): sceneHideTip = self._build.getLabel("headings.hideScene") self.lblScene = QLabel(self._build.getLabel("headings.fmtScene")) self.fmtScene = QLineEdit("") - self.fmtScene.setEnabled(False) + self.fmtScene.setReadOnly(True) self.btnScene = QToolButton() self.btnScene.setIcon(self.mainTheme.getIcon("edit")) self.btnScene.clicked.connect(lambda: self._editHeading(self.EDIT_SCENE)) @@ -668,7 +665,7 @@ class _HeadingsTab(QWidget): sectionHideTip = self._build.getLabel("headings.hideSection") self.lblSection = QLabel(self._build.getLabel("headings.fmtSection")) self.fmtSection = QLineEdit("") - self.fmtSection.setEnabled(False) + self.fmtSection.setReadOnly(True) self.btnSection = QToolButton() self.btnSection.setIcon(self.mainTheme.getIcon("edit")) self.btnSection.clicked.connect(lambda: self._editHeading(self.EDIT_SECTION)) @@ -814,7 +811,7 @@ class _HeadingsTab(QWidget): def _saveFormat(self): """Save the format from the edit text box.""" heading = self._editing - text = self.editTextBox.toPlainText().replace("\n", "//") + text = self.editTextBox.toPlainText().strip().replace("\n", "//") if heading == self.EDIT_TITLE: self.fmtTitle.setText(text) self._build.setValue("headings.fmtTitle", text) diff --git a/tests/conftest.py b/tests/conftest.py index acb1ae19..744fdff5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -153,6 +153,7 @@ def nwGUI(qtbot, monkeypatch, functionFixture): nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"]) qtbot.addWidget(nwGUI) resetConfigVars() + nwGUI.docEditor.initEditor() nwGUI.show() qtbot.wait(20) diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py new file mode 100644 index 00000000..b4ae8e12 --- /dev/null +++ b/tests/test_tools/test_tools_manussettings.py @@ -0,0 +1,617 @@ +""" +novelWriter – Manuscript Build Settings Dialog Tester +===================================================== + +This file is a part of novelWriter +Copyright 2018–2023, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +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 . +""" + +import pytest + +from tools import C, buildTestProject + +from pathlib import Path +from pytestqt.qtbot import QtBot + +from PyQt5.QtGui import QFont +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QDialogButtonBox, QFontDialog + +from novelwriter import CONFIG +from novelwriter.guimain import GuiMain +from novelwriter.constants import nwHeadFmt +from novelwriter.core.buildsettings import BuildSettings, FilterMode +from novelwriter.tools.manussettings import ( + GuiBuildSettings, _OutputTab, _FormatTab, _ContentTab, _HeadingsTab, _FilterTab +) + + +@pytest.mark.gui +def testBuildSettings_Init(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd): + """Test the initialisation of the GuiBuildSettings dialog.""" + buildTestProject(nwGUI, projPath) + nwGUI.openProject(projPath) + build = BuildSettings() + + # Create the dialog and populate it + bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings.show() + bSettings.loadContent() + + # Flip through pages + bSettings.optSideBar._group.button(bSettings.OPT_OUTPUT).click() + assert isinstance(bSettings.toolStack.currentWidget(), _OutputTab) + + bSettings.optSideBar._group.button(bSettings.OPT_FORMAT).click() + assert isinstance(bSettings.toolStack.currentWidget(), _FormatTab) + + bSettings.optSideBar._group.button(bSettings.OPT_CONTENT).click() + assert isinstance(bSettings.toolStack.currentWidget(), _ContentTab) + + bSettings.optSideBar._group.button(bSettings.OPT_HEADINGS).click() + assert isinstance(bSettings.toolStack.currentWidget(), _HeadingsTab) + + bSettings.optSideBar._group.button(bSettings.OPT_FILTERS).click() + assert isinstance(bSettings.toolStack.currentWidget(), _FilterTab) + + # Check dialog buttons + triggered = False + + @pyqtSlot(BuildSettings) + def _testNewSettingsReady(new: BuildSettings): + nonlocal build, triggered + assert new is build + triggered = True + + # Capture Apply button + with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): + bSettings.newSettingsReady.connect(_testNewSettingsReady) + bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Apply)) + + assert triggered + + # Capture Save button + triggered = False + + with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): + bSettings.newSettingsReady.connect(_testNewSettingsReady) + bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Save)) + + assert triggered + + # Close manually + # This pops the ask to save dialog, which is automatically handled + triggered = False + build._changed = True + bSettings.show() + + with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): + bSettings.newSettingsReady.connect(_testNewSettingsReady) + bSettings.close() + + assert triggered + + # Finish + bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) + # qtbot.stop() + +# END Test testBuildSettings_Init + + +@pytest.mark.gui +def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd): + """Test the Filter Tab of the GuiBuildSettings dialog.""" + buildTestProject(nwGUI, projPath) + nwGUI.openProject(projPath) + build = BuildSettings() + + switchMap = { + "incNovel": 1, + "incNotes": 2, + "incInactive": 3, + "novelRoot": 6, + "plotRoot": 7, + "charRoot": 8, + "worldRoot": 9, + } + + hPlotDoc = nwGUI.theProject.newFile("Main Plot", C.hPlotRoot) + hCharDoc = nwGUI.theProject.newFile("Jane Doe", C.hCharRoot) + nwGUI.projView.projTree.revealNewTreeItem(hPlotDoc) + nwGUI.projView.projTree.revealNewTreeItem(hCharDoc) + nwGUI.theProject.tree[hPlotDoc].setActive(False) + + # Create the dialog and populate it + bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings.show() + bSettings.loadContent() + + filterTab = bSettings.optTabSelect + bSettings.optSideBar._group.button(bSettings.OPT_FILTERS).click() + assert bSettings.toolStack.currentWidget() is filterTab + + # Check content + assert filterTab.optTree.topLevelItemCount() == 4 # The 4 root folders + assert filterTab.filterOpt._index == 10 # 2 headers, 1 sep, 3 opt and 4 roots + + # Untoggle note folders + filterTab.filterOpt._widgets[switchMap["worldRoot"]].setChecked(False) # World Root + assert filterTab.optTree.topLevelItemCount() == 3 + assert C.hWorldRoot in build._skipRoot + + filterTab.filterOpt._widgets[switchMap["charRoot"]].setChecked(False) # Char Root + assert filterTab.optTree.topLevelItemCount() == 2 + assert C.hCharRoot in build._skipRoot + + filterTab.filterOpt._widgets[switchMap["plotRoot"]].setChecked(False) # Plot Root + assert filterTab.optTree.topLevelItemCount() == 1 + assert C.hPlotRoot in build._skipRoot + + # Reset Plot and Char + filterTab.filterOpt._widgets[switchMap["plotRoot"]].setChecked(True) + filterTab.filterOpt._widgets[switchMap["charRoot"]].setChecked(True) + assert filterTab.optTree.topLevelItemCount() == 3 + + # Switch off novel docs + filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(False) + assert build.buildItemFilter(nwGUI.theProject) == { + C.hNovelRoot: (False, FilterMode.SKIPPED), + C.hTitlePage: (False, FilterMode.FILTERED), + C.hChapterDir: (False, FilterMode.SKIPPED), + C.hChapterDoc: (False, FilterMode.FILTERED), + C.hSceneDoc: (False, FilterMode.FILTERED), + C.hPlotRoot: (False, FilterMode.SKIPPED), + hPlotDoc: (False, FilterMode.FILTERED), + C.hCharRoot: (False, FilterMode.SKIPPED), + hCharDoc: (False, FilterMode.FILTERED), + C.hWorldRoot: (False, FilterMode.SKIPPED), + } + + # Switch on note docs + filterTab.filterOpt._widgets[switchMap["incNotes"]].setChecked(True) + assert build.buildItemFilter(nwGUI.theProject) == { + C.hNovelRoot: (False, FilterMode.SKIPPED), + C.hTitlePage: (False, FilterMode.FILTERED), + C.hChapterDir: (False, FilterMode.SKIPPED), + C.hChapterDoc: (False, FilterMode.FILTERED), + C.hSceneDoc: (False, FilterMode.FILTERED), + C.hPlotRoot: (False, FilterMode.SKIPPED), + hPlotDoc: (False, FilterMode.FILTERED), # Set to inactive + C.hCharRoot: (False, FilterMode.SKIPPED), + hCharDoc: (True, FilterMode.FILTERED), # Now enabled + C.hWorldRoot: (False, FilterMode.SKIPPED), + } + + # Switch on inactive docs + filterTab.filterOpt._widgets[switchMap["incInactive"]].setChecked(True) + assert build.buildItemFilter(nwGUI.theProject) == { + C.hNovelRoot: (False, FilterMode.SKIPPED), + C.hTitlePage: (False, FilterMode.FILTERED), + C.hChapterDir: (False, FilterMode.SKIPPED), + C.hChapterDoc: (False, FilterMode.FILTERED), + C.hSceneDoc: (False, FilterMode.FILTERED), + C.hPlotRoot: (False, FilterMode.SKIPPED), + hPlotDoc: (True, FilterMode.FILTERED), # Now enabled + C.hCharRoot: (False, FilterMode.SKIPPED), + hCharDoc: (True, FilterMode.FILTERED), + C.hWorldRoot: (False, FilterMode.SKIPPED), + } + + # Set chapter and scene docs to included + filterTab._treeMap[C.hChapterDoc].setSelected(True) + filterTab._treeMap[C.hSceneDoc].setSelected(True) + filterTab.includedButton.click() + assert build.buildItemFilter(nwGUI.theProject) == { + C.hNovelRoot: (False, FilterMode.SKIPPED), + C.hTitlePage: (False, FilterMode.FILTERED), + C.hChapterDir: (False, FilterMode.SKIPPED), + C.hChapterDoc: (True, FilterMode.INCLUDED), # Now included + C.hSceneDoc: (True, FilterMode.INCLUDED), # Now included + C.hPlotRoot: (False, FilterMode.SKIPPED), + hPlotDoc: (True, FilterMode.FILTERED), + C.hCharRoot: (False, FilterMode.SKIPPED), + hCharDoc: (True, FilterMode.FILTERED), + C.hWorldRoot: (False, FilterMode.SKIPPED), + } + + # Set char and plot docs to excluded + filterTab.optTree.clearSelection() + filterTab._treeMap[hPlotDoc].setSelected(True) + filterTab._treeMap[hCharDoc].setSelected(True) + filterTab.excludedButton.click() + assert build.buildItemFilter(nwGUI.theProject) == { + C.hNovelRoot: (False, FilterMode.SKIPPED), + C.hTitlePage: (False, FilterMode.FILTERED), + C.hChapterDir: (False, FilterMode.SKIPPED), + C.hChapterDoc: (True, FilterMode.INCLUDED), + C.hSceneDoc: (True, FilterMode.INCLUDED), + C.hPlotRoot: (False, FilterMode.SKIPPED), + hPlotDoc: (False, FilterMode.EXCLUDED), # Now excluded + C.hCharRoot: (False, FilterMode.SKIPPED), + hCharDoc: (False, FilterMode.EXCLUDED), # Now excluded + C.hWorldRoot: (False, FilterMode.SKIPPED), + } + + # Switch on novel docs + filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(True) + assert build.buildItemFilter(nwGUI.theProject) == { + C.hNovelRoot: (False, FilterMode.SKIPPED), + C.hTitlePage: (True, FilterMode.FILTERED), # Now enabled + C.hChapterDir: (False, FilterMode.SKIPPED), + C.hChapterDoc: (True, FilterMode.INCLUDED), + C.hSceneDoc: (True, FilterMode.INCLUDED), + C.hPlotRoot: (False, FilterMode.SKIPPED), + hPlotDoc: (False, FilterMode.EXCLUDED), + C.hCharRoot: (False, FilterMode.SKIPPED), + hCharDoc: (False, FilterMode.EXCLUDED), + C.hWorldRoot: (False, FilterMode.SKIPPED), + } + + # Set everything back to filtered + filterTab.optTree.clearSelection() + filterTab._treeMap[C.hChapterDoc].setSelected(True) + filterTab._treeMap[C.hSceneDoc].setSelected(True) + filterTab._treeMap[hPlotDoc].setSelected(True) + filterTab._treeMap[hCharDoc].setSelected(True) + filterTab.filteredButton.click() + assert build.buildItemFilter(nwGUI.theProject) == { + C.hNovelRoot: (False, FilterMode.SKIPPED), + C.hTitlePage: (True, FilterMode.FILTERED), + C.hChapterDir: (False, FilterMode.SKIPPED), + C.hChapterDoc: (True, FilterMode.FILTERED), + C.hSceneDoc: (True, FilterMode.FILTERED), + C.hPlotRoot: (False, FilterMode.SKIPPED), + hPlotDoc: (True, FilterMode.FILTERED), + C.hCharRoot: (False, FilterMode.SKIPPED), + hCharDoc: (True, FilterMode.FILTERED), + C.hWorldRoot: (False, FilterMode.SKIPPED), + } + + # Check handling of invalid project items + assert list(filterTab._treeMap.keys()) == [ + C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, + C.hPlotRoot, hPlotDoc, C.hCharRoot, hCharDoc, + ] + nwGUI.theProject.tree[hCharDoc].setRoot(None) # Char doc has no root handle + nwGUI.theProject.tree[hPlotDoc].setParent(None) # Plot doc has no parent handle + filterTab._populateTree() + assert list(filterTab._treeMap.keys()) == [ + C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, + C.hPlotRoot, C.hCharRoot + ] + + # Finish + bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) + # qtbot.stop() + +# END Test testBuildSettings_Filter + + +@pytest.mark.gui +def testBuildSettings_Headings(qtbot: QtBot, nwGUI: GuiMain): + """Test the Headings Tab of the GuiBuildSettings dialog.""" + build = BuildSettings() + + ttTitle = f"Title: {nwHeadFmt.TITLE}" + chTitle = f"Chapter: {nwHeadFmt.TITLE}" + unTitle = f"Interlude: {nwHeadFmt.TITLE}" + scTitle = f"Scene: {nwHeadFmt.TITLE}" + sxTitle = f"Section: {nwHeadFmt.TITLE}" + + build.setValue("headings.fmtTitle", ttTitle) + build.setValue("headings.fmtChapter", chTitle) + build.setValue("headings.fmtUnnumbered", unTitle) + build.setValue("headings.fmtScene", scTitle) + build.setValue("headings.fmtSection", sxTitle) + build.setValue("headings.hideScene", False) + build.setValue("headings.hideSection", False) + + # Create the dialog and populate it + bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings.show() + bSettings.loadContent() + + headTab = bSettings.optTabHeadings + bSettings.optSideBar._group.button(bSettings.OPT_HEADINGS).click() + assert bSettings.toolStack.currentWidget() is headTab + + # Check initial values + assert headTab.fmtTitle.text() == ttTitle + assert headTab.fmtChapter.text() == chTitle + assert headTab.fmtUnnumbered.text() == unTitle + assert headTab.fmtScene.text() == scTitle + assert headTab.fmtSection.text() == sxTitle + assert headTab.swtScene.isChecked() is False + assert headTab.swtSection.isChecked() is False + + # Edit in Turn + # ============ + + # Nothing to apply + assert headTab._editing == 0 + assert headTab.editTextBox.isEnabled() is False + headTab.btnApply.click() + assert headTab.editTextBox.isEnabled() is False + + # Title + headTab.btnTitle.click() + assert headTab._editing == headTab.EDIT_TITLE + assert headTab.editTextBox.isEnabled() is True + assert headTab.editTextBox.toPlainText() == ttTitle + headTab.btnApply.click() + assert headTab.editTextBox.isEnabled() is False + + # Chapter + headTab.btnChapter.click() + assert headTab._editing == headTab.EDIT_CHAPTER + assert headTab.editTextBox.isEnabled() is True + assert headTab.editTextBox.toPlainText() == chTitle + headTab.btnApply.click() + assert headTab.editTextBox.isEnabled() is False + + # Unnumbered + headTab.btnUnnumbered.click() + assert headTab._editing == headTab.EDIT_UNNUM + assert headTab.editTextBox.isEnabled() is True + assert headTab.editTextBox.toPlainText() == unTitle + headTab.btnApply.click() + assert headTab.editTextBox.isEnabled() is False + + # Scene + headTab.btnScene.click() + assert headTab._editing == headTab.EDIT_SCENE + assert headTab.editTextBox.isEnabled() is True + assert headTab.editTextBox.toPlainText() == scTitle + headTab.btnApply.click() + assert headTab.editTextBox.isEnabled() is False + + # Section + headTab.btnSection.click() + assert headTab._editing == headTab.EDIT_SECTION + assert headTab.editTextBox.isEnabled() is True + assert headTab.editTextBox.toPlainText() == sxTitle + headTab.btnApply.click() + assert headTab.editTextBox.isEnabled() is False + + # Edit a Heading + # ============== + + # Create new format of all bits + headTab.btnChapter.click() + allFmt = ( + f"{nwHeadFmt.TITLE}{nwHeadFmt.CH_NUM}{nwHeadFmt.CH_WORD}{nwHeadFmt.CH_ROMU}" + f"{nwHeadFmt.CH_ROML}{nwHeadFmt.SC_NUM}{nwHeadFmt.SC_ABS}" + ) + headTab.editTextBox.clear() + assert headTab.editTextBox.toPlainText() == "" + headTab.aInsTitle.trigger() + headTab.aInsChNum.trigger() + headTab.aInsChWord.trigger() + headTab.aInsChRomU.trigger() + headTab.aInsChRomL.trigger() + headTab.aInsScNum.trigger() + headTab.aInsScAbs.trigger() + assert headTab.editTextBox.toPlainText() == allFmt + headTab.btnApply.click() + assert build.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") == f"Chapter {nwHeadFmt.CH_NUM}//{nwHeadFmt.TITLE}" + + # Set all to plain title + headTab.btnTitle.click() + headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) + headTab.btnApply.click() + assert build.getStr("headings.fmtTitle") == nwHeadFmt.TITLE + + headTab.btnChapter.click() + headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) + headTab.btnApply.click() + assert build.getStr("headings.fmtChapter") == nwHeadFmt.TITLE + + headTab.btnUnnumbered.click() + headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) + headTab.btnApply.click() + assert build.getStr("headings.fmtUnnumbered") == nwHeadFmt.TITLE + + headTab.btnScene.click() + headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) + headTab.btnApply.click() + assert build.getStr("headings.fmtScene") == nwHeadFmt.TITLE + + headTab.btnSection.click() + headTab.editTextBox.setPlainText(nwHeadFmt.TITLE) + headTab.btnApply.click() + assert build.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 + + # Finish + bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) + # qtbot.stop() + +# END Test testBuildSettings_Headings + + +@pytest.mark.gui +def testBuildSettings_Content(qtbot: QtBot, nwGUI: GuiMain): + """Test the Content Tab of the GuiBuildSettings dialog.""" + build = BuildSettings() + + build.setValue("text.includeSynopsis", False) + build.setValue("text.includeComments", False) + build.setValue("text.includeKeywords", False) + build.setValue("text.includeBodyText", False) + + build.setValue("text.addNoteHeadings", False) + + # Create the dialog and populate it + bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings.show() + bSettings.loadContent() + + contTab = bSettings.optTabContent + bSettings.optSideBar._group.button(bSettings.OPT_CONTENT).click() + assert bSettings.toolStack.currentWidget() is contTab + + # Check initial values + assert contTab.incSynopsis.isChecked() is False + assert contTab.incComments.isChecked() is False + assert contTab.incKeywords.isChecked() is False + assert contTab.incBodyText.isChecked() is False + + assert contTab.addNoteHead.isChecked() is False + + # Toggle all + contTab.incSynopsis.setChecked(True) + contTab.incComments.setChecked(True) + contTab.incKeywords.setChecked(True) + contTab.incBodyText.setChecked(True) + + contTab.addNoteHead.setChecked(True) + + # Save values + contTab.saveContent() + + assert build.getBool("text.includeSynopsis") is True + assert build.getBool("text.includeComments") is True + assert build.getBool("text.includeKeywords") is True + assert build.getBool("text.includeBodyText") is True + + assert build.getBool("text.addNoteHeadings") is True + + # Finish + bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) + # qtbot.stop() + +# END Test testBuildSettings_Content + + +@pytest.mark.gui +def testBuildSettings_Format(monkeypatch, qtbot: QtBot, nwGUI: GuiMain): + """Test the Format Tab of the GuiBuildSettings dialog.""" + build = BuildSettings() + + textFont = str(CONFIG.textFont) + + build.setValue("format.buildLang", "en_US") + build.setValue("format.textFont", "") # Will fall back to config value + build.setValue("format.textSize", 12) + build.setValue("format.lineHeight", 1.2) + build.setValue("format.justifyText", False) + build.setValue("format.stripUnicode", False) + build.setValue("format.replaceTabs", False) + + # Create the dialog and populate it + bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings.show() + bSettings.loadContent() + + fmtTab = bSettings.optTabFormat + bSettings.optSideBar._group.button(bSettings.OPT_FORMAT).click() + assert bSettings.toolStack.currentWidget() is fmtTab + + # Check initial values + assert fmtTab.buildLang.currentData() == "en_US" + assert fmtTab.textFont.text() == textFont + assert fmtTab.textSize.value() == 12 + assert fmtTab.lineHeight.value() == 1.2 + assert fmtTab.justifyText.isChecked() is False + assert fmtTab.stripUnicode.isChecked() is False + assert fmtTab.replaceTabs.isChecked() is False + + # Change values + fmtTab.buildLang.setCurrentIndex(fmtTab.buildLang.findData("en_GB")) + fmtTab.textFont.setText("Arial") + fmtTab.textSize.setValue(11) + fmtTab.lineHeight.setValue(1.15) + fmtTab.justifyText.setChecked(True) + fmtTab.stripUnicode.setChecked(True) + fmtTab.replaceTabs.setChecked(True) + + # Save values + fmtTab.saveContent() + + assert build.getStr("format.buildLang") == "en_GB" + assert build.getStr("format.textFont") == "Arial" + assert build.getInt("format.textSize") == 11 + assert build.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 + + # Check that the font dialog doesn't fail + with monkeypatch.context() as mp: + font = QFont() + font.setFamily("Times") + font.setPointSize(10) + mp.setattr(QFontDialog, "getFont", lambda *a: (font, True)) + + fmtTab.btnTextFont.click() + assert fmtTab.textFont.text() == "Times" + assert fmtTab.textSize.value() == 10 + + # Finish + bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) + # qtbot.stop() + +# END Test testBuildSettings_Format + + +@pytest.mark.gui +def testBuildSettings_Output(qtbot: QtBot, nwGUI: GuiMain): + """Test the Output Tab of the GuiBuildSettings dialog.""" + build = BuildSettings() + + build.setValue("odt.addColours", False) + build.setValue("html.addStyles", False) + + # Create the dialog and populate it + bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings.show() + bSettings.loadContent() + + outTab = bSettings.optTabOutput + bSettings.optSideBar._group.button(bSettings.OPT_OUTPUT).click() + assert bSettings.toolStack.currentWidget() is outTab + + # Check initial values + assert outTab.odtAddColours.isChecked() is False + assert outTab.htmlAddStyles.isChecked() is False + + # Toggle all + outTab.odtAddColours.setChecked(True) + outTab.htmlAddStyles.setChecked(True) + + # Save values + outTab.saveContent() + + assert build.getBool("odt.addColours") is True + assert build.getBool("html.addStyles") is True + + # Finish + bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close)) + # qtbot.stop() + +# END Test testBuildSettings_Output