Add a Spellchecker abstraction layer and clean up code

This is in preparation for adding support for additional spellchecking libraries
other than PyEnchant which seems to be unmaintained and does not build in
Windows 64 bit.
This commit is contained in:
Youness Alaoui 2019-02-21 15:32:34 -05:00 committed by Curtis Gedak
parent 0238ccec7b
commit d0f02cb2a7
8 changed files with 172 additions and 71 deletions

View file

@ -12,6 +12,7 @@ from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import qApp, QTextEdit
from manuskript.enums import Outline
from manuskript.functions.spellchecker import Spellchecker
# Used to detect multiple connections
AUC = Qt.AutoConnection | Qt.UniqueConnection

View file

@ -0,0 +1,139 @@
from PyQt5.QtCore import QLocale
try:
import enchant
except ImportError:
enchant = None
class Spellchecker:
dictionaries = {}
def __init__(self):
pass
@staticmethod
def isInstalled():
return enchant is not None
@staticmethod
def availableDictionaries():
return EnchantDictionary.availableDictionaries()
@staticmethod
def getDefaultDictionary():
return EnchantDictionary.getDefaultDictionary()
@staticmethod
def getLibraryURL():
return EnchantDictionary.getLibraryURL()
@staticmethod
def getDictionary(dictionary):
if not dictionary:
dictionary = Spellchecker.getDefaultDictionary()
try:
d = Spellchecker.dictionaries.get(dictionary, None)
if d is None:
d = EnchantDictionary(dictionary)
Spellchecker.dictionaries[d.name] = d
return d
except Exception as e:
return None
class BasicDictionary:
def __init__(self, name):
pass
@property
def name(self):
raise NotImplemented
@staticmethod
def getLibraryName():
raise NotImplemented
@staticmethod
def getLibraryURL():
raise NotImplemented
@staticmethod
def getDefaultDictionary():
raise NotImplemented
@staticmethod
def availableDictionaries():
raise NotImplemented
def isMisspelled(self, word):
raise NotImplemented
def getSuggestions(self, word):
raise NotImplemented
def isCustomWord(self, word):
raise NotImplemented
def addWord(self, word):
raise NotImplemented
def removeWord(self, word):
raise NotImplemented
class EnchantDictionary(BasicDictionary):
def __init__(self, name):
self._lang = name
if not (self._lang and enchant.dict_exists(self._lang)):
self._lang = self.getDefaultDictionary()
self._dict = enchant.Dict(self._lang)
@property
def name(self):
return self._lang
@staticmethod
def getLibraryName():
return "pyEnchant"
@staticmethod
def getLibraryURL():
return "https://pypi.org/project/pyenchant/"
@staticmethod
def availableDictionaries():
if enchant:
return list(map(lambda i: str(i[0]), enchant.list_dicts()))
return []
@staticmethod
def getDefaultDictionary():
if not enchant:
return None
default_locale = enchant.get_default_language()
if default_locale and not enchant.dict_exists(default_locale):
default_locale = None
if default_locale is None:
default_locale = QLocale.system().name()
if default_locale is None:
default_locale = self.availableDictionaries()[0]
return default_locale
def isMisspelled(self, word):
return not self._dict.check(word)
def getSuggestions(self, word):
return self._dict.suggest(word)
def isCustomWord(self, word):
return self._dict.is_added(word)
def addWord(self, word):
self._dict.add(word)
def removeWord(self, word):
self._dict.remove(word)

View file

