diff --git a/nw/config.py b/nw/config.py index 05a30428..22fc6844 100644 --- a/nw/config.py +++ b/nw/config.py @@ -142,6 +142,8 @@ class Config: self.bigDocLimit = 800 # Size threshold for heavy editor features in kilobytes self.highlightQuotes = True # Highlight text in quotes + self.allowOpenSQuote = False # Allow open-ended single quotes + self.allowOpenDQuote = True # Allow open-ended double quotes self.highlightEmph = True # Add colour to text emphasis ## User-Selected Symbols @@ -520,6 +522,12 @@ class Config: self.highlightQuotes = self._parseLine( cnfParse, cnfSec, "highlightquotes", self.CNF_BOOL, self.highlightQuotes ) + self.allowOpenSQuote = self._parseLine( + cnfParse, cnfSec, "allowopensquote", self.CNF_BOOL, self.allowOpenSQuote + ) + self.allowOpenDQuote = self._parseLine( + cnfParse, cnfSec, "allowopendquote", self.CNF_BOOL, self.allowOpenDQuote + ) self.highlightEmph = self._parseLine( cnfParse, cnfSec, "highlightemph", self.CNF_BOOL, self.highlightEmph ) @@ -575,6 +583,15 @@ class Config: # Check Certain Values for None self.spellLanguage = self._checkNone(self.spellLanguage) + # If we're using straight quotes, disable auto-replace + if self.fmtSingleQuotes == ["'", "'"] and self.doReplaceSQuote: + logger.info("Using straight single quotes, so disabling auto-replace") + self.doReplaceSQuote = False + + if self.fmtDoubleQuotes == ["\"", "\""] and self.doReplaceDQuote: + logger.info("Using straight double quotes, so disabling auto-replace") + self.doReplaceDQuote = False + return True def saveConfig(self): @@ -651,6 +668,8 @@ class Config: cnfParse.set(cnfSec, "bigdoclimit", str(self.bigDocLimit)) cnfParse.set(cnfSec, "showfullpath", str(self.showFullPath)) cnfParse.set(cnfSec, "highlightquotes", str(self.highlightQuotes)) + cnfParse.set(cnfSec, "allowopensquote", str(self.allowOpenSQuote)) + cnfParse.set(cnfSec, "allowopendquote", str(self.allowOpenDQuote)) cnfParse.set(cnfSec, "highlightemph", str(self.highlightEmph)) ## Backup diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 8fa9220e..273fd5cd 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -143,15 +143,15 @@ class GuiDocEditor(QTextEdit): ) # Set Up Word Counter - self.wcInterval = self.mainConf.wordCountTimer self.wcTimer = QTimer() - self.wcTimer.setInterval(int(self.wcInterval*1000)) self.wcTimer.timeout.connect(self._runCounter) self.wCounter = BackgroundWordCounter(self) self.wCounter.setAutoDelete(False) self.wCounter.signals.countsReady.connect(self._updateCounts) + self.wcInterval = self.mainConf.wordCountTimer + self.initEditor() logger.debug("GuiDocEditor initialisation complete") @@ -258,6 +258,10 @@ class GuiDocEditor(QTextEdit): # Initialise the syntax highlighter self.hLight.initHighlighter() + # Configure word count timer + self.wcInterval = self.mainConf.wordCountTimer + self.wcTimer.setInterval(int(self.wcInterval*1000)) + # If we have a document open, we should reload it in case the # font changed, otherwise we just clear the editor entirely, # which makes it read only. @@ -1721,7 +1725,9 @@ class BackgroundWordCounter(QRunnable): ## END Class BackgroundWordCounter class BackgroundWordCounterSignals(QObject): - + """The QRunnable cannot emit a signal, so we need a simple QObject + to hold the word counter signal. + """ countsReady = pyqtSignal(int, int, int) # END Class BackgroundWordCounterSignals diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 81a4956f..620b8b74 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -147,22 +147,29 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Quoted Strings if self.mainConf.highlightQuotes: - fmtDO = self.mainConf.fmtDoubleQuotes[0] - fmtDC = self.mainConf.fmtDoubleQuotes[1] - fmtSO = self.mainConf.fmtSingleQuotes[0] - fmtSC = self.mainConf.fmtSingleQuotes[1] + fmtDbl = self.mainConf.fmtDoubleQuotes + fmtSng = self.mainConf.fmtSingleQuotes + + # Straight Quotes + if fmtDbl != ["\"", "\""]: + self.hRules.append(( + "(\\B\")(.*?)(\"\\B)", { + 0 : self.hStyles["dialogue1"], + } + )) + + # Double Quotes + dblEnd = "|$" if self.mainConf.allowOpenDQuote else "" self.hRules.append(( - "\\B\"(.*?)\"\\B", { - 0 : self.hStyles["dialogue1"], - } - )) - self.hRules.append(( - f"\\B{fmtDO:s}(.*?){fmtDC:s}\\B", { + f"(\\B{fmtDbl[0]})(.*?)({fmtDbl[1]}\\B{dblEnd})", { 0 : self.hStyles["dialogue2"], } )) + + # Single Quotes + sngEnd = "|$" if self.mainConf.allowOpenSQuote else "" self.hRules.append(( - f"\\B{fmtSO:s}(.*?){fmtSC:s}\\B", { + f"(\\B{fmtSng[0]})(.*?)({fmtSng[1]}\\B{sngEnd})", { 0 : self.hStyles["dialogue3"], } )) diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 68c5c1ba..231fbadc 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -31,13 +31,13 @@ import os from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( - QDialog, QWidget, QComboBox, QSpinBox, QPushButton, QLineEdit, QMessageBox, - QDialogButtonBox, QFileDialog, QFontDialog + QDialog, QWidget, QComboBox, QSpinBox, QPushButton, QDialogButtonBox, + QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox ) from nw.gui.custom import QSwitch, QConfigLayout, PagedDialog, QuotesDialog from nw.core import NWSpellSimple, NWSpellEnchant -from nw.constants import nwConst +from nw.constants import nwConst, nwAlert logger = logging.getLogger(__name__) @@ -55,17 +55,19 @@ class GuiPreferences(PagedDialog): self.setWindowTitle("Preferences") - self.tabGeneral = GuiConfigEditGeneralTab(self.theParent) - self.tabProjects = GuiConfigEditProjectsTab(self.theParent) - self.tabLayout = GuiConfigEditLayoutTab(self.theParent) - self.tabEditing = GuiConfigEditEditingTab(self.theParent) - self.tabAutoRep = GuiConfigEditAutoReplaceTab(self.theParent) + self.tabGeneral = GuiPreferencesGeneral(self.theParent) + self.tabProjects = GuiPreferencesProjects(self.theParent) + self.tabDocs = GuiPreferencesDocuments(self.theParent) + self.tabEditor = GuiPreferencesEditor(self.theParent) + self.tabSyntax = GuiPreferencesSyntax(self.theParent) + self.tabAuto = GuiPreferencesAutomation(self.theParent) self.addTab(self.tabGeneral, "General") self.addTab(self.tabProjects, "Projects") - self.addTab(self.tabLayout, "Text Layout") - self.addTab(self.tabEditing, "Editor") - self.addTab(self.tabAutoRep, "Auto-Replace") + self.addTab(self.tabDocs, "Documents") + self.addTab(self.tabEditor, "Editor") + self.addTab(self.tabSyntax, "Syntax") + self.addTab(self.tabAuto, "Automation") self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.accepted.connect(self._doSave) @@ -84,53 +86,35 @@ class GuiPreferences(PagedDialog): """Trigger all the save functions in the tabs, and collect the status of the saves. """ - logger.verbose("ConfigEditor save button clicked") + logger.debug("Saving new preferences") - validEntries = True - needsRestart = False + needsRestart = self.tabGeneral.saveValues() - retA, retB = self.tabGeneral.saveValues() - validEntries &= retA - needsRestart |= retB - - retA, retB = self.tabProjects.saveValues() - validEntries &= retA - needsRestart |= retB - - retA, retB = self.tabLayout.saveValues() - validEntries &= retA - needsRestart |= retB - - retA, retB = self.tabEditing.saveValues() - validEntries &= retA - needsRestart |= retB - - retA, retB = self.tabAutoRep.saveValues() - validEntries &= retA - needsRestart |= retB + self.tabProjects.saveValues() + self.tabDocs.saveValues() + self.tabEditor.saveValues() + self.tabSyntax.saveValues() + self.tabAuto.saveValues() if needsRestart: - msgBox = QMessageBox() - msgBox.information( - self, "Preferences", - "Some changes will not be applied until novelWriter has been restarted." + self.theParent.makeAlert( + "Some changes will not be applied until novelWriter has been restarted.", + nwAlert.INFO ) - if validEntries: - self.accept() + self.accept() return def _doClose(self): """Close the preferences without saving the changes. """ - logger.verbose("ConfigEditor close button clicked") self.reject() return # END Class GuiPreferences -class GuiConfigEditGeneralTab(QWidget): +class GuiPreferencesGeneral(QWidget): def __init__(self, theParent): QWidget.__init__(self, theParent) @@ -250,21 +234,17 @@ class GuiConfigEditGeneralTab(QWidget): def saveValues(self): """Save the values set for this tab. """ - validEntries = True - needsRestart = False - guiTheme = self.selectTheme.currentData() guiIcons = self.selectIcons.currentData() guiDark = self.preferDarkIcons.isChecked() guiFont = self.guiFont.text() guiFontSize = self.guiFontSize.value() - showFullPath = self.showFullPath.isChecked() - hideVScroll = self.hideVScroll.isChecked() - hideHScroll = self.hideHScroll.isChecked() # Check if restart is needed + needsRestart = False needsRestart |= self.mainConf.guiTheme != guiTheme needsRestart |= self.mainConf.guiIcons != guiIcons + needsRestart |= self.mainConf.guiDark != guiDark needsRestart |= self.mainConf.guiFont != guiFont needsRestart |= self.mainConf.guiFontSize != guiFontSize @@ -273,13 +253,13 @@ class GuiConfigEditGeneralTab(QWidget): self.mainConf.guiDark = guiDark self.mainConf.guiFont = guiFont self.mainConf.guiFontSize = guiFontSize - self.mainConf.showFullPath = showFullPath - self.mainConf.hideVScroll = hideVScroll - self.mainConf.hideHScroll = hideHScroll + self.mainConf.showFullPath = self.showFullPath.isChecked() + self.mainConf.hideVScroll = self.hideVScroll.isChecked() + self.mainConf.hideHScroll = self.hideHScroll.isChecked() self.mainConf.confChanged = True - return validEntries, needsRestart + return needsRestart ## # Slots @@ -297,9 +277,9 @@ class GuiConfigEditGeneralTab(QWidget): self.guiFontSize.setValue(theFont.pointSize()) return -# END Class GuiConfigEditGeneralTab +# END Class GuiPreferencesGeneral -class GuiConfigEditProjectsTab(QWidget): +class GuiPreferencesProjects(QWidget): def __init__(self, theParent): QWidget.__init__(self, theParent) @@ -313,8 +293,8 @@ class GuiConfigEditProjectsTab(QWidget): self.mainForm.setHelpTextStyle(self.theTheme.helpText) self.setLayout(self.mainForm) - # AutoSave Settings - # ================= + # Automatic Save + # ============== self.mainForm.addGroupLabel("Automatic Save") ## Document Save Timer @@ -323,7 +303,7 @@ class GuiConfigEditProjectsTab(QWidget): self.autoSaveDoc.setMaximum(600) self.autoSaveDoc.setSingleStep(1) self.autoSaveDoc.setValue(self.mainConf.autoSaveDoc) - self.backupPathRow = self.mainForm.addRow( + self.mainForm.addRow( "Save document interval", self.autoSaveDoc, "How often the open document is automatically saved.", @@ -336,15 +316,15 @@ class GuiConfigEditProjectsTab(QWidget): self.autoSaveProj.setMaximum(600) self.autoSaveProj.setSingleStep(1) self.autoSaveProj.setValue(self.mainConf.autoSaveProj) - self.backupPathRow = self.mainForm.addRow( + self.mainForm.addRow( "Save project interval", self.autoSaveProj, "How often the open project is automatically saved.", theUnit="seconds" ) - # Backup Settings - # =============== + # Project Backup + # ============== self.mainForm.addGroupLabel("Project Backup") ## Backup Path @@ -383,24 +363,18 @@ class GuiConfigEditProjectsTab(QWidget): def saveValues(self): """Save the values set for this tab. """ - validEntries = True - needsRestart = False + # Automatic Save + self.mainConf.autoSaveDoc = self.autoSaveDoc.value() + self.mainConf.autoSaveProj = self.autoSaveProj.value() - autoSaveDoc = self.autoSaveDoc.value() - autoSaveProj = self.autoSaveProj.value() - backupPath = self.backupPath - backupOnClose = self.backupOnClose.isChecked() - askBeforeBackup = self.askBeforeBackup.isChecked() - - self.mainConf.autoSaveDoc = autoSaveDoc - self.mainConf.autoSaveProj = autoSaveProj - self.mainConf.backupPath = backupPath - self.mainConf.backupOnClose = backupOnClose - self.mainConf.askBeforeBackup = askBeforeBackup + # Project Backup + self.mainConf.backupPath = self.backupPath + self.mainConf.backupOnClose = self.backupOnClose.isChecked() + self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked() self.mainConf.confChanged = True - return validEntries, needsRestart + return ## # Slots @@ -433,9 +407,9 @@ class GuiConfigEditProjectsTab(QWidget): self.askBeforeBackup.setEnabled(theState) return -# END Class GuiConfigEditProjectsTab +# END Class GuiPreferencesProjects -class GuiConfigEditLayoutTab(QWidget): +class GuiPreferencesDocuments(QWidget): def __init__(self, theParent): QWidget.__init__(self, theParent) @@ -451,7 +425,7 @@ class GuiConfigEditLayoutTab(QWidget): # Text Style # ========== - self.mainForm.addGroupLabel("Document Text Style") + self.mainForm.addGroupLabel("Text Style") ## Font Family self.textStyleFont = QLineEdit() @@ -483,7 +457,7 @@ class GuiConfigEditLayoutTab(QWidget): # Text Flow # ========= - self.mainForm.addGroupLabel("Document Text Flow") + self.mainForm.addGroupLabel("Text Flow") ## Max Text Width in Normal Mode self.textFlowMax = QSpinBox(self) @@ -564,78 +538,27 @@ class GuiConfigEditLayoutTab(QWidget): theUnit="px" ) - # Scroll Behaviour - # ================ - self.mainForm.addGroupLabel("Scroll Behaviour") - - ## Scroll Past End - self.scrollPastEnd = QSwitch() - self.scrollPastEnd.setChecked(self.mainConf.scrollPastEnd) - self.mainForm.addRow( - "Scroll past end of the document", - self.scrollPastEnd, - "Allow scrolling until the last line is centred in the editor." - ) - - ## Typewriter Scrolling - self.autoScroll = QSwitch() - self.autoScroll.setChecked(self.mainConf.autoScroll) - self.mainForm.addRow( - "Typewriter style scrolling when you type", - self.autoScroll, - "Try to keep the cursor at a fixed vertical position." - ) - - ## Font Size - self.autoScrollPos = QSpinBox(self) - self.autoScrollPos.setMinimum(10) - self.autoScrollPos.setMaximum(90) - self.autoScrollPos.setSingleStep(1) - self.autoScrollPos.setValue(int(self.mainConf.autoScrollPos)) - self.mainForm.addRow( - "Minimum position for Typewriter scrolling", - self.autoScrollPos, - "In units of percentage of the editor height.", - theUnit = "%" - ) - return def saveValues(self): """Save the values set for this tab. """ - validEntries = True - needsRestart = False + # Text Style + self.mainConf.textFont = self.textStyleFont.text() + self.mainConf.textSize = self.textStyleSize.value() - textFont = self.textStyleFont.text() - textSize = self.textStyleSize.value() - textWidth = self.textFlowMax.value() - focusWidth = self.focusDocWidth.value() - textFixedW = not self.textFlowFixed.isChecked() - hideFocusFooter = self.hideFocusFooter.isChecked() - doJustify = self.textJustify.isChecked() - textMargin = self.textMargin.value() - tabWidth = self.tabWidth.value() - scrollPastEnd = self.scrollPastEnd.isChecked() - autoScroll = self.autoScroll.isChecked() - autoScrollPos = self.autoScrollPos.value() - - self.mainConf.textFont = textFont - self.mainConf.textSize = textSize - self.mainConf.textWidth = textWidth - self.mainConf.focusWidth = focusWidth - self.mainConf.textFixedW = textFixedW - self.mainConf.hideFocusFooter = hideFocusFooter - self.mainConf.doJustify = doJustify - self.mainConf.textMargin = textMargin - self.mainConf.tabWidth = tabWidth - self.mainConf.scrollPastEnd = scrollPastEnd - self.mainConf.autoScroll = autoScroll - self.mainConf.autoScrollPos = autoScrollPos + # Text Flow + self.mainConf.textWidth = self.textFlowMax.value() + self.mainConf.focusWidth = self.focusDocWidth.value() + self.mainConf.textFixedW = not self.textFlowFixed.isChecked() + self.mainConf.hideFocusFooter = self.hideFocusFooter.isChecked() + self.mainConf.doJustify = self.textJustify.isChecked() + self.mainConf.textMargin = self.textMargin.value() + self.mainConf.tabWidth = self.tabWidth.value() self.mainConf.confChanged = True - return validEntries, needsRestart + return ## # Slots @@ -651,11 +574,12 @@ class GuiConfigEditLayoutTab(QWidget): if theStatus: self.textStyleFont.setText(theFont.family()) self.textStyleSize.setValue(theFont.pointSize()) + return -# END Class GuiConfigEditLayoutTab +# END Class GuiPreferencesDocuments -class GuiConfigEditEditingTab(QWidget): +class GuiPreferencesEditor(QWidget): def __init__(self, theParent): QWidget.__init__(self, theParent) @@ -669,42 +593,6 @@ class GuiConfigEditEditingTab(QWidget): self.mainForm.setHelpTextStyle(self.theTheme.helpText) self.setLayout(self.mainForm) - # Spell Checking - # ============== - self.mainForm.addGroupLabel("Syntax Highlighting") - - ## Syntax Highlighting - self.selectSyntax = QComboBox() - self.selectSyntax.setMinimumWidth(self.mainConf.pxInt(200)) - self.theSyntaxes = self.theTheme.listSyntax() - for syntaxFile, syntaxName in self.theSyntaxes: - self.selectSyntax.addItem(syntaxName, syntaxFile) - syntaxIdx = self.selectSyntax.findData(self.mainConf.guiSyntax) - if syntaxIdx != -1: - self.selectSyntax.setCurrentIndex(syntaxIdx) - - self.mainForm.addRow( - "Highlight theme", - self.selectSyntax, - "Colour theme to apply to the editor and viewer." - ) - - self.highlightQuotes = QSwitch() - self.highlightQuotes.setChecked(self.mainConf.highlightQuotes) - self.mainForm.addRow( - "Highlight text wrapped in quotes", - self.highlightQuotes, - "Applies to single, double and straight quotes." - ) - - self.highlightEmph = QSwitch() - self.highlightEmph.setChecked(self.mainConf.highlightEmph) - self.mainForm.addRow( - "Add highlight colour to emphasised text", - self.highlightEmph, - "Applies to emphasis, strong and strikethrough." - ) - # Spell Checking # ============== self.mainForm.addGroupLabel("Spell Checking") @@ -749,6 +637,24 @@ class GuiConfigEditEditingTab(QWidget): theUnit="kB" ) + # Word Count + # ========== + self.mainForm.addGroupLabel("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(self.mainConf.wordCountTimer) + self.mainForm.addRow( + "Word count interval", + self.wordCountTimer, + "How often the word count is updated.", + theUnit="seconds" + ) + # Writing Guides # ============== self.mainForm.addGroupLabel("Writing Guides") @@ -771,35 +677,66 @@ class GuiConfigEditEditingTab(QWidget): "Add a symbol to indicate line endings in the editor." ) + # Scroll Behaviour + # ================ + self.mainForm.addGroupLabel("Scroll Behaviour") + + ## Scroll Past End + self.scrollPastEnd = QSwitch() + self.scrollPastEnd.setChecked(self.mainConf.scrollPastEnd) + self.mainForm.addRow( + "Scroll past end of the document", + self.scrollPastEnd, + "Allow scrolling until the last line is centred in the editor." + ) + + ## Typewriter Scrolling + self.autoScroll = QSwitch() + self.autoScroll.setChecked(self.mainConf.autoScroll) + self.mainForm.addRow( + "Typewriter style scrolling when you type", + self.autoScroll, + "Try to keep the cursor at a fixed vertical position." + ) + + ## Typewriter Position + self.autoScrollPos = QSpinBox(self) + self.autoScrollPos.setMinimum(10) + self.autoScrollPos.setMaximum(90) + self.autoScrollPos.setSingleStep(1) + self.autoScrollPos.setValue(int(self.mainConf.autoScrollPos)) + self.mainForm.addRow( + "Minimum position for Typewriter scrolling", + self.autoScrollPos, + "In units of percentage of the editor height.", + theUnit = "%" + ) + return def saveValues(self): """Save the values set for this tab. """ - validEntries = True - needsRestart = False + # Spell Checking + self.mainConf.spellTool = self.spellToolList.currentData() + self.mainConf.spellLanguage = self.spellLangList.currentData() + self.mainConf.bigDocLimit = self.bigDocLimit.value() - guiSyntax = self.selectSyntax.currentData() - highlightQuotes = self.highlightQuotes.isChecked() - highlightEmph = self.highlightEmph.isChecked() - spellTool = self.spellToolList.currentData() - spellLanguage = self.spellLangList.currentData() - bigDocLimit = self.bigDocLimit.value() - showTabsNSpaces = self.showTabsNSpaces.isChecked() - showLineEndings = self.showLineEndings.isChecked() + # Word Count + self.mainConf.wordCountTimer = self.wordCountTimer.value() - self.mainConf.guiSyntax = guiSyntax - self.mainConf.highlightQuotes = highlightQuotes - self.mainConf.highlightEmph = highlightEmph - self.mainConf.spellTool = spellTool - self.mainConf.spellLanguage = spellLanguage - self.mainConf.bigDocLimit = bigDocLimit - self.mainConf.showTabsNSpaces = showTabsNSpaces - self.mainConf.showLineEndings = showLineEndings + # Writing Guides + self.mainConf.showTabsNSpaces = self.showTabsNSpaces.isChecked() + self.mainConf.showLineEndings = self.showLineEndings.isChecked() + + # Scroll Behaviour + self.mainConf.scrollPastEnd = self.scrollPastEnd.isChecked() + self.mainConf.autoScroll = self.autoScroll.isChecked() + self.mainConf.autoScrollPos = self.autoScrollPos.value() self.mainConf.confChanged = True - return validEntries, needsRestart + return ## # Internal Functions @@ -833,9 +770,117 @@ class GuiConfigEditEditingTab(QWidget): return -# END Class GuiConfigEditEditingTab +# END Class GuiPreferencesEditor -class GuiConfigEditAutoReplaceTab(QWidget): +class GuiPreferencesSyntax(QWidget): + + def __init__(self, theParent): + QWidget.__init__(self, theParent) + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.theTheme = theParent.theTheme + + # The Form + self.mainForm = QConfigLayout() + self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.setLayout(self.mainForm) + + # Highlighting Theme + # ================== + self.mainForm.addGroupLabel("Highlighting Theme") + + self.selectSyntax = QComboBox() + self.selectSyntax.setMinimumWidth(self.mainConf.pxInt(200)) + self.theSyntaxes = self.theTheme.listSyntax() + for syntaxFile, syntaxName in self.theSyntaxes: + self.selectSyntax.addItem(syntaxName, syntaxFile) + syntaxIdx = self.selectSyntax.findData(self.mainConf.guiSyntax) + if syntaxIdx != -1: + self.selectSyntax.setCurrentIndex(syntaxIdx) + + self.mainForm.addRow( + "Highlighting theme", + self.selectSyntax, + "Colour theme to apply to the editor and viewer." + ) + + # Quotes & Dialogue + # ================= + self.mainForm.addGroupLabel("Quotes & Dialogue") + + self.highlightQuotes = QSwitch() + self.highlightQuotes.setChecked(self.mainConf.highlightQuotes) + self.highlightQuotes.toggled.connect(self._toggleHighlightQuotes) + self.mainForm.addRow( + "Highlight text wrapped in quotes", + self.highlightQuotes, + "Applies to single, double and straight quotes." + ) + + self.allowOpenSQuote = QSwitch() + self.allowOpenSQuote.setChecked(self.mainConf.allowOpenSQuote) + self.mainForm.addRow( + "Allow open-ended single quotes", + self.allowOpenSQuote, + "Highlight single-quoted line with no closing quote." + ) + + self.allowOpenDQuote = QSwitch() + self.allowOpenDQuote.setChecked(self.mainConf.allowOpenDQuote) + self.mainForm.addRow( + "Allow open-ended double quotes", + self.allowOpenDQuote, + "Highlight double-quoted line with no closing quote." + ) + + # Text Emphasis + # ============= + self.mainForm.addGroupLabel("Text Emphasis") + + self.highlightEmph = QSwitch() + self.highlightEmph.setChecked(self.mainConf.highlightEmph) + self.mainForm.addRow( + "Add highlight colour to emphasised text", + self.highlightEmph, + "Applies to emphasis (italic) and strong (bold)." + ) + + return + + def saveValues(self): + """Save the values set for this tab. + """ + # Highlighting Theme + self.mainConf.guiSyntax = self.selectSyntax.currentData() + + # Quotes & Dialogue + self.mainConf.highlightQuotes = self.highlightQuotes.isChecked() + self.mainConf.allowOpenSQuote = self.allowOpenSQuote.isChecked() + self.mainConf.allowOpenDQuote = self.allowOpenDQuote.isChecked() + + # Text Emphasis + self.mainConf.highlightEmph = self.highlightEmph.isChecked() + + self.mainConf.confChanged = True + + return + + ## + # Slots + ## + + def _toggleHighlightQuotes(self, theState): + """Enables or disables switches controlled by the highlight + quotes switch. + """ + self.allowOpenSQuote.setEnabled(theState) + self.allowOpenDQuote.setEnabled(theState) + return + +# END Class GuiPreferencesSyntax + +class GuiPreferencesAutomation(QWidget): def __init__(self, theParent): QWidget.__init__(self, theParent) @@ -872,8 +917,8 @@ class GuiConfigEditAutoReplaceTab(QWidget): "Allow the editor to replace symbols as you type." ) - # Auto-Replace - # ============ + # Replace as You Type + # =================== self.mainForm.addGroupLabel("Replace as You Type") ## Auto-Replace Single Quotes @@ -913,7 +958,7 @@ class GuiConfigEditAutoReplaceTab(QWidget): self.mainForm.addRow( "Auto-replace dots", self.autoReplaceDots, - "Three consecutive dots becomes ellipsis." + "Three consecutive dots become ellipsis." ) # Quotation Style @@ -995,36 +1040,25 @@ class GuiConfigEditAutoReplaceTab(QWidget): def saveValues(self): """Save the values set for this tab. """ - validEntries = True - needsRestart = False + # Automatic Features + self.mainConf.autoSelect = self.autoSelect.isChecked() + self.mainConf.doReplace = self.autoReplaceMain.isChecked() - autoSelect = self.autoSelect.isChecked() - doReplace = self.autoReplaceMain.isChecked() - doReplaceSQuote = self.autoReplaceSQ.isChecked() - doReplaceDQuote = self.autoReplaceDQ.isChecked() - doReplaceDash = self.autoReplaceDash.isChecked() - doReplaceDots = self.autoReplaceDots.isChecked() + # Replace as You Type + self.mainConf.doReplaceSQuote = self.autoReplaceSQ.isChecked() + self.mainConf.doReplaceDQuote = self.autoReplaceDQ.isChecked() + self.mainConf.doReplaceDash = self.autoReplaceDash.isChecked() + self.mainConf.doReplaceDots = self.autoReplaceDots.isChecked() - self.mainConf.autoSelect = autoSelect - self.mainConf.doReplace = doReplace - self.mainConf.doReplaceSQuote = doReplaceSQuote - self.mainConf.doReplaceDQuote = doReplaceDQuote - self.mainConf.doReplaceDash = doReplaceDash - self.mainConf.doReplaceDots = doReplaceDots - - fmtSingleQuotesO = self.quoteSym["SO"].text() - fmtSingleQuotesC = self.quoteSym["SC"].text() - fmtDoubleQuotesO = self.quoteSym["DO"].text() - fmtDoubleQuotesC = self.quoteSym["DC"].text() - - self.mainConf.fmtSingleQuotes[0] = fmtSingleQuotesO - self.mainConf.fmtSingleQuotes[1] = fmtSingleQuotesC - self.mainConf.fmtDoubleQuotes[0] = fmtDoubleQuotesO - self.mainConf.fmtDoubleQuotes[1] = fmtDoubleQuotesC + # Quotation Style + self.mainConf.fmtSingleQuotes[0] = self.quoteSym["SO"].text() + self.mainConf.fmtSingleQuotes[1] = self.quoteSym["SC"].text() + self.mainConf.fmtDoubleQuotes[0] = self.quoteSym["DO"].text() + self.mainConf.fmtDoubleQuotes[1] = self.quoteSym["DC"].text() self.mainConf.confChanged = True - return validEntries, needsRestart + return ## # Slots @@ -1046,6 +1080,7 @@ class GuiConfigEditAutoReplaceTab(QWidget): qtBox = QuotesDialog(self, currentQuote=self.quoteSym[qType].text()) if qtBox.exec_() == QDialog.Accepted: self.quoteSym[qType].setText(qtBox.selectedQuote) + return -# END Class GuiConfigEditAutoReplaceTab +# END Class GuiPreferencesAutomation diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index 27f2062d..588d5263 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -53,6 +53,8 @@ showlineendings = False bigdoclimit = 800 showfullpath = True highlightquotes = True +allowopensquote = False +allowopendquote = True highlightemph = True [Backup] diff --git a/tests/reference/guiPreferences_novelwriter.conf b/tests/reference/guiPreferences_novelwriter.conf index 5b10156f..e94d4129 100644 --- a/tests/reference/guiPreferences_novelwriter.conf +++ b/tests/reference/guiPreferences_novelwriter.conf @@ -53,6 +53,8 @@ showlineendings = True bigdoclimit = 500 showfullpath = False highlightquotes = False +allowopensquote = False +allowopendquote = True highlightemph = False [Backup] diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 558a621c..7b0b6f4f 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -74,7 +74,7 @@ def testBaseConfig_Constructor(monkeypatch): monkeypatch.undo() # Other - monkeypatch.setattr("sys.platform", "some_ther_os") + monkeypatch.setattr("sys.platform", "some_other_os") tstConf = Config() assert tstConf.osLinux is False assert tstConf.osDarwin is False @@ -170,6 +170,28 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir): assert tstConf.loadConfig() assert tstConf.saveConfig() + # Test Correcting Quote Settings + origDbl = tstConf.fmtDoubleQuotes + origSng = tstConf.fmtSingleQuotes + orDoDbl = tstConf.doReplaceDQuote + orDoSng = tstConf.doReplaceSQuote + + tstConf.fmtDoubleQuotes = ["\"", "\""] + tstConf.fmtSingleQuotes = ["'", "'"] + tstConf.doReplaceDQuote = True + tstConf.doReplaceSQuote = True + assert tstConf.saveConfig() + + assert tstConf.loadConfig() + assert not tstConf.doReplaceDQuote + assert not tstConf.doReplaceSQuote + + tstConf.fmtDoubleQuotes = origDbl + tstConf.fmtSingleQuotes = origSng + tstConf.doReplaceDQuote = orDoDbl + tstConf.doReplaceSQuote = orDoSng + assert tstConf.saveConfig() + copyfile(confFile, testFile) assert cmpFiles(testFile, compFile, [2, 9]) diff --git a/tests/test_gui/test_gui_preferences.py b/tests/test_gui/test_gui_preferences.py index 6247433a..4a732b1c 100644 --- a/tests/test_gui/test_gui_preferences.py +++ b/tests/test_gui/test_gui_preferences.py @@ -35,6 +35,7 @@ from PyQt5.QtWidgets import ( from nw.gui import GuiPreferences from nw.config import Config from nw.gui.custom import QuotesDialog +from nw.constants import nwConst keyDelay = 2 typeDelay = 1 @@ -67,6 +68,7 @@ def testGuiPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir): theConf = nwGUI.mainConf assert theConf.confPath == fncDir + theConf.spellTool = nwConst.SP_INTERNAL monkeypatch.setattr(GuiPreferences, "exec_", lambda *args: None) monkeypatch.setattr(GuiPreferences, "result", lambda *args: QDialog.Accepted) @@ -78,7 +80,6 @@ def testGuiPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir): nwPrefs.show() assert nwPrefs.mainConf.confPath == fncDir - # qtbot.stopForInteraction() # General Settings qtbot.wait(keyDelay) tabGeneral = nwPrefs.tabGeneral @@ -122,6 +123,8 @@ def testGuiPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir): qtbot.mouseClick(tabProjects.backupOnClose, Qt.LeftButton) assert tabProjects.backupOnClose.isChecked() + # qtbot.stopForInteraction() + # Check Browse button monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "") assert not tabProjects._backupFolder() @@ -132,98 +135,103 @@ def testGuiPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir): tabProjects.autoSaveDoc.setValue(20) tabProjects.autoSaveProj.setValue(40) - # Text Layout Settings + # Document Settings qtbot.wait(keyDelay) - tabLayout = nwPrefs.tabLayout - nwPrefs._tabBox.setCurrentWidget(tabLayout) + tabDocs = nwPrefs.tabDocs + nwPrefs._tabBox.setCurrentWidget(tabDocs) qtbot.wait(keyDelay) - qtbot.mouseClick(tabLayout.fontButton, Qt.LeftButton) + qtbot.mouseClick(tabDocs.fontButton, Qt.LeftButton) qtbot.wait(keyDelay) - tabLayout.textStyleSize.setValue(13) - tabLayout.textFlowMax.setValue(700) - tabLayout.focusDocWidth.setValue(900) - tabLayout.textMargin.setValue(45) - tabLayout.tabWidth.setValue(45) + tabDocs.textStyleSize.setValue(13) + tabDocs.textFlowMax.setValue(700) + tabDocs.focusDocWidth.setValue(900) + tabDocs.textMargin.setValue(45) + tabDocs.tabWidth.setValue(45) qtbot.wait(keyDelay) - assert not tabLayout.textFlowFixed.isChecked() - qtbot.mouseClick(tabLayout.textFlowFixed, Qt.LeftButton) - assert tabLayout.textFlowFixed.isChecked() + assert not tabDocs.textFlowFixed.isChecked() + qtbot.mouseClick(tabDocs.textFlowFixed, Qt.LeftButton) + assert tabDocs.textFlowFixed.isChecked() qtbot.wait(keyDelay) - assert not tabLayout.hideFocusFooter.isChecked() - qtbot.mouseClick(tabLayout.hideFocusFooter, Qt.LeftButton) - assert tabLayout.hideFocusFooter.isChecked() + assert not tabDocs.hideFocusFooter.isChecked() + qtbot.mouseClick(tabDocs.hideFocusFooter, Qt.LeftButton) + assert tabDocs.hideFocusFooter.isChecked() qtbot.wait(keyDelay) - assert tabLayout.textJustify.isChecked() - qtbot.mouseClick(tabLayout.textJustify, Qt.LeftButton) - assert not tabLayout.textJustify.isChecked() - - qtbot.wait(keyDelay) - assert tabLayout.scrollPastEnd.isChecked() - qtbot.mouseClick(tabLayout.scrollPastEnd, Qt.LeftButton) - assert not tabLayout.scrollPastEnd.isChecked() - - qtbot.wait(keyDelay) - assert not tabLayout.autoScroll.isChecked() - qtbot.mouseClick(tabLayout.autoScroll, Qt.LeftButton) - assert tabLayout.autoScroll.isChecked() + assert tabDocs.textJustify.isChecked() + qtbot.mouseClick(tabDocs.textJustify, Qt.LeftButton) + assert not tabDocs.textJustify.isChecked() # Editor Settings qtbot.wait(keyDelay) - tabEditing = nwPrefs.tabEditing - nwPrefs._tabBox.setCurrentWidget(tabEditing) + tabEditor = nwPrefs.tabEditor + nwPrefs._tabBox.setCurrentWidget(tabEditor) qtbot.wait(keyDelay) - assert tabEditing.highlightQuotes.isChecked() - qtbot.mouseClick(tabEditing.highlightQuotes, Qt.LeftButton) - assert not tabEditing.highlightQuotes.isChecked() + assert not tabEditor.showTabsNSpaces.isChecked() + qtbot.mouseClick(tabEditor.showTabsNSpaces, Qt.LeftButton) + assert tabEditor.showTabsNSpaces.isChecked() qtbot.wait(keyDelay) - assert tabEditing.highlightEmph.isChecked() - qtbot.mouseClick(tabEditing.highlightEmph, Qt.LeftButton) - assert not tabEditing.highlightEmph.isChecked() + assert not tabEditor.showLineEndings.isChecked() + qtbot.mouseClick(tabEditor.showLineEndings, Qt.LeftButton) + assert tabEditor.showLineEndings.isChecked() qtbot.wait(keyDelay) - assert not tabEditing.showTabsNSpaces.isChecked() - qtbot.mouseClick(tabEditing.showTabsNSpaces, Qt.LeftButton) - assert tabEditing.showTabsNSpaces.isChecked() + assert tabEditor.scrollPastEnd.isChecked() + qtbot.mouseClick(tabEditor.scrollPastEnd, Qt.LeftButton) + assert not tabEditor.scrollPastEnd.isChecked() qtbot.wait(keyDelay) - assert not tabEditing.showLineEndings.isChecked() - qtbot.mouseClick(tabEditing.showLineEndings, Qt.LeftButton) - assert tabEditing.showLineEndings.isChecked() + assert not tabEditor.autoScroll.isChecked() + qtbot.mouseClick(tabEditor.autoScroll, Qt.LeftButton) + assert tabEditor.autoScroll.isChecked() qtbot.wait(keyDelay) - tabEditing.bigDocLimit.setValue(500) + tabEditor.bigDocLimit.setValue(500) - # Auto-Replace Settings + # Syntax Settings qtbot.wait(keyDelay) - tabAutoRep = nwPrefs.tabAutoRep - nwPrefs._tabBox.setCurrentWidget(tabAutoRep) + tabSyntax = nwPrefs.tabSyntax + nwPrefs._tabBox.setCurrentWidget(tabSyntax) qtbot.wait(keyDelay) - assert tabAutoRep.autoSelect.isChecked() - qtbot.mouseClick(tabAutoRep.autoSelect, Qt.LeftButton) - assert not tabAutoRep.autoSelect.isChecked() + assert tabSyntax.highlightQuotes.isChecked() + qtbot.mouseClick(tabSyntax.highlightQuotes, Qt.LeftButton) + assert not tabSyntax.highlightQuotes.isChecked() qtbot.wait(keyDelay) - assert tabAutoRep.autoReplaceMain.isChecked() - qtbot.mouseClick(tabAutoRep.autoReplaceMain, Qt.LeftButton) - assert not tabAutoRep.autoReplaceMain.isChecked() + assert tabSyntax.highlightEmph.isChecked() + qtbot.mouseClick(tabSyntax.highlightEmph, Qt.LeftButton) + assert not tabSyntax.highlightEmph.isChecked() + + # Automation Settings + qtbot.wait(keyDelay) + tabAuto = nwPrefs.tabAuto + nwPrefs._tabBox.setCurrentWidget(tabAuto) qtbot.wait(keyDelay) - assert not tabAutoRep.autoReplaceSQ.isEnabled() - assert not tabAutoRep.autoReplaceDQ.isEnabled() - assert not tabAutoRep.autoReplaceDash.isEnabled() - assert not tabAutoRep.autoReplaceDots.isEnabled() + assert tabAuto.autoSelect.isChecked() + qtbot.mouseClick(tabAuto.autoSelect, Qt.LeftButton) + assert not tabAuto.autoSelect.isChecked() + + qtbot.wait(keyDelay) + assert tabAuto.autoReplaceMain.isChecked() + qtbot.mouseClick(tabAuto.autoReplaceMain, Qt.LeftButton) + assert not tabAuto.autoReplaceMain.isChecked() + + qtbot.wait(keyDelay) + assert not tabAuto.autoReplaceSQ.isEnabled() + assert not tabAuto.autoReplaceDQ.isEnabled() + assert not tabAuto.autoReplaceDash.isEnabled() + assert not tabAuto.autoReplaceDots.isEnabled() monkeypatch.setattr(QuotesDialog, "selectedQuote", "'") monkeypatch.setattr(QuotesDialog, "exec_", lambda *args: QDialog.Accepted) - qtbot.mouseClick(tabAutoRep.btnDoubleStyleC, Qt.LeftButton) + qtbot.mouseClick(tabAuto.btnDoubleStyleC, Qt.LeftButton) # Save and Check Config qtbot.mouseClick(nwPrefs.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton)