From ee21045ee2953c07cbe37149d26a4c430b7f633f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:50:17 +0100 Subject: [PATCH 1/7] Update the design on the Project Settings dialog --- .../{projsettings.py => projectsettings.py} | 318 ++++++++++-------- novelwriter/extensions/configlayout.py | 18 +- novelwriter/gui/projtree.py | 6 +- novelwriter/guimain.py | 12 +- 4 files changed, 195 insertions(+), 159 deletions(-) rename novelwriter/dialogs/{projsettings.py => projectsettings.py} (72%) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projectsettings.py similarity index 72% rename from novelwriter/dialogs/projsettings.py rename to novelwriter/dialogs/projectsettings.py index 8a9354a0..51efce1b 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -3,7 +3,8 @@ novelWriter – GUI Project Settings ================================== File History: -Created: 2018-09-29 [0.0.1] GuiProjectSettings +Created: 2018-09-29 [0.0.1] GuiProjectSettings +Rewritten: 2024-01-26 [2.3b1] GuiProjectSettings This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen @@ -25,75 +26,102 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING - -from PyQt5.QtGui import QCloseEvent, QIcon, QPixmap, QColor +from PyQt5.QtGui import QCloseEvent, QColor, QIcon, QPixmap from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QColorDialog, QComboBox, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, qApp + QColorDialog, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QStackedWidget, QTreeWidget, QTreeWidgetItem, + QVBoxLayout, QWidget, qApp ) from novelwriter import CONFIG, SHARED from novelwriter.common import simplified from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.pageddialog import NPagedDialog -from novelwriter.extensions.configlayout import NConfigLayout - -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain +from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm +from novelwriter.extensions.pagedsidebar import NPagedSideBar logger = logging.getLogger(__name__) -class GuiProjectSettings(NPagedDialog): +class GuiProjectSettings(QDialog): - TAB_MAIN = 0 - TAB_STATUS = 1 - TAB_IMPORT = 2 - TAB_REPLACE = 3 + PAGE_SETTINGS = 0 + PAGE_STATUS = 1 + PAGE_IMPORT = 2 + PAGE_REPLACE = 3 - newProjectSettingsReady = pyqtSignal() + newProjectSettingsReady = pyqtSignal(bool) - def __init__(self, mainGui: GuiMain, focusTab: int = TAB_MAIN) -> None: - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget, gotoPage: int = PAGE_SETTINGS) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiProjectSettings") self.setObjectName("GuiProjectSettings") - - self.mainGui = mainGui - SHARED.project.countStatus() self.setWindowTitle(self.tr("Project Settings")) - wW = CONFIG.pxInt(570) - wH = CONFIG.pxInt(375) - pOptions = SHARED.project.options + wW = CONFIG.pxInt(500) + wH = CONFIG.pxInt(400) + options = SHARED.project.options - self.setMinimumWidth(wW) - self.setMinimumHeight(wH) + self.setMinimumSize(wW, wH) self.resize( - CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winWidth", wW)), - CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH)) + CONFIG.pxInt(options.getInt("GuiProjectSettings", "winWidth", wW)), + CONFIG.pxInt(options.getInt("GuiProjectSettings", "winHeight", wH)) ) - self.tabMain = GuiProjectEditMain(self) - self.tabStatus = GuiProjectEditStatus(self, True) - self.tabImport = GuiProjectEditStatus(self, False) - self.tabReplace = GuiProjectEditReplace(self) + # Title + self.titleLabel = NColourLabel( + self.tr("Project Settings"), SHARED.theme.helpText, parent=self, scale=1.25 + ) + self.titleLabel.setIndent(CONFIG.pxInt(4)) - self.addTab(self.tabMain, self.tr("Settings")) - self.addTab(self.tabStatus, self.tr("Status")) - self.addTab(self.tabImport, self.tr("Importance")) - self.addTab(self.tabReplace, self.tr("Auto-Replace")) + # SideBar + self.sidebar = NPagedSideBar(self) + self.sidebar.setLabelColor(SHARED.theme.helpText) + self.sidebar.addButton(self.tr("Settings"), self.PAGE_SETTINGS) + self.sidebar.addButton(self.tr("Status"), self.PAGE_STATUS) + self.sidebar.addButton(self.tr("Importance"), self.PAGE_IMPORT) + self.sidebar.addButton(self.tr("Auto-Replace"), self.PAGE_REPLACE) + self.sidebar.setSelected(gotoPage) + self.sidebar.buttonClicked.connect(self._sidebarClicked) - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + # Buttons + self.buttonBox = QDialogButtonBox( + QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel + ) self.buttonBox.accepted.connect(self._doSave) self.buttonBox.rejected.connect(self.close) - self.rejected.connect(self.close) - self.addControls(self.buttonBox) - # Focus Tab - self._focusTab(focusTab) + # Content + self.settingsPage = _SettingsPage(self) + self.statusPage = _StatusPage(self, True) + self.importPage = _StatusPage(self, False) + self.replacePage = _ReplacePage(self) + + self.mainStack = QStackedWidget(self) + self.mainStack.addWidget(self.settingsPage) + self.mainStack.addWidget(self.statusPage) + self.mainStack.addWidget(self.importPage) + self.mainStack.addWidget(self.replacePage) + + # Assemble + self.topBox = QHBoxLayout() + self.topBox.addWidget(self.titleLabel) + self.topBox.addStretch(1) + + self.mainBox = QHBoxLayout() + self.mainBox.addWidget(self.sidebar) + self.mainBox.addWidget(self.mainStack) + self.mainBox.setContentsMargins(0, 0, 0, 0) + + self.outerBox = QVBoxLayout() + self.outerBox.addLayout(self.topBox) + self.outerBox.addLayout(self.mainBox) + self.outerBox.addWidget(self.buttonBox) + self.outerBox.setSpacing(CONFIG.pxInt(8)) + + self.setLayout(self.outerBox) + self.setSizeGripEnabled(True) logger.debug("Ready: GuiProjectSettings") @@ -103,9 +131,13 @@ class GuiProjectSettings(NPagedDialog): logger.debug("Delete: GuiProjectSettings") return + ## + # Events + ## + def closeEvent(self, event: QCloseEvent) -> None: - """Capture the close event and perform cleanup.""" - self._saveGuiSettings() + """Capture the user closing the window and save settings.""" + self._saveSettings() event.accept() self.deleteLater() return @@ -114,40 +146,52 @@ class GuiProjectSettings(NPagedDialog): # Private Slots ## + @pyqtSlot(int) + def _sidebarClicked(self, pageId: int) -> None: + """Process a user request to switch page.""" + if pageId == self.PAGE_SETTINGS: + self.mainStack.setCurrentWidget(self.settingsPage) + elif pageId == self.PAGE_STATUS: + self.mainStack.setCurrentWidget(self.statusPage) + elif pageId == self.PAGE_IMPORT: + self.mainStack.setCurrentWidget(self.importPage) + elif pageId == self.PAGE_REPLACE: + self.mainStack.setCurrentWidget(self.replacePage) + return + @pyqtSlot() def _doSave(self) -> None: """Save settings and close dialog.""" project = SHARED.project - projName = self.tabMain.editName.text() - bookTitle = self.tabMain.editTitle.text() - bookAuthor = self.tabMain.editAuthor.text() - projLang = self.tabMain.projLang.currentData() - spellLang = self.tabMain.spellLang.currentData() - doBackup = not self.tabMain.doBackup.isChecked() + projName = self.settingsPage.projName.text() + projAuthor = self.settingsPage.projAuthor.text() + projLang = self.settingsPage.projLang.currentData() + spellLang = self.settingsPage.spellLang.currentData() + doBackup = not self.settingsPage.doBackup.isChecked() project.data.setName(projName) - project.data.setTitle(bookTitle) - project.data.setAuthor(bookAuthor) + project.data.setAuthor(projAuthor) project.data.setDoBackup(doBackup) project.data.setSpellLang(spellLang) project.setProjectLang(projLang) - if self.tabStatus.colChanged: - newList, delList = self.tabStatus.getNewList() + rebuildTrees = False + + if self.statusPage.colChanged: + newList, delList = self.statusPage.getNewList() project.setStatusColours(newList, delList) + rebuildTrees = True - if self.tabImport.colChanged: - newList, delList = self.tabImport.getNewList() + if self.importPage.colChanged: + newList, delList = self.importPage.getNewList() project.setImportColours(newList, delList) + rebuildTrees = True - if self.tabStatus.colChanged or self.tabImport.colChanged: - self.mainGui.rebuildTrees() - - if self.tabReplace.arChanged: - newList = self.tabReplace.getNewList() + if self.replacePage.arChanged: + newList = self.replacePage.getNewList() project.data.setAutoReplace(newList) - self.newProjectSettingsReady.emit() + self.newProjectSettingsReady.emit(rebuildTrees) qApp.processEvents() self.close() @@ -157,134 +201,104 @@ class GuiProjectSettings(NPagedDialog): # Internal Functions ## - def _focusTab(self, tab: int) -> None: - """Change which is the focused tab.""" - if tab == self.TAB_MAIN: - self.setCurrentWidget(self.tabMain) - elif tab == self.TAB_STATUS: - self.setCurrentWidget(self.tabStatus) - elif tab == self.TAB_IMPORT: - self.setCurrentWidget(self.tabImport) - elif tab == self.TAB_REPLACE: - self.setCurrentWidget(self.tabReplace) - return - - def _saveGuiSettings(self) -> None: + def _saveSettings(self) -> None: """Save GUI settings.""" winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) - replaceColW = CONFIG.rpxInt(self.tabReplace.listBox.columnWidth(0)) - statusColW = CONFIG.rpxInt(self.tabStatus.listBox.columnWidth(0)) - importColW = CONFIG.rpxInt(self.tabImport.listBox.columnWidth(0)) + statusColW = CONFIG.rpxInt(self.statusPage.columnWidth()) + importColW = CONFIG.rpxInt(self.importPage.columnWidth()) + replaceColW = CONFIG.rpxInt(self.replacePage.columnWidth()) logger.debug("Saving State: GuiProjectSettings") - pOptions = SHARED.project.options - pOptions.setValue("GuiProjectSettings", "winWidth", winWidth) - pOptions.setValue("GuiProjectSettings", "winHeight", winHeight) - pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW) - pOptions.setValue("GuiProjectSettings", "statusColW", statusColW) - pOptions.setValue("GuiProjectSettings", "importColW", importColW) + options = SHARED.project.options + options.setValue("GuiProjectSettings", "winWidth", winWidth) + options.setValue("GuiProjectSettings", "winHeight", winHeight) + options.setValue("GuiProjectSettings", "statusColW", statusColW) + options.setValue("GuiProjectSettings", "importColW", importColW) + options.setValue("GuiProjectSettings", "replaceColW", replaceColW) return # END Class GuiProjectSettings -class GuiProjectEditMain(QWidget): +class _SettingsPage(NScrollableForm): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - # The Form - self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(SHARED.theme.helpText) - self.setLayout(self.mainForm) - - self.mainForm.addGroupLabel(self.tr("Project Settings")) - xW = CONFIG.pxInt(250) - pData = SHARED.project.data + data = SHARED.project.data + self.setHelpTextStyle(SHARED.theme.helpText) + self.setRowIndent(0) - self.editName = QLineEdit() - self.editName.setMaxLength(200) - self.editName.setMaximumWidth(xW) - self.editName.setText(pData.name) - self.mainForm.addRow( + # Project Name + self.projName = QLineEdit(self) + self.projName.setMaxLength(200) + self.projName.setMaximumWidth(xW) + self.projName.setText(data.name) + self.addRow( self.tr("Project name"), - self.editName, - self.tr("Should be set only once.") + self.projName, + self.tr("Changing this will affect the backup path.") ) - self.editTitle = QLineEdit() - self.editTitle.setMaxLength(200) - self.editTitle.setMaximumWidth(xW) - self.editTitle.setText(pData.title) - self.mainForm.addRow( - self.tr("Novel title"), - self.editTitle, - self.tr("Change whenever you want!") - ) - - self.editAuthor = QLineEdit() - self.editAuthor.setMaxLength(200) - self.editAuthor.setMaximumWidth(xW) - self.editAuthor.setText(pData.author) - self.mainForm.addRow( + # Project Author + self.projAuthor = QLineEdit(self) + self.projAuthor.setMaxLength(200) + self.projAuthor.setMaximumWidth(xW) + self.projAuthor.setText(data.author) + self.addRow( self.tr("Author(s)"), - self.editAuthor, - self.tr("Change whenever you want!") + self.projAuthor, + self.tr("Only used when building the manuscript.") ) + # Project Language self.projLang = QComboBox(self) self.projLang.setMaximumWidth(xW) for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ): self.projLang.addItem(language, tag) - self.mainForm.addRow( + self.addRow( self.tr("Project language"), self.projLang, - self.tr("Used when building the manuscript.") + self.tr("Only used when building the manuscript.") ) + if (idx := self.projLang.findData(data.language)) != -1: + self.projLang.setCurrentIndex(idx) - langIdx = 0 - if pData.language is not None: - langIdx = self.projLang.findData(pData.language) - if langIdx == -1: - langIdx = self.projLang.findData("en_GB") - if langIdx != -1: - self.projLang.setCurrentIndex(langIdx) - + # Spell Check Language self.spellLang = QComboBox(self) self.spellLang.setMaximumWidth(xW) self.spellLang.addItem(self.tr("Default"), "None") if CONFIG.hasEnchant: for tag, language in SHARED.spelling.listDictionaries(): self.spellLang.addItem(language, tag) - self.mainForm.addRow( + self.addRow( self.tr("Spell check language"), self.spellLang, self.tr("Overrides main preferences.") ) + if (idx := self.spellLang.findData(data.spellLang)) != -1: + self.spellLang.setCurrentIndex(idx) - langIdx = 0 - if pData.spellLang is not None: - langIdx = self.spellLang.findData(pData.spellLang) - if langIdx != -1: - self.spellLang.setCurrentIndex(langIdx) - + # Backup on Close self.doBackup = NSwitch(self) - self.doBackup.setChecked(not pData.doBackup) - self.mainForm.addRow( - self.tr("No backup on close"), + self.doBackup.setChecked(not data.doBackup) + self.addRow( + self.tr("Disable backup on close"), self.doBackup, self.tr("Overrides main preferences.") ) + self.finalise() + return -# END Class GuiProjectEditMain +# END Class _SettingsPage -class GuiProjectEditStatus(QWidget): +class _StatusPage(QWidget): COL_LABEL = 0 COL_USAGE = 1 @@ -318,7 +332,7 @@ class GuiProjectEditStatus(QWidget): # The List # ======== - self.listBox = QTreeWidget() + self.listBox = QTreeWidget(self) self.listBox.setHeaderLabels([ self.tr("Label"), self.tr("Usage"), ]) @@ -332,16 +346,16 @@ class GuiProjectEditStatus(QWidget): # List Controls # ============= - self.addButton = QPushButton(SHARED.theme.getIcon("add"), "") + self.addButton = QPushButton(SHARED.theme.getIcon("add"), "", self) self.addButton.clicked.connect(self._newItem) - self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "") + self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "", self) self.delButton.clicked.connect(self._delItem) - self.upButton = QPushButton(SHARED.theme.getIcon("up"), "") + self.upButton = QPushButton(SHARED.theme.getIcon("up"), "", self) self.upButton.clicked.connect(lambda: self._moveItem(-1)) - self.dnButton = QPushButton(SHARED.theme.getIcon("down"), "") + self.dnButton = QPushButton(SHARED.theme.getIcon("down"), "", self) self.dnButton.clicked.connect(lambda: self._moveItem(1)) # Edit Form @@ -389,6 +403,7 @@ class GuiProjectEditStatus(QWidget): self.outerBox = QVBoxLayout() self.outerBox.addWidget(QLabel("%s" % pageLabel)) self.outerBox.addLayout(self.innerBox) + self.outerBox.setContentsMargins(0, 0, 0, 0) self.setLayout(self.outerBox) @@ -410,6 +425,10 @@ class GuiProjectEditStatus(QWidget): return [], [] + def columnWidth(self) -> int: + """Return the size of the header column.""" + return self.listBox.columnWidth(0) + ## # Private Slots ## @@ -553,10 +572,10 @@ class GuiProjectEditStatus(QWidget): else: return self.tr("Used by {0} items").format(nUse) -# END Class GuiProjectEditStatus +# END Class _StatusPage -class GuiProjectEditReplace(QWidget): +class _ReplacePage(QWidget): COL_KEY = 0 COL_REPL = 1 @@ -638,6 +657,7 @@ class GuiProjectEditReplace(QWidget): self.outerBox = QVBoxLayout() self.outerBox.addWidget(QLabel("%s" % pageLabel)) self.outerBox.addLayout(self.innerBox) + self.outerBox.setContentsMargins(0, 0, 0, 0) self.setLayout(self.outerBox) @@ -655,6 +675,10 @@ class GuiProjectEditReplace(QWidget): new[aKey] = aVal return new + def columnWidth(self) -> int: + """Return the size of the header column.""" + return self.listBox.columnWidth(0) + ## # Internal Functions ## @@ -725,4 +749,4 @@ class GuiProjectEditReplace(QWidget): result += c return result -# END Class GuiProjectEditReplace +# END Class _ReplacePage diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index c71036be..2db50ded 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -50,6 +50,7 @@ class NScrollableForm(QScrollArea): self._helpCol = QColor(0, 0, 0) self._fontScale = FONT_SCALE self._first = True + self._indent = CONFIG.pxInt(12) self._sections: dict[int, QLabel] = {} self._editable: dict[str, NColourLabel] = {} @@ -92,6 +93,16 @@ class NScrollableForm(QScrollArea): qHelp.setText(text) return + def setRowIndent(self, indent: int) -> None: + """Set the indentation of each row.""" + self._indent = max(indent, 0) + return + + def setLayoutMargins(self, left: int, top: int, right: int, bottom: int) -> None: + """Set the contents margins on the layout.""" + self._layout.setContentsMargins(left, top, right, bottom) + return + ## # Methods ## @@ -128,14 +139,13 @@ class NScrollableForm(QScrollArea): row = QHBoxLayout() row.setSpacing(CONFIG.pxInt(4)) - mPx = CONFIG.pxInt(12) qLabel = QLabel(label, self) - qLabel.setIndent(mPx) + qLabel.setIndent(self._indent) qLabel.setBuddy(widget) if helpText: qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) - qHelp.setIndent(mPx) + qHelp.setIndent(self._indent) labelBox = QVBoxLayout() labelBox.addWidget(qLabel) labelBox.addWidget(qHelp) @@ -147,7 +157,7 @@ class NScrollableForm(QScrollArea): else: row.addWidget(qLabel) - row.addSpacing(mPx) + row.addSpacing(CONFIG.pxInt(12)) row.addWidget(widget) if isinstance(unit, str): diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 8c42a107..da050876 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -48,7 +48,7 @@ from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel -from novelwriter.dialogs.projsettings import GuiProjectSettings +from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.enum import ( nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwWidget ) @@ -1748,7 +1748,7 @@ class _TreeContextMenu(QMenu): menu.addSeparator() action = menu.addAction(self.tr("Manage Labels ...")) action.triggered.connect( - lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS) + lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS) ) else: menu = self.addMenu(self.tr("Set Importance to ...")) @@ -1765,7 +1765,7 @@ class _TreeContextMenu(QMenu): menu.addSeparator() action = menu.addAction(self.tr("Manage Labels ...")) action.triggered.connect( - lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT) + lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT) ) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 795c058a..0282b8d7 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -53,7 +53,7 @@ from novelwriter.dialogs.about import GuiAbout from novelwriter.dialogs.updates import GuiUpdates from novelwriter.dialogs.wordlist import GuiWordList from novelwriter.dialogs.preferences import GuiPreferences -from novelwriter.dialogs.projsettings import GuiProjectSettings +from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.tools.welcome import GuiWelcome from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.dictionaries import GuiDictionaries @@ -809,10 +809,10 @@ class GuiMain(QMainWindow): @pyqtSlot() @pyqtSlot(int) - def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> None: + def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.PAGE_SETTINGS) -> None: """Open the project settings dialog.""" if SHARED.hasProject: - dialog = GuiProjectSettings(self, focusTab=focusTab) + dialog = GuiProjectSettings(self, gotoPage=focusTab) dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges) dialog.exec_() return @@ -1111,13 +1111,15 @@ class GuiMain(QMainWindow): return - @pyqtSlot() - def _processProjectSettingsChanges(self) -> None: + @pyqtSlot(bool) + def _processProjectSettingsChanges(self, rebuildTrees: bool) -> None: """Refresh data dependent on project settings.""" logger.debug("Applying new project settings") SHARED.updateSpellCheckLanguage() self.itemDetails.refreshDetails() self._updateWindowTitle(SHARED.project.data.name) + if rebuildTrees: + self.rebuildTrees() return @pyqtSlot() From c6ee6746a295ca05fdc59ef0497c7d5635757b18 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:51:31 +0100 Subject: [PATCH 2/7] Make layout improvements to Project Settings --- novelwriter/dialogs/projectsettings.py | 170 ++++++++++++------------- novelwriter/extensions/configlayout.py | 24 ++-- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 51efce1b..57bd4237 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -177,17 +177,17 @@ class GuiProjectSettings(QDialog): rebuildTrees = False - if self.statusPage.colChanged: + if self.statusPage.wasChanged: newList, delList = self.statusPage.getNewList() project.setStatusColours(newList, delList) rebuildTrees = True - if self.importPage.colChanged: + if self.importPage.wasChanged: newList, delList = self.importPage.getNewList() project.setImportColours(newList, delList) rebuildTrees = True - if self.replacePage.arChanged: + if self.replacePage.wasChanged: newList = self.replacePage.getNewList() project.data.setAutoReplace(newList) @@ -227,7 +227,7 @@ class _SettingsPage(NScrollableForm): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - xW = CONFIG.pxInt(250) + xW = CONFIG.pxInt(200) data = SHARED.project.data self.setHelpTextStyle(SHARED.theme.helpText) self.setRowIndent(0) @@ -235,23 +235,23 @@ class _SettingsPage(NScrollableForm): # Project Name self.projName = QLineEdit(self) self.projName.setMaxLength(200) - self.projName.setMaximumWidth(xW) + self.projName.setMinimumWidth(xW) self.projName.setText(data.name) self.addRow( - self.tr("Project name"), - self.projName, - self.tr("Changing this will affect the backup path.") + self.tr("Project name"), self.projName, + self.tr("Changing this will affect the backup path."), + stretch=(3, 2) ) # Project Author self.projAuthor = QLineEdit(self) self.projAuthor.setMaxLength(200) - self.projAuthor.setMaximumWidth(xW) + self.projAuthor.setMinimumWidth(xW) self.projAuthor.setText(data.author) self.addRow( - self.tr("Author(s)"), - self.projAuthor, - self.tr("Only used when building the manuscript.") + self.tr("Author(s)"), self.projAuthor, + self.tr("Only used when building the manuscript."), + stretch=(2, 1) ) # Project Language @@ -260,8 +260,7 @@ class _SettingsPage(NScrollableForm): for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ): self.projLang.addItem(language, tag) self.addRow( - self.tr("Project language"), - self.projLang, + self.tr("Project language"), self.projLang, self.tr("Only used when building the manuscript.") ) if (idx := self.projLang.findData(data.language)) != -1: @@ -275,8 +274,7 @@ class _SettingsPage(NScrollableForm): for tag, language in SHARED.spelling.listDictionaries(): self.spellLang.addItem(language, tag) self.addRow( - self.tr("Spell check language"), - self.spellLang, + self.tr("Spell check language"), self.spellLang, self.tr("Overrides main preferences.") ) if (idx := self.spellLang.findData(data.spellLang)) != -1: @@ -286,8 +284,7 @@ class _SettingsPage(NScrollableForm): self.doBackup = NSwitch(self) self.doBackup.setChecked(not data.doBackup) self.addRow( - self.tr("Disable backup on close"), - self.doBackup, + self.tr("Disable backup on close"), self.doBackup, self.tr("Overrides main preferences.") ) @@ -323,9 +320,9 @@ class _StatusPage(QWidget): SHARED.project.options.getInt("GuiProjectSettings", colSetting, 130) ) - self.colDeleted = [] - self.colChanged = False - self.selColour = QColor(100, 100, 100) + self._changed = False + self._colDeleted = [] + self._selColour = QColor(100, 100, 100) self.iPx = SHARED.theme.baseIconSize @@ -344,8 +341,6 @@ class _StatusPage(QWidget): self._addItem(key, entry["name"], entry["cols"], entry["count"]) # List Controls - # ============= - self.addButton = QPushButton(SHARED.theme.getIcon("add"), "", self) self.addButton.clicked.connect(self._newItem) @@ -359,8 +354,6 @@ class _StatusPage(QWidget): self.dnButton.clicked.connect(lambda: self._moveItem(1)) # Edit Form - # ========= - self.editName = QLineEdit() self.editName.setMaxLength(40) self.editName.setPlaceholderText(self.tr("Select item to edit")) @@ -378,8 +371,6 @@ class _StatusPage(QWidget): self.saveButton.clicked.connect(self._saveItem) # Assemble - # ======== - self.listControls = QVBoxLayout() self.listControls.addWidget(self.addButton) self.listControls.addWidget(self.delButton) @@ -409,9 +400,18 @@ class _StatusPage(QWidget): return + @property + def wasChanged(self) -> bool: + """The user changed these settings.""" + return self._changed + + ## + # Methods + ## + def getNewList(self) -> tuple[list, list]: """Return list of entries.""" - if self.colChanged: + if self._changed: newList = [] for n in range(self.listBox.topLevelItemCount()): item = self.listBox.topLevelItem(n) @@ -421,7 +421,7 @@ class _StatusPage(QWidget): "name": item.text(self.COL_LABEL), "cols": item.data(self.COL_LABEL, self.COL_ROLE), }) - return newList, self.colDeleted + return newList, self._colDeleted return [], [] @@ -436,12 +436,12 @@ class _StatusPage(QWidget): @pyqtSlot() def _selectColour(self) -> None: """Open a dialog to select the status icon colour.""" - if self.selColour is not None: + if self._selColour is not None: newCol = QColorDialog.getColor( - self.selColour, self, self.tr("Select Colour") + self._selColour, self, self.tr("Select Colour") ) if newCol.isValid(): - self.selColour = newCol + self._selColour = newCol pixmap = QPixmap(self.iPx, self.iPx) pixmap.fill(newCol) self.colButton.setIcon(QIcon(pixmap)) @@ -452,7 +452,7 @@ class _StatusPage(QWidget): def _newItem(self) -> None: """Create a new status item.""" self._addItem(None, self.tr("New Item"), (100, 100, 100), 0) - self.colChanged = True + self._changed = True return @pyqtSlot() @@ -465,8 +465,8 @@ class _StatusPage(QWidget): SHARED.error(self.tr("Cannot delete a status item that is in use.")) else: self.listBox.takeTopLevelItem(iRow) - self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) - self.colChanged = True + self._colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) + self._changed = True return @pyqtSlot() @@ -477,9 +477,9 @@ class _StatusPage(QWidget): selItem.setText(self.COL_LABEL, simplified(self.editName.text())) selItem.setIcon(self.COL_LABEL, self.colButton.icon()) selItem.setData(self.COL_LABEL, self.COL_ROLE, ( - self.selColour.red(), self.selColour.green(), self.selColour.blue() + self._selColour.red(), self._selColour.green(), self._selColour.blue() )) - self.colChanged = True + self._changed = True return @pyqtSlot() @@ -493,7 +493,7 @@ class _StatusPage(QWidget): name = selItem.text(self.COL_LABEL) pixmap = QPixmap(self.iPx, self.iPx) pixmap.fill(QColor(*cols)) - self.selColour = QColor(*cols) + self._selColour = QColor(*cols) self.editName.setText(name) self.colButton.setIcon(QIcon(pixmap)) self.editName.selectAll() @@ -504,7 +504,7 @@ class _StatusPage(QWidget): else: pixmap = QPixmap(self.iPx, self.iPx) pixmap.fill(QColor(100, 100, 100)) - self.selColour = QColor(100, 100, 100) + self._selColour = QColor(100, 100, 100) self.editName.setText("") self.colButton.setIcon(QIcon(pixmap)) self.editName.setEnabled(False) @@ -552,7 +552,7 @@ class _StatusPage(QWidget): if cItem is not None: cItem.setSelected(True) - self.colChanged = True + self._changed = True return @@ -583,7 +583,7 @@ class _ReplacePage(QWidget): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - self.arChanged = False + self._changed = False wCol0 = CONFIG.pxInt( SHARED.project.options.getInt("GuiProjectSettings", "replaceColW", 130) @@ -591,16 +591,11 @@ class _ReplacePage(QWidget): pageLabel = self.tr("Text Replace List for Preview and Export") # List Box - # ======== - self.listBox = QTreeWidget() - self.listBox.setHeaderLabels([ - self.tr("Keyword"), - self.tr("Replace With"), - ]) - self.listBox.itemSelectionChanged.connect(self._selectedItem) + self.listBox.setHeaderLabels([self.tr("Keyword"), self.tr("Replace With")]) self.listBox.setColumnWidth(self.COL_KEY, wCol0) self.listBox.setIndentation(0) + self.listBox.itemSelectionChanged.connect(self._selectedItem) for aKey, aVal in SHARED.project.data.autoReplace.items(): newItem = QTreeWidgetItem(["<%s>" % aKey, aVal]) @@ -610,8 +605,6 @@ class _ReplacePage(QWidget): self.listBox.setSortingEnabled(True) # List Controls - # ============= - self.addButton = QPushButton(SHARED.theme.getIcon("add"), "") self.addButton.clicked.connect(self._addEntry) @@ -619,8 +612,6 @@ class _ReplacePage(QWidget): self.delButton.clicked.connect(self._delEntry) # Edit Form - # ========= - self.editKey = QLineEdit() self.editKey.setPlaceholderText(self.tr("Select item to edit")) self.editKey.setEnabled(False) @@ -634,8 +625,6 @@ class _ReplacePage(QWidget): self.saveButton.clicked.connect(self._saveEntry) # Assemble - # ======== - self.listControls = QVBoxLayout() self.listControls.addWidget(self.addButton) self.listControls.addWidget(self.delButton) @@ -663,12 +652,20 @@ class _ReplacePage(QWidget): return + @property + def wasChanged(self) -> bool: + """The user changed these settings.""" + return self._changed + + ## + # Methods + ## + def getNewList(self) -> dict: """Extract the list from the widget.""" new = {} for n in range(self.listBox.topLevelItemCount()): - tItem = self.listBox.topLevelItem(n) - if tItem is not None: + if tItem := self.listBox.topLevelItem(n): aKey = self._stripNotAllowed(tItem.text(0)) aVal = tItem.text(1) if len(aKey) > 0: @@ -680,30 +677,29 @@ class _ReplacePage(QWidget): return self.listBox.columnWidth(0) ## - # Internal Functions + # Private Slots ## - def _selectedItem(self) -> bool: + @pyqtSlot() + def _selectedItem(self) -> None: """Extract the details from the selected item and populate the edit form. """ - selItem = self._getSelectedItem() - if selItem is None: - return False - editKey = self._stripNotAllowed(selItem.text(0)) - editVal = selItem.text(1) - self.editKey.setText(editKey) - self.editValue.setText(editVal) - self.editKey.setEnabled(True) - self.editValue.setEnabled(True) - self.editKey.selectAll() - self.editKey.setFocus() - return True + if selItem := self._getSelectedItem(): + editKey = self._stripNotAllowed(selItem.text(0)) + editVal = selItem.text(1) + self.editKey.setText(editKey) + self.editValue.setText(editVal) + self.editKey.setEnabled(True) + self.editValue.setEnabled(True) + self.editKey.selectAll() + self.editKey.setFocus() + return + @pyqtSlot() def _saveEntry(self) -> None: """Save the form data into the list widget.""" - selItem = self._getSelectedItem() - if selItem: + if selItem := self._getSelectedItem(): newKey = self.editKey.text() newVal = self.editValue.text() saveKey = self._stripNotAllowed(newKey) @@ -715,38 +711,36 @@ class _ReplacePage(QWidget): self.editKey.setEnabled(False) self.editValue.setEnabled(False) self.listBox.clearSelection() - self.arChanged = True + self._changed = True return + @pyqtSlot() def _addEntry(self) -> None: """Add a new list entry.""" saveKey = "" % (self.listBox.topLevelItemCount() + 1) - newVal = "" - newItem = QTreeWidgetItem([saveKey, newVal]) - self.listBox.addTopLevelItem(newItem) + self.listBox.addTopLevelItem(QTreeWidgetItem([saveKey, ""])) return + @pyqtSlot() def _delEntry(self) -> None: """Delete the selected entry.""" - selItem = self._getSelectedItem() - if selItem: + if selItem := self._getSelectedItem(): self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem)) - self.arChanged = True + self._changed = True return + ## + # Internal Functions + ## + def _getSelectedItem(self) -> QTreeWidgetItem | None: """Extract the currently selected item.""" - selItem = self.listBox.selectedItems() - if len(selItem) == 0: - return None - return selItem[0] + if items := self.listBox.selectedItems(): + return items[0] + return None def _stripNotAllowed(self, key: str) -> str: """Clean up the replace key string.""" - result = "" - for c in key: - if c.isalnum(): - result += c - return result + return "".join(c for c in key if c.isalnum()) # END Class _ReplacePage diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 2db50ded..39e1e651 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -134,10 +134,11 @@ class NScrollableForm(QScrollArea): return def addRow(self, label: str, widget: QWidget, helpText: str = "", unit: str | None = None, - button: QWidget | None = None, editable: str | None = None) -> None: + button: QWidget | None = None, editable: str | None = None, + stretch: tuple[int, int] = (0, 0)) -> None: """Add a label and a widget as a new row of the form.""" row = QHBoxLayout() - row.setSpacing(CONFIG.pxInt(4)) + row.setSpacing(CONFIG.pxInt(12)) qLabel = QLabel(label, self) qLabel.setIndent(self._indent) @@ -151,19 +152,24 @@ class NScrollableForm(QScrollArea): labelBox.addWidget(qHelp) labelBox.setSpacing(0) labelBox.addStretch(1) - row.addLayout(labelBox) + row.addLayout(labelBox, stretch[0]) if editable: self._editable[editable] = qHelp else: - row.addWidget(qLabel) - - row.addSpacing(CONFIG.pxInt(12)) - row.addWidget(widget) + row.addWidget(qLabel, stretch[0]) if isinstance(unit, str): - row.addWidget(QLabel(unit, self)) + box = QHBoxLayout() + box.addWidget(widget) + box.addWidget(QLabel(unit, self)) + row.addLayout(box, stretch[1]) elif isinstance(button, QAbstractButton): - row.addWidget(button) + box = QHBoxLayout() + box.addWidget(widget) + box.addWidget(button) + row.addLayout(box, stretch[1]) + else: + row.addWidget(widget, stretch[1]) self._layout.addLayout(row) self._index[label.strip()] = widget From 0dc47194007b27953d7de2fac3d695ee8d2546f5 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:51:54 +0100 Subject: [PATCH 3/7] Fix log formatting for longer file names --- novelwriter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 5b3fe49c..164c5b35 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -131,7 +131,7 @@ def main(sysArgs: list | None = None): elif inOpt == "--debug": CONFIG.isDebug = True logLevel = logging.DEBUG - logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}" + logFormat = "[{asctime:}] {filename:>18}:{lineno:<4d} {levelname:8} {message:}" elif inOpt == "--style": qtStyle = inArg elif inOpt == "--config": From 3761b1729f2f4e466beddb65a69a82c95725dc70 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 21:31:14 +0100 Subject: [PATCH 4/7] Make the style of config dialogs consistent --- novelwriter/dialogs/preferences.py | 4 +- novelwriter/dialogs/projectsettings.py | 82 ++++++++++++----------- novelwriter/extensions/configlayout.py | 91 ++++++++++++++++++++------ novelwriter/tools/noveldetails.py | 52 ++++++++------- 4 files changed, 144 insertions(+), 85 deletions(-) diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 5ece68b1..7653734e 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -59,9 +59,9 @@ class GuiPreferences(QDialog): # Title self.titleLabel = NColourLabel( - self.tr("Preferences"), SHARED.theme.helpText, parent=self, scale=1.25 + self.tr("Preferences"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4) ) - self.titleLabel.setIndent(CONFIG.pxInt(4)) # Search Box self.searchText = QLineEdit(self) diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 57bd4237..b7f50f24 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -29,15 +29,15 @@ import logging from PyQt5.QtGui import QCloseEvent, QColor, QIcon, QPixmap from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QColorDialog, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, - QLineEdit, QPushButton, QStackedWidget, QTreeWidget, QTreeWidgetItem, - QVBoxLayout, QWidget, qApp + QColorDialog, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLineEdit, + QPushButton, QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget, qApp ) from novelwriter import CONFIG, SHARED from novelwriter.common import simplified from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm +from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollableForm from novelwriter.extensions.pagedsidebar import NPagedSideBar logger = logging.getLogger(__name__) @@ -59,21 +59,18 @@ class GuiProjectSettings(QDialog): self.setObjectName("GuiProjectSettings") self.setWindowTitle(self.tr("Project Settings")) - wW = CONFIG.pxInt(500) - wH = CONFIG.pxInt(400) options = SHARED.project.options - - self.setMinimumSize(wW, wH) + self.setMinimumSize(CONFIG.pxInt(500), CONFIG.pxInt(400)) self.resize( - CONFIG.pxInt(options.getInt("GuiProjectSettings", "winWidth", wW)), - CONFIG.pxInt(options.getInt("GuiProjectSettings", "winHeight", wH)) + CONFIG.pxInt(options.getInt("GuiProjectSettings", "winWidth", CONFIG.pxInt(650))), + CONFIG.pxInt(options.getInt("GuiProjectSettings", "winHeight", CONFIG.pxInt(500))) ) # Title self.titleLabel = NColourLabel( - self.tr("Project Settings"), SHARED.theme.helpText, parent=self, scale=1.25 + self.tr("Project Settings"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4) ) - self.titleLabel.setIndent(CONFIG.pxInt(4)) # SideBar self.sidebar = NPagedSideBar(self) @@ -251,31 +248,33 @@ class _SettingsPage(NScrollableForm): self.addRow( self.tr("Author(s)"), self.projAuthor, self.tr("Only used when building the manuscript."), - stretch=(2, 1) + stretch=(3, 2) ) # Project Language self.projLang = QComboBox(self) - self.projLang.setMaximumWidth(xW) + self.projLang.setMinimumWidth(xW) for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ): self.projLang.addItem(language, tag) self.addRow( self.tr("Project language"), self.projLang, - self.tr("Only used when building the manuscript.") + self.tr("Only used when building the manuscript."), + stretch=(3, 2) ) if (idx := self.projLang.findData(data.language)) != -1: self.projLang.setCurrentIndex(idx) # Spell Check Language self.spellLang = QComboBox(self) - self.spellLang.setMaximumWidth(xW) + self.spellLang.setMinimumWidth(xW) self.spellLang.addItem(self.tr("Default"), "None") if CONFIG.hasEnchant: for tag, language in SHARED.spelling.listDictionaries(): self.spellLang.addItem(language, tag) self.addRow( self.tr("Spell check language"), self.spellLang, - self.tr("Overrides main preferences.") + self.tr("Overrides main preferences."), + stretch=(3, 2) ) if (idx := self.spellLang.findData(data.spellLang)) != -1: self.spellLang.setCurrentIndex(idx) @@ -295,7 +294,7 @@ class _SettingsPage(NScrollableForm): # END Class _SettingsPage -class _StatusPage(QWidget): +class _StatusPage(NFixedPage): COL_LABEL = 0 COL_USAGE = 1 @@ -326,9 +325,13 @@ class _StatusPage(QWidget): self.iPx = SHARED.theme.baseIconSize - # The List - # ======== + # Title + self.pageTitle = NColourLabel( + pageLabel, SHARED.theme.helpText, parent=self, + scale=NColourLabel.HEADER_SCALE + ) + # List Box self.listBox = QTreeWidget(self) self.listBox.setHeaderLabels([ self.tr("Label"), self.tr("Usage"), @@ -392,11 +395,10 @@ class _StatusPage(QWidget): self.innerBox.addLayout(self.listControls) self.outerBox = QVBoxLayout() - self.outerBox.addWidget(QLabel("%s" % pageLabel)) + self.outerBox.addWidget(self.pageTitle) self.outerBox.addLayout(self.innerBox) - self.outerBox.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.outerBox) + self.setCentralLayout(self.outerBox) return @@ -422,7 +424,6 @@ class _StatusPage(QWidget): "cols": item.data(self.COL_LABEL, self.COL_ROLE), }) return newList, self._colDeleted - return [], [] def columnWidth(self) -> int: @@ -517,16 +518,16 @@ class _StatusPage(QWidget): ## def _addItem(self, key: str | None, name: str, - cols: tuple[int, int, int], count: int) -> None: + colour: tuple[int, int, int], count: int) -> None: """Add a status item to the list.""" pixmap = QPixmap(self.iPx, self.iPx) - pixmap.fill(QColor(*cols)) + pixmap.fill(QColor(*colour)) item = QTreeWidgetItem() item.setText(self.COL_LABEL, name) item.setIcon(self.COL_LABEL, QIcon(pixmap)) item.setData(self.COL_LABEL, self.KEY_ROLE, key) - item.setData(self.COL_LABEL, self.COL_ROLE, cols) + item.setData(self.COL_LABEL, self.COL_ROLE, colour) item.setData(self.COL_LABEL, self.NUM_ROLE, count) item.setText(self.COL_USAGE, self._usageString(count)) @@ -558,24 +559,23 @@ class _StatusPage(QWidget): def _getSelectedItem(self) -> QTreeWidgetItem | None: """Get the currently selected item.""" - selItem = self.listBox.selectedItems() - if len(selItem) > 0: - return selItem[0] + if items := self.listBox.selectedItems(): + return items[0] return None - def _usageString(self, nUse: int) -> str: + def _usageString(self, count: int) -> str: """Generate usage string.""" - if nUse == 0: + if count == 0: return self.tr("Not in use") - elif nUse == 1: + elif count == 1: return self.tr("Used once") else: - return self.tr("Used by {0} items").format(nUse) + return self.tr("Used by {0} items").format(count) # END Class _StatusPage -class _ReplacePage(QWidget): +class _ReplacePage(NFixedPage): COL_KEY = 0 COL_REPL = 1 @@ -588,7 +588,12 @@ class _ReplacePage(QWidget): wCol0 = CONFIG.pxInt( SHARED.project.options.getInt("GuiProjectSettings", "replaceColW", 130) ) - pageLabel = self.tr("Text Replace List for Preview and Export") + + # Title + self.pageTitle = NColourLabel( + self.tr("Text Auto-Replace for Preview and Build"), + SHARED.theme.helpText, parent=self, scale=NColourLabel.HEADER_SCALE + ) # List Box self.listBox = QTreeWidget() @@ -644,11 +649,10 @@ class _ReplacePage(QWidget): self.innerBox.addLayout(self.listControls) self.outerBox = QVBoxLayout() - self.outerBox.addWidget(QLabel("%s" % pageLabel)) + self.outerBox.addWidget(self.pageTitle) self.outerBox.addLayout(self.innerBox) - self.outerBox.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.outerBox) + self.setCentralLayout(self.outerBox) return diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 39e1e651..809068bc 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -6,6 +6,8 @@ File History: Created: 2020-05-03 [0.4.5] NConfigLayout, NColourLabel Created: 2023-05-23 [2.1b1] NSimpleLayout Created: 2024-01-08 [2.3b1] NScrollableForm +Created: 2024-01-26 [2.3b1] NScrollablePage +Created: 2024-01-26 [2.3b1] NFixedPage This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen @@ -28,17 +30,59 @@ from __future__ import annotations from PyQt5.QtGui import QColor, QPalette from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QAbstractButton, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QScrollArea, QSizePolicy, - QVBoxLayout, QWidget + QAbstractButton, QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout, QLineEdit, + QScrollArea, QSizePolicy, QVBoxLayout, QWidget ) from novelwriter import CONFIG -FONT_SCALE = 0.9 +DEFAULT_SCALE = 0.9 RIGHT_TOP = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop +class NFixedPage(QFrame): + """Extension: Fixed Page Widget + + A custom widget that holds a layout. This is just a wrapper around a + QFrame that sets the same frame style as the other Page widgets. + """ + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + self.setFrameShadow(QFrame.Shadow.Sunken) + self.setFrameShape(QFrame.Shape.StyledPanel) + self.setCentralLayout = self.setLayout + return + +# END Class NFixedPage + + +class NScrollablePage(QScrollArea): + """Extension: Scrollable Page Widget + + A custom widget that holds a layout within a scrollable area. + """ + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + self._widget = QWidget(self) + self.setWidget(self._widget) + self.setWidgetResizable(True) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.setFrameShadow(QFrame.Shadow.Sunken) + self.setFrameShape(QFrame.Shape.StyledPanel) + return + + def setCentralLayout(self, layout: QLayout) -> None: + """Set the central layout of the scroll page.""" + self._widget.setLayout(layout) + return + +# END Class NScrollablePage + + class NScrollableForm(QScrollArea): """Extension: Scrollable Form Widget @@ -48,7 +92,7 @@ class NScrollableForm(QScrollArea): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) self._helpCol = QColor(0, 0, 0) - self._fontScale = FONT_SCALE + self._fontScale = DEFAULT_SCALE self._first = True self._indent = CONFIG.pxInt(12) @@ -66,6 +110,8 @@ class NScrollableForm(QScrollArea): self.setWidgetResizable(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.setFrameShadow(QFrame.Shadow.Sunken) + self.setFrameShape(QFrame.Shape.StyledPanel) return @@ -81,7 +127,7 @@ class NScrollableForm(QScrollArea): # Setters ## - def setHelpTextStyle(self, color: QColor, scale: float = FONT_SCALE) -> None: + def setHelpTextStyle(self, color: QColor, scale: float = DEFAULT_SCALE) -> None: """Set the text color for the help text.""" self._helpCol = color self._fontScale = scale @@ -145,13 +191,14 @@ class NScrollableForm(QScrollArea): qLabel.setBuddy(widget) if helpText: - qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) - qHelp.setIndent(self._indent) + qHelp = NColourLabel( + str(helpText), self._helpCol, parent=self, + scale=self._fontScale, wrap=True, indent=self._indent + ) labelBox = QVBoxLayout() labelBox.addWidget(qLabel) labelBox.addWidget(qHelp) labelBox.setSpacing(0) - labelBox.addStretch(1) row.addLayout(labelBox, stretch[0]) if editable: self._editable[editable] = qHelp @@ -193,7 +240,7 @@ class NConfigLayout(QGridLayout): self._nextRow = 0 self._helpCol = QColor(0, 0, 0) - self._fontScale = FONT_SCALE + self._fontScale = DEFAULT_SCALE self._itemMap = {} wSp = CONFIG.pxInt(8) @@ -207,7 +254,7 @@ class NConfigLayout(QGridLayout): # Getters and Setters ## - def setHelpTextStyle(self, color: QColor, scale: float = FONT_SCALE) -> None: + def setHelpTextStyle(self, color: QColor, scale: float = DEFAULT_SCALE) -> None: """Set the text color for the help text.""" self._helpCol = color if isinstance(color, QColor) else QColor(*color) self._fontScale = scale @@ -238,8 +285,10 @@ class NConfigLayout(QGridLayout): qHelp = None if helpText is not None: - qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) - qHelp.setIndent(wSp) + qHelp = NColourLabel( + str(helpText), self._helpCol, + scale=self._fontScale, wrap=True, indent=wSp + ) labelBox = QVBoxLayout() labelBox.addWidget(qLabel) labelBox.addWidget(qHelp) @@ -337,17 +386,21 @@ class NColourLabel(QLabel): optionally at a specific size, and word wrapped. """ + HELP_SCALE = DEFAULT_SCALE + HEADER_SCALE = 1.25 + def __init__(self, text: str, color: QColor, parent: QWidget | None = None, - scale: float = FONT_SCALE, wrap: bool = False) -> None: + scale: float = HELP_SCALE, wrap: bool = False, indent: int = 0) -> None: super().__init__(text, parent=parent) - lblCol = self.palette() - lblCol.setColor(QPalette.WindowText, color) - self.setPalette(lblCol) + font = self.font() + font.setPointSizeF(scale*font.pointSizeF()) + colour = self.palette() + colour.setColor(QPalette.WindowText, color) - lblFont = self.font() - lblFont.setPointSizeF(scale*lblFont.pointSizeF()) - self.setFont(lblFont) + self.setPalette(colour) + self.setFont(font) + self.setIndent(indent) if wrap: self.setWordWrap(True) diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index 3fcd54cc..71375d7f 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -38,14 +38,12 @@ from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime, numberToRoman from novelwriter.constants import nwUnicode from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.configlayout import NColourLabel +from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollablePage from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.novelselector import NovelSelector logger = logging.getLogger(__name__) -HEADER_SIZE = 1.4 - class GuiNovelDetails(QDialog): @@ -59,21 +57,18 @@ class GuiNovelDetails(QDialog): self.setObjectName("GuiNovelDetails") self.setWindowTitle(self.tr("Novel Details")) - wW = CONFIG.pxInt(500) - wH = CONFIG.pxInt(400) options = SHARED.project.options - - self.setMinimumSize(wW, wH) + self.setMinimumSize(CONFIG.pxInt(500), CONFIG.pxInt(400)) self.resize( - CONFIG.pxInt(options.getInt("GuiNovelDetails", "winWidth", wW)), - CONFIG.pxInt(options.getInt("GuiNovelDetails", "winHeight", wH)) + CONFIG.pxInt(options.getInt("GuiNovelDetails", "winWidth", CONFIG.pxInt(650))), + CONFIG.pxInt(options.getInt("GuiNovelDetails", "winHeight", CONFIG.pxInt(500))) ) # Title self.titleLabel = NColourLabel( - self.tr("Novel Details"), SHARED.theme.helpText, parent=self, scale=1.25 + self.tr("Novel Details"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4) ) - self.titleLabel.setIndent(CONFIG.pxInt(4)) # Novel Selector self.novelSelector = NovelSelector(self) @@ -192,7 +187,7 @@ class GuiNovelDetails(QDialog): # END Class GuiNovelDetails -class _OverviewPage(QWidget): +class _OverviewPage(NScrollablePage): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -204,7 +199,8 @@ class _OverviewPage(QWidget): # Project Info self.projLabel = NColourLabel( - self.tr("Project"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE + self.tr("Project"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE ) self.projName = QLabel("", self) @@ -227,7 +223,8 @@ class _OverviewPage(QWidget): # Novel Info self.novelLabel = NColourLabel( - self.tr("Selected Novel"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE + self.tr("Selected Novel"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE ) self.novelName = QLabel("", self) @@ -250,11 +247,10 @@ class _OverviewPage(QWidget): self.outerBox.addLayout(self.projForm) self.outerBox.addWidget(self.novelLabel) self.outerBox.addLayout(self.novelForm) - self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setSpacing(sPx) self.outerBox.addStretch(1) - self.setLayout(self.outerBox) + self.setCentralLayout(self.outerBox) return @@ -299,7 +295,7 @@ class _OverviewPage(QWidget): # END Class _OverviewPage -class _ContentsPage(QWidget): +class _ContentsPage(NFixedPage): C_TITLE = 0 C_WORDS = 1 @@ -320,7 +316,8 @@ class _ContentsPage(QWidget): # Title self.contentLabel = NColourLabel( - self.tr("Table of Contents"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE + self.tr("Table of Contents"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE ) # Contents Tree @@ -407,20 +404,25 @@ class _ContentsPage(QWidget): self.outerBox.addWidget(self.contentLabel) self.outerBox.addWidget(self.tocTree) self.outerBox.addLayout(self.optionsBox) - self.outerBox.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.outerBox) + self.setCentralLayout(self.outerBox) return def saveSettings(self) -> None: """Save the user GUI settings.""" + widthCol0 = CONFIG.rpxInt(self.tocTree.columnWidth(0)) + widthCol1 = CONFIG.rpxInt(self.tocTree.columnWidth(1)) + widthCol2 = CONFIG.rpxInt(self.tocTree.columnWidth(2)) + widthCol3 = CONFIG.rpxInt(self.tocTree.columnWidth(3)) + widthCol4 = CONFIG.rpxInt(self.tocTree.columnWidth(4)) + options = SHARED.project.options - options.setValue("GuiNovelDetails", "widthCol0", self.tocTree.columnWidth(0)) - options.setValue("GuiNovelDetails", "widthCol1", self.tocTree.columnWidth(1)) - options.setValue("GuiNovelDetails", "widthCol2", self.tocTree.columnWidth(2)) - options.setValue("GuiNovelDetails", "widthCol3", self.tocTree.columnWidth(3)) - options.setValue("GuiNovelDetails", "widthCol4", self.tocTree.columnWidth(4)) + options.setValue("GuiNovelDetails", "widthCol0", widthCol0) + options.setValue("GuiNovelDetails", "widthCol1", widthCol1) + options.setValue("GuiNovelDetails", "widthCol2", widthCol2) + options.setValue("GuiNovelDetails", "widthCol3", widthCol3) + options.setValue("GuiNovelDetails", "widthCol4", widthCol4) options.setValue("GuiNovelDetails", "wordsPerPage", self.wpValue.value()) options.setValue("GuiNovelDetails", "countFrom", self.poValue.value()) options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked()) From a7a2eaff557df34be89a4c7a38f3a9a899807548 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 21:36:05 +0100 Subject: [PATCH 5/7] Delete the old config dialog base class --- novelwriter/extensions/pageddialog.py | 130 -------------------------- 1 file changed, 130 deletions(-) delete mode 100644 novelwriter/extensions/pageddialog.py diff --git a/novelwriter/extensions/pageddialog.py b/novelwriter/extensions/pageddialog.py deleted file mode 100644 index a620cb74..00000000 --- a/novelwriter/extensions/pageddialog.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -novelWriter – Custom Widget: Paged Dialog -========================================= - -File History: -Created: 2020-05-17 [0.5.1] - -This file is a part of novelWriter -Copyright 2018–2024, 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 . -""" -from __future__ import annotations - -from PyQt5.QtGui import QPaintEvent -from PyQt5.QtCore import QRect, QPoint, QSize -from PyQt5.QtWidgets import ( - QDialog, QHBoxLayout, QStyle, QStyleOptionTab, QStylePainter, QTabBar, - QTabWidget, QVBoxLayout, QWidget -) - -from novelwriter import CONFIG - - -class NPagedDialog(QDialog): - - def __init__(self, parent: QWidget) -> None: - super().__init__(parent=parent) - - self._tabBar = NVerticalTabBar(self) - self._tabBar.setExpanding(False) - - self._tabBox = QTabWidget(self) - self._tabBox.setTabBar(self._tabBar) - self._tabBox.setTabPosition(QTabWidget.West) - - self._buttonBox = QHBoxLayout() - - self._outerBox = QVBoxLayout() - self._outerBox.addWidget(self._tabBox) - self._outerBox.addLayout(self._buttonBox) - - # Default Margins - thisStyle = self.style() - mL = thisStyle.pixelMetric(QStyle.PM_LayoutLeftMargin) - mR = thisStyle.pixelMetric(QStyle.PM_LayoutRightMargin) - mT = thisStyle.pixelMetric(QStyle.PM_LayoutLeftMargin) - mB = thisStyle.pixelMetric(QStyle.PM_LayoutBottomMargin) - - # Set Margins - self.setContentsMargins(0, 0, 0, 0) - self._outerBox.setContentsMargins(0, 0, 0, mB) - self._buttonBox.setContentsMargins(mL, 0, mR, 0) - self._outerBox.setSpacing(mT) - - self.setLayout(self._outerBox) - - return - - def addTab(self, widget: QWidget, label: str) -> None: - """Forward the adding of tabs to the QTabWidget.""" - self._tabBox.addTab(widget, label) - return - - def addControls(self, buttonBar: QWidget) -> None: - """Add a button bar to the dialog.""" - self._buttonBox.addWidget(buttonBar) - return - - def setCurrentWidget(self, widget: QWidget) -> None: - """Forward the changing of tab to the QTabWidget.""" - self._tabBox.setCurrentWidget(widget) - return - -# END Class NPagedDialog - - -class NVerticalTabBar(QTabBar): - - def __init__(self, parent: QWidget) -> None: - super().__init__(parent=parent) - self._mW = CONFIG.pxInt(150) - return - - def tabSizeHint(self, index: int) -> QSize: - """Return a transposed size hint for the rotated bar.""" - tSize = super().tabSizeHint(index) - tSize.transpose() - tSize.setWidth(min(tSize.width(), self._mW)) - return tSize - - def paintEvent(self, event: QPaintEvent) -> None: - """Custom implementation of the label painter that rotates the - label 90 degrees. - """ - pObj = QStylePainter(self) - oObj = QStyleOptionTab() - - for i in range(self.count()): - self.initStyleOption(oObj, i) - pObj.drawControl(QStyle.CE_TabBarTabShape, oObj) - pObj.save() - - oSize = oObj.rect.size() - oSize.transpose() - oRect = QRect(QPoint(), oSize) - oRect.moveCenter(oObj.rect.center()) - oObj.rect = oRect - - oCenter = self.tabRect(i).center() - pObj.translate(oCenter) - pObj.rotate(90) - pObj.translate(-oCenter) - pObj.drawControl(QStyle.CE_TabBarTabLabel, oObj) - pObj.restore() - - return - -# END Class NVerticalTabBar From 40a8d728bb205c912349c67b8901347c9d2be494 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 22:32:20 +0100 Subject: [PATCH 6/7] Update project settings tests --- novelwriter/dialogs/projectsettings.py | 16 +- ...ettings.py => test_dlg_projectsettings.py} | 285 +++++++++--------- 2 files changed, 143 insertions(+), 158 deletions(-) rename tests/test_dialogs/{test_dlg_projsettings.py => test_dlg_projectsettings.py} (50%) diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index b7f50f24..f6ff7730 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -90,6 +90,8 @@ class GuiProjectSettings(QDialog): self.buttonBox.rejected.connect(self.close) # Content + SHARED.project.countStatus() + self.settingsPage = _SettingsPage(self) self.statusPage = _StatusPage(self, True) self.importPage = _StatusPage(self, False) @@ -307,12 +309,12 @@ class _StatusPage(NFixedPage): super().__init__(parent=parent) if isStatus: - self.theStatus = SHARED.project.data.itemStatus - pageLabel = self.tr("Novel File Status Levels") + status = SHARED.project.data.itemStatus + pageLabel = self.tr("Novel Document Status Levels") colSetting = "statusColW" else: - self.theStatus = SHARED.project.data.itemImport - pageLabel = self.tr("Note File Importance Levels") + status = SHARED.project.data.itemImport + pageLabel = self.tr("Project Note Importance Levels") colSetting = "importColW" wCol0 = CONFIG.pxInt( @@ -333,14 +335,12 @@ class _StatusPage(NFixedPage): # List Box self.listBox = QTreeWidget(self) - self.listBox.setHeaderLabels([ - self.tr("Label"), self.tr("Usage"), - ]) + self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")]) self.listBox.itemSelectionChanged.connect(self._selectedItem) self.listBox.setColumnWidth(self.COL_LABEL, wCol0) self.listBox.setIndentation(0) - for key, entry in self.theStatus.items(): + for key, entry in status.items(): self._addItem(key, entry["name"], entry["cols"], entry["count"]) # List Controls diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projectsettings.py similarity index 50% rename from tests/test_dialogs/test_dlg_projsettings.py rename to tests/test_dialogs/test_dlg_projectsettings.py index 37556245..5b4d5499 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projectsettings.py @@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QDialog, QAction, QColorDialog from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemType from novelwriter.dialogs.editlabel import GuiEditLabel -from novelwriter.dialogs.projsettings import GuiProjectSettings +from novelwriter.dialogs.projectsettings import GuiProjectSettings KEY_DELAY = 1 @@ -63,17 +63,17 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): qtbot.addWidget(projSettings) # Switch Tabs - projSettings._focusTab(GuiProjectSettings.TAB_REPLACE) - assert projSettings._tabBox.currentWidget() == projSettings.tabReplace + projSettings.sidebar.button(GuiProjectSettings.PAGE_SETTINGS).click() + assert projSettings.mainStack.currentWidget() == projSettings.settingsPage - projSettings._focusTab(GuiProjectSettings.TAB_IMPORT) - assert projSettings._tabBox.currentWidget() == projSettings.tabImport + projSettings.sidebar.button(GuiProjectSettings.PAGE_STATUS).click() + assert projSettings.mainStack.currentWidget() == projSettings.statusPage - projSettings._focusTab(GuiProjectSettings.TAB_STATUS) - assert projSettings._tabBox.currentWidget() == projSettings.tabStatus + projSettings.sidebar.button(GuiProjectSettings.PAGE_IMPORT).click() + assert projSettings.mainStack.currentWidget() == projSettings.importPage - projSettings._focusTab(GuiProjectSettings.TAB_MAIN) - assert projSettings._tabBox.currentWidget() == projSettings.tabMain + projSettings.sidebar.button(GuiProjectSettings.PAGE_REPLACE).click() + assert projSettings.mainStack.currentWidget() == projSettings.replacePage # Clean Up projSettings.close() @@ -83,9 +83,11 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): @pytest.mark.gui -def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): - """Test the main tab of the project settings dialog.""" - monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) +def testDlgProjSettings_SettingsPage(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): + """Test the settings page of the dialog.""" + languages = [("en", "English"), ("de", "German")] + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda *a: languages) + monkeypatch.setattr(CONFIG, "listLanguages", lambda *a: languages) # Create new project buildTestProject(nwGUI, projPath) @@ -94,72 +96,57 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockR # Set some values project = SHARED.project + project.data.setLanguage("en") project.data.setSpellLang("en") project.data.setAuthor("Jane Smith") project.data.setAutoReplace({"A": "B", "C": "D"}) # Create Dialog - projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_MAIN) + projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_SETTINGS) projSettings.show() qtbot.addWidget(projSettings) # Settings Tab - # ============ + settings = projSettings.settingsPage - tabMain = projSettings.tabMain + assert settings.projName.text() == "New Project" + assert settings.projAuthor.text() == "Jane Smith" + assert settings.projLang.currentData() == "en" + assert settings.spellLang.currentData() == "en" + assert settings.doBackup.isChecked() is False - assert tabMain.editName.text() == "New Project" - assert tabMain.editTitle.text() == "New Novel" - assert tabMain.editAuthor.text() == "Jane Smith" - assert tabMain.spellLang.currentData() == "en" - assert tabMain.doBackup.isChecked() is False - - tabMain.editName.setText("") - for c in "Project Name": - qtbot.keyClick(tabMain.editName, c, delay=KEY_DELAY) - tabMain.editTitle.setText("") - for c in "Project Title": - qtbot.keyClick(tabMain.editTitle, c, delay=KEY_DELAY) - - tabMain.editAuthor.clear() - for c in "Jane Doe": - qtbot.keyClick(tabMain.editAuthor, c, delay=KEY_DELAY) - - assert tabMain.editName.text() == "Project Name" - assert tabMain.editTitle.text() == "Project Title" - assert tabMain.editAuthor.text() == "Jane Doe" + settings.projName.setText("Project Name") + settings.projAuthor.setText("Jane Doe") + settings.projLang.setCurrentIndex(settings.projLang.findData("de")) + settings.spellLang.setCurrentIndex(settings.spellLang.findData("de")) + settings.doBackup.setChecked(True) projSettings._doSave() assert project.data.name == "Project Name" - assert project.data.title == "Project Title" assert project.data.author == "Jane Doe" + assert project.data.language == "de" + assert project.data.spellLang == "de" + assert project.data.doBackup is False - nwGUI._processProjectSettingsChanges() + nwGUI._processProjectSettingsChanges(False) assert nwGUI.windowTitle() == "novelWriter - Project Name" # qtbot.stop() -# END Test testDlgProjSettings_Main +# END Test testDlgProjSettings_SettingsPage @pytest.mark.gui -def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): - """Test the status and importance tabs of the project settings - dialog. - """ +def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRnd): + """Test the status and importance pages of the dialog.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) - - # Create new project - mockRnd.reset() buildTestProject(nwGUI, projPath) - CONFIG.setBackupPath(fncPath) # Set some values - theProject = SHARED.project - theProject.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore - theProject.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore - theProject.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore + project = SHARED.project + project.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore + project.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore + project.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore nwGUI.projView.projTree.setSelectedHandle(C.hPlotRoot) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) @@ -172,51 +159,54 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat hCharNote = "0000000000011" hWorldNote = "0000000000012" - theProject.tree[hPlotNote].setImport(C.iMajor) # type: ignore - theProject.tree[hCharNote].setImport(C.iMajor) # type: ignore - theProject.tree[hWorldNote].setImport(C.iMain) # type: ignore + project.tree[hPlotNote].setImport(C.iMajor) # type: ignore + project.tree[hCharNote].setImport(C.iMajor) # type: ignore + project.tree[hWorldNote].setImport(C.iMain) # type: ignore + + nwGUI.rebuildTrees() + project.countStatus() + + assert [e["count"] for _, e in project.data.itemStatus.items()] == [2, 0, 2, 1] + assert [e["count"] for _, e in project.data.itemImport.items()] == [3, 0, 2, 1] # Create Dialog - projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_STATUS) + projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_STATUS) projSettings.show() qtbot.addWidget(projSettings) # Status Tab # ========== - tabStatus = projSettings.tabStatus + status = projSettings.statusPage - assert tabStatus.colChanged is False - assert tabStatus.getNewList() == ([], []) - assert tabStatus.listBox.topLevelItemCount() == 4 + assert status.wasChanged is False + assert status.getNewList() == ([], []) + assert status.listBox.topLevelItemCount() == 4 # Can't delete the first item (it's in use) - tabStatus.listBox.clearSelection() - tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0)) - qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton) - assert tabStatus.listBox.topLevelItemCount() == 4 + status.listBox.clearSelection() + status.listBox.setCurrentItem(status.listBox.topLevelItem(0)) + qtbot.mouseClick(status.delButton, Qt.LeftButton) + assert status.listBox.topLevelItemCount() == 4 # Can delete the second item - tabStatus.listBox.clearSelection() - tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(1)) - qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton) - assert tabStatus.listBox.topLevelItemCount() == 3 + status.listBox.clearSelection() + status.listBox.setCurrentItem(status.listBox.topLevelItem(1)) + qtbot.mouseClick(status.delButton, Qt.LeftButton) + assert status.listBox.topLevelItemCount() == 3 # Add a new item with monkeypatch.context() as mp: mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40)) - qtbot.mouseClick(tabStatus.addButton, Qt.LeftButton) - tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3)) - for _ in range(8): - qtbot.keyClick(tabStatus.editName, Qt.Key_Backspace, delay=KEY_DELAY) - for c in "Final": - qtbot.keyClick(tabStatus.editName, c, delay=KEY_DELAY) - qtbot.mouseClick(tabStatus.colButton, Qt.LeftButton) - qtbot.mouseClick(tabStatus.saveButton, Qt.LeftButton) - assert tabStatus.listBox.topLevelItemCount() == 4 + status.addButton.click() + status.listBox.setCurrentItem(status.listBox.topLevelItem(3)) + status.editName.setText("Final") + status.colButton.click() + status.saveButton.click() + assert status.listBox.topLevelItemCount() == 4 - assert tabStatus.colChanged is True - assert tabStatus.getNewList() == ( + assert status.wasChanged is True + assert status.getNewList() == ( [ { "key": C.sNew, @@ -241,62 +231,62 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat ) # Move items, none selected -> no change - tabStatus.listBox.clearSelection() - tabStatus._moveItem(1) - assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + status.listBox.clearSelection() + status._moveItem(1) + assert [x["key"] for x in status.getNewList()[0]] == [ C.sNew, C.sDraft, C.sFinished, None ] # Move items, first selected, move up -> no change - tabStatus.listBox.clearSelection() - tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0)) - tabStatus._moveItem(-1) - assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + status.listBox.clearSelection() + status.listBox.setCurrentItem(status.listBox.topLevelItem(0)) + status._moveItem(-1) + assert [x["key"] for x in status.getNewList()[0]] == [ C.sNew, C.sDraft, C.sFinished, None ] # Move items, last selected, move up -> allowed - tabStatus.listBox.clearSelection() - tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3)) - tabStatus._moveItem(-1) - assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + status.listBox.clearSelection() + status.listBox.setCurrentItem(status.listBox.topLevelItem(3)) + status._moveItem(-1) + assert [x["key"] for x in status.getNewList()[0]] == [ C.sNew, C.sDraft, None, C.sFinished ] # Move items, same selected, move down -> allowed - tabStatus._moveItem(1) - assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + status._moveItem(1) + assert [x["key"] for x in status.getNewList()[0]] == [ C.sNew, C.sDraft, C.sFinished, None ] # Importance Tab # ============== - tabImport = projSettings.tabImport - projSettings._focusTab(GuiProjectSettings.TAB_IMPORT) + importance = projSettings.importPage + projSettings._sidebarClicked(GuiProjectSettings.PAGE_IMPORT) # Delete unused entry - tabImport.listBox.clearSelection() - tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(1)) - qtbot.mouseClick(tabImport.delButton, Qt.LeftButton) - assert tabImport.listBox.topLevelItemCount() == 3 + importance.listBox.clearSelection() + importance.listBox.setCurrentItem(importance.listBox.topLevelItem(1)) + qtbot.mouseClick(importance.delButton, Qt.LeftButton) + assert importance.listBox.topLevelItemCount() == 3 # Add a new entry with monkeypatch.context() as mp: mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40)) - qtbot.mouseClick(tabImport.addButton, Qt.LeftButton) - tabImport.listBox.clearSelection() - tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(3)) + qtbot.mouseClick(importance.addButton, Qt.LeftButton) + importance.listBox.clearSelection() + importance.listBox.setCurrentItem(importance.listBox.topLevelItem(3)) for _ in range(8): - qtbot.keyClick(tabImport.editName, Qt.Key_Backspace, delay=KEY_DELAY) + qtbot.keyClick(importance.editName, Qt.Key_Backspace, delay=KEY_DELAY) for c in "Final": - qtbot.keyClick(tabImport.editName, c, delay=KEY_DELAY) - qtbot.mouseClick(tabImport.colButton, Qt.LeftButton) - qtbot.mouseClick(tabImport.saveButton, Qt.LeftButton) - assert tabImport.listBox.topLevelItemCount() == 4 + qtbot.keyClick(importance.editName, c, delay=KEY_DELAY) + qtbot.mouseClick(importance.colButton, Qt.LeftButton) + qtbot.mouseClick(importance.saveButton, Qt.LeftButton) + assert importance.listBox.topLevelItemCount() == 4 - assert tabImport.colChanged is True - assert tabImport.getNewList() == ( + assert importance.wasChanged is True + assert importance.getNewList() == ( [ { "key": C.iNew, @@ -323,13 +313,13 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat # Check Project projSettings._doSave() - statusItems = dict(theProject.data.itemStatus.items()) + statusItems = dict(project.data.itemStatus.items()) assert statusItems[C.sNew]["name"] == "New" assert statusItems[C.sDraft]["name"] == "Draft" assert statusItems[C.sFinished]["name"] == "Finished" assert statusItems["s000013"]["name"] == "Final" - importItems = dict(theProject.data.itemImport.items()) + importItems = dict(project.data.itemImport.items()) assert importItems[C.iNew]["name"] == "New" assert importItems[C.iMajor]["name"] == "Major" assert importItems[C.iMain]["name"] == "Main" @@ -341,83 +331,78 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat @pytest.mark.gui -def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): - """Test the auto-replace tab of the project settings dialog.""" +def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, projPath, mockRnd): + """Test the auto-replace page of the dialog.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) - - # Create new project - mockRnd.reset() buildTestProject(nwGUI, projPath) - CONFIG.setBackupPath(fncPath) # Set some values - theProject = SHARED.project - theProject.data.setAutoReplace({ + project = SHARED.project + project.data.setAutoReplace({ "A": "B", "C": "D" }) # Create Dialog - projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_REPLACE) + projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_REPLACE) projSettings.show() qtbot.addWidget(projSettings) # Auto-Replace Tab # ================ - tabReplace = projSettings.tabReplace + replace = projSettings.replacePage - assert tabReplace.listBox.topLevelItem(0).text(0) == "" # type: ignore - assert tabReplace.listBox.topLevelItem(0).text(1) == "B" # type: ignore - assert tabReplace.listBox.topLevelItem(1).text(0) == "" # type: ignore - assert tabReplace.listBox.topLevelItem(1).text(1) == "D" # type: ignore - assert tabReplace.listBox.topLevelItemCount() == 2 + assert replace.listBox.topLevelItem(0).text(0) == "" # type: ignore + assert replace.listBox.topLevelItem(0).text(1) == "B" # type: ignore + assert replace.listBox.topLevelItem(1).text(0) == "" # type: ignore + assert replace.listBox.topLevelItem(1).text(1) == "D" # type: ignore + assert replace.listBox.topLevelItemCount() == 2 # Nothing to save or delete - tabReplace.listBox.clearSelection() - tabReplace._saveEntry() - tabReplace._delEntry() - assert tabReplace.listBox.topLevelItemCount() == 2 + replace.listBox.clearSelection() + replace._saveEntry() + replace._delEntry() + assert replace.listBox.topLevelItemCount() == 2 # Create a new entry - qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton) - assert tabReplace.listBox.topLevelItemCount() == 3 - assert tabReplace.listBox.topLevelItem(2).text(0) == "" # type: ignore - assert tabReplace.listBox.topLevelItem(2).text(1) == "" # type: ignore + qtbot.mouseClick(replace.addButton, Qt.LeftButton) + assert replace.listBox.topLevelItemCount() == 3 + assert replace.listBox.topLevelItem(2).text(0) == "" # type: ignore + assert replace.listBox.topLevelItem(2).text(1) == "" # type: ignore # Edit the entry - tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(2)) - tabReplace.editKey.setText("") + replace.listBox.setCurrentItem(replace.listBox.topLevelItem(2)) + replace.editKey.setText("") for c in "Th is ": - qtbot.keyClick(tabReplace.editKey, c, delay=KEY_DELAY) - tabReplace.editValue.setText("") + qtbot.keyClick(replace.editKey, c, delay=KEY_DELAY) + replace.editValue.setText("") for c in "With This Stuff ": - qtbot.keyClick(tabReplace.editValue, c, delay=KEY_DELAY) - qtbot.mouseClick(tabReplace.saveButton, Qt.LeftButton) - assert tabReplace.listBox.topLevelItem(2).text(0) == "" # type: ignore - assert tabReplace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore + qtbot.keyClick(replace.editValue, c, delay=KEY_DELAY) + qtbot.mouseClick(replace.saveButton, Qt.LeftButton) + assert replace.listBox.topLevelItem(2).text(0) == "" # type: ignore + assert replace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore # Create a new entry again - tabReplace.listBox.clearSelection() - qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton) - assert tabReplace.listBox.topLevelItemCount() == 4 + replace.listBox.clearSelection() + qtbot.mouseClick(replace.addButton, Qt.LeftButton) + assert replace.listBox.topLevelItemCount() == 4 # The list is sorted, so we must find it newIdx = -1 - for i in range(tabReplace.listBox.topLevelItemCount()): - if tabReplace.listBox.topLevelItem(i).text(0) == "": # type: ignore + for i in range(replace.listBox.topLevelItemCount()): + if replace.listBox.topLevelItem(i).text(0) == "": # type: ignore newIdx = i break assert newIdx >= 0 # Then delete the new item - tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(newIdx)) - qtbot.mouseClick(tabReplace.delButton, Qt.LeftButton) - assert tabReplace.listBox.topLevelItemCount() == 3 + replace.listBox.setCurrentItem(replace.listBox.topLevelItem(newIdx)) + qtbot.mouseClick(replace.delButton, Qt.LeftButton) + assert replace.listBox.topLevelItemCount() == 3 # Check Project projSettings._doSave() - assert theProject.data.autoReplace == { + assert project.data.autoReplace == { "A": "B", "C": "D", "This": "With This Stuff" } From 4c5f68242aab7b480e65aa91eb02c7adf0b331f1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 22:32:39 +0100 Subject: [PATCH 7/7] Remove unused code in old config layout classes --- novelwriter/extensions/configlayout.py | 42 +++----------------------- 1 file changed, 5 insertions(+), 37 deletions(-) diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 809068bc..99d38c03 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -30,7 +30,7 @@ from __future__ import annotations from PyQt5.QtGui import QColor, QPalette from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QAbstractButton, QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout, QLineEdit, + QAbstractButton, QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout, QScrollArea, QSizePolicy, QVBoxLayout, QWidget ) @@ -144,11 +144,6 @@ class NScrollableForm(QScrollArea): self._indent = max(indent, 0) return - def setLayoutMargins(self, left: int, top: int, right: int, bottom: int) -> None: - """Set the contents margins on the layout.""" - self._layout.setContentsMargins(left, top, right, bottom) - return - ## # Methods ## @@ -250,16 +245,6 @@ class NConfigLayout(QGridLayout): return - ## - # Getters and Setters - ## - - def setHelpTextStyle(self, color: QColor, scale: float = DEFAULT_SCALE) -> None: - """Set the text color for the help text.""" - self._helpCol = color if isinstance(color, QColor) else QColor(*color) - self._fontScale = scale - return - ## # Class Methods ## @@ -275,8 +260,8 @@ class NConfigLayout(QGridLayout): self._nextRow += 1 return - def addRow(self, label: str, widget: QWidget, helpText: str | None = None, - unit: str | None = None, button: QWidget | None = None) -> int: + def addRow(self, label: str, widget: QWidget, unit: str | None = None, + button: QWidget | None = None) -> int: """Add a label and a widget as a new row of the grid.""" wSp = CONFIG.pxInt(8) qLabel = QLabel(label) @@ -284,19 +269,7 @@ class NConfigLayout(QGridLayout): qLabel.setBuddy(widget) qHelp = None - if helpText is not None: - qHelp = NColourLabel( - str(helpText), self._helpCol, - scale=self._fontScale, wrap=True, indent=wSp - ) - labelBox = QVBoxLayout() - labelBox.addWidget(qLabel) - labelBox.addWidget(qHelp) - labelBox.setSpacing(0) - labelBox.addStretch(1) - self.addLayout(labelBox, self._nextRow, 0, 1, 1, LEFT_TOP) - else: - self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP) + self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP) if isinstance(unit, str): controlBox = QHBoxLayout() @@ -313,12 +286,7 @@ class NConfigLayout(QGridLayout): self.addLayout(controlBox, self._nextRow, 1, 1, 1, RIGHT_TOP) else: - if isinstance(widget, QLineEdit): - qLayout = QHBoxLayout() - qLayout.addWidget(widget) - self.addLayout(qLayout, self._nextRow, 1, 1, 1, RIGHT_TOP) - else: - self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP) + self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP) self.setRowStretch(self._nextRow, 0) self.setRowStretch(self._nextRow+1, 1)