Add spell check languages to Tools menu

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