@ -34,15 +34,10 @@ from manuskript.ui.statusLabel import statusLabel
# Spellcheck support
from manuskript.ui.views.textEditView import textEditView
try:
import enchant
except ImportError:
enchant = None
from manuskript.functions import Spellchecker
class MainWindow(QMainWindow, Ui_MainWindow):
dictChanged = pyqtSignal(str)
# dictChanged = pyqtSignal(str)
# Tab indexes
TabInfos = 0
@ -1246,16 +1241,16 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.actShowHelp.setChecked(False)
# Spellcheck
if enchant:
if Spellchecker.isInstalled():
self.menuDict = QMenu(self.tr("Dictionary"))
self.menuDictGroup = QActionGroup(self)
self.updateMenuDict()
self.menuTools.addMenu(self.menuDict)
self.actSpellcheck.toggled.connect(self.toggleSpellcheck, F.AUC)
self.dictChanged.connect(self.mainEditor.setDict, F.AUC)
self.dictChanged.connect(self.redacMetadata.setDict, F.AUC)
self.dictChanged.connect(self.outlineItemEditor.setDict, F.AUC)
# self.dictChanged.connect(self.mainEditor.setDict, F.AUC)
# self.dictChanged.connect(self.redacMetadata.setDict, F.AUC)
# self.dictChanged.connect(self.outlineItemEditor.setDict, F.AUC)
else:
# No Spell check support
@ -1271,23 +1266,23 @@ class MainWindow(QMainWindow, Ui_MainWindow):
def updateMenuDict(self):
if not enchant:
if not Spellchecker.isInstalled():
return
self.menuDict.clear()
for i in enchant.list_dicts():
a = QAction(str(i[0]), self)
for i in Spellchecker.availableDictionaries():
a = QAction(i, self)
a.setCheckable(True)
if settings.dict is None:
settings.dict = enchant.get_default_language()
if str(i[0]) == settings.dict:
settings.dict = Spellchecker.getDefaultDictionary()
if i == settings.dict:
a.setChecked(True)
a.triggered.connect(self.setDictionary, F.AUC)
self.menuDictGroup.addAction(a)
self.menuDict.addAction(a)
def setDictionary(self):
if not enchant:
if not Spellchecker.isInstalled():
return
for i in self.menuDictGroup.actions():
@ -1301,7 +1296,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
w.setDict(settings.dict)
def openPyEnchantWebPage(self):
F.openURL("http://pythonhosted.org/pyenchant/")
F.openURL(Spellchecker.getLibraryURL())
def toggleSpellcheck(self, val):
settings.spellcheck = val

View file

@ -26,11 +26,6 @@ from manuskript.ui.views.textEditView import textEditView
from manuskript.ui.welcome import welcome
from manuskript.ui import style as S
try:
import enchant
except ImportError:
enchant = None
class settingsWindow(QWidget, Ui_Settings):
def __init__(self, mainWindow):

View file

@ -17,11 +17,7 @@ from manuskript.ui.editors.locker import locker
from manuskript.ui.editors.themes import findThemePath, generateTheme, setThemeEditorDatas
from manuskript.ui.editors.themes import loadThemeDatas
from manuskript.ui.views.MDEditView import MDEditView
try:
import enchant
except ImportError:
enchant = None
from manuskript.functions import Spellchecker
class fullScreenEditor(QWidget):
@ -54,7 +50,7 @@ class fullScreenEditor(QWidget):
# self.topPanel.layout().addStretch(1)
# Spell checking
if enchant:
if Spellchecker.isInstalled():
self.btnSpellCheck = QPushButton(self)
self.btnSpellCheck.setFlat(True)
self.btnSpellCheck.setIcon(QIcon.fromTheme("tools-check-spelling"))

View file

@ -152,7 +152,7 @@ class BasicHighlighter(QSyntaxHighlighter):
if hasattr(self.editor, "spellcheck") and self.editor.spellcheck:
for word_object in re.finditer(WORDS, textedText):
if (self.editor._dict
and not self.editor._dict.check(word_object.group(1))):
and self.editor._dict.isMisspelled(word_object.group(1))):
format = self.format(word_object.start(1))
format.setUnderlineColor(self._misspelledColor)
# SpellCheckUnderline fails with some fonts

View file

