diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index 44317317..f66874ee 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -61,6 +61,7 @@ forward = typ_chevron-right.svg maximise = typ_arrow-maximise.svg menu = typ_th-dot-menu.svg minimise = typ_arrow-minimise.svg +more = typ_th-dot-more.svg noncheckable = mixed_input-none.svg panel = nw_panel.svg proj_chapter = mixed_document-chapter.svg diff --git a/novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg b/novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg new file mode 100644 index 00000000..782996dc --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 7579bb70..3a6837d9 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -61,6 +61,7 @@ forward = typ_chevron-right.svg maximise = typ_arrow-maximise.svg menu = typ_th-dot-menu.svg minimise = typ_arrow-minimise.svg +more = typ_th-dot-more.svg noncheckable = mixed_input-none.svg panel = nw_panel.svg proj_chapter = mixed_document-chapter.svg diff --git a/novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg b/novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg new file mode 100644 index 00000000..779bc23f --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/config.py b/novelwriter/config.py index c8ff11ae..ad39b218 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -150,9 +150,6 @@ class Config: self.autoScrollPos = 30 # Start point for typewriter-like scrolling self.scrollPastEnd = True # Scroll past end of document, and centre cursor - self.wordCountTimer = 5.0 # Interval for word count update in seconds - self.incNotesWCount = True # The status bar word count includes notes - self.highlightQuotes = True # Highlight text in quotes self.allowOpenSQuote = False # Allow open-ended single quotes self.allowOpenDQuote = True # Allow open-ended double quotes @@ -160,6 +157,7 @@ class Config: self.stopWhenIdle = True # Stop the status bar clock when the user is idle self.userIdleTime = 300 # Time of inactivity to consider user idle + self.incNotesWCount = True # The status bar word count includes notes # User-Selected Symbol Settings self.fmtApostrophe = nwUnicode.U_RSQUO @@ -591,7 +589,6 @@ class Config: self.showTabsNSpaces = conf.rdBool(sec, "showtabsnspaces", self.showTabsNSpaces) self.showLineEndings = conf.rdBool(sec, "showlineendings", self.showLineEndings) self.showMultiSpaces = conf.rdBool(sec, "showmultispaces", self.showMultiSpaces) - self.wordCountTimer = conf.rdFlt(sec, "wordcounttimer", self.wordCountTimer) self.incNotesWCount = conf.rdBool(sec, "incnoteswcount", self.incNotesWCount) self.showFullPath = conf.rdBool(sec, "showfullpath", self.showFullPath) self.highlightQuotes = conf.rdBool(sec, "highlightquotes", self.highlightQuotes) @@ -698,7 +695,6 @@ class Config: "showtabsnspaces": str(self.showTabsNSpaces), "showlineendings": str(self.showLineEndings), "showmultispaces": str(self.showMultiSpaces), - "wordcounttimer": str(self.wordCountTimer), "incnoteswcount": str(self.incNotesWCount), "showfullpath": str(self.showFullPath), "highlightquotes": str(self.highlightQuotes), diff --git a/novelwriter/constants.py b/novelwriter/constants.py index ce12ced8..b133fcb9 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -56,6 +56,9 @@ class nwConst: # Gui Settings STATUS_MSG_TIMEOUT = 15000 # milliseconds + # Dialogs + DLG_FINISHED = 2 + # END Class nwConst diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 4509d3b8..635f0496 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -3,7 +3,8 @@ novelWriter – GUI Preferences ============================= File History: -Created: 2019-06-10 [0.1.5] GuiPreferences +Created: 2019-06-10 [0.1.5] GuiPreferences +Rewritten: 2024-01-08 [2.3b1] GuiPreferences This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen @@ -25,23 +26,27 @@ from __future__ import annotations import logging -from PyQt5.QtGui import QCloseEvent, QFont +from PyQt5.QtGui import QCloseEvent, QColor, QFont, QKeyEvent, QKeySequence, QPalette from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QDialog, QWidget, QComboBox, QSpinBox, QPushButton, QDialogButtonBox, - QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox, qApp + QAbstractButton, QComboBox, QCompleter, QDialog, QDialogButtonBox, + QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QSpinBox, QToolButton, QVBoxLayout, QWidget, qApp ) from novelwriter import CONFIG, SHARED +from novelwriter.constants import nwConst, nwUnicode from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.pageddialog import NPagedDialog -from novelwriter.extensions.configlayout import NConfigLayout +from novelwriter.extensions.configlayout import NScrollableForm +from novelwriter.extensions.pagedsidebar import NPagedSideBar logger = logging.getLogger(__name__) -class GuiPreferences(NPagedDialog): +class GuiPreferences(QDialog): + + NAV_APPEARANCE = 0 newPreferencesReady = pyqtSignal(bool, bool, bool, bool) @@ -51,36 +56,77 @@ 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.setMinimumSize(CONFIG.pxInt(600), CONFIG.pxInt(500)) self.resize(*CONFIG.preferencesWinSize) - # Settings - self._updateTheme = False - self._updateSyntax = False - self._needsRestart = False - self._refreshTree = False + # Title + font = self.font() + font.setPointSizeF(1.25*SHARED.theme.fontPointSize) + + palette = self.palette() + palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.helpText)) + + self.titleLabel = QLabel(self.tr("Preferences"), self) + self.titleLabel.setFont(font) + self.titleLabel.setPalette(palette) + self.titleLabel.setIndent(CONFIG.pxInt(4)) + + # SideBar + self.sidebar = NPagedSideBar(self) + self.sidebar.setLabelColor(SHARED.theme.helpText) + self.sidebar.buttonClicked.connect(self._sidebarClicked) + + # Search Box + self.searchText = QLineEdit(self) + self.searchText.setPlaceholderText(self.tr("Search")) + self.searchText.setMinimumWidth(CONFIG.pxInt(200)) + self.searchAction = self.searchText.addAction( + SHARED.theme.getIcon("search"), QLineEdit.ActionPosition.TrailingPosition + ) + self.searchAction.triggered.connect(self._gotoSearch) + + self.searchBox = QHBoxLayout() + self.searchBox.addWidget(self.titleLabel) + self.searchBox.addStretch(1) + self.searchBox.addWidget(self.searchText, 1) + + # Form + self.mainForm = NScrollableForm(self) + self.mainForm.setHelpTextStyle(SHARED.theme.helpText) + + # Buttons + self.buttonBox = QDialogButtonBox( + QDialogButtonBox.StandardButton.Apply + | QDialogButtonBox.StandardButton.Save + | QDialogButtonBox.StandardButton.Close + ) + self.buttonBox.clicked.connect(self._dialogButtonClicked) + + # Assemble + self.mainBox = QHBoxLayout() + self.mainBox.addWidget(self.sidebar) + self.mainBox.addWidget(self.mainForm) + self.mainBox.setContentsMargins(0, 0, 0, 0) + + self.outerBox = QVBoxLayout() + self.outerBox.addLayout(self.searchBox) + self.outerBox.addLayout(self.mainBox) + self.outerBox.addWidget(self.buttonBox) + self.outerBox.setSpacing(CONFIG.pxInt(8)) + + self.setLayout(self.outerBox) + self.setSizeGripEnabled(True) + + # Build Form + self.buildForm() + + # Populate Search + self.searchCompleter = QCompleter(self.mainForm.labels, self) + self.searchCompleter.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) + self.searchCompleter.setFilterMode(Qt.MatchFlag.MatchContains) + self.searchCompleter.activated.connect(self._gotoSearch) + + self.searchText.setCompleter(self.searchCompleter) logger.debug("Ready: GuiPreferences") @@ -90,170 +136,78 @@ class GuiPreferences(NPagedDialog): logger.debug("Delete: GuiPreferences") return - ## - # Events - ## - - def closeEvent(self, event: QCloseEvent) -> None: - """Capture the close event and perform cleanup.""" - logger.debug("Close: GuiPreferences") - self._saveWindowSize() - event.accept() - self.deleteLater() - return - - ## - # Private Slots - ## - - @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() - - CONFIG.saveConfig() - self.newPreferencesReady.emit( - self._needsRestart, self._refreshTree, self._updateTheme, self._updateSyntax - ) - qApp.processEvents() - self.close() - - return - - ## - # Internal Functions - ## - - def _saveWindowSize(self) -> None: - """Save the dialog window size.""" - CONFIG.setPreferencesWinSize(self.width(), self.height()) - return - -# END Class GuiPreferences - - -class GuiPreferencesGeneral(QWidget): - - def __init__(self, prefsGui: GuiPreferences) -> None: - super().__init__(parent=prefsGui) - - self.prefsGui = prefsGui - - # The Form - self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(SHARED.theme.helpText) - self.setLayout(self.mainForm) - - # Look and Feel - # ============= - self.mainForm.addGroupLabel(self.tr("Look and Feel")) + def buildForm(self) -> None: + """Build the settings form.""" + section = 0 minWidth = CONFIG.pxInt(200) + mIcon = SHARED.theme.getIcon("more") - # Select Locale + # Appearance + # ========== + + title = self.tr("Appearance") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) + + # Display Language self.guiLocale = QComboBox(self) self.guiLocale.setMinimumWidth(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) + for lang, name in CONFIG.listLanguages(CONFIG.LANG_NW): + self.guiLocale.addItem(name, lang) + if (idx := self.guiLocale.findData(CONFIG.guiLocale)) != -1: + self.guiLocale.setCurrentIndex(idx) self.mainForm.addRow( - self.tr("Main GUI language"), + self.tr("Display language"), self.guiLocale, self.tr("Requires restart to take effect.") ) - # Select Theme + # Colour Theme self.guiTheme = QComboBox(self) self.guiTheme.setMinimumWidth(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) + for theme, name in SHARED.theme.listThemes(): + self.guiTheme.addItem(name, theme) + if (idx := self.guiTheme.findData(CONFIG.guiTheme)) != -1: + self.guiTheme.setCurrentIndex(idx) self.mainForm.addRow( - self.tr("Main GUI theme"), + self.tr("Colour theme"), self.guiTheme, self.tr("General colour theme and icons.") ) - # Editor Theme - self.guiSyntax = QComboBox(self) - self.guiSyntax.setMinimumWidth(CONFIG.pxInt(200)) - 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 + # Application 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.guiFontButton = QToolButton(self) + self.guiFontButton.setIcon(mIcon) + self.guiFontButton.clicked.connect(self._selectGuiFont) self.mainForm.addRow( - self.tr("Font family"), + self.tr("Application font family"), self.guiFont, self.tr("Requires restart to take effect."), - button=self.fontButton + button=self.guiFontButton ) - # Font Size + # Application 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.tr("Application font size"), self.guiFontSize, self.tr("Requires restart to take effect."), unit=self.tr("pt") ) - # GUI Settings - # ============ - self.mainForm.addGroupLabel(self.tr("GUI Settings")) - - self.emphLabels = NSwitch() - self.emphLabels.setChecked(CONFIG.emphLabels) - self.mainForm.addRow( - self.tr("Emphasise partition and chapter labels"), - self.emphLabels, - self.tr("Makes them stand out in the project tree."), - ) - - self.showFullPath = NSwitch() - self.showFullPath.setChecked(CONFIG.showFullPath) - self.mainForm.addRow( - self.tr("Show full path in document header"), - self.showFullPath, - self.tr("Add the parent folder names to the header.") - ) - - self.hideVScroll = NSwitch() + # Vertical Scrollbars + self.hideVScroll = NSwitch(self) self.hideVScroll.setChecked(CONFIG.hideVScroll) self.mainForm.addRow( self.tr("Hide vertical scroll bars in main windows"), @@ -261,7 +215,8 @@ class GuiPreferencesGeneral(QWidget): self.tr("Scrolling available with mouse wheel and keys only.") ) - self.hideHScroll = NSwitch() + # Horizontal Scrollbars + self.hideHScroll = NSwitch(self) self.hideHScroll.setChecked(CONFIG.hideHScroll) self.mainForm.addRow( self.tr("Hide horizontal scroll bars in main windows"), @@ -269,69 +224,89 @@ class GuiPreferencesGeneral(QWidget): self.tr("Scrolling available with mouse wheel and keys only.") ) - return - - def saveValues(self) -> None: - """Save the values set for this tab.""" - guiLocale = self.guiLocale.currentData() - guiTheme = self.guiTheme.currentData() - guiSyntax = self.guiSyntax.currentData() - guiFont = self.guiFont.text() - guiFontSize = self.guiFontSize.value() - emphLabels = self.emphLabels.isChecked() - - # Update Flags - self.prefsGui._updateTheme |= CONFIG.guiTheme != guiTheme - self.prefsGui._updateSyntax |= CONFIG.guiSyntax != guiSyntax - self.prefsGui._needsRestart |= CONFIG.guiLocale != guiLocale - self.prefsGui._needsRestart |= CONFIG.guiFont != guiFont - self.prefsGui._needsRestart |= CONFIG.guiFontSize != guiFontSize - self.prefsGui._refreshTree |= CONFIG.emphLabels != emphLabels - - CONFIG.guiLocale = guiLocale - CONFIG.guiTheme = guiTheme - CONFIG.guiSyntax = guiSyntax - CONFIG.guiFont = guiFont - CONFIG.guiFontSize = guiFontSize - CONFIG.emphLabels = emphLabels - CONFIG.showFullPath = self.showFullPath.isChecked() - CONFIG.hideVScroll = self.hideVScroll.isChecked() - CONFIG.hideHScroll = self.hideHScroll.isChecked() - - return - - ## - # Private Slots - ## - - @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 GuiPreferencesGeneral - - -class GuiPreferencesProjects(QWidget): - - def __init__(self, prefsGui: GuiPreferences) -> None: - super().__init__(parent=prefsGui) - - # The Form - self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(SHARED.theme.helpText) - self.setLayout(self.mainForm) - - # Automatic Save + # Document Style # ============== - self.mainForm.addGroupLabel(self.tr("Automatic Save")) + + title = self.tr("Document Style") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) + + # Document Colour Theme + self.guiSyntax = QComboBox(self) + self.guiSyntax.setMinimumWidth(CONFIG.pxInt(200)) + for syntax, name in SHARED.theme.listSyntax(): + self.guiSyntax.addItem(name, syntax) + if (idx := self.guiSyntax.findData(CONFIG.guiSyntax)) != -1: + self.guiSyntax.setCurrentIndex(idx) + + self.mainForm.addRow( + self.tr("Document colour theme"), + self.guiSyntax, + self.tr("Colour theme for the editor and viewer.") + ) + + # Document Font Family + self.textFont = QLineEdit(self) + self.textFont.setReadOnly(True) + self.textFont.setFixedWidth(CONFIG.pxInt(162)) + self.textFont.setText(CONFIG.textFont) + self.textFontButton = QToolButton(self) + self.textFontButton.setIcon(mIcon) + self.textFontButton.clicked.connect(self._selectTextFont) + self.mainForm.addRow( + self.tr("Document font family"), + self.textFont, + self.tr("Applies to both document editor and viewer."), + button=self.textFontButton + ) + + # Document Font Size + self.textSize = QSpinBox(self) + self.textSize.setMinimum(8) + self.textSize.setMaximum(60) + self.textSize.setSingleStep(1) + self.textSize.setValue(CONFIG.textSize) + self.mainForm.addRow( + self.tr("Document font size"), + self.textSize, + self.tr("Applies to both document editor and viewer."), + unit=self.tr("pt") + ) + + # Emphasise Labels + self.emphLabels = NSwitch(self) + self.emphLabels.setChecked(CONFIG.emphLabels) + self.mainForm.addRow( + self.tr("Emphasise partition and chapter labels"), + self.emphLabels, + self.tr("Makes them stand out in the project tree."), + ) + + # Document Path + self.showFullPath = NSwitch(self) + self.showFullPath.setChecked(CONFIG.showFullPath) + self.mainForm.addRow( + self.tr("Show full path in document header"), + self.showFullPath, + self.tr("Add the parent folder names to the header.") + ) + + # Include Notes in Word Count + self.incNotesWCount = NSwitch(self) + self.incNotesWCount.setChecked(CONFIG.incNotesWCount) + self.mainForm.addRow( + self.tr("Include project notes in status bar word count"), + self.incNotesWCount + ) + + # Auto Save + # ========= + + title = self.tr("Auto Save") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) # Document Save Timer self.autoSaveDoc = QSpinBox(self) @@ -361,20 +336,25 @@ class GuiPreferencesProjects(QWidget): # Project Backup # ============== - self.mainForm.addGroupLabel(self.tr("Project Backup")) + + title = self.tr("Project Backup") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) # Backup Path self.backupPath = CONFIG.backupPath() - self.backupGetPath = QPushButton(self.tr("Browse"), self) + self.backupGetPath = QPushButton(SHARED.theme.getIcon("browse"), self.tr("Browse"), self) self.backupGetPath.clicked.connect(self._backupFolder) - self.backupPathRow = self.mainForm.addRow( + self.mainForm.addRow( self.tr("Backup storage location"), self.backupGetPath, - self.tr("Path: {0}").format(self.backupPath) + self.tr("Path: {0}").format(self.backupPath), + editable="backupPath" ) - # Run when closing - self.backupOnClose = NSwitch() + # Run When Closing + self.backupOnClose = NSwitch(self) self.backupOnClose.setChecked(CONFIG.backupOnClose) self.backupOnClose.toggled.connect(self._toggledBackupOnClose) self.mainForm.addRow( @@ -383,9 +363,9 @@ class GuiPreferencesProjects(QWidget): self.tr("Can be overridden for individual projects in Project Settings.") ) - # Ask before backup + # Ask Before Backup # Only enabled when "Run when closing" is checked - self.askBeforeBackup = NSwitch() + self.askBeforeBackup = NSwitch(self) self.askBeforeBackup.setChecked(CONFIG.askBeforeBackup) self.askBeforeBackup.setEnabled(CONFIG.backupOnClose) self.mainForm.addRow( @@ -396,10 +376,14 @@ class GuiPreferencesProjects(QWidget): # Session Timer # ============= - self.mainForm.addGroupLabel(self.tr("Session Timer")) - # Pause when idle - self.stopWhenIdle = NSwitch() + title = self.tr("Session Timer") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) + + # Pause When Idle + self.stopWhenIdle = NSwitch(self) self.stopWhenIdle.setChecked(CONFIG.stopWhenIdle) self.mainForm.addRow( self.tr("Pause the session timer when not writing"), @@ -407,7 +391,7 @@ class GuiPreferencesProjects(QWidget): self.tr("Also pauses when the application window does not have focus.") ) - # Inactive time for idle + # Inactive Time for Idle self.userIdleTime = QDoubleSpinBox(self) self.userIdleTime.setMinimum(0.5) self.userIdleTime.setMaximum(600.0) @@ -421,98 +405,13 @@ class GuiPreferencesProjects(QWidget): unit=self.tr("minutes") ) - return - - def saveValues(self) -> None: - """Save the values set for this tab.""" - # Automatic Save - CONFIG.autoSaveDoc = self.autoSaveDoc.value() - CONFIG.autoSaveProj = self.autoSaveProj.value() - - # Project Backup - CONFIG.setBackupPath(self.backupPath) - CONFIG.backupOnClose = self.backupOnClose.isChecked() - CONFIG.askBeforeBackup = self.askBeforeBackup.isChecked() - - # Session Timer - CONFIG.stopWhenIdle = self.stopWhenIdle.isChecked() - CONFIG.userIdleTime = round(self.userIdleTime.value() * 60) - - return - - ## - # Private Slots - ## - - @pyqtSlot() - def _backupFolder(self) -> None: - """Open a dialog to select the backup folder.""" - currDir = self.backupPath or "" - newDir = QFileDialog.getExistingDirectory( - self, self.tr("Backup Directory"), str(currDir), options=QFileDialog.ShowDirsOnly - ) - if newDir: - self.backupPath = newDir - self.mainForm.setHelpText( - self.backupPathRow, self.tr("Path: {0}").format(self.backupPath) - ) - return - return - - @pyqtSlot(bool) - def _toggledBackupOnClose(self, state: bool) -> None: - """Toggle switch that depends on the backup on close switch.""" - self.askBeforeBackup.setEnabled(state) - return - -# END Class GuiPreferencesProjects - - -class GuiPreferencesDocuments(QWidget): - - def __init__(self, prefsGui: GuiPreferences) -> None: - super().__init__(parent=prefsGui) - - # The Form - self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(SHARED.theme.helpText) - self.setLayout(self.mainForm) - - # Text Style - # ========== - self.mainForm.addGroupLabel(self.tr("Text Style")) - - # Font Family - self.textFont = QLineEdit(self) - self.textFont.setReadOnly(True) - self.textFont.setFixedWidth(CONFIG.pxInt(162)) - self.textFont.setText(CONFIG.textFont) - 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.textFont, - self.tr("Applies to both document editor and viewer."), - button=self.fontButton - ) - - # Font Size - self.textSize = QSpinBox(self) - self.textSize.setMinimum(8) - self.textSize.setMaximum(60) - self.textSize.setSingleStep(1) - self.textSize.setValue(CONFIG.textSize) - self.mainForm.addRow( - self.tr("Font size"), - self.textSize, - self.tr("Applies to both document editor and viewer."), - unit=self.tr("pt") - ) - # Text Flow # ========= - self.mainForm.addGroupLabel(self.tr("Text Flow")) + + title = self.tr("Text Flow") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) # Max Text Width in Normal Mode self.textWidth = QSpinBox(self) @@ -541,7 +440,7 @@ class GuiPreferencesDocuments(QWidget): ) # Focus Mode Footer - self.hideFocusFooter = NSwitch() + self.hideFocusFooter = NSwitch(self) self.hideFocusFooter.setChecked(CONFIG.hideFocusFooter) self.mainForm.addRow( self.tr("Hide document footer in \"Focus Mode\""), @@ -550,7 +449,7 @@ class GuiPreferencesDocuments(QWidget): ) # Justify Text - self.doJustify = NSwitch() + self.doJustify = NSwitch(self) self.doJustify.setChecked(CONFIG.doJustify) self.mainForm.addRow( self.tr("Justify the text margins"), @@ -584,73 +483,27 @@ class GuiPreferencesDocuments(QWidget): unit=self.tr("px") ) - return + # Text Editing + # ============ - def saveValues(self) -> None: - """Save the values set for this tab.""" - # Text Style - CONFIG.setTextFont(self.textFont.text(), self.textSize.value()) - - # Text Flow - CONFIG.textWidth = self.textWidth.value() - CONFIG.focusWidth = self.focusWidth.value() - CONFIG.hideFocusFooter = self.hideFocusFooter.isChecked() - CONFIG.doJustify = self.doJustify.isChecked() - CONFIG.textMargin = self.textMargin.value() - CONFIG.tabWidth = self.tabWidth.value() - - return - - ## - # Private Slots - ## - - @pyqtSlot() - def _selectFont(self): - """Open the QFontDialog and set a font for the font style.""" - currFont = QFont() - currFont.setFamily(CONFIG.textFont) - currFont.setPointSize(CONFIG.textSize) - theFont, theStatus = QFontDialog.getFont(currFont, self) - if theStatus: - self.textFont.setText(theFont.family()) - self.textSize.setValue(theFont.pointSize()) - - return - -# END Class GuiPreferencesDocuments - - -class GuiPreferencesEditor(QWidget): - - def __init__(self, prefsGui: GuiPreferences) -> None: - super().__init__(parent=prefsGui) - - # The Form - self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(SHARED.theme.helpText) - self.setLayout(self.mainForm) - - mW = CONFIG.pxInt(250) + title = self.tr("Text Editing") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) # Spell Checking - # ============== - self.mainForm.addGroupLabel(self.tr("Spell Checking")) - - # Spell Check Provider and Language self.spellLanguage = QComboBox(self) - self.spellLanguage.setMaximumWidth(mW) + self.spellLanguage.setMinimumWidth(minWidth) if CONFIG.hasEnchant: for tag, language in SHARED.spelling.listDictionaries(): self.spellLanguage.addItem(language, tag) else: - self.spellLanguage.addItem(self.tr("None"), "") + self.spellLanguage.addItem(nwUnicode.U_EMDASH, "") self.spellLanguage.setEnabled(False) - spellIdx = self.spellLanguage.findData(CONFIG.spellLanguage) - if spellIdx != -1: - self.spellLanguage.setCurrentIndex(spellIdx) + if (idx := self.spellLanguage.findData(CONFIG.spellLanguage)) != -1: + self.spellLanguage.setCurrentIndex(idx) self.mainForm.addRow( self.tr("Spell check language"), @@ -658,37 +511,17 @@ class GuiPreferencesEditor(QWidget): self.tr("Available languages are determined by your system.") ) - # Word Count - # ========== - self.mainForm.addGroupLabel(self.tr("Word Count")) - - # Word Count Timer - self.wordCountTimer = QDoubleSpinBox(self) - self.wordCountTimer.setDecimals(1) - self.wordCountTimer.setMinimum(2.0) - self.wordCountTimer.setMaximum(600.0) - self.wordCountTimer.setSingleStep(0.1) - self.wordCountTimer.setValue(CONFIG.wordCountTimer) + # Auto-Select Word Under Cursor + self.autoSelect = NSwitch(self) + self.autoSelect.setChecked(CONFIG.autoSelect) self.mainForm.addRow( - self.tr("Word count interval"), - self.wordCountTimer, - unit=self.tr("seconds") + self.tr("Auto-select word under cursor"), + self.autoSelect, + self.tr("Apply formatting to word under cursor if no selection is made.") ) - # Include Notes in Word Count - self.incNotesWCount = NSwitch() - self.incNotesWCount.setChecked(CONFIG.incNotesWCount) - self.mainForm.addRow( - self.tr("Include project notes in status bar word count"), - self.incNotesWCount - ) - - # Writing Guides - # ============== - self.mainForm.addGroupLabel(self.tr("Writing Guides")) - # Show Tabs and Spaces - self.showTabsNSpaces = NSwitch() + self.showTabsNSpaces = NSwitch(self) self.showTabsNSpaces.setChecked(CONFIG.showTabsNSpaces) self.mainForm.addRow( self.tr("Show tabs and spaces"), @@ -696,19 +529,23 @@ class GuiPreferencesEditor(QWidget): ) # Show Line Endings - self.showLineEndings = NSwitch() + self.showLineEndings = NSwitch(self) self.showLineEndings.setChecked(CONFIG.showLineEndings) self.mainForm.addRow( self.tr("Show line endings"), self.showLineEndings ) - # Scroll Behaviour + # Editor Scrolling # ================ - self.mainForm.addGroupLabel(self.tr("Scroll Behaviour")) + + title = self.tr("Editor Scrolling") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) # Scroll Past End - self.scrollPastEnd = NSwitch() + self.scrollPastEnd = NSwitch(self) self.scrollPastEnd.setChecked(CONFIG.scrollPastEnd) self.mainForm.addRow( self.tr("Scroll past end of the document"), @@ -717,7 +554,7 @@ class GuiPreferencesEditor(QWidget): ) # Typewriter Scrolling - self.autoScroll = NSwitch() + self.autoScroll = NSwitch(self) self.autoScroll.setChecked(CONFIG.autoScroll) self.mainForm.addRow( self.tr("Typewriter style scrolling when you type"), @@ -738,48 +575,15 @@ class GuiPreferencesEditor(QWidget): unit="%" ) - return - - def saveValues(self) -> None: - """Save the values set for this tab.""" - # Spell Checking - CONFIG.spellLanguage = self.spellLanguage.currentData() - - # Word Count - CONFIG.wordCountTimer = self.wordCountTimer.value() - CONFIG.incNotesWCount = self.incNotesWCount.isChecked() - - # Writing Guides - CONFIG.showTabsNSpaces = self.showTabsNSpaces.isChecked() - CONFIG.showLineEndings = self.showLineEndings.isChecked() - - # Scroll Behaviour - CONFIG.autoScroll = self.autoScroll.isChecked() - CONFIG.autoScrollPos = self.autoScrollPos.value() - CONFIG.scrollPastEnd = self.scrollPastEnd.isChecked() - - return - -# END Class GuiPreferencesEditor - - -class GuiPreferencesSyntax(QWidget): - - def __init__(self, prefsGui: GuiPreferences) -> None: - super().__init__(parent=prefsGui) - - self.prefsGui = prefsGui - - # The Form - self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(SHARED.theme.helpText) - self.setLayout(self.mainForm) - - # Quotes & Dialogue + # Text Highlighting # ================= - self.mainForm.addGroupLabel(self.tr("Quotes & Dialogue")) - self.highlightQuotes = NSwitch() + title = self.tr("Text Highlighting") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) + + self.highlightQuotes = NSwitch(self) self.highlightQuotes.setChecked(CONFIG.highlightQuotes) self.highlightQuotes.toggled.connect(self._toggleHighlightQuotes) self.mainForm.addRow( @@ -788,7 +592,7 @@ class GuiPreferencesSyntax(QWidget): self.tr("Applies to the document editor only.") ) - self.allowOpenSQuote = NSwitch() + self.allowOpenSQuote = NSwitch(self) self.allowOpenSQuote.setChecked(CONFIG.allowOpenSQuote) self.mainForm.addRow( self.tr("Allow open-ended single quotes"), @@ -796,7 +600,7 @@ class GuiPreferencesSyntax(QWidget): self.tr("Highlight single-quoted line with no closing quote.") ) - self.allowOpenDQuote = NSwitch() + self.allowOpenDQuote = NSwitch(self) self.allowOpenDQuote.setChecked(CONFIG.allowOpenDQuote) self.mainForm.addRow( self.tr("Allow open-ended double quotes"), @@ -804,11 +608,7 @@ class GuiPreferencesSyntax(QWidget): self.tr("Highlight double-quoted line with no closing quote.") ) - # Text Emphasis - # ============= - self.mainForm.addGroupLabel(self.tr("Text Emphasis")) - - self.highlightEmph = NSwitch() + self.highlightEmph = NSwitch(self) self.highlightEmph.setChecked(CONFIG.highlightEmph) self.mainForm.addRow( self.tr("Add highlight colour to emphasised text"), @@ -816,12 +616,7 @@ class GuiPreferencesSyntax(QWidget): self.tr("Applies to the document editor only.") ) - # Text Errors - # =========== - - self.mainForm.addGroupLabel(self.tr("Text Errors")) - - self.showMultiSpaces = NSwitch() + self.showMultiSpaces = NSwitch(self) self.showMultiSpaces.setChecked(CONFIG.showMultiSpaces) self.mainForm.addRow( self.tr("Highlight multiple or trailing spaces"), @@ -829,67 +624,18 @@ class GuiPreferencesSyntax(QWidget): self.tr("Applies to the document editor only.") ) - return + # Text Automation + # =============== - def saveValues(self) -> None: - """Save the values set for this tab.""" - highlightQuotes = self.highlightQuotes.isChecked() - allowOpenSQuote = self.allowOpenSQuote.isChecked() - allowOpenDQuote = self.allowOpenDQuote.isChecked() - highlightEmph = self.highlightEmph.isChecked() - showMultiSpaces = self.showMultiSpaces.isChecked() + title = self.tr("Text Automation") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) - self.prefsGui._updateSyntax |= CONFIG.highlightQuotes != highlightQuotes - self.prefsGui._updateSyntax |= CONFIG.highlightEmph != highlightEmph - self.prefsGui._updateSyntax |= CONFIG.showMultiSpaces != showMultiSpaces - - CONFIG.highlightQuotes = highlightQuotes - CONFIG.allowOpenSQuote = allowOpenSQuote - CONFIG.allowOpenDQuote = allowOpenDQuote - CONFIG.highlightEmph = highlightEmph - CONFIG.showMultiSpaces = showMultiSpaces - - return - - ## - # Private Slots - ## - - @pyqtSlot(bool) - def _toggleHighlightQuotes(self, state: bool) -> None: - """Toggle switches controlled by the highlight quotes switch.""" - self.allowOpenSQuote.setEnabled(state) - self.allowOpenDQuote.setEnabled(state) - return - -# END Class GuiPreferencesSyntax - - -class GuiPreferencesAutomation(QWidget): - - def __init__(self, prefsGui: GuiPreferences) -> None: - super().__init__(parent=prefsGui) - - # The Form - self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(SHARED.theme.helpText) - self.setLayout(self.mainForm) - - # Automatic Features - # ================== - self.mainForm.addGroupLabel(self.tr("Automatic Features")) - - # Auto-Select Word Under Cursor - self.autoSelect = NSwitch() - self.autoSelect.setChecked(CONFIG.autoSelect) - self.mainForm.addRow( - self.tr("Auto-select word under cursor"), - self.autoSelect, - self.tr("Apply formatting to word under cursor if no selection is made.") - ) + boxWidth = CONFIG.pxInt(150) # Auto-Replace as You Type Main Switch - self.doReplace = NSwitch() + self.doReplace = NSwitch(self) self.doReplace.setChecked(CONFIG.doReplace) self.doReplace.toggled.connect(self._toggleAutoReplaceMain) self.mainForm.addRow( @@ -898,12 +644,8 @@ class GuiPreferencesAutomation(QWidget): self.tr("Allow the editor to replace symbols as you type.") ) - # Replace as You Type - # =================== - self.mainForm.addGroupLabel(self.tr("Replace as You Type")) - # Auto-Replace Single Quotes - self.doReplaceSQuote = NSwitch() + self.doReplaceSQuote = NSwitch(self) self.doReplaceSQuote.setChecked(CONFIG.doReplaceSQuote) self.doReplaceSQuote.setEnabled(CONFIG.doReplace) self.mainForm.addRow( @@ -913,7 +655,7 @@ class GuiPreferencesAutomation(QWidget): ) # Auto-Replace Double Quotes - self.doReplaceDQuote = NSwitch() + self.doReplaceDQuote = NSwitch(self) self.doReplaceDQuote.setChecked(CONFIG.doReplaceDQuote) self.doReplaceDQuote.setEnabled(CONFIG.doReplace) self.mainForm.addRow( @@ -923,7 +665,7 @@ class GuiPreferencesAutomation(QWidget): ) # Auto-Replace Hyphens - self.doReplaceDash = NSwitch() + self.doReplaceDash = NSwitch(self) self.doReplaceDash.setChecked(CONFIG.doReplaceDash) self.doReplaceDash.setEnabled(CONFIG.doReplace) self.mainForm.addRow( @@ -933,7 +675,7 @@ class GuiPreferencesAutomation(QWidget): ) # Auto-Replace Dots - self.doReplaceDots = NSwitch() + self.doReplaceDots = NSwitch(self) self.doReplaceDots.setChecked(CONFIG.doReplaceDots) self.doReplaceDots.setEnabled(CONFIG.doReplace) self.mainForm.addRow( @@ -942,13 +684,10 @@ class GuiPreferencesAutomation(QWidget): self.tr("Three consecutive dots become ellipsis.") ) - # Automatic Padding - # ================= - self.mainForm.addGroupLabel(self.tr("Automatic Padding")) - # Pad Before self.fmtPadBefore = QLineEdit(self) self.fmtPadBefore.setMaxLength(32) + self.fmtPadBefore.setMaximumWidth(boxWidth) self.fmtPadBefore.setText(CONFIG.fmtPadBefore) self.mainForm.addRow( self.tr("Insert non-breaking space before"), @@ -959,6 +698,7 @@ class GuiPreferencesAutomation(QWidget): # Pad After self.fmtPadAfter = QLineEdit(self) self.fmtPadAfter.setMaxLength(32) + self.fmtPadAfter.setMaximumWidth(boxWidth) self.fmtPadAfter.setText(CONFIG.fmtPadAfter) self.mainForm.addRow( self.tr("Insert non-breaking space after"), @@ -967,7 +707,7 @@ class GuiPreferencesAutomation(QWidget): ) # Use Thin Space - self.fmtPadThin = NSwitch() + self.fmtPadThin = NSwitch(self) self.fmtPadThin.setChecked(CONFIG.fmtPadThin) self.fmtPadThin.setEnabled(CONFIG.doReplace) self.mainForm.addRow( @@ -976,61 +716,16 @@ class GuiPreferencesAutomation(QWidget): self.tr("Inserts a thin space instead of a regular space.") ) - return - - def saveValues(self) -> None: - """Save the values set for this tab.""" - # Automatic Features - CONFIG.autoSelect = self.autoSelect.isChecked() - CONFIG.doReplace = self.doReplace.isChecked() - - # Replace as You Type - CONFIG.doReplaceSQuote = self.doReplaceSQuote.isChecked() - CONFIG.doReplaceDQuote = self.doReplaceDQuote.isChecked() - CONFIG.doReplaceDash = self.doReplaceDash.isChecked() - CONFIG.doReplaceDots = self.doReplaceDots.isChecked() - - # Automatic Padding - CONFIG.fmtPadBefore = self.fmtPadBefore.text().strip() - CONFIG.fmtPadAfter = self.fmtPadAfter.text().strip() - CONFIG.fmtPadThin = self.fmtPadThin.isChecked() - - return - - ## - # Private Slots - ## - - @pyqtSlot(bool) - def _toggleAutoReplaceMain(self, state: bool) -> None: - """Toggle switches controlled by the auto replace switch.""" - self.doReplaceSQuote.setEnabled(state) - self.doReplaceDQuote.setEnabled(state) - self.doReplaceDash.setEnabled(state) - self.doReplaceDots.setEnabled(state) - self.fmtPadThin.setEnabled(state) - return - -# END Class GuiPreferencesAutomation - - -class GuiPreferencesQuotes(QWidget): - - def __init__(self, prefsGui: GuiPreferences) -> None: - super().__init__(parent=prefsGui) - - # The Form - self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(SHARED.theme.helpText) - self.setLayout(self.mainForm) - # Quotation Style # =============== - self.mainForm.addGroupLabel(self.tr("Quotation Style")) - qWidth = CONFIG.pxInt(40) - bWidth = int(2.5*SHARED.theme.getTextWidth("...")) + title = self.tr("Quotation Style") + section += 1 + self.sidebar.addButton(title, section) + self.mainForm.addGroupLabel(title, section) + self.quoteSym = {} + qWidth = CONFIG.pxInt(40) # Single Quote Style self.quoteSym["SO"] = QLineEdit(self) @@ -1039,8 +734,8 @@ class GuiPreferencesQuotes(QWidget): self.quoteSym["SO"].setFixedWidth(qWidth) self.quoteSym["SO"].setAlignment(Qt.AlignCenter) self.quoteSym["SO"].setText(CONFIG.fmtSQuoteOpen) - self.btnSingleStyleO = QPushButton("...", self) - self.btnSingleStyleO.setMaximumWidth(bWidth) + self.btnSingleStyleO = QToolButton(self) + self.btnSingleStyleO.setIcon(mIcon) self.btnSingleStyleO.clicked.connect(lambda: self._getQuote("SO")) self.mainForm.addRow( self.tr("Single quote open style"), @@ -1055,8 +750,8 @@ class GuiPreferencesQuotes(QWidget): self.quoteSym["SC"].setFixedWidth(qWidth) self.quoteSym["SC"].setAlignment(Qt.AlignCenter) self.quoteSym["SC"].setText(CONFIG.fmtSQuoteClose) - self.btnSingleStyleC = QPushButton("...", self) - self.btnSingleStyleC.setMaximumWidth(bWidth) + self.btnSingleStyleC = QToolButton(self) + self.btnSingleStyleC.setIcon(mIcon) self.btnSingleStyleC.clicked.connect(lambda: self._getQuote("SC")) self.mainForm.addRow( self.tr("Single quote close style"), @@ -1072,8 +767,8 @@ class GuiPreferencesQuotes(QWidget): self.quoteSym["DO"].setFixedWidth(qWidth) self.quoteSym["DO"].setAlignment(Qt.AlignCenter) self.quoteSym["DO"].setText(CONFIG.fmtDQuoteOpen) - self.btnDoubleStyleO = QPushButton("...", self) - self.btnDoubleStyleO.setMaximumWidth(bWidth) + self.btnDoubleStyleO = QToolButton(self) + self.btnDoubleStyleO.setIcon(mIcon) self.btnDoubleStyleO.clicked.connect(lambda: self._getQuote("DO")) self.mainForm.addRow( self.tr("Double quote open style"), @@ -1088,8 +783,8 @@ class GuiPreferencesQuotes(QWidget): self.quoteSym["DC"].setFixedWidth(qWidth) self.quoteSym["DC"].setAlignment(Qt.AlignCenter) self.quoteSym["DC"].setText(CONFIG.fmtDQuoteClose) - self.btnDoubleStyleC = QPushButton("...", self) - self.btnDoubleStyleC.setMaximumWidth(bWidth) + self.btnDoubleStyleC = QToolButton(self) + self.btnDoubleStyleC.setIcon(mIcon) self.btnDoubleStyleC.clicked.connect(lambda: self._getQuote("DC")) self.mainForm.addRow( self.tr("Double quote close style"), @@ -1098,27 +793,240 @@ class GuiPreferencesQuotes(QWidget): button=self.btnDoubleStyleC ) + self.mainForm.finalise() + self.sidebar.setSelected(1) + return - def saveValues(self) -> None: - """Save the values set for this tab.""" - # Quotation Style - CONFIG.fmtSQuoteOpen = self.quoteSym["SO"].text() - CONFIG.fmtSQuoteClose = self.quoteSym["SC"].text() - CONFIG.fmtDQuoteOpen = self.quoteSym["DO"].text() - CONFIG.fmtDQuoteClose = self.quoteSym["DC"].text() + ## + # Events + ## + + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the close event and perform cleanup.""" + logger.debug("Close: GuiPreferences") + self._saveWindowSize() + event.accept() + qApp.processEvents() + self.done(nwConst.DLG_FINISHED) + self.deleteLater() + return + + def keyPressEvent(self, event: QKeyEvent) -> None: + """Overload keyPressEvent to block enter key to save.""" + if event.matches(QKeySequence.StandardKey.Cancel): + self.close() + event.ignore() + return + + ## + # 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.ButtonRole.ApplyRole: + self._saveValues() + elif role == QDialogButtonBox.ButtonRole.AcceptRole: + self._saveValues() + self.close() + elif role == QDialogButtonBox.ButtonRole.RejectRole: + self.close() + return + + @pyqtSlot(int) + def _sidebarClicked(self, section: int) -> None: + """Process a user request to switch page.""" + self.mainForm.scrollToSection(section) + return + + @pyqtSlot() + def _gotoSearch(self) -> None: + """Go to the setting indicated by the search text.""" + self.mainForm.scrollToLabel(self.searchText.text().strip()) + return + + @pyqtSlot() + def _selectGuiFont(self) -> None: + """Open the QFontDialog and set a font for the font style.""" + current = QFont() + current.setFamily(CONFIG.guiFont) + current.setPointSize(CONFIG.guiFontSize) + font, status = QFontDialog.getFont(current, self) + if status: + self.guiFont.setText(font.family()) + self.guiFontSize.setValue(font.pointSize()) + return + + @pyqtSlot() + def _selectTextFont(self): + """Open the QFontDialog and set a font for the font style.""" + current = QFont() + current.setFamily(CONFIG.textFont) + current.setPointSize(CONFIG.textSize) + font, status = QFontDialog.getFont(current, self) + if status: + self.textFont.setText(font.family()) + self.textSize.setValue(font.pointSize()) + return + + @pyqtSlot() + def _backupFolder(self) -> None: + """Open a dialog to select the backup folder.""" + if path := QFileDialog.getExistingDirectory( + self, self.tr("Backup Directory"), str(self.backupPath) or "", + options=QFileDialog.ShowDirsOnly + ): + self.backupPath = path + self.mainForm.setHelpText("backupPath", self.tr("Path: {0}").format(path)) + return + + @pyqtSlot(bool) + def _toggledBackupOnClose(self, state: bool) -> None: + """Toggle switch that depends on the backup on close switch.""" + self.askBeforeBackup.setEnabled(state) + return + + @pyqtSlot(bool) + def _toggleHighlightQuotes(self, state: bool) -> None: + """Toggle switches controlled by the highlight quotes switch.""" + self.allowOpenSQuote.setEnabled(state) + self.allowOpenDQuote.setEnabled(state) + return + + @pyqtSlot(bool) + def _toggleAutoReplaceMain(self, state: bool) -> None: + """Toggle switches controlled by the auto replace switch.""" + self.doReplaceSQuote.setEnabled(state) + self.doReplaceDQuote.setEnabled(state) + self.doReplaceDash.setEnabled(state) + self.doReplaceDots.setEnabled(state) + self.fmtPadThin.setEnabled(state) + return + + def _getQuote(self, qType: str) -> None: + """Dialog for single quote open.""" + quote, status = GuiQuoteSelect.getQuote(self, current=self.quoteSym[qType].text()) + if status: + self.quoteSym[qType].setText(quote) return ## # Internal Functions ## - def _getQuote(self, qType: str) -> None: - """Dialog for single quote open.""" - qtBox = GuiQuoteSelect(self, currentQuote=self.quoteSym[qType].text()) - if qtBox.exec_() == QDialog.Accepted: - self.quoteSym[qType].setText(qtBox.selectedQuote) + def _saveWindowSize(self) -> None: + """Save the dialog window size.""" + CONFIG.setPreferencesWinSize(self.width(), self.height()) + return + + def _saveValues(self) -> None: + """Save the values set in the form.""" + updateTheme = False + needsRestart = False + updateSyntax = False + refreshTree = False + + # Appearance + guiLocale = self.guiLocale.currentData() + guiTheme = self.guiTheme.currentData() + guiFont = self.guiFont.text() + guiFontSize = self.guiFontSize.value() + + updateTheme |= CONFIG.guiTheme != guiTheme + needsRestart |= CONFIG.guiLocale != guiLocale + needsRestart |= CONFIG.guiFont != guiFont + needsRestart |= CONFIG.guiFontSize != guiFontSize + + CONFIG.guiLocale = guiLocale + CONFIG.guiTheme = guiTheme + CONFIG.guiFont = guiFont + CONFIG.guiFontSize = guiFontSize + CONFIG.hideVScroll = self.hideVScroll.isChecked() + CONFIG.hideHScroll = self.hideHScroll.isChecked() + + # Document Style + guiSyntax = self.guiSyntax.currentData() + emphLabels = self.emphLabels.isChecked() + + updateSyntax |= CONFIG.guiSyntax != guiSyntax + refreshTree |= CONFIG.emphLabels != emphLabels + + CONFIG.guiSyntax = guiSyntax + CONFIG.emphLabels = emphLabels + CONFIG.showFullPath = self.showFullPath.isChecked() + CONFIG.incNotesWCount = self.incNotesWCount.isChecked() + CONFIG.setTextFont(self.textFont.text(), self.textSize.value()) + + # Auto Save + CONFIG.autoSaveDoc = self.autoSaveDoc.value() + CONFIG.autoSaveProj = self.autoSaveProj.value() + + # Project Backup + CONFIG.setBackupPath(self.backupPath) + CONFIG.backupOnClose = self.backupOnClose.isChecked() + CONFIG.askBeforeBackup = self.askBeforeBackup.isChecked() + + # Session Timer + CONFIG.stopWhenIdle = self.stopWhenIdle.isChecked() + CONFIG.userIdleTime = round(self.userIdleTime.value() * 60) + + # Text Flow + CONFIG.textWidth = self.textWidth.value() + CONFIG.focusWidth = self.focusWidth.value() + CONFIG.hideFocusFooter = self.hideFocusFooter.isChecked() + CONFIG.doJustify = self.doJustify.isChecked() + CONFIG.textMargin = self.textMargin.value() + CONFIG.tabWidth = self.tabWidth.value() + + # Text Editing + CONFIG.spellLanguage = self.spellLanguage.currentData() + CONFIG.autoSelect = self.autoSelect.isChecked() + CONFIG.showTabsNSpaces = self.showTabsNSpaces.isChecked() + CONFIG.showLineEndings = self.showLineEndings.isChecked() + + # Editor Scrolling + CONFIG.autoScroll = self.autoScroll.isChecked() + CONFIG.autoScrollPos = self.autoScrollPos.value() + CONFIG.scrollPastEnd = self.scrollPastEnd.isChecked() + + # Text Highlighting + highlightQuotes = self.highlightQuotes.isChecked() + highlightEmph = self.highlightEmph.isChecked() + showMultiSpaces = self.showMultiSpaces.isChecked() + + updateSyntax |= CONFIG.highlightQuotes != highlightQuotes + updateSyntax |= CONFIG.highlightEmph != highlightEmph + updateSyntax |= CONFIG.showMultiSpaces != showMultiSpaces + + CONFIG.highlightQuotes = highlightQuotes + CONFIG.highlightEmph = highlightEmph + CONFIG.showMultiSpaces = showMultiSpaces + CONFIG.allowOpenSQuote = self.allowOpenSQuote.isChecked() + CONFIG.allowOpenDQuote = self.allowOpenDQuote.isChecked() + + # Text Automation + CONFIG.doReplace = self.doReplace.isChecked() + CONFIG.doReplaceSQuote = self.doReplaceSQuote.isChecked() + CONFIG.doReplaceDQuote = self.doReplaceDQuote.isChecked() + CONFIG.doReplaceDash = self.doReplaceDash.isChecked() + CONFIG.doReplaceDots = self.doReplaceDots.isChecked() + CONFIG.fmtPadBefore = self.fmtPadBefore.text().strip() + CONFIG.fmtPadAfter = self.fmtPadAfter.text().strip() + CONFIG.fmtPadThin = self.fmtPadThin.isChecked() + + # Quotation Style + CONFIG.fmtSQuoteOpen = self.quoteSym["SO"].text() + CONFIG.fmtSQuoteClose = self.quoteSym["SC"].text() + CONFIG.fmtDQuoteOpen = self.quoteSym["DO"].text() + CONFIG.fmtDQuoteClose = self.quoteSym["DC"].text() + + # Finalise + CONFIG.saveConfig() + self.newPreferencesReady.emit(needsRestart, refreshTree, updateTheme, updateSyntax) return -# END Class GuiPreferencesQuotes +# END Class GuiPreferences diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index db32cc23..4fd81c59 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -25,7 +25,7 @@ from __future__ import annotations import logging -from PyQt5.QtGui import QCloseEvent, QFontMetrics +from PyQt5.QtGui import QFontMetrics from PyQt5.QtCore import QSize, Qt, pyqtSlot from PyQt5.QtWidgets import ( QDialog, QDialogButtonBox, QFrame, QHBoxLayout, QLabel, QListWidget, @@ -40,11 +40,11 @@ logger = logging.getLogger(__name__) class GuiQuoteSelect(QDialog): - selectedQuote = "" + _selected = "" D_KEY = Qt.ItemDataRole.UserRole - def __init__(self, parent: QWidget, currentQuote: str = '"') -> None: + def __init__(self, parent: QWidget, current: str = '"') -> None: super().__init__(parent=parent) logger.debug("Create: GuiQuoteSelect") @@ -54,7 +54,7 @@ class GuiQuoteSelect(QDialog): self.innerBox = QHBoxLayout() self.labelBox = QVBoxLayout() - self.selectedQuote = currentQuote + self._selected = current qMetrics = QFontMetrics(self.font()) pxW = 7*qMetrics.boundingRectChar("M").width() @@ -65,7 +65,7 @@ class GuiQuoteSelect(QDialog): lblFont.setPointSizeF(4*lblFont.pointSizeF()) # Preview Label - self.previewLabel = QLabel(currentQuote) + self.previewLabel = QLabel(current) self.previewLabel.setFont(lblFont) self.previewLabel.setFixedSize(QSize(pxW, pxH)) self.previewLabel.setAlignment(Qt.AlignCenter) @@ -82,7 +82,7 @@ class GuiQuoteSelect(QDialog): qtItem = QListWidgetItem(theText) qtItem.setData(self.D_KEY, sKey) self.listBox.addItem(qtItem) - if sKey == currentQuote: + if sKey == current: self.listBox.setCurrentItem(qtItem) self.listBox.setMinimumWidth(minSize + CONFIG.pxInt(40)) @@ -113,15 +113,20 @@ class GuiQuoteSelect(QDialog): logger.debug("Delete: GuiQuoteSelect") return - ## - # Events - ## + @property + def selectedQuote(self) -> str: + """Return the selected quote symbol.""" + return self._selected - def closeEvent(self, event: QCloseEvent) -> None: - """Capture the close event and perform cleanup.""" - event.accept() - self.deleteLater() - return + @classmethod + def getQuote(cls, parent: QWidget, current: str = "") -> tuple[str, bool]: + """Pop the dialog and return the result.""" + cls = GuiQuoteSelect(parent, current=current) + cls.exec_() + quote = cls._selected + accepted = cls.result() == QDialog.DialogCode.Accepted + cls.deleteLater() + return quote, accepted ## # Private Slots @@ -130,11 +135,10 @@ class GuiQuoteSelect(QDialog): @pyqtSlot() def _selectedSymbol(self) -> None: """Update the preview label and the selected quote style.""" - selItems = self.listBox.selectedItems() - if selItems: - theSymbol = selItems[0].data(self.D_KEY) - self.previewLabel.setText(theSymbol) - self.selectedQuote = theSymbol + if items := self.listBox.selectedItems(): + quote = items[0].data(self.D_KEY) + self.previewLabel.setText(quote) + self._selected = quote return # END Class GuiQuoteSelect diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 687dc715..d714cf00 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -3,7 +3,9 @@ novelWriter – Custom Widget: Config Layout ========================================== File History: -Created: 2020-05-03 [0.4.5] +Created: 2020-05-03 [0.4.5] NConfigLayout, NHelpLabel +Created: 2023-05-23 [2.1b1] NSimpleLayout +Created: 2024-01-08 [2.3b1] NScrollableForm This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen @@ -26,7 +28,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 +39,133 @@ 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._first = True + + self._sections: dict[int, QLabel] = {} + self._editable: dict[str, NHelpLabel] = {} + self._index: dict[str, QWidget] = {} + + 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 + + ## + # Properties + ## + + @property + def labels(self) -> list[str]: + return list(self._index.keys()) + + ## + # Setters + ## + + 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 + + ## + # Methods + ## + + def scrollToSection(self, identifier: int) -> 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 scrollToLabel(self, label: str) -> None: + """Scroll to the requested label.""" + if label in self._index: + yPos = self._index[label].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) + if not self._first: + self._layout.addSpacing(5*hM) + self._layout.addWidget(qLabel) + self._sections[identifier] = qLabel + self._first = False + 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 form.""" + row = QHBoxLayout() + row.setSpacing(CONFIG.pxInt(4)) + + mPx = CONFIG.pxInt(12) + qLabel = QLabel(label, self) + qLabel.setIndent(mPx) + qLabel.setBuddy(widget) + + if helpText: + qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale) + qHelp.setIndent(mPx) + 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.addSpacing(mPx) + row.addWidget(widget) + + if isinstance(unit, str): + row.addWidget(QLabel(unit, self)) + elif isinstance(button, QAbstractButton): + row.addWidget(button) + + self._layout.addLayout(row) + self._index[label.strip()] = widget + self._first = False + + return + + def finalise(self) -> None: + """Finalise the layout when the form is built.""" + self._layout.addSpacing(CONFIG.pxInt(20)) + self._layout.addStretch(1) + return + +# END Class NScrollableForm + + class NConfigLayout(QGridLayout): def __init__(self) -> None: @@ -58,11 +187,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: diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index 0141d90a..2f417b44 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -26,7 +26,7 @@ along with this program. If not, see . from __future__ import annotations from PyQt5.QtGui import QColor, QPaintEvent, QPainter, QPolygon -from PyQt5.QtCore import QPoint, QRectF, Qt, pyqtSignal, pyqtSlot +from PyQt5.QtCore import QPoint, QRectF, QSize, Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QAbstractButton, QAction, QButtonGroup, QLabel, QSizePolicy, QStyle, QStyleOptionToolButton, QToolBar, QToolButton, QWidget @@ -63,6 +63,10 @@ class NPagedSideBar(QToolBar): return + def button(self, buttonId: int) -> _NPagedToolButton: + """Return a specific button.""" + return self._buttons[buttonId] + def setLabelColor(self, color: list | QColor) -> None: """Set the text color for the labels.""" self._labelCol = color if isinstance(color, QColor) else QColor(*color) @@ -136,6 +140,10 @@ class _NPagedToolButton(QToolButton): return + def sizeHint(self) -> QSize: + """Return a size hint that includes the arrow.""" + return super().sizeHint() + QSize(4*self._aH, 0) + def paintEvent(self, event: QPaintEvent) -> None: """Overload the paint event to draw a simple, left aligned text label, with a highlight when selected and a transparent base diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py index cb7177ce..54db8914 100644 --- a/novelwriter/extensions/switch.py +++ b/novelwriter/extensions/switch.py @@ -24,31 +24,22 @@ along with this program. If not, see . from __future__ import annotations from PyQt5.QtGui import QMouseEvent, QPainter, QPaintEvent, QResizeEvent -from PyQt5.QtCore import QEvent, QPropertyAnimation, QRectF, Qt, pyqtProperty +from PyQt5.QtCore import QEvent, QPropertyAnimation, Qt, pyqtProperty from PyQt5.QtWidgets import QAbstractButton, QSizePolicy, QWidget from novelwriter import CONFIG -from novelwriter.constants import nwUnicode class NSwitch(QAbstractButton): - def __init__(self, parent: QWidget | None = None, - width: int | None = None, height: int | None = None) -> None: + __slots__ = ("_xW", "_xH", "_xR", "_rB", "_rH", "_rR", "_offset") + + def __init__(self, parent: QWidget | None = None, width: int = 0, height: int = 0) -> None: super().__init__(parent=parent) - if width is None: - self._xW = CONFIG.pxInt(40) - else: - self._xW = width - - if height is None: - self._xH = CONFIG.pxInt(20) - else: - self._xH = height - + self._xW = width or CONFIG.pxInt(40) + self._xH = height or CONFIG.pxInt(20) self._xR = int(self._xH*0.5) - self._xT = int(self._xH*0.6) self._rB = int(CONFIG.guiScale*2) self._rH = self._xH - 2*self._rB self._rR = self._xR - self._rB @@ -82,10 +73,7 @@ class NSwitch(QAbstractButton): def setChecked(self, checked: bool) -> None: """Overload setChecked to also alter the offset.""" super().setChecked(checked) - if checked: - self._offset = self._xW - self._xR - else: - self._offset = self._xR + self._offset = (self._xW - self._xR) if checked else self._xR return ## @@ -95,53 +83,36 @@ class NSwitch(QAbstractButton): def resizeEvent(self, event: QResizeEvent) -> None: """Overload resize to ensure correct offset.""" super().resizeEvent(event) - if self.isChecked(): - self._offset = self._xW - self._xR - else: - self._offset = self._xR + self._offset = (self._xW - self._xR) if self.isChecked() else self._xR return def paintEvent(self, event: QPaintEvent) -> None: """Drawing the switch itself.""" - qPaint = QPainter(self) - qPaint.setRenderHint(QPainter.Antialiasing, True) - qPaint.setPen(Qt.NoPen) + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing, True) + painter.setPen(Qt.NoPen) - qPalette = self.palette() + palette = self.palette() if self.isChecked(): - trackBrush = qPalette.highlight() - thumbBrush = qPalette.highlightedText() - textColor = qPalette.highlight().color() - thumbText = nwUnicode.U_CHECK + trackBrush = palette.highlight() + thumbBrush = palette.highlightedText() else: - trackBrush = qPalette.dark() - thumbBrush = qPalette.light() - textColor = qPalette.dark().color() - thumbText = nwUnicode.U_CROSS + trackBrush = palette.dark() + thumbBrush = palette.light() if self.isEnabled(): trackOpacity = 1.0 else: trackOpacity = 0.6 - trackBrush = qPalette.shadow() - thumbBrush = qPalette.mid() - textColor = qPalette.shadow().color() + trackBrush = palette.shadow() + thumbBrush = palette.mid() - qPaint.setBrush(trackBrush) - qPaint.setOpacity(trackOpacity) - qPaint.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR) + painter.setBrush(trackBrush) + painter.setOpacity(trackOpacity) + painter.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR) - qPaint.setBrush(thumbBrush) - qPaint.drawEllipse(self._offset - self._rR, self._rB, self._rH, self._rH) - - font = qPaint.font() - font.setPixelSize(self._xT) - qPaint.setPen(textColor) - qPaint.setFont(font) - qPaint.drawText( - QRectF(self._offset - self._rR, self._rB, self._rH, self._rH), - Qt.AlignCenter, thumbText - ) + painter.setBrush(thumbBrush) + painter.drawEllipse(self._offset - self._rR, self._rB, self._rH, self._rH) return @@ -149,14 +120,11 @@ class NSwitch(QAbstractButton): """Animate the switch on mouse release.""" super().mouseReleaseEvent(event) if event.button() == Qt.LeftButton: - doAnim = QPropertyAnimation(self, b"offset", self) - doAnim.setDuration(120) - doAnim.setStartValue(self._offset) - if self.isChecked(): - doAnim.setEndValue(self._xW - self._xR) - else: - doAnim.setEndValue(self._xR) - doAnim.start() + anim = QPropertyAnimation(self, b"offset", self) + anim.setDuration(120) + anim.setStartValue(self._offset) + anim.setEndValue((self._xW - self._xR) if self.isChecked() else self._xR) + anim.start() return def enterEvent(self, event: QEvent) -> None: diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 7cecb546..c7554591 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -187,13 +187,12 @@ class GuiDocEditor(QPlainTextEdit): # Set Up Document Word Counter self.wcTimerDoc = QTimer() self.wcTimerDoc.timeout.connect(self._runDocCounter) + self.wcTimerDoc.setInterval(5000) self.wCounterDoc = BackgroundWordCounter(self) self.wCounterDoc.setAutoDelete(False) self.wCounterDoc.signals.countsReady.connect(self._updateDocCounts) - self.wcInterval = CONFIG.wordCountTimer - # Set Up Selection Word Counter self.wcTimerSel = QTimer() self.wcTimerSel.timeout.connect(self._runSelCounter) @@ -320,10 +319,10 @@ class GuiDocEditor(QPlainTextEdit): SHARED.updateSpellCheckLanguage() # Set font - textFont = QFont() - textFont.setFamily(CONFIG.textFont) - textFont.setPointSize(CONFIG.textSize) - self.setFont(textFont) + font = QFont() + font.setFamily(CONFIG.textFont) + font.setPointSize(CONFIG.textSize) + self.setFont(font) # Set default text margins # Due to cursor visibility, a part of the margin must be @@ -359,17 +358,14 @@ class GuiDocEditor(QPlainTextEdit): # Refresh the tab stops self.setTabStopDistance(CONFIG.getTabWidth()) - # Configure word count timer - self.wcInterval = CONFIG.wordCountTimer - self.wcTimerDoc.setInterval(int(self.wcInterval*1000)) - - # If we have a document open, we should reload it in case the + # If we have a document open, we should refresh it in case the # font changed, otherwise we just clear the editor entirely, # which makes it read only. - if self._docHandle is None: - self.clearEditor() - else: + if self._docHandle: self._qDocument.syntaxHighlighter.rehighlight() + self.docHeader.setTitleFromHandle(self._docHandle) + else: + self.clearEditor() return @@ -1190,7 +1186,7 @@ class GuiDocEditor(QPlainTextEdit): logger.debug("Word counter is busy") return - if time() - self._lastEdit < 5.0 * self.wcInterval: + if time() - self._lastEdit < 25.0: logger.debug("Running word counter") SHARED.runInThreadPool(self.wCounterDoc) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index dc0562d3..5a066ccc 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -142,10 +142,10 @@ class GuiDocViewer(QTextBrowser): self._makeStyleSheet() # Set Font - textFont = QFont() - textFont.setFamily(CONFIG.textFont) - textFont.setPointSize(CONFIG.textSize) - self.setFont(textFont) + font = QFont() + font.setFamily(CONFIG.textFont) + font.setPointSize(CONFIG.textSize) + self.setFont(font) # Set the widget colours to match syntax theme mainPalette = self.palette() @@ -164,10 +164,10 @@ class GuiDocViewer(QTextBrowser): # Set default text margins self.document().setDocumentMargin(0) - theOpt = QTextOption() + options = QTextOption() if CONFIG.doJustify: - theOpt.setAlignment(Qt.AlignmentFlag.AlignJustify) - self.document().setDefaultTextOption(theOpt) + options.setAlignment(Qt.AlignmentFlag.AlignJustify) + self.document().setDefaultTextOption(options) # Scroll bars if CONFIG.hideVScroll: diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 65642ea4..2e3e8b94 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -466,7 +466,7 @@ class GuiIcons: # General Button Icons "add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross", - "document", "down", "edit", "export", "forward", "maximise", "menu", "minimise", + "document", "down", "edit", "export", "forward", "maximise", "menu", "minimise", "more", "noncheckable", "panel", "refresh", "remove", "revert", "search_replace", "search", "settings", "star", "unchecked", "up", "view", diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index 6f064701..32a9883e 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -57,7 +57,6 @@ spellcheck = en showtabsnspaces = False showlineendings = False showmultispaces = True -wordcounttimer = 5.0 incnoteswcount = True showfullpath = True highlightquotes = True diff --git a/tests/reference/guiPreferences_novelwriter.conf b/tests/reference/guiPreferences_novelwriter.conf deleted file mode 100644 index b291bf4a..00000000 --- a/tests/reference/guiPreferences_novelwriter.conf +++ /dev/null @@ -1,82 +0,0 @@ -[Meta] -timestamp = 2023-12-29 14:09:13 - -[Main] -theme = default -syntax = default_light -font = Cantarell -fontsize = 12 -localisation = en_GB -hidevscroll = True -hidehscroll = True -lastnotes = 0x0 -lastpath = - -[Sizes] -mainwindow = 1200, 650 -welcome = 800, 500 -preferences = 713, 614 -mainpane = 300, 800 -viewpane = 500, 150 -outlinepane = 500, 150 - -[Project] -autosaveproject = 40 -autosavedoc = 20 -emphlabels = True -backuppath = some/dir -backuponclose = True -askbeforebackup = True - -[Editor] -textfont = None -textsize = 13 -width = 700 -margin = 45 -tabwidth = 45 -focuswidth = 900 -hidefocusfooter = True -justify = True -autoselect = False -autoreplace = False -repsquotes = True -repdquotes = True -repdash = True -repdots = True -autoscroll = True -autoscrollpos = 30 -scrollpastend = True -fmtsquoteopen = ‘ -fmtsquoteclose = ’ -fmtdquoteopen = “ -fmtdquoteclose = ” -fmtpadbefore = -fmtpadafter = -fmtpadthin = False -spellcheck = en -showtabsnspaces = True -showlineendings = True -showmultispaces = True -wordcounttimer = 5.0 -incnoteswcount = True -showfullpath = False -highlightquotes = False -allowopensquote = False -allowopendquote = True -highlightemph = False -stopwhenidle = True -useridletime = 300 - -[State] -showviewerpanel = True -showedittoolbar = False -useshortcodes = False -viewcomments = True -viewsynopsis = True -searchcase = False -searchword = False -searchregex = False -searchloop = False -searchnextfile = False -searchmatchcap = False - diff --git a/tests/test_dialogs/test_dlg_dialogs.py b/tests/test_dialogs/test_dlg_dialogs.py index 0aa43e39..94d488ed 100644 --- a/tests/test_dialogs/test_dlg_dialogs.py +++ b/tests/test_dialogs/test_dlg_dialogs.py @@ -31,8 +31,10 @@ from novelwriter.dialogs.editlabel import GuiEditLabel @pytest.mark.gui -def testDlgOther_QuoteSelect(qtbot, nwGUI): +def testDlgOther_QuoteSelect(qtbot, monkeypatch, nwGUI): """Test the quote symbols dialog.""" + monkeypatch.setattr(GuiQuoteSelect, "exec_", lambda *a: None) + nwQuot = GuiQuoteSelect(nwGUI) nwQuot.show() @@ -41,16 +43,25 @@ def testDlgOther_QuoteSelect(qtbot, nwGUI): anItem = nwQuot.listBox.item(i) assert isinstance(anItem, QListWidgetItem) nwQuot.listBox.clearSelection() - nwQuot.listBox.setCurrentItem(anItem, QItemSelectionModel.Select) + nwQuot.listBox.setCurrentItem(anItem, QItemSelectionModel.SelectionFlag.Select) lastItem = anItem.text()[2] assert nwQuot.previewLabel.text() == lastItem nwQuot.accept() assert nwQuot.result() == QDialog.Accepted assert nwQuot.selectedQuote == lastItem + nwQuot.close() + + # Test Class Method + with monkeypatch.context() as mp: + mp.setattr(GuiQuoteSelect, "result", lambda *a: QDialog.DialogCode.Accepted) + assert GuiQuoteSelect.getQuote(nwGUI, current="X") == ("X", True) + + with monkeypatch.context() as mp: + mp.setattr(GuiQuoteSelect, "result", lambda *a: QDialog.DialogCode.Rejected) + assert GuiQuoteSelect.getQuote(nwGUI, current="X") == ("X", False) # qtbot.stop() - nwQuot.close() # END Test testDlgOther_QuoteSelect diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index a56f4978..c4c2e590 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -22,191 +22,389 @@ from __future__ import annotations import pytest -from shutil import copyfile - -from tools import cmpFiles, getGuiItem - -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import ( - QDialogButtonBox, QDialog, QAction, QFileDialog, QFontDialog -) +from PyQt5.QtGui import QFontDatabase, QKeyEvent +from PyQt5.QtCore import QEvent, Qt +from PyQt5.QtWidgets import QDialogButtonBox, QFileDialog, QFontDialog from novelwriter import CONFIG, SHARED -from novelwriter.dialogs.quotes import GuiQuoteSelect +from novelwriter.constants import nwConst, nwUnicode from novelwriter.dialogs.preferences import GuiPreferences +from novelwriter.dialogs.quotes import GuiQuoteSelect KEY_DELAY = 1 @pytest.mark.gui def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): - """Test the preferences dialog.""" - monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None) - monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted) + """Test the preferences dialog loading.""" monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) - nwGUI.mainMenu.aPreferences.activate(QAction.Trigger) - qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000) - nwPrefs = getGuiItem("GuiPreferences") - assert isinstance(nwPrefs, GuiPreferences) - nwPrefs.show() + # Load GUI with standard values + prefs = GuiPreferences(nwGUI) + prefs.show() - # General Settings - qtbot.wait(KEY_DELAY) - tabGeneral = nwPrefs.tabGeneral - nwPrefs._tabBox.setCurrentWidget(tabGeneral) + # Check Languages + languages = [prefs.guiLocale.itemData(i) for i in range(prefs.guiLocale.count())] + assert len(languages) > 0 + assert "en_GB" in languages - qtbot.wait(KEY_DELAY) - assert tabGeneral.showFullPath.isChecked() - qtbot.mouseClick(tabGeneral.showFullPath, Qt.LeftButton) - assert not tabGeneral.showFullPath.isChecked() + # Check GUI Themes + themes = [prefs.guiTheme.itemData(i) for i in range(prefs.guiTheme.count())] + assert len(themes) >= 5 + assert "default" in themes - qtbot.wait(KEY_DELAY) - assert not tabGeneral.hideVScroll.isChecked() - qtbot.mouseClick(tabGeneral.hideVScroll, Qt.LeftButton) - assert tabGeneral.hideVScroll.isChecked() + # Check GUI Syntax + syntax = [prefs.guiSyntax.itemData(i) for i in range(prefs.guiSyntax.count())] + assert len(syntax) >= 10 + assert "default_dark" in syntax + assert "default_light" in syntax - qtbot.wait(KEY_DELAY) - assert not tabGeneral.hideHScroll.isChecked() - qtbot.mouseClick(tabGeneral.hideHScroll, Qt.LeftButton) - assert tabGeneral.hideHScroll.isChecked() + # Check Spell Checking + spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())] + assert len(spelling) == 1 + assert spelling == ["en"] - # Check font button - monkeypatch.setattr(QFontDialog, "getFont", lambda font, obj: (font, True)) - qtbot.mouseClick(tabGeneral.fontButton, Qt.LeftButton) + prefs.close() - qtbot.wait(KEY_DELAY) - tabGeneral.guiFontSize.setValue(12) + # Check Fallback Values + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "hasEnchant", False) + prefs = GuiPreferences(nwGUI) + prefs.show() - # Projects Settings - qtbot.wait(KEY_DELAY) - tabProjects = nwPrefs.tabProjects - nwPrefs._tabBox.setCurrentWidget(tabProjects) - tabProjects.backupPath = "no/where" + # Check Spell Checking + spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())] + assert len(spelling) == 1 + assert spelling == [""] - qtbot.wait(KEY_DELAY) - assert not tabProjects.backupOnClose.isChecked() - qtbot.mouseClick(tabProjects.backupOnClose, Qt.LeftButton) - assert tabProjects.backupOnClose.isChecked() - - # Check Browse button - monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: "") - assert not tabProjects._backupFolder() - monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: "some/dir") - qtbot.mouseClick(tabProjects.backupGetPath, Qt.LeftButton) - - qtbot.wait(KEY_DELAY) - tabProjects.autoSaveDoc.setValue(20) - tabProjects.autoSaveProj.setValue(40) - - # Document Settings - qtbot.wait(KEY_DELAY) - tabDocs = nwPrefs.tabDocs - nwPrefs._tabBox.setCurrentWidget(tabDocs) - - qtbot.wait(KEY_DELAY) - qtbot.mouseClick(tabDocs.fontButton, Qt.LeftButton) - - qtbot.wait(KEY_DELAY) - tabDocs.textSize.setValue(13) - tabDocs.textWidth.setValue(700) - tabDocs.focusWidth.setValue(900) - tabDocs.textMargin.setValue(45) - tabDocs.tabWidth.setValue(45) - - qtbot.wait(KEY_DELAY) - assert not tabDocs.hideFocusFooter.isChecked() - qtbot.mouseClick(tabDocs.hideFocusFooter, Qt.LeftButton) - assert tabDocs.hideFocusFooter.isChecked() - - qtbot.wait(KEY_DELAY) - assert not tabDocs.doJustify.isChecked() - qtbot.mouseClick(tabDocs.doJustify, Qt.LeftButton) - assert tabDocs.doJustify.isChecked() - - # Editor Settings - qtbot.wait(KEY_DELAY) - tabEditor = nwPrefs.tabEditor - nwPrefs._tabBox.setCurrentWidget(tabEditor) - - qtbot.wait(KEY_DELAY) - assert not tabEditor.showTabsNSpaces.isChecked() - qtbot.mouseClick(tabEditor.showTabsNSpaces, Qt.LeftButton) - assert tabEditor.showTabsNSpaces.isChecked() - - qtbot.wait(KEY_DELAY) - assert not tabEditor.showLineEndings.isChecked() - qtbot.mouseClick(tabEditor.showLineEndings, Qt.LeftButton) - assert tabEditor.showLineEndings.isChecked() - - qtbot.wait(KEY_DELAY) - assert not tabEditor.autoScroll.isChecked() - qtbot.mouseClick(tabEditor.autoScroll, Qt.LeftButton) - assert tabEditor.autoScroll.isChecked() - - # Syntax Settings - qtbot.wait(KEY_DELAY) - tabSyntax = nwPrefs.tabSyntax - nwPrefs._tabBox.setCurrentWidget(tabSyntax) - - qtbot.wait(KEY_DELAY) - assert tabSyntax.highlightQuotes.isChecked() - qtbot.mouseClick(tabSyntax.highlightQuotes, Qt.LeftButton) - assert not tabSyntax.highlightQuotes.isChecked() - - qtbot.wait(KEY_DELAY) - assert tabSyntax.highlightEmph.isChecked() - qtbot.mouseClick(tabSyntax.highlightEmph, Qt.LeftButton) - assert not tabSyntax.highlightEmph.isChecked() - - # Automation Settings - qtbot.wait(KEY_DELAY) - tabAuto = nwPrefs.tabAuto - nwPrefs._tabBox.setCurrentWidget(tabAuto) - - qtbot.wait(KEY_DELAY) - assert tabAuto.autoSelect.isChecked() - qtbot.mouseClick(tabAuto.autoSelect, Qt.LeftButton) - assert not tabAuto.autoSelect.isChecked() - - qtbot.wait(KEY_DELAY) - assert tabAuto.doReplace.isChecked() - qtbot.mouseClick(tabAuto.doReplace, Qt.LeftButton) - assert not tabAuto.doReplace.isChecked() - - qtbot.wait(KEY_DELAY) - assert not tabAuto.doReplaceSQuote.isEnabled() - assert not tabAuto.doReplaceDQuote.isEnabled() - assert not tabAuto.doReplaceDash.isEnabled() - assert not tabAuto.doReplaceDots.isEnabled() - - # Quotation Style - qtbot.wait(KEY_DELAY) - tabQuote = nwPrefs.tabQuote - nwPrefs._tabBox.setCurrentWidget(tabQuote) - - monkeypatch.setattr(GuiQuoteSelect, "selectedQuote", "'") - monkeypatch.setattr(GuiQuoteSelect, "exec_", lambda *a: QDialog.Accepted) - qtbot.mouseClick(tabQuote.btnDoubleStyleC, Qt.LeftButton) - - # Save and Check Config - qtbot.mouseClick(nwPrefs.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) - - assert CONFIG.saveConfig() - projFile = tstPaths.cnfDir / "novelwriter.conf" - testFile = tstPaths.outDir / "guiPreferences_novelwriter.conf" - compFile = tstPaths.refDir / "guiPreferences_novelwriter.conf" - copyfile(projFile, testFile) - ignTuple = ( - "timestamp", "font", "lastnotes", "localisation", "geometry", - "preferences", "projcols", "mainpane", "docpane", "viewpane", - "outlinepane", "textfont", "textsize", "lastpath", "backuppath" - ) - assert cmpFiles(testFile, compFile, ignoreStart=ignTuple) - - # Clean up - nwGUI.closeMain() + prefs.close() # qtbot.stop() # END Test testDlgPreferences_Main + + +@pytest.mark.gui +def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI): + """Test the preferences dialog actions.""" + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) + monkeypatch.setattr(GuiPreferences, "deleteLater", lambda *a: None) + prefs = GuiPreferences(nwGUI) + prefs.show() + + # Check Navigation + vBar = prefs.mainForm.verticalScrollBar() + old = -1 + with qtbot.waitSignal(vBar.valueChanged) as value: + prefs.sidebar.button(0).click() + assert value.args[0] > old + old = value.args[0] + with qtbot.waitSignal(vBar.valueChanged) as value: + prefs.sidebar.button(1).click() + assert value.args[0] > old + old = value.args[0] + with qtbot.waitSignal(vBar.valueChanged) as value: + prefs.sidebar.button(2).click() + assert value.args[0] > old + old = value.args[0] + + # Check Search + prefs.searchText.setText("Display language") + with qtbot.waitSignal(vBar.valueChanged) as value: + prefs._gotoSearch() + assert value.args[0] < old + + # Check Apply Button + prefs.show() + with qtbot.waitSignal(prefs.newPreferencesReady) as signal: + prefs.buttonBox.button(QDialogButtonBox.StandardButton.Apply).click() + assert signal.args == [False, False, False, False] + + # Check Save Button + prefs.show() + with qtbot.waitSignal(prefs.newPreferencesReady) as signal: + with qtbot.waitSignal(prefs.finished) as status: + prefs.buttonBox.button(QDialogButtonBox.StandardButton.Save).click() + assert signal.args == [False, False, False, False] + assert status.args == [nwConst.DLG_FINISHED] + + # Check Close Button + prefs.show() + with qtbot.waitSignal(prefs.finished) as status: + prefs.buttonBox.button(QDialogButtonBox.StandardButton.Close).click() + assert status.args == [nwConst.DLG_FINISHED] + + # Close Using Escape Key + prefs.show() + with qtbot.waitSignal(prefs.finished) as status: + event = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Escape, Qt.KeyboardModifier.NoModifier) + prefs.keyPressEvent(event) + assert status.args == [nwConst.DLG_FINISHED] + + # qtbot.stop() + +# END Test testDlgPreferences_Actions + + +@pytest.mark.gui +def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, tstPaths): + """Test the preferences dialog settings.""" + spelling = [("en", "English [en]"), ("de", "Deutch [de]")] + languages = [("en_GB", "British English"), ("en_US", "US English")] + + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: spelling) + monkeypatch.setattr(CONFIG, "listLanguages", lambda *a: languages) + monkeypatch.setattr(GuiPreferences, "deleteLater", lambda *a: None) + + prefs = GuiPreferences(nwGUI) + prefs.show() + + # Mock Font + class MockFont: + + def family(self): + return "TestFont" + + def pointSize(self): + return 42 + + # Appearance + prefs.guiLocale.setCurrentIndex(prefs.guiLocale.findData("en_US")) + prefs.guiTheme.setCurrentIndex(prefs.guiTheme.findData("default_dark")) + with monkeypatch.context() as mp: + mp.setattr(QFontDialog, "getFont", lambda *a: (MockFont(), True)) + prefs.guiFontButton.click() + prefs.guiFontSize.stepDown() # Should change it to 41 + prefs.hideVScroll.setChecked(True) + prefs.hideHScroll.setChecked(True) + + assert CONFIG.guiLocale != "en_US" + assert CONFIG.guiTheme != "default_dark" + assert CONFIG.guiFont != "TestFont" + assert CONFIG.guiFontSize < 42 + assert CONFIG.hideVScroll is False + assert CONFIG.hideHScroll is False + + # Document Style + prefs.guiSyntax.setCurrentIndex(prefs.guiSyntax.findData("default_dark")) + with monkeypatch.context() as mp: + mp.setattr(QFontDialog, "getFont", lambda *a: (MockFont(), True)) + prefs.textFontButton.click() + prefs.textSize.stepDown() # Should change it to 41 + prefs.emphLabels.setChecked(False) + prefs.showFullPath.setChecked(False) + prefs.incNotesWCount.setChecked(False) + + assert CONFIG.guiSyntax != "default_dark" + assert CONFIG.textFont != "testFont" + assert CONFIG.textSize < 42 + assert CONFIG.emphLabels is True + assert CONFIG.showFullPath is True + assert CONFIG.incNotesWCount is True + + # Auto Save + prefs.autoSaveDoc.stepUp() + prefs.autoSaveProj.stepUp() + + assert CONFIG.autoSaveDoc == 30 + assert CONFIG.autoSaveProj == 60 + + # Project Backup + with monkeypatch.context() as mp: + mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: str(tstPaths.testDir)) + prefs.backupGetPath.click() + assert prefs.backupPath == str(tstPaths.testDir) + prefs.backupOnClose.setChecked(True) + assert prefs.askBeforeBackup.isEnabled() is True + prefs.askBeforeBackup.setChecked(False) + + assert CONFIG._backupPath != tstPaths.testDir + assert CONFIG.backupOnClose is False + assert CONFIG.askBeforeBackup is True + + # Session Timer + prefs.stopWhenIdle.setChecked(False) + prefs.userIdleTime.stepUp() + + assert CONFIG.stopWhenIdle is True + assert CONFIG.userIdleTime == 300 + + # Text Flow + prefs.textWidth.stepDown() + prefs.focusWidth.stepDown() + prefs.hideFocusFooter.setChecked(True) + prefs.doJustify.setChecked(True) + prefs.textMargin.stepUp() + prefs.tabWidth.stepUp() + + assert CONFIG.textWidth == 700 + assert CONFIG.focusWidth == 800 + assert CONFIG.hideFocusFooter is False + assert CONFIG.doJustify is False + assert CONFIG.textMargin == 40 + assert CONFIG.tabWidth == 40 + + # Text Editing + prefs.spellLanguage.setCurrentIndex(prefs.spellLanguage.findData("de")) + prefs.autoSelect.setChecked(False) + prefs.showTabsNSpaces.setChecked(True) + prefs.showLineEndings.setChecked(True) + + assert CONFIG.spellLanguage != "de" + assert CONFIG.autoSelect is True + assert CONFIG.showTabsNSpaces is False + assert CONFIG.showLineEndings is False + + # Editor Scrolling + prefs.scrollPastEnd.setChecked(False) + prefs.autoScroll.setChecked(True) + prefs.autoScrollPos.stepUp() + + assert CONFIG.scrollPastEnd is True + assert CONFIG.autoScroll is False + assert CONFIG.autoScrollPos == 30 + + # Text Highlighting + prefs.allowOpenSQuote.setChecked(True) + prefs.allowOpenDQuote.setChecked(False) + prefs.highlightQuotes.setChecked(False) + prefs.highlightEmph.setChecked(False) + prefs.showMultiSpaces.setChecked(False) + + assert prefs.allowOpenSQuote.isEnabled() is False + assert prefs.allowOpenDQuote.isEnabled() is False + + assert CONFIG.highlightQuotes is True + assert CONFIG.allowOpenSQuote is False + assert CONFIG.allowOpenDQuote is True + assert CONFIG.highlightEmph is True + assert CONFIG.showMultiSpaces is True + + # Text Automation + prefs.doReplaceSQuote.setChecked(False) + prefs.doReplaceDQuote.setChecked(False) + prefs.doReplaceDash.setChecked(False) + prefs.doReplaceDots.setChecked(False) + prefs.doReplace.setChecked(False) + prefs.fmtPadBefore.setText("!?:") + prefs.fmtPadAfter.setText("¡¿") + prefs.fmtPadThin.setChecked(True) + + assert prefs.doReplaceSQuote.isEnabled() is False + assert prefs.doReplaceDQuote.isEnabled() is False + assert prefs.doReplaceDash.isEnabled() is False + assert prefs.doReplaceDots.isEnabled() is False + assert prefs.fmtPadThin.isEnabled() is False + + assert CONFIG.doReplace is True + assert CONFIG.doReplaceSQuote is True + assert CONFIG.doReplaceDQuote is True + assert CONFIG.doReplaceDash is True + assert CONFIG.doReplaceDots is True + assert CONFIG.fmtPadBefore == "" + assert CONFIG.fmtPadAfter == "" + assert CONFIG.fmtPadThin is False + + # Quotation Style + with monkeypatch.context() as mp: + mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_LSAQUO, True)) + prefs.btnSingleStyleO.click() + with monkeypatch.context() as mp: + mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_RSAQUO, True)) + prefs.btnSingleStyleC.click() + with monkeypatch.context() as mp: + mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_LAQUO, True)) + prefs.btnDoubleStyleO.click() + with monkeypatch.context() as mp: + mp.setattr(GuiQuoteSelect, "getQuote", lambda *a, **k: (nwUnicode.U_RAQUO, True)) + prefs.btnDoubleStyleC.click() + + assert CONFIG.fmtSQuoteOpen == nwUnicode.U_LSQUO + assert CONFIG.fmtSQuoteClose == nwUnicode.U_RSQUO + assert CONFIG.fmtDQuoteOpen == nwUnicode.U_LDQUO + assert CONFIG.fmtDQuoteClose == nwUnicode.U_RDQUO + + # Save Settings + with monkeypatch.context() as mp: + mp.setattr(QFontDatabase, "families", lambda *a: ["TestFont"]) + with qtbot.waitSignal(prefs.newPreferencesReady) as signal: + prefs.buttonBox.button(QDialogButtonBox.StandardButton.Apply).click() + assert signal.args == [True, True, True, True] + + # Check Settings + # ============== + + # Appearance + assert CONFIG.guiLocale == "en_US" + assert CONFIG.guiTheme == "default_dark" + assert CONFIG.guiFont == "TestFont" + assert CONFIG.guiFontSize == 41 + assert CONFIG.hideVScroll is True + assert CONFIG.hideHScroll is True + + # Document Style + assert CONFIG.guiSyntax == "default_dark" + assert CONFIG.textFont == "TestFont" + assert CONFIG.textSize == 41 + assert CONFIG.emphLabels is False + assert CONFIG.showFullPath is False + assert CONFIG.incNotesWCount is False + + # Auto Save + assert CONFIG.autoSaveDoc == 31 + assert CONFIG.autoSaveProj == 61 + + # Project Backup + assert CONFIG._backupPath == tstPaths.testDir + assert CONFIG.backupOnClose is True + assert CONFIG.askBeforeBackup is False + + # Session Timer + assert CONFIG.stopWhenIdle is False + assert CONFIG.userIdleTime == 330 + + # Text Flow + assert CONFIG.textWidth == 690 + assert CONFIG.focusWidth == 790 + assert CONFIG.hideFocusFooter is True + assert CONFIG.doJustify is True + assert CONFIG.textMargin == 41 + assert CONFIG.tabWidth == 41 + + # Text Editing + assert CONFIG.spellLanguage == "de" + assert CONFIG.autoSelect is False + assert CONFIG.showTabsNSpaces is True + assert CONFIG.showLineEndings is True + + # Editor Scrolling + assert CONFIG.scrollPastEnd is False + assert CONFIG.autoScroll is True + assert CONFIG.autoScrollPos == 31 + + # Text Highlighting + assert CONFIG.highlightQuotes is False + assert CONFIG.allowOpenSQuote is True + assert CONFIG.allowOpenDQuote is False + assert CONFIG.highlightEmph is False + assert CONFIG.showMultiSpaces is False + + # Text Automation + assert CONFIG.doReplace is False + assert CONFIG.doReplaceSQuote is False + assert CONFIG.doReplaceDQuote is False + assert CONFIG.doReplaceDash is False + assert CONFIG.doReplaceDots is False + assert CONFIG.fmtPadBefore == "!?:" + assert CONFIG.fmtPadAfter == "¡¿" + assert CONFIG.fmtPadThin is True + + # Quotation Style + assert CONFIG.fmtSQuoteOpen == nwUnicode.U_LSAQUO + assert CONFIG.fmtSQuoteClose == nwUnicode.U_RSAQUO + assert CONFIG.fmtDQuoteOpen == nwUnicode.U_LAQUO + assert CONFIG.fmtDQuoteClose == nwUnicode.U_RAQUO + + # qtbot.stop() + +# END Test testDlgPreferences_Settings