diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py index 879007a0..e2a022e9 100644 --- a/novelwriter/core/spellcheck.py +++ b/novelwriter/core/spellcheck.py @@ -29,6 +29,8 @@ import logging from typing import TYPE_CHECKING, Iterator from pathlib import Path +from PyQt5.QtCore import QLocale + from novelwriter.error import logException from novelwriter.constants import nwFiles @@ -138,15 +140,15 @@ class NWSpellEnchant: return added def listDictionaries(self) -> list[tuple[str, str]]: - """Wrapper function for pyenchant.""" - retList = [] + """List available dictionaries.""" + lang = [] try: import enchant - for spTag, spProvider in enchant.list_dicts(): - retList.append((spTag, spProvider.name)) + tags = [x for x, _ in enchant.list_dicts()] + lang = [(x, f"{QLocale(x).nativeLanguageName().title()} [{x}]") for x in set(tags)] except Exception: logger.error("Failed to list languages for enchant spell checking") - return retList + return sorted(lang, key=lambda x: x[1]) def describeDict(self) -> tuple[str, str]: """Describe the currently loaded dictionary.""" diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 642b6362..72120448 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -26,7 +26,7 @@ from __future__ import annotations import logging from PyQt5.QtGui import QFont -from PyQt5.QtCore import Qt, QLocale +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QDialog, QWidget, QComboBox, QSpinBox, QPushButton, QDialogButtonBox, QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox @@ -658,12 +658,9 @@ class GuiPreferencesEditor(QWidget): self.spellLanguage = QComboBox(self) self.spellLanguage.setMaximumWidth(mW) - langAvail = SHARED.spelling.listDictionaries() - if CONFIG.hasEnchant and langAvail: - for spTag, spProv in langAvail: - qLocal = QLocale(spTag) - spLang = qLocal.nativeLanguageName().title() - self.spellLanguage.addItem("%s [%s]" % (spLang, spProv), spTag) + if CONFIG.hasEnchant: + for tag, language in SHARED.spelling.listDictionaries(): + self.spellLanguage.addItem(language, tag) else: self.spellLanguage.addItem(self.tr("None"), "") self.spellLanguage.setEnabled(False) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index cd4280e4..ac79a986 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -28,7 +28,7 @@ import logging from typing import TYPE_CHECKING from PyQt5.QtGui import QIcon, QPixmap, QColor -from PyQt5.QtCore import Qt, QLocale, pyqtSlot +from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( QColorDialog, QComboBox, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget @@ -233,11 +233,9 @@ class GuiProjectEditMain(QWidget): self.spellLang = QComboBox(self) self.spellLang.setMaximumWidth(xW) self.spellLang.addItem(self.tr("Default"), "None") - langAvail = SHARED.spelling.listDictionaries() - for spTag, spProv in langAvail: - qLocal = QLocale(spTag) - spLang = qLocal.nativeLanguageName().title() - self.spellLang.addItem("%s [%s]" % (spLang, spProv), spTag) + if CONFIG.hasEnchant: + for tag, language in SHARED.spelling.listDictionaries(): + self.spellLang.addItem(language, tag) self.mainForm.addRow( self.tr("Spell check language"), diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 2063627b..d4204a36 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -690,7 +690,7 @@ class GuiDocEditor(QTextEdit): # Spell Checking ## - def toggleSpellCheck(self, state: bool) -> None: + def toggleSpellCheck(self, state: bool | None) -> None: """This is the main spell check setting function, and this one should call all other setSpellCheck functions in other classes. If the spell check mode (theMode) is not defined (None), then diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 5823a101..d265ade8 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -436,4 +436,4 @@ class GuiDocHighlighter(QSyntaxHighlighter): return charFormat -# END Class DocHighlighter +# END Class GuiDocHighlighter diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index b12f655f..55813a71 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -25,11 +25,12 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING from pathlib import Path from urllib.parse import urljoin from urllib.request import pathname2url -from PyQt5.QtCore import QUrl +from PyQt5.QtCore import QUrl, pyqtSlot from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction @@ -37,6 +38,9 @@ from novelwriter import CONFIG, SHARED from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -46,7 +50,7 @@ class GuiMainMenu(QMenuBar): add them from this class. """ - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiMainMenu") @@ -77,46 +81,52 @@ class GuiMainMenu(QMenuBar): # Update Menu on Settings Changed ## - def setSpellCheck(self, theMode): - """Forward spell check check state to its action. - """ - self.aSpellCheck.setChecked(theMode) + def setSpellCheck(self, state: bool) -> None: + """Forward spell check check state to its action.""" + self.aSpellCheck.setChecked(state) return ## - # Slots + # Private Slots ## - def _toggleSpellCheck(self): + @pyqtSlot() + def _toggleSpellCheck(self) -> None: """Toggle spell checking. The active status of the spell check flag is handled by the document editor class, so we make no decision, just pass a None to the function and let it decide. """ self.mainGui.docEditor.toggleSpellCheck(None) - return True + return - def _openWebsite(self, theUrl): - """Open a URL in the system's default browser. - """ - QDesktopServices.openUrl(QUrl(theUrl)) - return True + @pyqtSlot(str) + def _openWebsite(self, url: str) -> None: + """Open a URL in the system's default browser.""" + QDesktopServices.openUrl(QUrl(url)) + return - def _openUserManualFile(self): - """Open the documentation in PDF format. - """ + @pyqtSlot() + def _openUserManualFile(self) -> None: + """Open the documentation in PDF format.""" if isinstance(CONFIG.pdfDocs, Path): QDesktopServices.openUrl( QUrl(urljoin("file:", pathname2url(str(CONFIG.pdfDocs)))) ) return + @pyqtSlot(str) + def _changeSpelling(self, language: str) -> None: + """Change the spell check language.""" + SHARED.project.data.setSpellLang(language) + SHARED.updateSpellCheckLanguage() + return + ## - # Menu Builders + # Internal Functions ## - def _buildProjectMenu(self): - """Assemble the Project menu. - """ + def _buildProjectMenu(self) -> None: + """Assemble the Project menu.""" # Project self.projMenu = self.addMenu(self.tr("&Project")) @@ -190,9 +200,8 @@ class GuiMainMenu(QMenuBar): return - def _buildDocumentMenu(self): - """Assemble the Document menu. - """ + def _buildDocumentMenu(self) -> None: + """Assemble the Document menu.""" # Document self.docuMenu = self.addMenu(self.tr("&Document")) @@ -245,9 +254,8 @@ class GuiMainMenu(QMenuBar): return - def _buildEditMenu(self): - """Assemble the Edit menu. - """ + def _buildEditMenu(self) -> None: + """Assemble the Edit menu.""" # Edit self.editMenu = self.addMenu(self.tr("&Edit")) @@ -301,9 +309,8 @@ class GuiMainMenu(QMenuBar): return - def _buildViewMenu(self): - """Assemble the View menu. - """ + def _buildViewMenu(self) -> None: + """Assemble the View menu.""" # View self.viewMenu = self.addMenu(self.tr("&View")) @@ -375,9 +382,8 @@ class GuiMainMenu(QMenuBar): return - def _buildInsertMenu(self): - """Assemble the Insert menu. - """ + def _buildInsertMenu(self) -> None: + """Assemble the Insert menu.""" # Insert self.insMenu = self.addMenu(self.tr("&Insert")) @@ -589,9 +595,8 @@ class GuiMainMenu(QMenuBar): return - def _buildFormatMenu(self): - """Assemble the Format menu. - """ + def _buildFormatMenu(self) -> None: + """Assemble the Format menu.""" # Format self.fmtMenu = self.addMenu(self.tr("&Format")) @@ -739,9 +744,8 @@ class GuiMainMenu(QMenuBar): return - def _buildSearchMenu(self): - """Assemble the Search menu. - """ + def _buildSearchMenu(self) -> None: + """Assemble the Search menu.""" # Search self.srcMenu = self.addMenu(self.tr("&Search")) @@ -753,28 +757,21 @@ class GuiMainMenu(QMenuBar): # Search > Replace self.aReplace = QAction(self.tr("Replace"), self) - if CONFIG.osDarwin: - self.aReplace.setShortcut("Ctrl+=") - else: - self.aReplace.setShortcut("Ctrl+H") + self.aReplace.setShortcut("Ctrl+=" if CONFIG.osDarwin else "Ctrl+H") self.aReplace.triggered.connect(lambda: self.mainGui.docEditor.beginReplace()) self.srcMenu.addAction(self.aReplace) # Search > Find Next self.aFindNext = QAction(self.tr("Find Next"), self) - if CONFIG.osDarwin: - self.aFindNext.setShortcuts(["Ctrl+G", "F3"]) - else: - self.aFindNext.setShortcuts(["F3", "Ctrl+G"]) + self.aFindNext.setShortcuts(["Ctrl+G", "F3"] if CONFIG.osDarwin else ["F3", "Ctrl+G"]) self.aFindNext.triggered.connect(lambda: self.mainGui.docEditor.findNext()) self.srcMenu.addAction(self.aFindNext) # Search > Find Prev self.aFindPrev = QAction(self.tr("Find Previous"), self) - if CONFIG.osDarwin: - self.aFindPrev.setShortcuts(["Ctrl+Shift+G", "Shift+F3"]) - else: - self.aFindPrev.setShortcuts(["Shift+F3", "Ctrl+Shift+G"]) + self.aFindPrev.setShortcuts( + ["Ctrl+Shift+G", "Shift+F3"] if CONFIG.osDarwin else ["Shift+F3", "Ctrl+Shift+G"] + ) self.aFindPrev.triggered.connect(lambda: self.mainGui.docEditor.findNext(goBack=True)) self.srcMenu.addAction(self.aFindPrev) @@ -786,9 +783,8 @@ class GuiMainMenu(QMenuBar): return - def _buildToolsMenu(self): - """Assemble the Tools menu. - """ + def _buildToolsMenu(self) -> None: + """Assemble the Tools menu.""" # Tools self.toolsMenu = self.addMenu(self.tr("&Tools")) @@ -800,6 +796,15 @@ class GuiMainMenu(QMenuBar): self.aSpellCheck.setShortcut("Ctrl+F7") self.toolsMenu.addAction(self.aSpellCheck) + self.mSelectLanguage = self.toolsMenu.addMenu(self.tr("Spell Check Language")) + languages = SHARED.spelling.listDictionaries() + languages.insert(0, ("None", self.tr("Default"))) + for n, (tag, language) in enumerate(languages): + aSpell = QAction(self.mSelectLanguage) + aSpell.setText(language) + aSpell.triggered.connect(lambda n, tag=tag: self._changeSpelling(tag)) + self.mSelectLanguage.addAction(aSpell) + # Tools > Re-Run Spell Check self.aReRunSpell = QAction(self.tr("Re-Run Spell Check"), self) self.aReRunSpell.setShortcut("F7") @@ -849,9 +854,8 @@ class GuiMainMenu(QMenuBar): return - def _buildHelpMenu(self): - """Assemble the Help menu. - """ + def _buildHelpMenu(self) -> None: + """Assemble the Help menu.""" # Help self.helpMenu = self.addMenu(self.tr("&Help")) diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index a80864ca..cb5ca85a 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -3,8 +3,7 @@ novelWriter – GUI Main Window Status Bar ======================================== File History: -Created: 2019-04-20 [0.0.1] GuiMainStatus -Created: 2020-05-17 [0.5.1] StatusLED +Created: 2019-04-20 [0.0.1] This file is a part of novelWriter Copyright 2018–2023, Veronica Berglyd Olsen @@ -208,13 +207,8 @@ class GuiMainStatus(QStatusBar): self.langText.setText(self.tr("None")) self.langText.setToolTip("") else: - qLocal = QLocale(language) - spLang = qLocal.nativeLanguageName().title() - self.langText.setText(spLang) - if provider: - self.langText.setToolTip("%s (%s)" % (language, provider)) - else: - self.langText.setToolTip(language) + self.langText.setText(QLocale(language).nativeLanguageName().title()) + self.langText.setToolTip(f"{language} ({provider})" if provider else language) return @pyqtSlot(bool) diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py index 80f80954..bbf706e0 100644 --- a/tests/test_base/test_base_shared.py +++ b/tests/test_base/test_base_shared.py @@ -42,6 +42,8 @@ def testBaseSharedData_Init(): shared.theme with pytest.raises(Exception): shared.project + with pytest.raises(Exception): + shared.spelling # Create some mock objects mockGui = MockGuiMain() @@ -113,7 +115,7 @@ def testBaseSharedData_Projects(fncPath, caplog: pytest.LogCaptureFixture): project.openProject(fncPath) # First open with our independent project instance assert shared.hasProject is False assert shared.projectLock is None - assert shared.openProject(fncPath) is False # Then with out shared instance + assert shared.openProject(fncPath) is False # Then with our shared instance assert shared.hasProject is False assert isinstance(shared.projectLock, list) diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index 09a858fc..14712795 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -42,7 +42,7 @@ 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) - monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "none")]) + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) with monkeypatch.context() as mp: mp.setattr(GuiPreferences, "updateTheme", lambda *a: True) diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index a54639fc..f56f01bf 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -84,8 +84,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): @pytest.mark.gui def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): """Test the main tab of the project settings dialog.""" - # Mock components - monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "none")]) + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) # Create new project buildTestProject(nwGUI, projPath) @@ -147,9 +146,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat dialog. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - - # Mock components - monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "none")]) + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) # Create new project mockRnd.reset() @@ -347,9 +344,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): """Test the auto-replace tab of the project settings dialog.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - - # Mock components - monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "none")]) + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) # Create new project mockRnd.reset() diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 0c65cb5a..a7ddd3a6 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -242,7 +242,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None nwGUI.mainMenu.aSpellCheck.setChecked(True) - assert nwGUI.mainMenu._toggleSpellCheck() + nwGUI.mainMenu._toggleSpellCheck() + assert nwGUI.mainMenu.aSpellCheck.isChecked() is True # Change some settings CONFIG.hideHScroll = True