From c65f80d22e5a6825aca82a149cc29f5dd32854ad Mon Sep 17 00:00:00 2001 From: "B.Melnik" Date: Mon, 13 Sep 2021 16:18:05 +0300 Subject: [PATCH] feat(Spellchecker): Add Spellchecker class Closes: #399 --- sandbox/sandbox.pro | 6 +- sandbox/sandboxapp.cpp | 2 + sandbox/spellchecker.cpp | 174 +++++++++++++++++++++++++++++++++++++++ sandbox/spellchecker.h | 54 ++++++++++++ 4 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 sandbox/spellchecker.cpp create mode 100644 sandbox/spellchecker.h diff --git a/sandbox/sandbox.pro b/sandbox/sandbox.pro index 93ee684c..531bb87d 100644 --- a/sandbox/sandbox.pro +++ b/sandbox/sandbox.pro @@ -12,7 +12,8 @@ DEFINES += QT_DEPRECATED_WARNINGS SOURCES += \ handler.cpp \ main.cpp \ - sandboxapp.cpp + sandboxapp.cpp \ + spellchecker.cpp !macx { SOURCES += statuswindow.cpp @@ -59,7 +60,8 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin HEADERS += \ handler.h \ sandboxapp.h \ - statuswindow.h + statuswindow.h \ + spellchecker.h OTHER_FILES += $$files($$PWD/../*.qml, true) OTHER_FILES += $$files($$PWD/*.qml, true) diff --git a/sandbox/sandboxapp.cpp b/sandbox/sandboxapp.cpp index 3c4b4e14..6adc1815 100644 --- a/sandbox/sandboxapp.cpp +++ b/sandbox/sandboxapp.cpp @@ -5,6 +5,7 @@ #include #include "statuswindow.h" +#include "spellchecker.h" SandboxApp::SandboxApp(int &argc, char **argv) : QGuiApplication(argc, argv), @@ -16,6 +17,7 @@ SandboxApp::SandboxApp(int &argc, char **argv) void SandboxApp::startEngine() { qmlRegisterType("Sandbox", 0, 1, "StatusWindow"); + qmlRegisterType("Sandbox", 0, 1, "Spellchecker"); #ifdef QT_DEBUG const QUrl url(applicationDirPath() + "/../main.qml"); diff --git a/sandbox/spellchecker.cpp b/sandbox/spellchecker.cpp new file mode 100644 index 00000000..0babe47f --- /dev/null +++ b/sandbox/spellchecker.cpp @@ -0,0 +1,174 @@ +#include "spellchecker.h" + +#include "hunspell.hxx" +#include +#include +#include +#include + +#include +#include +#include + +SpellChecker::SpellChecker(QObject *parent) + : QObject(parent) + , m_hunspell(nullptr) + , m_userDict("userDict_") +{ + +} + +SpellChecker::~SpellChecker() +{ +#ifdef Q_OS_MACOS + delete m_hunspell; +#endif +} + +bool SpellChecker::spell(const QString &word) +{ +#ifdef Q_OS_MACOS + return m_hunspell->spell(m_codec->fromUnicode(word).toStdString()); +#else + return true; +#endif +} + +bool SpellChecker::isInit() const +{ + return !m_hunspell; +} + +void SpellChecker::initHunspell() +{ +#ifdef Q_OS_MACOS + if (m_hunspell) { + delete m_hunspell; + } + + QString dictFile = QApplication::applicationDirPath() + "/dictionaries/" + m_lang + "/index.dic"; + QString affixFile = QApplication::applicationDirPath() + "/dictionaries/" + m_lang + "/index.aff"; + QByteArray dictFilePathBA = dictFile.toLocal8Bit(); + QByteArray affixFilePathBA = affixFile.toLocal8Bit(); + m_hunspell = new Hunspell(affixFilePathBA.constData(), + dictFilePathBA.constData()); + + // detect encoding analyzing the SET option in the affix file + auto encoding = QStringLiteral("ISO8859-15"); + QFile _affixFile(affixFile); + if (_affixFile.open(QIODevice::ReadOnly)) { + QTextStream stream(&_affixFile); + QRegularExpression enc_detector( + QStringLiteral("^\\s*SET\\s+([A-Z0-9\\-]+)\\s*"), + QRegularExpression::CaseInsensitiveOption); + QString sLine; + QRegularExpressionMatch match; + while (!stream.atEnd()) { + sLine = stream.readLine(); + if (sLine.isEmpty()) { continue; } + match = enc_detector.match(sLine); + if (match.hasMatch()) { + encoding = match.captured(1); + qDebug() << "Encoding set to " + encoding; + break; + } + } + _affixFile.close(); + } + m_codec = QTextCodec::codecForName(encoding.toLatin1().constData()); + + QString userDict = m_userDict + m_lang + ".txt"; + + if (!userDict.isEmpty()) { + QFile userDictonaryFile(userDict); + if (userDictonaryFile.open(QIODevice::ReadOnly)) { + QTextStream stream(&userDictonaryFile); + for (QString word = stream.readLine(); + !word.isEmpty(); + word = stream.readLine()) + ignoreWord(word); + userDictonaryFile.close(); + } else { + qWarning() << "User dictionary in " << userDict + << "could not be opened"; + } + } else { + qDebug() << "User dictionary not set."; + } +#endif +} + +QVariantList SpellChecker::suggest(const QString &word) +{ + int numSuggestions = 0; + QVariantList suggestions; +#ifdef Q_OS_MACOS + std::vector wordlist; + wordlist = m_hunspell->suggest(m_codec->fromUnicode(word).toStdString()); + + numSuggestions = static_cast(wordlist.size()); + if (numSuggestions > 0) { + suggestions.reserve(numSuggestions); + for (int i = 0; i < numSuggestions; i++) { + suggestions << m_codec->toUnicode( + QByteArray::fromStdString(wordlist[i])); + } + } +#endif + + return suggestions; +} + +void SpellChecker::ignoreWord(const QString &word) +{ +#ifdef Q_OS_MACOS + m_hunspell->add(m_codec->fromUnicode(word).constData()); +#endif +} + +void SpellChecker::addToUserWordlist(const QString &word) +{ +#ifdef Q_OS_MACOS + QString userDict = m_userDict + m_lang + ".txt"; + if (!userDict.isEmpty()) { + QFile userDictonaryFile(userDict); + if (userDictonaryFile.open(QIODevice::Append)) { + QTextStream stream(&userDictonaryFile); + stream << word << "\n"; + userDictonaryFile.close(); + } else { + qWarning() << "User dictionary in " << userDict + << "could not be opened for appending a new word"; + } + } else { + qDebug() << "User dictionary not set."; + } +#endif +} + +const QString& SpellChecker::lang() const +{ + return m_lang; +} + +void SpellChecker::setLang(const QString& lang) +{ + if (m_lang != lang) { + m_lang = lang; + initHunspell(); + emit langChanged(); + } +} + +const QString& SpellChecker::userDict() const +{ + return m_userDict; +} + +void SpellChecker::setUserDict(const QString& userDict) +{ + if (m_userDict != userDict) { + m_userDict = userDict; + emit userDictChanged(); + } +} diff --git a/sandbox/spellchecker.h b/sandbox/spellchecker.h new file mode 100644 index 00000000..ba123ee0 --- /dev/null +++ b/sandbox/spellchecker.h @@ -0,0 +1,54 @@ +#ifndef SPELLCHECKER_H +#define SPELLCHECKER_H + +#include +#include +#include +#include + +#ifdef Q_OS_MACOS +class Hunspell; +#endif +class QTextCodec; + +class SpellChecker : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString lang READ lang WRITE setLang NOTIFY langChanged) + Q_PROPERTY(QString userDict READ userDict WRITE setUserDict NOTIFY userDictChanged) + +public: + explicit SpellChecker(QObject *parent = nullptr); + ~SpellChecker(); + + Q_INVOKABLE bool spell(const QString& word); + Q_INVOKABLE QVariantList suggest(const QString &word); + Q_INVOKABLE void ignoreWord(const QString &word); + Q_INVOKABLE void addToUserWordlist(const QString &word); + Q_INVOKABLE bool isInit() const; + + const QString& lang() const; + void setLang(const QString& lang); + + const QString& userDict() const; + void setUserDict(const QString& userDict); + +signals: + void langChanged(); + void userDictChanged(); + +private: + void initHunspell(); + +private: + QString m_lang; + QString m_userDict; + + QQuickTextDocument *m_document; +#ifdef Q_OS_MACOS + Hunspell *m_hunspell; +#endif + QTextCodec *m_codec; +}; + +#endif // SPELLCHECKER_H