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 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."""
+4 -7
View File
@@ -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)
+4 -6
View File
@@ -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"),
+1 -1
View File
@@ -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
+1 -1
View File
@@ -436,4 +436,4 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return charFormat
# END Class DocHighlighter
# END Class GuiDocHighlighter
+61 -57
View File
@@ -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"))
+3 -9
View File
@@ -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 20182023, 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)
+3 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+3 -8
View File
@@ -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()
+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
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