feat(Spellchecker): Add spellchecker class
This commit is contained in:
parent
64c960fdb3
commit
ec0b962bad
|
@ -0,0 +1,60 @@
|
||||||
|
#ifndef DOSSPELLCHECKER_H
|
||||||
|
#define DOSSPELLCHECKER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QQuickTextDocument>
|
||||||
|
#include <QSyntaxHighlighter>
|
||||||
|
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
class Hunspell;
|
||||||
|
#endif
|
||||||
|
class QTextCodec;
|
||||||
|
|
||||||
|
class SpellChecker : public QSyntaxHighlighter
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
|
||||||
|
Q_PROPERTY(QString displayText READ displayText NOTIFY displayTextChanged)
|
||||||
|
Q_PROPERTY(QQuickTextDocument* document READ textDocument WRITE setTextDocument NOTIFY textDocumentChanged)
|
||||||
|
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);
|
||||||
|
|
||||||
|
void setText(const QString& text);
|
||||||
|
const QString& text() const;
|
||||||
|
|
||||||
|
const QString& displayText() const;
|
||||||
|
|
||||||
|
QQuickTextDocument* textDocument() const;
|
||||||
|
void setTextDocument(QQuickTextDocument* document);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void textChanged();
|
||||||
|
void displayTextChanged();
|
||||||
|
void textDocumentChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void highlightBlock(const QString &text) final;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void makeDisplayText(const QString& text);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_text;
|
||||||
|
QString m_displayText;
|
||||||
|
QString m_dictionaryPath;
|
||||||
|
|
||||||
|
QQuickTextDocument *m_document;
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
Hunspell *m_hunspell;
|
||||||
|
#endif
|
||||||
|
QTextCodec *m_codec;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DOSSPELLCHECKER_H
|
|
@ -65,6 +65,7 @@
|
||||||
#include "DOtherSide/StatusEvents/StatusDockShowAppEvent.h"
|
#include "DOtherSide/StatusEvents/StatusDockShowAppEvent.h"
|
||||||
#include "DOtherSide/StatusEvents/StatusOSThemeEvent.h"
|
#include "DOtherSide/StatusEvents/StatusOSThemeEvent.h"
|
||||||
#include "DOtherSide/StatusNotification/StatusOSNotification.h"
|
#include "DOtherSide/StatusNotification/StatusOSNotification.h"
|
||||||
|
#include "DOtherSide/DosSpellchecker.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
@ -73,6 +74,7 @@ void register_meta_types()
|
||||||
qRegisterMetaType<QVector<int>>();
|
qRegisterMetaType<QVector<int>>();
|
||||||
qmlRegisterType<StatusWindow>("DotherSide", 0 , 1, "StatusWindow");
|
qmlRegisterType<StatusWindow>("DotherSide", 0 , 1, "StatusWindow");
|
||||||
qmlRegisterType<StatusSyntaxHighlighterHelper>("DotherSide", 0 , 1, "StatusSyntaxHighlighter");
|
qmlRegisterType<StatusSyntaxHighlighterHelper>("DotherSide", 0 , 1, "StatusSyntaxHighlighter");
|
||||||
|
qmlRegisterType<SpellChecker>("DotherSide", 0, 1, "SpellChecker");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,202 @@
|
||||||
|
#include "DosSpellchecker.h"
|
||||||
|
|
||||||
|
#include "hunspell.hxx"
|
||||||
|
#include <QTextCodec>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QLocale>
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
|
#include <QInputMethod>
|
||||||
|
|
||||||
|
SpellChecker::SpellChecker(QObject *parent)
|
||||||
|
: QSyntaxHighlighter(parent)
|
||||||
|
{
|
||||||
|
auto language = QLocale::system().bcp47Name();
|
||||||
|
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
QString dictFile = QApplication::applicationDirPath() + + "/dictionaries/" + language + "/index.dic";
|
||||||
|
QString affixFile = QApplication::applicationDirPath() + + "/dictionaries/" + language + "/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 = "userDict_" + language + ".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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList SpellChecker::suggest(const QString &word)
|
||||||
|
{
|
||||||
|
int numSuggestions = 0;
|
||||||
|
QVariantList suggestions;
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
std::vector<std::string> wordlist;
|
||||||
|
wordlist = m_hunspell->suggest(m_codec->fromUnicode(word).toStdString());
|
||||||
|
|
||||||
|
numSuggestions = static_cast<int>(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
|
||||||
|
auto language = QLocale::scriptToString(QLocale::system().script());
|
||||||
|
|
||||||
|
QString userDict = "userDict_" + language + ".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
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpellChecker::setText(const QString &text)
|
||||||
|
{
|
||||||
|
if (m_text != text) {
|
||||||
|
m_text = text;
|
||||||
|
emit textChanged();
|
||||||
|
makeDisplayText(m_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString &SpellChecker::text() const
|
||||||
|
{
|
||||||
|
return m_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString &SpellChecker::displayText() const
|
||||||
|
{
|
||||||
|
return m_displayText;
|
||||||
|
}
|
||||||
|
|
||||||
|
QQuickTextDocument *SpellChecker::textDocument() const
|
||||||
|
{
|
||||||
|
return m_document;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpellChecker::setTextDocument(QQuickTextDocument *document)
|
||||||
|
{
|
||||||
|
if (m_document != document) {
|
||||||
|
m_document = document;
|
||||||
|
setDocument(m_document->textDocument());
|
||||||
|
emit textDocumentChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpellChecker::highlightBlock(const QString &text)
|
||||||
|
{
|
||||||
|
QTextCharFormat format;
|
||||||
|
format.setFontUnderline(true);
|
||||||
|
|
||||||
|
QRegularExpression expression("\\S+");
|
||||||
|
QRegularExpressionMatchIterator i = expression.globalMatch(text);
|
||||||
|
while(i.hasNext()) {
|
||||||
|
QRegularExpressionMatch match = i.next();
|
||||||
|
if (!spell(match.captured())) {
|
||||||
|
setFormat(match.capturedStart(), match.capturedLength(), format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpellChecker::makeDisplayText(const QString &text)
|
||||||
|
{
|
||||||
|
auto words = text.split(" ");
|
||||||
|
QString gPattern = "<u style=\"color: red;\">%1</u>";
|
||||||
|
|
||||||
|
m_displayText = ""; // todo optimize delta
|
||||||
|
for (const auto& word: qAsConst(words)) {
|
||||||
|
if (!spell(word)) {
|
||||||
|
m_displayText.append(" " + gPattern.arg(word));
|
||||||
|
} else {
|
||||||
|
m_displayText.append(" " + word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit displayTextChanged();
|
||||||
|
}
|
Loading…
Reference in New Issue