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] 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()