diff --git a/novelwriter/common.py b/novelwriter/common.py index 7cb6c5e0..a4e37f62 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -40,7 +40,7 @@ from PyQt6.QtCore import QCoreApplication, QMimeData, QUrl from PyQt6.QtGui import QAction, QDesktopServices, QFont, QFontDatabase, QFontInfo from PyQt6.QtWidgets import QMenu, QMenuBar, QWidget -from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst +from novelwriter.constants import nwConst, nwLabels, nwQuotes, nwUnicode, trConst from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.error import logException @@ -298,6 +298,15 @@ def uniqueCompact(text: str) -> str: return "".join(sorted(set(compact(text)))) +def processDialogSymbols(symbols: str) -> str: + """Process dialogue line symbols.""" + result = "" + for c in uniqueCompact(symbols): + if c in nwQuotes.ALLOWED: + result += c + return result + + def elide(text: str, length: int) -> str: """Elide a piece of text to a maximum length.""" if len(text) > (cut := max(4, length)): diff --git a/novelwriter/config.py b/novelwriter/config.py index 35690669..d820c66a 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -42,9 +42,9 @@ from PyQt6.QtWidgets import QApplication from novelwriter.common import ( NWConfigParser, checkInt, checkPath, describeFont, fontMatcher, - formatTimeStamp + formatTimeStamp, processDialogSymbols ) -from novelwriter.constants import nwFiles, nwHtmlUnicode, nwUnicode +from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode from novelwriter.error import formatException, logException if TYPE_CHECKING: @@ -680,9 +680,9 @@ class Config: self.showFullPath = conf.rdBool(sec, "showfullpath", self.showFullPath) self.dialogStyle = conf.rdInt(sec, "dialogstyle", self.dialogStyle) self.allowOpenDial = conf.rdBool(sec, "allowopendial", self.allowOpenDial) - self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine) - self.narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak) - self.narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog) + dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine) + narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak) + narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog) self.altDialogOpen = conf.rdStr(sec, "altdialogopen", self.altDialogOpen) self.altDialogClose = conf.rdStr(sec, "altdialogclose", self.altDialogClose) self.highlightEmph = conf.rdBool(sec, "highlightemph", self.highlightEmph) @@ -721,6 +721,10 @@ class Config: logger.info("Using straight double quotes, so disabling auto-replace") self.doReplaceDQuote = False + self.dialogLine = processDialogSymbols(dialogLine) + self.narratorBreak = narratorBreak if narratorBreak in nwQuotes.DASHES else "" + self.narratorDialog = narratorDialog if narratorDialog in nwQuotes.DASHES else "" + return True def saveConfig(self) -> bool: diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 9928ece4..16c5a082 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -505,6 +505,19 @@ class nwQuotes: "\u300f": QT_TRANSLATE_NOOP("Constant", "Right white corner bracket"), } + DASHES: Final[dict[str, str]] = { + "": QT_TRANSLATE_NOOP("Constant", "None"), + "\u2013": QT_TRANSLATE_NOOP("Constant", "Short dash"), + "\u2014": QT_TRANSLATE_NOOP("Constant", "Long dash"), + "\u2015": QT_TRANSLATE_NOOP("Constant", "Horizontal bar"), + } + + ALLOWED: Final[list[str]] = [ + "\u0027", "\u0022", "\u2018", "\u2019", "\u201a", "\u201b", "\u201c", "\u201d", "\u201e", + "\u201f", "\u2e42", "\u2039", "\u203a", "\u00ab", "\u00bb", "\u300c", "\u300d", "\u300e", + "\u300f", "\u2013", "\u2014", "\u2015", + ] + class nwUnicode: """Supported unicode character constants and their HTML equivalents.""" diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index aa4f9cd1..f98374af 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -34,9 +34,9 @@ from PyQt6.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import compact, describeFont, uniqueCompact +from novelwriter.common import compact, describeFont, processDialogSymbols, uniqueCompact from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL -from novelwriter.constants import nwLabels, nwUnicode, trConst +from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.extensions.configlayout import NColorLabel, NScrollableForm from novelwriter.extensions.modified import ( @@ -636,21 +636,20 @@ class GuiPreferences(NDialog): self.tr("Lines starting with any of these symbols are dialogue.") ) - self.narratorBreak = QLineEdit(self) - self.narratorBreak.setMaxLength(1) - self.narratorBreak.setFixedWidth(boxFixed) - self.narratorBreak.setAlignment(QtAlignCenter) - self.narratorBreak.setText(CONFIG.narratorBreak) + self.narratorBreak = NComboBox(self) + self.narratorDialog = NComboBox(self) + for key, value in nwQuotes.DASHES.items(): + label = trConst(value) + self.narratorBreak.addItem(label, key) + self.narratorDialog.addItem(label, key) + + self.narratorBreak.setCurrentData(CONFIG.narratorBreak, "") + self.narratorDialog.setCurrentData(CONFIG.narratorDialog, "") + self.mainForm.addRow( self.tr("Narrator break symbol"), self.narratorBreak, self.tr("Symbol to indicate a narrator break in dialogue.") ) - - self.narratorDialog = QLineEdit(self) - self.narratorDialog.setMaxLength(1) - self.narratorDialog.setFixedWidth(boxFixed) - self.narratorDialog.setAlignment(QtAlignCenter) - self.narratorDialog.setText(CONFIG.narratorDialog) self.mainForm.addRow( self.tr("Alternating dialogue/narration symbol"), self.narratorDialog, self.tr("Alternates dialogue highlighting within any paragraph.") @@ -1034,9 +1033,9 @@ class GuiPreferences(NDialog): # Text Highlighting dialogueStyle = self.dialogStyle.currentData() allowOpenDial = self.allowOpenDial.isChecked() - dialogueLine = uniqueCompact(self.dialogLine.text()) - narratorBreak = self.narratorBreak.text().strip() - narratorDialog = self.narratorDialog.text().strip() + dialogueLine = processDialogSymbols(self.dialogLine.text()) + narratorBreak = self.narratorBreak.currentData() + narratorDialog = self.narratorDialog.currentData() altDialogOpen = compact(self.altDialogOpen.text()) altDialogClose = compact(self.altDialogClose.text()) highlightEmph = self.highlightEmph.isChecked() diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index bfd2b55c..006daa42 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -37,8 +37,9 @@ from novelwriter.common import ( formatFileFilter, formatInt, formatTime, formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout, isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe, - minmax, numberToRoman, openExternalPath, readTextFile, simplified, - transferCase, uniqueCompact, xmlElement, xmlIndent, xmlSubElem, yesNo + minmax, numberToRoman, openExternalPath, processDialogSymbols, + readTextFile, simplified, transferCase, uniqueCompact, xmlElement, + xmlIndent, xmlSubElem, yesNo ) from tests.mocked import causeOSError @@ -377,6 +378,14 @@ def testBaseCommon_uniqueCompact(): assert uniqueCompact("3 2 1") == "123" +@pytest.mark.base +def testBaseCommon_processDialogSymbols(): + """Test the processDialogSymbols function.""" + assert processDialogSymbols("abc") == "" + assert processDialogSymbols("\u00ab\u00ab\u00bb\u00bb") == "\u00ab\u00bb" + assert processDialogSymbols("-\u2013\u2014\u2015") == "\u2013\u2014\u2015" + + @pytest.mark.base def testBaseCommon_elide(): """Test the elide function.""" diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index c7a1b7e4..5cb97dd0 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -265,8 +265,8 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): prefs.dialogStyle.setCurrentData(3, 0) prefs.allowOpenDial.setChecked(False) prefs.dialogLine.setText("–") - prefs.narratorBreak.setText("–") - prefs.narratorDialog.setText("–") + prefs.narratorBreak.setCurrentData("–", "") + prefs.narratorDialog.setCurrentData("–", "") prefs.altDialogOpen.setText("<") prefs.altDialogClose.setText(">") prefs.highlightEmph.setChecked(False)