@@ -68,7 +68,7 @@ if(ANDROID)
|
|||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
find_package(Qt5 ${QT_MIN_VERSION} COMPONENTS Widgets)
|
find_package(Qt5 ${QT_MIN_VERSION} COMPONENTS Widgets)
|
||||||
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO)
|
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO Sonnet)
|
||||||
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
|
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
|
||||||
TYPE RUNTIME
|
TYPE RUNTIME
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
|
|||||||
target_sources(neochat PRIVATE ${NEOCHAT_ICON})
|
target_sources(neochat PRIVATE ${NEOCHAT_ICON})
|
||||||
|
|
||||||
if(NOT ANDROID)
|
if(NOT ANDROID)
|
||||||
target_sources(neochat PRIVATE trayicon.cpp colorschemer.cpp)
|
target_sources(neochat PRIVATE trayicon.cpp colorschemer.cpp spellcheckhighlighter.cpp)
|
||||||
target_link_libraries(neochat PRIVATE KF5::ConfigWidgets)
|
target_link_libraries(neochat PRIVATE KF5::ConfigWidgets KF5::SonnetCore)
|
||||||
target_compile_definitions(neochat PRIVATE -DHAVE_COLORSCHEME)
|
target_compile_definitions(neochat PRIVATE -DHAVE_COLORSCHEME)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
|
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
#ifndef Q_OS_ANDROID
|
||||||
|
#include "spellcheckhighlighter.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -34,6 +37,9 @@ void ChatDocumentHandler::setDocument(QQuickTextDocument *document)
|
|||||||
m_document->textDocument()->disconnect(this);
|
m_document->textDocument()->disconnect(this);
|
||||||
}
|
}
|
||||||
m_document = document;
|
m_document = document;
|
||||||
|
#ifndef Q_OS_ANDROID
|
||||||
|
new SpellcheckHighlighter(m_document->textDocument());
|
||||||
|
#endif
|
||||||
Q_EMIT documentChanged();
|
Q_EMIT documentChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
109
src/spellcheckhighlighter.cpp
Normal file
109
src/spellcheckhighlighter.cpp
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// Copyright (c) 2020 Christian Mollekopf <mollekopf@kolabsystems.com>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "spellcheckhighlighter.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QTextBoundaryFinder>
|
||||||
|
|
||||||
|
QVector<QStringRef> split(QTextBoundaryFinder::BoundaryType boundary, const QString &text, int reasonMask = 0)
|
||||||
|
{
|
||||||
|
QVector<QStringRef> parts;
|
||||||
|
QTextBoundaryFinder boundaryFinder(boundary, text);
|
||||||
|
|
||||||
|
while (boundaryFinder.position() < text.length()) {
|
||||||
|
const int start = boundaryFinder.position();
|
||||||
|
|
||||||
|
//Advance until we find a break that matches the mask or are at the end
|
||||||
|
for (;;) {
|
||||||
|
if (boundaryFinder.toNextBoundary() == -1) {
|
||||||
|
boundaryFinder.toEnd();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!reasonMask || boundaryFinder.boundaryReasons() & reasonMask) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto length = boundaryFinder.position() - start;
|
||||||
|
|
||||||
|
if (length < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parts << QStringRef{&text, start, length};
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SpellcheckHighlighter::SpellcheckHighlighter(QTextDocument *parent)
|
||||||
|
: QSyntaxHighlighter(parent),
|
||||||
|
mSpellchecker{new Sonnet::Speller()},
|
||||||
|
mLanguageGuesser{new Sonnet::GuessLanguage()}
|
||||||
|
{
|
||||||
|
//Danger red from our color scheme
|
||||||
|
mErrorFormat.setForeground(QColor{"#ed1515"});
|
||||||
|
mErrorFormat.setUnderlineColor(QColor{"#ed1515"});
|
||||||
|
mErrorFormat.setUnderlineStyle(QTextCharFormat::SingleUnderline);
|
||||||
|
mQuoteFormat.setForeground(QColor{"#7f8c8d"});
|
||||||
|
|
||||||
|
|
||||||
|
if (!mSpellchecker->isValid()) {
|
||||||
|
qWarning() << "Spellchecker is invalid";
|
||||||
|
}
|
||||||
|
qDebug() << "Available dictionaries: " << mSpellchecker->availableDictionaries();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpellcheckHighlighter::autodetectLanguage(const QString &sentence)
|
||||||
|
{
|
||||||
|
const auto lang = mLanguageGuesser->identify(sentence, mSpellchecker->availableLanguages());
|
||||||
|
if (lang.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mSpellchecker->setLanguage(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isSpellcheckable(const QStringRef &token)
|
||||||
|
{
|
||||||
|
if (token.isNull() || token.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!token.at(0).isLetter() || token.at(0).isUpper() || token.startsWith(QStringLiteral("http"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//TODO ignore urls and uppercase?
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpellcheckHighlighter::highlightBlock(const QString &text)
|
||||||
|
{
|
||||||
|
//Avoid spellchecking quotes
|
||||||
|
if (text.isEmpty() || text.at(0) == QChar{'>'}) {
|
||||||
|
setFormat(0, text.length(), mQuoteFormat);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto &sentenceRef : split(QTextBoundaryFinder::Sentence, text)) {
|
||||||
|
//Avoid spellchecking quotes
|
||||||
|
if (sentenceRef.isEmpty() || sentenceRef.at(0) == QChar{'>'}) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto sentence = QString::fromRawData(sentenceRef.data(), sentenceRef.length());
|
||||||
|
|
||||||
|
autodetectLanguage(sentence);
|
||||||
|
|
||||||
|
const int offset = sentenceRef.position();
|
||||||
|
for (const auto &wordRef : split(QTextBoundaryFinder::Word, sentence)) {
|
||||||
|
//Avoid spellchecking words in progress
|
||||||
|
//FIXME this will also prevent spellchecking a single word on a line.
|
||||||
|
if (offset + wordRef.position() + wordRef.length() >= text.length()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isSpellcheckable(wordRef)) {
|
||||||
|
const auto word = QString::fromRawData(wordRef.data(), wordRef.length());
|
||||||
|
const auto format = mSpellchecker->isMisspelled(word) ? mErrorFormat : QTextCharFormat{};
|
||||||
|
setFormat(offset + wordRef.position(), wordRef.length(), format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/spellcheckhighlighter.h
Normal file
26
src/spellcheckhighlighter.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2020 Christian Mollekopf <mollekopf@kolabsystems.com>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QTextDocument>
|
||||||
|
#include <QSyntaxHighlighter>
|
||||||
|
#include <Sonnet/Speller>
|
||||||
|
#include <Sonnet/GuessLanguage>
|
||||||
|
|
||||||
|
class SpellcheckHighlighter: public QSyntaxHighlighter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SpellcheckHighlighter(QTextDocument *parent);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void highlightBlock(const QString &text) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void autodetectLanguage(const QString &sentence);
|
||||||
|
QTextCharFormat mErrorFormat;
|
||||||
|
QTextCharFormat mQuoteFormat;
|
||||||
|
QScopedPointer<Sonnet::Speller> mSpellchecker;
|
||||||
|
QScopedPointer<Sonnet::GuessLanguage> mLanguageGuesser;
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user