@ -10,11 +10,6 @@ from manuskript.ui.editors.completer import completer
from manuskript.ui.views.MDEditView import MDEditView
from manuskript.models import references as Ref
try:
import enchant
except ImportError:
enchant = None
class MDEditCompleter(MDEditView):
def __init__(self, parent=None, index=None, html=None, spellcheck=True, highlighting=False, dict="",

View file

@ -13,12 +13,7 @@ from manuskript import functions as F
from manuskript.models import outlineModel, outlineItem
from manuskript.ui.highlighters import BasicHighlighter
from manuskript.ui import style as S
try:
import enchant
except ImportError:
enchant = None
from manuskript.functions import Spellchecker
class textEditView(QTextEdit):
def __init__(self, parent=None, index=None, html=None, spellcheck=None,
@ -77,29 +72,16 @@ class textEditView(QTextEdit):
self.setReadOnly(True)
# Spellchecking
if enchant and self.spellcheck:
try:
self._dict = enchant.Dict(self.currentDict if self.currentDict
else self.getDefaultLocale())
except enchant.errors.DictNotFoundError:
self.spellcheck = False
if self.spellcheck:
self._dict = Spellchecker.getDictionary(self.currentDict)
else:
if not self._dict:
self.spellcheck = False
if self._highlighting and not self.highlighter:
self.highlighter = self._highlighterClass(self)
self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat)
def getDefaultLocale(self):
default_locale = enchant.get_default_language()
if default_locale is None:
default_locale = QLocale.system().name()
if default_locale is None:
default_locale = enchant.list_dicts()[0][0]
return default_locale
def setModel(self, model):
self._model = model
try:
@ -389,20 +371,18 @@ class textEditView(QTextEdit):
def setDict(self, d):
self.currentDict = d
if d and enchant.dict_exists(d):
self._dict = enchant.Dict(d)
if d:
self._dict = Spellchecker.getDictionary(d)
if self.highlighter:
self.highlighter.rehighlight()
def toggleSpellcheck(self, v):
self.spellcheck = v
if enchant and self.spellcheck and not self._dict:
if self.currentDict and enchant.dict_exists(self.currentDict):
self._dict = enchant.Dict(self.currentDict)
elif enchant.get_default_language() and enchant.dict_exists(enchant.get_default_language()):
self._dict = enchant.Dict(enchant.get_default_language())
else:
self.spellcheck = False
if self.spellcheck and not self._dict:
self._dict = Spellchecker.getDictionary(self.currentDict)
if not self._dict:
self.spellcheck = False
if self.highlighter:
self.highlighter.rehighlight()
@ -475,12 +455,12 @@ class textEditView(QTextEdit):
# suggestions if it is.
if self._dict and cursor.hasSelection():
text = str(cursor.selectedText())
valid = self._dict.check(text)
valid = not self._dict.isMisspelled(text)
selectedWord = cursor.selectedText()
if not valid:
spell_menu = QMenu(self.tr('Spelling Suggestions'), self)
spell_menu.setIcon(F.themeIcon("spelling"))
for word in self._dict.suggest(text):
for word in self._dict.getSuggestions(text):
action = self.SpellAction(word, spell_menu)
action.correct.connect(self.correctWord)
spell_menu.addAction(action)
@ -499,7 +479,7 @@ class textEditView(QTextEdit):
# popup_menu.insertSeparator(popup_menu.actions()[0])
# If word was added to custom dict, give the possibility to remove it
elif valid and self._dict.is_added(selectedWord):
elif valid and self._dict.isCustomWord(selectedWord):
popup_menu.insertSeparator(popup_menu.actions()[0])
# Adds: remove from dictionary
rmAction = QAction(self.tr("&Remove from custom dictionary"), popup_menu)
@ -524,12 +504,12 @@ class textEditView(QTextEdit):
def addWordToDict(self):
word = self.sender().data()
self._dict.add(word)
self._dict.addWord(word)
self.highlighter.rehighlight()
def rmWordFromDict(self):
word = self.sender().data()
self._dict.remove(word)
self._dict.removeWord(word)
self.highlighter.rehighlight()
###############################################################################