diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 4509d3b8..2e4617a9 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -28,20 +28,23 @@ import logging from PyQt5.QtGui import QCloseEvent, QFont from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QDialog, QWidget, QComboBox, QSpinBox, QPushButton, QDialogButtonBox, + QAbstractButton, QDialog, QHBoxLayout, QLabel, QScrollArea, QVBoxLayout, QWidget, QComboBox, QSpinBox, QPushButton, QDialogButtonBox, QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox, qApp ) from novelwriter import CONFIG, SHARED from novelwriter.dialogs.quotes import GuiQuoteSelect +from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.pageddialog import NPagedDialog -from novelwriter.extensions.configlayout import NConfigLayout +from novelwriter.extensions.configlayout import NConfigLayout, NScrollableForm logger = logging.getLogger(__name__) -class GuiPreferences(NPagedDialog): +class GuiPreferences(QDialog): + + NAV_APPEARANCE = 0 newPreferencesReady = pyqtSignal(bool, bool, bool, bool) @@ -50,32 +53,44 @@ class GuiPreferences(NPagedDialog): logger.debug("Create: GuiPreferences") self.setObjectName("GuiPreferences") - self.setWindowTitle(self.tr("Preferences")) - - self.tabGeneral = GuiPreferencesGeneral(self) - self.tabProjects = GuiPreferencesProjects(self) - self.tabDocs = GuiPreferencesDocuments(self) - self.tabEditor = GuiPreferencesEditor(self) - self.tabSyntax = GuiPreferencesSyntax(self) - self.tabAuto = GuiPreferencesAutomation(self) - self.tabQuote = GuiPreferencesQuotes(self) - - self.addTab(self.tabGeneral, self.tr("General")) - self.addTab(self.tabProjects, self.tr("Projects")) - self.addTab(self.tabDocs, self.tr("Documents")) - self.addTab(self.tabEditor, self.tr("Editor")) - self.addTab(self.tabSyntax, self.tr("Highlighting")) - self.addTab(self.tabAuto, self.tr("Automation")) - self.addTab(self.tabQuote, self.tr("Quotes")) - - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) - self.buttonBox.accepted.connect(self._doSave) - self.buttonBox.rejected.connect(self.close) - self.rejected.connect(self.close) - self.addControls(self.buttonBox) + self.setWindowTitle(CONFIG.appName) + mPx = CONFIG.pxInt(150) self.resize(*CONFIG.preferencesWinSize) + self.minWidth = CONFIG.pxInt(200) + + # SideBar + self.optSideBar = NPagedSideBar(self) + self.optSideBar.setMinimumWidth(mPx) + self.optSideBar.setMaximumWidth(mPx) + self.optSideBar.setLabelColor(SHARED.theme.helpText) + self.optSideBar.addLabel(self.tr("Settings")) + + # Form + self.mainForm = NScrollableForm(self) + self.mainForm.setHelpTextStyle(SHARED.theme.helpText) + + # Buttons + self.buttonBox = QDialogButtonBox( + QDialogButtonBox.Apply | QDialogButtonBox.Save | QDialogButtonBox.Close + ) + self.buttonBox.clicked.connect(self._dialogButtonClicked) + + # Assemble + self.mainBox = QHBoxLayout() + self.mainBox.addWidget(self.optSideBar) + self.mainBox.addWidget(self.mainForm) + self.mainBox.setContentsMargins(0, 0, 0, 0) + + self.outerBox = QVBoxLayout() + self.outerBox.addLayout(self.mainBox) + self.outerBox.addWidget(self.buttonBox) + self.outerBox.setSpacing(CONFIG.pxInt(12)) + + self.setLayout(self.outerBox) + self.buildForm() + # Settings self._updateTheme = False self._updateSyntax = False @@ -90,6 +105,16 @@ class GuiPreferences(NPagedDialog): logger.debug("Delete: GuiPreferences") return + def buildForm(self) -> None: + """""" + title = self.tr("Appearance") + self.optSideBar.addButton(title, self.NAV_APPEARANCE) + self.mainForm.addGroupLabel(title, self.NAV_APPEARANCE) + self._buildAppearance() + + self.mainForm.finalise() + return + ## # Events ## @@ -106,22 +131,34 @@ class GuiPreferences(NPagedDialog): # Private Slots ## + @pyqtSlot("QAbstractButton*") + def _dialogButtonClicked(self, button: QAbstractButton) -> None: + """Handle button clicks from the dialog button box.""" + role = self.buttonBox.buttonRole(button) + if role == QDialogButtonBox.ApplyRole: + pass + elif role == QDialogButtonBox.AcceptRole: + self.close() + elif role == QDialogButtonBox.RejectRole: + self.close() + return + @pyqtSlot() def _doSave(self) -> None: """Trigger save functions in the tabs and emit ready signal.""" - self.tabGeneral.saveValues() - self.tabProjects.saveValues() - self.tabDocs.saveValues() - self.tabEditor.saveValues() - self.tabSyntax.saveValues() - self.tabAuto.saveValues() - self.tabQuote.saveValues() + # self.tabGeneral.saveValues() + # self.tabProjects.saveValues() + # self.tabDocs.saveValues() + # self.tabEditor.saveValues() + # self.tabSyntax.saveValues() + # self.tabAuto.saveValues() + # self.tabQuote.saveValues() - CONFIG.saveConfig() - self.newPreferencesReady.emit( - self._needsRestart, self._refreshTree, self._updateTheme, self._updateSyntax - ) - qApp.processEvents() + # CONFIG.saveConfig() + # self.newPreferencesReady.emit( + # self._needsRestart, self._refreshTree, self._updateTheme, self._updateSyntax + # ) + # qApp.processEvents() self.close() return @@ -135,6 +172,99 @@ class GuiPreferences(NPagedDialog): CONFIG.setPreferencesWinSize(self.width(), self.height()) return + def _buildAppearance(self) -> None: + """Build the appearance section.""" + # Select Locale + self.guiLocale = QComboBox(self) + self.guiLocale.setMinimumWidth(self.minWidth) + theLangs = CONFIG.listLanguages(CONFIG.LANG_NW) + for lang, langName in theLangs: + self.guiLocale.addItem(langName, lang) + langIdx = self.guiLocale.findData(CONFIG.guiLocale) + if langIdx < 0: + langIdx = self.guiLocale.findData("en_GB") + if langIdx != -1: + self.guiLocale.setCurrentIndex(langIdx) + + self.mainForm.addRow( + self.tr("Main GUI language"), + self.guiLocale, + self.tr("Requires restart to take effect.") + ) + + # Select Theme + self.guiTheme = QComboBox(self) + self.guiTheme.setMinimumWidth(self.minWidth) + self.theThemes = SHARED.theme.listThemes() + for themeDir, themeName in self.theThemes: + self.guiTheme.addItem(themeName, themeDir) + themeIdx = self.guiTheme.findData(CONFIG.guiTheme) + if themeIdx != -1: + self.guiTheme.setCurrentIndex(themeIdx) + + self.mainForm.addRow( + self.tr("Main GUI theme"), + self.guiTheme, + self.tr("General colour theme and icons.") + ) + + # Editor Theme + self.guiSyntax = QComboBox(self) + self.guiSyntax.setMinimumWidth(self.minWidth) + self.theSyntaxes = SHARED.theme.listSyntax() + for syntaxFile, syntaxName in self.theSyntaxes: + self.guiSyntax.addItem(syntaxName, syntaxFile) + syntaxIdx = self.guiSyntax.findData(CONFIG.guiSyntax) + if syntaxIdx != -1: + self.guiSyntax.setCurrentIndex(syntaxIdx) + + self.mainForm.addRow( + self.tr("Editor theme"), + self.guiSyntax, + self.tr("Colour theme for the editor and viewer.") + ) + + # Font Family + self.guiFont = QLineEdit(self) + self.guiFont.setReadOnly(True) + self.guiFont.setFixedWidth(CONFIG.pxInt(162)) + self.guiFont.setText(CONFIG.guiFont) + self.fontButton = QPushButton("...", self) + self.fontButton.setMaximumWidth(int(2.5*SHARED.theme.getTextWidth("..."))) + # self.fontButton.clicked.connect(self._selectFont) + self.mainForm.addRow( + self.tr("Font family"), + self.guiFont, + self.tr("Requires restart to take effect."), + button=self.fontButton + ) + + # Font Size + self.guiFontSize = QSpinBox(self) + self.guiFontSize.setMinimum(8) + self.guiFontSize.setMaximum(60) + self.guiFontSize.setSingleStep(1) + self.guiFontSize.setValue(CONFIG.guiFontSize) + self.mainForm.addRow( + self.tr("Font size"), + self.guiFontSize, + self.tr("Requires restart to take effect."), + unit=self.tr("pt") + ) + + return + @pyqtSlot() + def _selectFont(self) -> None: + """Open the QFontDialog and set a font for the font style.""" + currFont = QFont() + currFont.setFamily(CONFIG.guiFont) + currFont.setPointSize(CONFIG.guiFontSize) + theFont, theStatus = QFontDialog.getFont(currFont, self) + if theStatus: + self.guiFont.setText(theFont.family()) + self.guiFontSize.setValue(theFont.pointSize()) + return + # END Class GuiPreferences diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 687dc715..3a541559 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -26,7 +26,7 @@ from __future__ import annotations from PyQt5.QtGui import QColor, QPalette from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QAbstractButton, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QSizePolicy, + QAbstractButton, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QScrollArea, QSizePolicy, QVBoxLayout, QWidget ) @@ -37,6 +37,99 @@ RIGHT_TOP = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop +class NScrollableForm(QScrollArea): + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + self._helpCol = QColor(0, 0, 0) + self._fontScale = FONT_SCALE + self._sections: dict[int, QLabel] = {} + self._editable: dict[str, NHelpLabel] = {} + + self._layout = QVBoxLayout() + self._layout.setSpacing(CONFIG.pxInt(12)) + + self._widget = QWidget(self) + self._widget.setLayout(self._layout) + + self.setWidget(self._widget) + self.setWidgetResizable(True) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + + return + + def setHelpTextStyle(self, color: QColor | list | tuple, scale: float = FONT_SCALE) -> None: + """Set the text color for the help text.""" + self._helpCol = color if isinstance(color, QColor) else QColor(*color) + self._fontScale = scale + return + + def setHelpText(self, key: str, text: str) -> None: + """Set the text for the help label.""" + if qHelp := self._editable.get(key): + qHelp.setText(text) + return + + def finalise(self) -> None: + """Finalise the layout when the form is built.""" + self._layout.addStretch(1) + return + + def scrollToSection(self, identifier: int, offset: int = 50) -> None: + """Scroll to the requested section identifier.""" + if identifier in self._sections: + yPos = self._sections[identifier].pos().y() - CONFIG.pxInt(8) + self.verticalScrollBar().setValue(yPos) + return + + def addGroupLabel(self, label: str, identifier: int) -> None: + """Add a text label to separate groups of settings.""" + hM = CONFIG.pxInt(4) + qLabel = QLabel(f"{label}", self) + qLabel.setContentsMargins(0, hM, 0, hM) + self._layout.addWidget(qLabel) + self._sections[identifier] = qLabel + return + + def addRow(self, label: str, widget: QWidget, helpText: str = "", unit: str | None = None, + button: QWidget | None = None, editable: str | None = None) -> None: + """Add a label and a widget as a new row of the grid.""" + row = QHBoxLayout() + + wSp = CONFIG.pxInt(8) + qLabel = QLabel(label, self) + qLabel.setIndent(wSp) + qLabel.setBuddy(widget) + + if helpText: + qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale) + qHelp.setIndent(wSp) + labelBox = QVBoxLayout() + labelBox.addWidget(qLabel) + labelBox.addWidget(qHelp) + labelBox.setSpacing(0) + labelBox.addStretch(1) + row.addLayout(labelBox) + if editable: + self._editable[editable] = qHelp + else: + row.addWidget(qLabel) + + row.addWidget(widget) + + if isinstance(unit, str): + row.addWidget(QLabel(unit, self)) + elif isinstance(button, QAbstractButton): + row.addWidget(button) + + self._layout.addLayout(row) + + return + +# END Class NScrollableForm + + class NConfigLayout(QGridLayout): def __init__(self) -> None: @@ -58,11 +151,10 @@ class NConfigLayout(QGridLayout): # Getters and Setters ## - def setHelpTextStyle(self, color: QColor | list | tuple, - fontScale: float = FONT_SCALE) -> None: + def setHelpTextStyle(self, color: QColor | list | tuple, scale: float = FONT_SCALE) -> None: """Set the text color for the help text.""" self._helpCol = color if isinstance(color, QColor) else QColor(*color) - self._fontScale = fontScale + self._fontScale = scale return def setHelpText(self, row: int, text: str) -> None: