mirror of
https://github.com/olivierkes/manuskript.git
synced 2024-05-14 09:52:27 +12:00
Add support for pyspellchecker as an alternative to PyEnchant
This modifies the Spellchecker abstraction to add a new dictionary support, with support for pyspellchecker. It also changes the main UI so that multiple libraries can be supported and dictionaries provided to the user. The custom dictionary of pyspellchecker has to be handled manually, and the performance and words of this library isn't on par with PyEnchant, but at least it works with 64 bits. Fixes #505
This commit is contained in:
parent
d0f02cb2a7
commit
20c5586a6c
|
@ -12,7 +12,6 @@ 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
|
||||
|
@ -398,3 +397,6 @@ def inspect():
|
|||
s.lineno,
|
||||
s.function))
|
||||
print(" " + "".join(s.code_context))
|
||||
|
||||
# Spellchecker loads writablePath from this file, so we need to load it after they get defined
|
||||
from manuskript.functions.spellchecker import Spellchecker
|
||||
|
|
|
@ -1,45 +1,111 @@
|
|||
|
||||
import os, gzip, json
|
||||
from PyQt5.QtCore import QLocale
|
||||
from collections import OrderedDict
|
||||
from manuskript.functions import writablePath
|
||||
|
||||
try:
|
||||
import enchant
|
||||
except ImportError:
|
||||
enchant = None
|
||||
|
||||
try:
|
||||
import spellchecker as pyspellchecker
|
||||
except ImportError:
|
||||
pyspellchecker = None
|
||||
|
||||
|
||||
class Spellchecker:
|
||||
dictionaries = {}
|
||||
# In order of priority
|
||||
implementations = []
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def isInstalled():
|
||||
return enchant is not None
|
||||
for impl in Spellchecker.implementations:
|
||||
if impl.isInstalled():
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def supportedLibraries():
|
||||
libs = []
|
||||
for impl in Spellchecker.implementations:
|
||||
libs.append(impl.getLibraryName())
|
||||
return libs
|
||||
|
||||
@staticmethod
|
||||
def availableLibraries():
|
||||
ret = []
|
||||
for impl in Spellchecker.implementations:
|
||||
if impl.isInstalled():
|
||||
ret.append(impl.getLibraryName())
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def availableDictionaries():
|
||||
return EnchantDictionary.availableDictionaries()
|
||||
dictionaries = OrderedDict()
|
||||
for impl in Spellchecker.implementations:
|
||||
dictionaries[impl.getLibraryName()] = impl.availableDictionaries()
|
||||
return dictionaries
|
||||
|
||||
@staticmethod
|
||||
def normalizeDictName(lib, dictionary):
|
||||
return "{}:{}".format(lib, dictionary)
|
||||
|
||||
@staticmethod
|
||||
def getDefaultDictionary():
|
||||
return EnchantDictionary.getDefaultDictionary()
|
||||
for impl in Spellchecker.implementations:
|
||||
default = impl.getDefaultDictionary()
|
||||
if default:
|
||||
return Spellchecker.normalizeDictName(impl.getLibraryName(), default)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def getLibraryURL():
|
||||
return EnchantDictionary.getLibraryURL()
|
||||
def getLibraryURL(lib=None):
|
||||
urls = {}
|
||||
for impl in Spellchecker.implementations:
|
||||
urls[impl.getLibraryName()] = impl.getLibraryURL()
|
||||
if lib:
|
||||
return urls.get(lib, None)
|
||||
return urls
|
||||
|
||||
|
||||
@staticmethod
|
||||
def getResourcesPath(library):
|
||||
path = os.path.join(writablePath(), "resources", "dictionaries", library)
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def getDictionary(dictionary):
|
||||
if not dictionary:
|
||||
dictionary = Spellchecker.getDefaultDictionary()
|
||||
if not dictionary:
|
||||
return None
|
||||
|
||||
values = dictionary.split(":", 1)
|
||||
if len(values) == 1:
|
||||
(lib, name) = (Spellchecker.implementations[0].getLibraryName(), dictionary)
|
||||
dictionary = Spellchecker.normalizeDictName(lib, name)
|
||||
else:
|
||||
(lib, name) = values
|
||||
try:
|
||||
d = Spellchecker.dictionaries.get(dictionary, None)
|
||||
if d is None:
|
||||
d = EnchantDictionary(dictionary)
|
||||
Spellchecker.dictionaries[d.name] = d
|
||||
for impl in Spellchecker.implementations:
|
||||
if lib == impl.getLibraryName():
|
||||
d = impl(name)
|
||||
Spellchecker.dictionaries[dictionary] = d
|
||||
break
|
||||
return d
|
||||
except Exception as e:
|
||||
return None
|
||||
pass
|
||||
return None
|
||||
|
||||
class BasicDictionary:
|
||||
def __init__(self, name):
|
||||
|
@ -57,6 +123,10 @@ class BasicDictionary:
|
|||
def getLibraryURL():
|
||||
raise NotImplemented
|
||||
|
||||
@staticmethod
|
||||
def isInstalled():
|
||||
raise NotImplemented
|
||||
|
||||
@staticmethod
|
||||
def getDefaultDictionary():
|
||||
raise NotImplemented
|
||||
|
@ -95,21 +165,25 @@ class EnchantDictionary(BasicDictionary):
|
|||
|
||||
@staticmethod
|
||||
def getLibraryName():
|
||||
return "pyEnchant"
|
||||
return "PyEnchant"
|
||||
|
||||
@staticmethod
|
||||
def getLibraryURL():
|
||||
return "https://pypi.org/project/pyenchant/"
|
||||
|
||||
@staticmethod
|
||||
def isInstalled():
|
||||
return enchant is not None
|
||||
|
||||
@staticmethod
|
||||
def availableDictionaries():
|
||||
if enchant:
|
||||
if EnchantDictionary.isInstalled():
|
||||
return list(map(lambda i: str(i[0]), enchant.list_dicts()))
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def getDefaultDictionary():
|
||||
if not enchant:
|
||||
if not EnchantDictionary.isInstalled():
|
||||
return None
|
||||
|
||||
default_locale = enchant.get_default_language()
|
||||
|
@ -137,3 +211,90 @@ class EnchantDictionary(BasicDictionary):
|
|||
|
||||
def removeWord(self, word):
|
||||
self._dict.remove(word)
|
||||
Spellchecker.implementations.append(EnchantDictionary)
|
||||
|
||||
|
||||
class PySpellcheckerDictionary(BasicDictionary):
|
||||
|
||||
def __init__(self, name):
|
||||
self._lang = name
|
||||
if not self._lang:
|
||||
self._lang = self.getDefaultDictionary()
|
||||
|
||||
self._dict = pyspellchecker.SpellChecker(self._lang)
|
||||
self._customDict = None
|
||||
customPath = self.getCustomDictionaryPath()
|
||||
try:
|
||||
self._customDict = pyspellchecker.SpellChecker(local_dictionary=customPath)
|
||||
self._dict.word_frequency.load_dictionary(customPath)
|
||||
except:
|
||||
# If error loading the file, overwrite with empty dictionary
|
||||
with gzip.open(customPath, "wt") as f:
|
||||
f.write(json.dumps({}))
|
||||
|
||||
self._customDict = pyspellchecker.SpellChecker(local_dictionary=customPath)
|
||||
self._dict.word_frequency.load_dictionary(customPath)
|
||||
|
||||
def getCustomDictionaryPath(self):
|
||||
return os.path.join(Spellchecker.getResourcesPath(self.getLibraryName()), "{}.json.gz".format(self._lang))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._lang
|
||||
|
||||
@staticmethod
|
||||
def getLibraryName():
|
||||
return "pyspellchecker"
|
||||
|
||||
@staticmethod
|
||||
def getLibraryURL():
|
||||
return "https://pyspellchecker.readthedocs.io/en/latest/"
|
||||
|
||||
@staticmethod
|
||||
def isInstalled():
|
||||
return pyspellchecker is not None
|
||||
|
||||
@staticmethod
|
||||
def availableDictionaries():
|
||||
if PySpellcheckerDictionary.isInstalled():
|
||||
# TODO: If pyspellchecker eventually adds a way to get this list
|
||||
# programmatically or if the list changes, we need to update it here
|
||||
return ["de", "en", "es", "fr", "pt"]
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def getDefaultDictionary():
|
||||
if not PySpellcheckerDictionary.isInstalled():
|
||||
return None
|
||||
|
||||
default_locale = QLocale.system().name()
|
||||
if default_locale:
|
||||
default_locale = default_locale[0:2]
|
||||
if default_locale is None:
|
||||
default_locale = "en"
|
||||
|
||||
return default_locale
|
||||
|
||||
def isMisspelled(self, word):
|
||||
return len(self._dict.unknown([word])) > 0
|
||||
|
||||
def getSuggestions(self, word):
|
||||
candidates = self._dict.candidates(word)
|
||||
if word in candidates:
|
||||
candidates.remove(word)
|
||||
return candidates
|
||||
|
||||
def isCustomWord(self, word):
|
||||
return len(self._customDict.known([word])) > 0
|
||||
|
||||
def addWord(self, word):
|
||||
self._dict.word_frequency.add(word)
|
||||
self._customDict.word_frequency.add(word)
|
||||
self._customDict.export(self.getCustomDictionaryPath(), gzipped=True)
|
||||
|
||||
def removeWord(self, word):
|
||||
self._dict.word_frequency.remove(word)
|
||||
self._customDict.word_frequency.remove(word)
|
||||
self._customDict.export(self.getCustomDictionaryPath(), gzipped=True)
|
||||
|
||||
Spellchecker.implementations.append(PySpellcheckerDictionary)
|
||||
|
|
|
@ -1255,10 +1255,15 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||
else:
|
||||
# No Spell check support
|
||||
self.actSpellcheck.setVisible(False)
|
||||
a = QAction(self.tr("Install PyEnchant to use spellcheck"), self)
|
||||
a.setIcon(self.style().standardIcon(QStyle.SP_MessageBoxWarning))
|
||||
a.triggered.connect(self.openPyEnchantWebPage, F.AUC)
|
||||
self.menuTools.addAction(a)
|
||||
for lib in Spellchecker.supportedLibraries():
|
||||
a = QAction(self.tr("Install {} to use spellcheck").format(lib), self)
|
||||
a.setIcon(self.style().standardIcon(QStyle.SP_MessageBoxWarning))
|
||||
# Need to bound the lib argument otherwise the lambda uses the same lib value across all calls
|
||||
def gen_slot_cb(l):
|
||||
return lambda: self.openSpellcheckWebPage(l)
|
||||
a.triggered.connect(gen_slot_cb(lib), F.AUC)
|
||||
self.menuTools.addAction(a)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# SPELLCHECK
|
||||
|
@ -1270,16 +1275,25 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||
return
|
||||
|
||||
self.menuDict.clear()
|
||||
for i in Spellchecker.availableDictionaries():
|
||||
a = QAction(i, self)
|
||||
a.setCheckable(True)
|
||||
if settings.dict is None:
|
||||
settings.dict = Spellchecker.getDefaultDictionary()
|
||||
if i == settings.dict:
|
||||
a.setChecked(True)
|
||||
a.triggered.connect(self.setDictionary, F.AUC)
|
||||
self.menuDictGroup.addAction(a)
|
||||
for lib, dicts in Spellchecker.availableDictionaries().items():
|
||||
if len(dicts) > 1:
|
||||
a = QAction(lib, self)
|
||||
else:
|
||||
a = QAction(self.tr("{} is not installed").format(lib), self)
|
||||
a.setEnabled(False)
|
||||
self.menuDict.addAction(a)
|
||||
for i in dicts:
|
||||
a = QAction(i, self)
|
||||
a.data = lib
|
||||
a.setCheckable(True)
|
||||
if settings.dict is None:
|
||||
settings.dict = Spellchecker.getDefaultDictionary()
|
||||
if Spellchecker.normalizeDictName(lib, i) == settings.dict:
|
||||
a.setChecked(True)
|
||||
a.triggered.connect(self.setDictionary, F.AUC)
|
||||
self.menuDictGroup.addAction(a)
|
||||
self.menuDict.addAction(a)
|
||||
self.menuDict.addSeparator()
|
||||
|
||||
def setDictionary(self):
|
||||
if not Spellchecker.isInstalled():
|
||||
|
@ -1288,15 +1302,15 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||
for i in self.menuDictGroup.actions():
|
||||
if i.isChecked():
|
||||
# self.dictChanged.emit(i.text().replace("&", ""))
|
||||
settings.dict = i.text().replace("&", "")
|
||||
settings.dict = Spellchecker.normalizeDictName(i.data, i.text().replace("&", ""))
|
||||
|
||||
# Find all textEditView from self, and toggle spellcheck
|
||||
for w in self.findChildren(textEditView, QRegExp(".*"),
|
||||
Qt.FindChildrenRecursively):
|
||||
w.setDict(settings.dict)
|
||||
|
||||
def openPyEnchantWebPage(self):
|
||||
F.openURL(Spellchecker.getLibraryURL())
|
||||
def openSpellcheckWebPage(self, lib):
|
||||
F.openURL(Spellchecker.getLibraryURL(lib))
|
||||
|
||||
def toggleSpellcheck(self, val):
|
||||
settings.spellcheck = val
|
||||
|
|
Loading…
Reference in a new issue