Add spell check languages to Tools menu
This commit is contained in:
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -436,4 +436,4 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
return charFormat
|
||||
|
||||
# END Class DocHighlighter
|
||||
# END Class GuiDocHighlighter
|
||||
|
||||
+61
-57
@@ -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,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user